6.6 KiB
Chatflow
可视化对话编排(MVP)。人设可在 prompt 与 chatflow 两种模式间切换。
入口
- 用户中心「我的人设」:运行模式选 Chatflow 流程
- 编辑器:
/chatflow?persona=<id> - API:
GET /api/v1/square/personas/:id/graphPUT /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 仅 owner,requireUser 之外无审核),
响应正文还会回灌进 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、::1、localhost - 私有段
10/8、172.16/12、192.168/16 - 链路本地
169.254/16—— 含 AWS/Azure IMDS169.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/8、192.0/16、198.18/15、多播与保留段
判断走地址段而不是字面量,因为 new URL() 会把 2130706433、0x7f.0.0.1、
0177.0.0.1、::ffff:127.0.0.1 归一成别的写法 —— 只比字符串是拦不住的。
第二道:主机名
- 单标签主机名(docker service 名、
metadata、instance-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_URL的 host:port —— 同一台机器换个端口都不给,TOOLS_BASE_URL指向 loopback 时尤其重要。
已知边界
DNS rebinding 仍有窗口。 这里解析一次、fetch 自己再解析一次,控制着权威 DNS 且
TTL 压到极短的攻击者可以在两次之间翻记录。要堵死得把 socket 钉在已经校验过的那个
地址上,即 undici Agent 的 connect.lookup —— 需要把 undici 收成直接依赖,当前没做。
现在挡住的是所有直接写内网地址、以及所有静态解析到内网的名字。
白名单是全站共享的:加进去的 host 等于允许所有人的人设去请求它,别放带鉴权的内部接口。
联网搜索是两道闸的与:全局开关 + 人设自身的「联网」开关(用户中心「我的人设」里勾选)。
详见 docs/ai-gateway.md。