mirror of
https://github.com/SMNETSTUDIO/WeChat-AI.git
synced 2026-08-12 22:23:42 +08:00
135 lines
6.6 KiB
Markdown
135 lines
6.6 KiB
Markdown
# Chatflow
|
||
|
||
可视化对话编排(MVP)。人设可在 **prompt** 与 **chatflow** 两种模式间切换。
|
||
|
||
## 入口
|
||
|
||
- 用户中心「我的人设」:运行模式选 **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(知识库、代码节点等)不做
|
||
|
||
## 环境变量
|
||
|
||
```env
|
||
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 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/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`。
|