Files

290 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# WeChat-AI 运维手册
## 1. 首次部署清单
### 1.1 依赖
- Node.js 20+
- pnpm
- **Upstash Redis**`rediss://...`
- **LINUX DO OAuth** 应用
- LLM API KeyOpenAI 兼容)
### 1.2 `.env` 必填
```env
REDIS_URL=rediss://default:密码@xxxx.upstash.io:6379
LLM_API_KEY=...
LLM_BASE_URL=...
LLM_MODEL=...
LINUXDO_CLIENT_ID=...
LINUXDO_CLIENT_SECRET=...
LINUXDO_REDIRECT_URI=http://127.0.0.1:8787/api/v1/auth/callback
# 可选:数字 ID 或用户名;留空时「第一个登录的用户」自动成为管理员
LINUXDO_ADMIN_IDS=
PUBLIC_BASE_URL=http://127.0.0.1:8787
```
LINUX DO 应用回调地址必须与 `LINUXDO_REDIRECT_URI` **完全一致**
### 1.4 多节点运维要点
| 项 | 说明 |
|----|------|
| 共享 | 所有节点同一 `REDIS_URL``PUBLIC_BASE_URL`(主域名)、OAuth 回调 |
| 每机 | 唯一 `WORKER_ID`;可选 `NODE_LABEL` / `NODE_REGION` |
| 入口 | 主域名 → `cloudflare-worker``ORIGINS`(源站 IP:端口) |
| 后台 | `/admin`**节点**:进程心跳与 bot 租约;**不显示**源站 URL |
| 扫码 | 登录会话在 Redis,无需粘性会话 |
| 探活 | LB 用 `/health/ready`Docker 可用 `/health` |
| 下线 | 从 Worker `ORIGINS` 移除并 deploy;停容器后租约 TTL 过期自动转移 |
| 扩容 | 新机起容器 + 更新 `ORIGINS`bot 由租约自动分片 |
| 日常代码热修 | `pnpm release:pack``pnpm docker:build``/admin` 上传通道包 → 节点「更新」 |
| 基础镜像变更 | 仍需各机 `docker build` / 拉新镜像 |
进程内 HTTP 限流为单机计数;生产建议在 Cloudflare 对 `/api/v1/auth/*` 做 Rate Limiting。
### 1.3 启动
```powershell
cd F:\Code-Other-4\WeChat-AI
pnpm install
pnpm diag
pnpm db:seed
pnpm dev
```
| 页面 | URL |
|------|-----|
| 用户中心 | http://127.0.0.1:8787/app |
| 管理后台 | http://127.0.0.1:8787/admin |
## 2. 用户操作流程
1. 打开 `/app`**LINUX DO 登录**
2. **扫码添加微信机器人**ClawBot
3. 微信好友私聊机器人 → 在用户中心 **批准**
4. 可选:分配人设(猫娘 / 女友)
5. 正常聊天(AI 会分多条气泡回复)
### 2.0 输入状态(「对方正在输入中」)
两步协议,`packages/ilink` 内部完成,无需配置:
1. `POST /ilink/bot/getconfig { ilink_user_id, context_token }``typing_ticket`
2. `POST /ilink/bot/sendtyping { ilink_user_id, typing_ticket, status }``status: 1` 开始,`status: 2` 停止
票据**按用户缓存**(服务端有效期约 24h,本地按 20h 过期后自动重取),所以每个 peer 大约一天一次 `getconfig`;并发的多次输入调用会合并成一次取票。
指示器的生命周期:收到消息立刻开始 → 调模型前再次开始 → 多气泡之间每条前再次开始 → **`handleJob``finally` 统一停止**。回复、拒绝、限流、用户互聊中继、异常,任何出口都会停止,不会把「正在输入中」留在对方屏幕上。主动联系发完也会停止。
排查:`getconfig` 失败时会退化成不带票据的 `sendtyping`(指示器可能不显示,但**绝不影响回复**);票据被服务端提前失效时会强制重取并重试一次。
### 2.0.1 图片理解(入站 Vision
默认**关闭**,且由 `apps/api/src/shipped-defaults.test.ts` 守着——`VISION_ENABLED` 只认精确的 `"true"``"1"` / `"yes"` / `"TRUE"` 都不算开。关闭状态下:不去 CDN 取字节、不调任何模型,收到图片直接回一句按类型区分的话。
> **出站语音 / 视频 / 文件**`sendVoice` / `sendVideo` / `sendFile` 已在 `packages/ilink` 实现并有测试,但**没有任何调用点**,回复路径不会用到。要真正让角色发语音,还差两段:TTS 产出音频、再转成微信用的 SILK 编码(TTS 给的是 mp3/wav/opus,直接发大概率放不出来)。另外出站 voice 的 item type 只在入站验证过,需真机确认。
**关键点:人设模型不需要支持视觉。** 默认的 `caption` 模式先让一个识图端点把图片转成文字描述,只把这段**文字**交给人设模型——所以 `deepseek-v4-flash` 这类纯文本模型照样能"看图"。描述还会写进对话历史,隔几轮再问"刚那张图里的猫呢"仍然接得上。
| 模式 | 人设模型要求 | 说明 |
|------|--------------|------|
| `caption`(默认) | 无 | 识图端点出描述 → 文字进人设模型 |
| `direct` | **必须支持视觉** | 图片原样交给人设模型;不支持就直接报错 |
| 环境变量 | 默认 | 说明 |
|----------|------|------|
| `VISION_ENABLED` | `false` | 总闸。关闭时不下载、不调模型 |
| `VISION_MODE` | `caption` | 见上表 |
| `VISION_BASE_URL` / `VISION_API_KEY` | 空 | 识图端点。**env-only**(与平台 LLM 同信任级,直连不走 tools)。留空则复用 `LLM_BASE_URL` / `LLM_API_KEY` |
| `VISION_MODEL` | 空 | **必填**,留空则所有图片按「看不了」处理。对「我的模型连接」无效(那条链路只认连接里的模型名) |
| `VISION_CAPTION_MAX_TOKENS` | `300` | 描述长度上限 |
| `VISION_MAX_IMAGES` | `2` | 单条消息最多识别几张(每张都实打实花 token) |
| `INBOUND_MEDIA_MAX_BYTES` | `4194304` | 单个附件下载上限(解密后原始大小;base64 再涨约 1/3 |
`caption` 模式的成本:每张图多一次识图调用(记在机器人主人账上),人设模型那一轮只多几十个 token 的描述文字——比把 4MB base64 塞进上下文便宜得多。识图失败会降级成「看不了」,**不会让回复失败**。
行为说明:
- **只有图片会被下载**。语音/视频/文件的字节拿来也喂不进模型,所以根本不去 CDN 取——微信语音是 SILK/AMR,没有 OpenAI 兼容端点收。
- `caption` 模式下人设模型**收不到字节**,只收到方括号里的描述;提示词会要求它「当作亲眼所见,但只依据描述内容,不要往外扩写」。
- **语音靠微信自带转写**`VOICE_TRANSCRIPT_ENABLED`,**默认开**):转写文字随入站消息一起到,`extractText` 把它并入文本,这类语音当文字处理,不再额外列为附件(否则会告诉模型「你听不到」它正要读的内容)。这个开关与 `VISION_ENABLED` **互不相干**——用转写不花钱、不需要任何模型。设 `false` 后语音一律回「没听清,麻烦打字」。
- 模型看不到的附件仍会写进系统提示,并明确要求**不许猜测内容**——否则人设会张口就编视频里有什么。
- 整条消息只有看不了的媒体且没有文字时,直接回一句按类型区分的话(不调模型,不花钱)。
- 附件下载失败不会拖垮回复,降级成「只告知存在」。
- Chatflow 模式的人设看到的是 `[图片]` 占位符——图执行器没有多模态节点。
- 历史里存的也是 `[图片]` 占位符(字节不落库),所以下一轮追问「所以呢?」时上下文仍知道发过图。
### 2.1 智能体主动找用户(空闲触发)
默认**全局关闭**。开启后,空闲一段时间的用户可被角色主动联系。
1. `.env` 设置 `PROACTIVE_ENABLED=true` 并重启服务
2.`/app` 机器人卡片中打开 **「主动找用户聊天」** 并保存参数(空闲小时 / 间隔 / 每日上限 / 安静时段)
3. 在用户列表对该 peer 勾选 **「允许主动」**(仅已批准用户)
4. 对方须**曾经聊过**(系统存有 iLink `context_token`);从未发过消息的人无法冷启动主动触达
| 环境变量 | 默认 | 说明 |
|----------|------|------|
| `PROACTIVE_ENABLED` | `false` | 全局总闸 |
| `PROACTIVE_IDLE_HOURS` | `12` | 默认空闲阈值(小时) |
| `PROACTIVE_MIN_INTERVAL_HOURS` | `24` | 两次主动最小间隔 |
| `PROACTIVE_MAX_PER_DAY` | `1` | 每用户每日上限 |
| `PROACTIVE_QUIET_HOURS` | `0-8` | 安静时段(上海时区);空字符串关闭 |
| `PROACTIVE_SCAN_INTERVAL_SEC` | `300` | 扫描周期 |
| `PROACTIVE_MAX_PER_SCAN` | `10` | 每轮最多发送数 |
### 2.2 用户之间通过 @LINUX DO 用户名对话
机器人可中继两个已绑定用户的文字消息(**不经过 LLM**)。
1. 双方均用 LINUX DO 登录 `/app`
2. 用户中心 → **用户对话** → 生成绑定码 → 微信给任意机器人发 `/绑定 ABC123`
3. 双方至少各给机器人发过一次消息(写入 `context_token`,否则不可达)
4. A 发送整条消息 `@对方用户名` → B 收到请求 → `/同意`
5. 会话中直接发文字(前缀 `[用户名]`);`/断开` 结束;空闲约 30 分钟自动结束
| 命令 | 说明 |
|------|------|
| `/绑定 CODE` | 认领绑定码 |
| `/解绑` | 解除绑定 |
| `/我的身份` | 查看绑定与会话状态 |
| `@username` | 发起对话请求(整条消息) |
| `/同意` `/拒绝` | 处理入站请求 |
| `/取消请求` | 取消自己发出的请求 |
| `/断开` | 结束当前用户对话 |
| `/拉黑 用户名` | 拉黑(无法再互相 @ |
| `/取消拉黑 用户名` | 移出黑名单 |
| `/黑名单` | 查看黑名单 |
### 2.3 管理后台广播(全站 / 单 bot 推送)
管理员在 `/admin`**广播** 可向微信用户推送**纯文本**(系统更新、通知等)。
1. 登录管理后台 → 侧栏 **广播**
2. 撰写文本,选择范围:
- **全部机器人**:每个 bot 下全部有 `context_token` 的 peer(含未批准)
- **指定机器人**:多选 bot,再向其可触达 peer 群发
- **指定 Peer**:选 bot → 勾选 peers
3. **预估人数** → 确认后创建异步任务;列表可看进度 / 取消
4. 机器人详情页也可 **向此 bot 群发** / 对某 peer **发消息**(跳转并预填)
| 环境变量 | 默认 | 说明 |
|----------|------|------|
| `BROADCAST_INTERVAL_MS` | `200` | 两条消息间隔(限速,降低 iLink 风险) |
| `BROADCAST_MAX_TEXT` | `2000` | 文本最大长度 |
| `BROADCAST_HISTORY` | `100` | 保留的历史任务数 |
约束:
- 从未给 bot 发过消息的用户**无法送达**(无 context_token
- 需 Worker 开启(`WORKER_ENABLED` 默认 true);Worker 关则任务一直 `pending`
- 已发出的消息**无法撤回**;取消只停止剩余目标
- 不进 LLM / 人设记忆
| 环境变量 | 默认 | 说明 |
|----------|------|------|
| `P2P_ENABLED` | `true` | 总开关(`false` 关闭) |
| `P2P_BIND_CODE_TTL_SEC` | `600` | 绑定码有效期 |
| `P2P_REQUEST_TTL_SEC` | `300` | 对话请求有效期 |
| `P2P_SESSION_IDLE_SEC` | `1800` | 会话空闲超时 |
| `P2P_RELAY_MAX_CHARS` | `500` | 单条中继最大字数 |
| `P2P_MAX_REQUESTS_PER_DAY` | `20` | 每 peer 每日发起 `@` 次数 |
跨 Bot 可用:A 绑在 Bot1、B 绑在 Bot2,中继走目标方 bot 凭证 + 其 `context_token`
### 2.4 记忆检索与时间工具
| 环境变量 | 默认 | 说明 |
|----------|------|------|
| `MEMORY_TOP_K` | `12` | 记忆超过全量阈值时注入条数 |
| `MEMORY_FULL_INJECT_MAX` | `20` | ≤ 此数时仍全量注入(与旧行为一致) |
| `MEMORY_MAX_ITEMS` | `100` | 每个 peer+人设最多存储条数 |
| `TIME_TOOL_ENABLED` | `true` | 允许模型调用 `get_current_time` |
| `TIME_TOOL_TIMEZONE` | `Asia/Shanghai` | 默认时区 |
用户中心 → 微信用户行 → **记忆**:查看 / 删单条 / 清空。
日志关键字:`[proactive]``action=send|skip|lock_miss|no_ctx`);`[broadcast]`(管理广播任务)。
### 2.5 自定义模型与联网搜索(经 HF 工具服务)
主站**只**直连管理员配置的平台 LLM;用户自定义 API 与联网搜索一律经
`huggingface/wechat-ai-tools` 出站。
| 环境变量 | 说明 |
|----------|------|
| `TOOLS_BASE_URL` | 工具服务根地址(HF Space / 自托管容器) |
| `TOOLS_API_KEY` | 与工具服务共享的调用密钥 |
| `LLM_PROVIDER_SECRET` | 加密用户保存的自定义 API Key(必填,否则无法添加连接) |
| `WEB_SEARCH_ENABLED` | 全局搜索开关;人设还需自行开启 |
用户路径:`/app`**我的模型** 添加连接 → 人设编辑器里选择该连接 / 勾选联网搜索。
排查:
- `pnpm diag` 会探测 `TOOLS_BASE_URL/health`
- `/health/ready``WEB_SEARCH_ENABLED=true` 且 tools 不可达时返回 503(结果缓存 15s)
- 日志与审计**不记录** upstream api_key
### 2.6 Chatflow
人设可切 `chatflow` 模式,用 `/chatflow?persona=<id>` 编排流程图。
| 环境变量 | 默认 | 说明 |
|----------|------|------|
| `CHATFLOW_HTTP_ALLOWLIST` | 空 | http 节点额外允许的 hosttools host:port 始终允许);`*` = 任意公网,内网/云元数据仍拦 |
| `CHATFLOW_MAX_STEPS` | `32` | 单次执行最大步数 |
| `CHATFLOW_MAX_NODES` | `40` | 图最大节点数 |
要点:chatflow 人设**不参与主动联系**;试聊强制走平台模型。详见 `docs/chatflow.md`
## 3. 管理员
- 配置了 `LINUXDO_ADMIN_IDS`:名单内用户登录后为管理员
- 未配置:`FIRST_USER_IS_ADMIN` 默认 true,**首个登录用户**自动管理员
- 打开 `/admin`:今日 Token、用户数、机器人、审计、**广播**
## 4. Worker 与规模
API 与 iLink Worker **同进程**(单镜像 / 单容器)。
- 收消息:`getUpdates` 长轮询(每 bot 一路,有 `MAX_BOTS_PER_WORKER` 上限)
- 回消息:进程内 inbox 队列 + `REPLY_CONCURRENCY` 并发,避免 LLM 堵住轮询
- 日志出现 `at capacity`:提高 `MAX_BOTS_PER_WORKER`,或同镜像多副本分片
## 5. 故障
| 现象 | 处理 |
|------|------|
| Redis Connection closed / 占位符 | 填真实 Upstash `rediss://` URL |
| OAuth redirect_uri mismatch | 控制台回调与 `.env` 一致 |
| OAuth userinfo 失败 | 确认 scope `openid profile`(已默认) |
| 无管理员 | 清空 Redis 用户或设置 `LINUXDO_ADMIN_IDS` 后重登 |
| 微信 session expired | 在用户中心删除机器人后重新扫码 |
| 无 AI 回复 | 检查 LLM_API_KEY、用户是否批准 |
| `at capacity` / 部分 bot 不 poll | 调高 `MAX_BOTS_PER_WORKER` |
| 自定义模型报 TOOLS_BASE_URL required | 部署工具服务并配置 `TOOLS_BASE_URL` / `TOOLS_API_KEY` |
| 添加模型连接 503 | 未设置 `LLM_PROVIDER_SECRET` |
| chatflow `http_blocked` | 目标域名不在 tools host / `CHATFLOW_HTTP_ALLOWLIST`;或命中内网段(报文里带 `private host blocked` 与原因);或重定向跳进内网 |
| chatflow http 节点全部 `resolves to ...` 被拦 | 本机 DNS 在劫持解析(把所有域名答成 CGNAT/`198.18` 之类占位地址)。先 `nslookup` 确认,再排查 resolver,不要直接放宽白名单 |
| chatflow `search_disabled` | 人设未开搜索,或 `WEB_SEARCH_ENABLED` / TOOLS 未配 |
| `/health/ready` 503 但 Redis 正常 | tools 网关不可达(见 §2.5 |
## 6. 备份
- Upstash:控制台备份 / 导出(按套餐)
- Redis `wa:bot:{id}:creds`Bot token,敏感)
- `.env`(勿提交 Git
## 7. 文档
- Upstash`docs/upstash-redis.md`
- OAuth`docs/oauth-linuxdo.md`
- API`docs/admin-api.md`
- AI 网关(主站↔HF 契约):`docs/ai-gateway.md`
- Chatflow`docs/chatflow.md`