Files
WeChat-AI/docs/chatflow.md
T

6.6 KiB
Raw Blame History

Chatflow

可视化对话编排(MVP)。人设可在 promptchatflow 两种模式间切换。

入口

  • 用户中心「我的人设」:运行模式选 Chatflow 流程
  • 编辑器:/chatflow?persona=<id>
  • API
    • GET /api/v1/square/personas/:id/graph
    • PUT /api/v1/square/personas/:id/graph(保存图并切换为 chatflow

节点

类型 说明 出站
start 唯一入口 本地
llm 调用模型 平台 LLM 直连,或用户自定义 → HF tools /v1/chat/completions
search 联网搜索 HF tools /v1/web-search
http HTTP 请求 TOOLS 主机 + CHATFLOW_HTTP_ALLOWLIST* = 任意公网,见下)
memory 注入本轮已选记忆 本地
if-else 条件分支(true/false 句柄) 本地
answer 最终回复模板 本地

默认图:start → llm → answer

模板变量

节点文案支持 {{query}}{{system_prompt}}{{history}}{{memories}}{{bot_name}}{{llm_text}}{{节点id.text}} 等。

试聊

网页试聊支持 chatflow 人设:执行同一张已发布的图,但 强制平台上游upstream: null), 避免消耗或泄露作者的自定义模型额度。

限制(MVP

  • Chatflow 人设 禁用主动联系skipReason: chatflow_no_proactive
  • Fork 复制 graph/mode/web_search不复制 llm_provider_id
  • 试聊不注入长期记忆(memories 为空)与表情目录
  • LLM 节点的 temperature 暂用客户端默认值
  • 完整 Dify(知识库、代码节点等)不做

环境变量

CHATFLOW_HTTP_ALLOWLIST=          # 额外允许的 http 节点 host`*` = 任意公网
CHATFLOW_MAX_STEPS=32
CHATFLOW_MAX_NODES=40
CHATFLOW_HTTP_TIMEOUT_MS=15000    # 单个 http 节点的墙钟上限
TOOLS_BASE_URL=...                # 搜索 / 用户自定义 LLM 必需
TOOLS_API_KEY=...
WEB_SEARCH_ENABLED=true           # 全局搜索开关;人设还需 webSearchEnabled
WEB_SEARCH_MAX_RESULTS=5          # 默认条数;search 节点可用 max_results 覆盖

以上都是默认值/admin → 设置 → 「Chatflow」「联网搜索与工具网关」可直接改, 覆盖存 Redis,各节点 5 秒内生效,无需重启。详见 docs/runtime-settings.md

http 节点的出站边界

图是人设作者写的(PUT .../graph 仅 ownerrequireUser 之外无审核), 响应正文还会回灌进 vars(截断 50KB)供 answer 引用。也就是说 http 节点等于把一次 可读的服务端请求交到普通注册用户手里,所以出站要按白名单管。

两个放大器值得单独记住:

  • URL 是模板renderTemplate(node.data.url, vars),而 vars.query 就是原始 用户消息(engine.ts:127-133)、vars.llm_text 是模型输出。作者写成 https://{{query}}/ 就等于把 host 的选择权交给任何发消息的人;写成引用上游 llm 节点的变量,就等于交给可被 prompt 注入的模型输出。所以校验必须发生在 渲染之后(现实现即如此),作者时校验 URL 是没用的。
  • 触发不需要微信:网页试聊走同一张图,人设可以一直是 private,作者从不出现在广场 或任何审核面上。

CHATFLOW_HTTP_ALLOWLIST 的两种写法:

含义
http 节点只能打 TOOLS_BASE_URL 的 host:port。没配 TOOLS 就完全不可用
a.com,b.com 精确匹配 hostname不支持通配a.com 不覆盖 api.a.com
* 任意公网地址

* 不是「不做检查」。放行前仍然逐项拦掉(packages/core/src/chatflow/http-guard.ts):

第一道:地址段(写死的 IP 字面量)

  • 回环 127.0.0.0/8::1localhost
  • 私有段 10/8172.16/12192.168/16
  • 链路本地 169.254/16 —— 含 AWS/Azure IMDS 169.254.169.254
  • 运营商级 NAT 100.64/10 —— 含阿里云元数据 100.100.100.200
  • IPv6 fc00::/7整个 fe00::/8(不只 fe80::/10,站点本地 fec0::/10 同样在内)、 ff00::/8,以及 ::ffff:64:ff9b:: 里嵌的 IPv4
  • IPv6 兜底:不在全球单播 2000::/3 内的一律拒,免得再漏某个保留前缀
  • 0/8192.0/16198.18/15、多播与保留段

判断走地址段而不是字面量,因为 new URL() 会把 21307064330x7f.0.0.10177.0.0.1::ffff:127.0.0.1 归一成别的写法 —— 只比字符串是拦不住的。

第二道:主机名

  • 单标签主机名(docker service 名、metadatainstance-data
  • .local .localhost .internal .svc .lan .intranet .corp .private .home.arpa
  • 长得像公网、其实是云元数据的名字:metadata.tencentyun.com(腾讯云 CVM,直接给 角色凭证,无 token 步骤)、metadata.goog

第三道:解析结果

只看字面量挡不住 169-254-169-254.nip.io 这种通配 DNS(一个普通 .io 域名,解析到 IMDS 地址,不需要攻击者自建任何东西),也挡不住攻击者把自家 A 记录指向 10.0.0.5 或 docker 内嵌 DNS 把 service.network 解析成网桥地址。所以放行前会 dns.lookup 一次,任一返回地址落在上面的段里就拒。

解析失败算拦截:解析不出来的名字 fetch 也出不去,没有任何流量发生,不该把一次 DNS 抖动升级成 http_blocked 把整个流程打断。

第四道:重定向与凭证

  • 每一跳重定向都重新过一遍全部检查redirect: "manual",最多 3 跳)。否则一个允许的 公网域名只要回一个 302 → http://169.254.169.254/,前面三道全白做。
  • Authorization 跨 origin 会被摘掉;TOOLS_API_KEY 只发给 TOOLS_BASE_URLhost:port —— 同一台机器换个端口都不给,TOOLS_BASE_URL 指向 loopback 时尤其重要。

已知边界

DNS rebinding 仍有窗口。 这里解析一次、fetch 自己再解析一次,控制着权威 DNS 且 TTL 压到极短的攻击者可以在两次之间翻记录。要堵死得把 socket 钉在已经校验过的那个 地址上,即 undici Agentconnect.lookup —— 需要把 undici 收成直接依赖,当前没做。 现在挡住的是所有直接写内网地址、以及所有静态解析到内网的名字。

白名单是全站共享的:加进去的 host 等于允许所有人的人设去请求它,别放带鉴权的内部接口。

联网搜索是两道闸的与:全局开关 + 人设自身的「联网」开关(用户中心「我的人设」里勾选)。 详见 docs/ai-gateway.md