# OpenClaw WeChat-Agent Channel Plugin - MVP 技术设计 > 版本:draft-0.1(方案阶段,未开工) > 作者:Cascade + @fmode > 依据文档: > - [OpenClaw Channel Plugin SDK](https://docs.openclaw.ai/plugins/sdk-channel-plugins) > - [OpenClaw Plugins](https://docs.openclaw.ai/tools/plugin) > - [Chat Channels](https://docs.openclaw.ai/channels) > - 客户微信后端:`http://8.138.37.248/api/wechat-agent`(xbot/wcf 类 PC hook) --- ## 1. 目标与范围 将客户现有的微信 HTTP 后端(`wechat-agent`)包装成一个 OpenClaw **原生 Channel Plugin**,让: 1. 微信收到的消息直接进入 OpenClaw agent 的对话流 2. Agent 用 LLM + system prompt + skills + memory 生成回复 3. 回复通过 plugin 的 `outbound.sendText` 钩子调用 `wechat-agent` 的 `/message/send-text` 发出 4. 客户通过 `openclaw plugins install ./xxx.tgz` 一条命令装好,在 OpenClaw UI 的「通信 → Channels」里配置 `wechatApiBase` 即可上线 **MVP 覆盖**: - 私聊(1-to-1)接收 + 回复 - 文本消息 - 轮询式 inbound(每 10 秒拉一次 `/messages`) - 单账户(一台服务器一个微信号) - DM 安全策略(allowlist / open 可配) - Tarball 交付(不走公开 npm) --- ## 2. 非目标(MVP 不做) - 群聊回复(v1.1+,需要 mention gating 逻辑) - 图片/语音/视频/文件的 inbound 解析和 outbound 发送 - Pairing 流程(Allowlist 陌生人自动配对) - Approval capability(审批流,不适用微信) - 多账户(一个插件实例挂多个微信号) - Webhook 推送模式(客户后端是否支持 callback 推送待确认) - @tencent-weixin/openclaw-weixin 并存兼容(假设客户只装我们这一个微信 channel) --- ## 3. 架构数据流 ``` ┌─────────────────────────────────────────────┐ │ OpenClaw Gateway (127.0.0.1:18789) │ │ │ [微信 App] │ ┌──────────────────────────────────────┐ │ │ │ │ wechat-agent-channel plugin │ │ │ A. 轮询拉消息 │ │ ┌────────────────────────────────┐ │ │ ▼ │ │ │ Inbound Poller (setInterval) │ │ │ [客户后端]───GET ────┼──┼──┤ GET /messages?direction=received│ │ │ 8.138.37.248 │ │ │ 过滤 + dispatchInbound() │ │ │ /api/wechat-agent │ │ └────────────┬───────────────────┘ │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ ┌────────────────────────────────┐ │ │ │ │ │ Core Inbound Envelope │ │ │ │ │ │ -> Agent Session (LLM + skills)│ │ │ │ │ └────────────┬───────────────────┘ │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ ┌────────────────────────────────┐ │ │ [客户后端] ◄─POST ──┼──┼──┤ outbound.sendText │ │ │ /message/send-text │ │ │ POST /message/send-text │ │ │ │ │ │ └────────────────────────────────┘ │ │ │ B. 发出回复 │ └──────────────────────────────────────┘ │ ▼ │ │ [微信 App] └─────────────────────────────────────────────┘ ``` 相比当前 v1.2.2 的区别: | 组件 | v1.2.2 | Channel Plugin | |---|---|---| | 主进程 | 独立 `auto-reply-daemon.js`(nohup) | OpenClaw Gateway 进程内 | | 管理技能 | `wechat-auto-reply-start/stop/status` | 由 OpenClaw CLI/UI 原生管 | | 回复内容 | 静态 FAQ + defaultReply | Agent + LLM + memory | | 配置 | `~/.openclaw/wechat-auto-reply-config.json` | OpenClaw config 的 `plugins.entries.wechat-agent.config` | | 升级 | 重新解压 zip + quick-start.sh | `openclaw plugins install new.tgz --force` | --- ## 4. 项目结构 独立新仓库 `openclaw-wechat-agent-channel/`(不放在当前 `openclaw-wechat-skill` 里): ``` openclaw-wechat-agent-channel/ ├── package.json # scope @fmode,openclaw.channel 元数据 ├── openclaw.plugin.json # config schema JSON ├── tsconfig.json # ES2022, module=nodenext ├── esbuild.config.mjs # 打包成单文件 dist/index.mjs ├── README.md ├── LICENSE # MIT 或客户要求的 ├── index.ts # defineChannelPluginEntry 入口 ├── setup-entry.ts # defineSetupPluginEntry 入口 ├── src/ │ ├── channel.ts # ChannelPlugin 主体(createChatChannelPlugin) │ ├── config.ts # resolveAccount / inspectAccount │ ├── client.ts # wechat-agent HTTP 客户端 │ ├── inbound.ts # 轮询器 + dispatchInbound 逻辑 │ ├── outbound.ts # sendText 实现 │ ├── security.ts # DM 策略解析 │ ├── session.ts # wxid <-> conversation 映射 │ ├── types.ts # 共享类型(ResolvedAccount、RawMessage 等) │ └── logger.ts # 日志工具 └── tests/ ├── channel.test.ts # vitest ├── client.test.ts └── inbound.test.ts ``` --- ## 5. 模块职责 ### 5.1 `package.json` ```json { "name": "@fmode/openclaw-wechat-agent-channel", "version": "0.1.0", "type": "module", "main": "./dist/index.mjs", "files": ["dist", "openclaw.plugin.json", "README.md", "LICENSE"], "scripts": { "build": "node esbuild.config.mjs", "test": "vitest run", "pack": "npm run build && npm pack" }, "openclaw": { "extensions": ["./dist/index.mjs"], "setupEntry": "./dist/setup-entry.mjs", "channel": { "id": "wechat-agent", "label": "WeChat (Agent Backend)", "blurb": "Connect OpenClaw to a third-party WeChat HTTP bot (wcf/xbot)." } }, "peerDependencies": { "openclaw": "2026.4.15" } } ``` ### 5.2 `openclaw.plugin.json`(config schema) 定义 OpenClaw UI 里「Channels → WeChat Agent」的表单字段: ```json { "id": "wechat-agent", "config": { "schema": { "type": "object", "properties": { "channels": { "type": "object", "properties": { "wechat-agent": { "type": "object", "required": ["apiBase"], "properties": { "apiBase": { "type": "string", "title": "WeChat Agent API Base", "description": "后端根地址,如 http://8.138.37.248/api/wechat-agent", "default": "http://127.0.0.1:8138" }, "pollIntervalMs": { "type": "integer", "title": "轮询间隔(毫秒)", "default": 10000, "minimum": 3000 }, "dmSecurity": { "type": "string", "enum": ["open", "allowlist"], "default": "open", "title": "私信安全策略" }, "allowFrom": { "type": "array", "items": { "type": "string" }, "title": "白名单 wxid(dmSecurity=allowlist 时生效)", "default": [] }, "ignoreWxidPrefixes": { "type": "array", "items": { "type": "string" }, "default": ["gh_"] }, "ignoreWxidExact": { "type": "array", "items": { "type": "string" }, "default": ["weixin", "fmessage", "medianote", "filehelper"] }, "ignoreTypes": { "type": "array", "items": { "type": "string" }, "default": ["system", "emoji", "voice", "image", "video", "location"] } } } } } } } } } ``` ### 5.3 `index.ts`(插件入口) ```ts import { defineChannelPluginEntry } from "openclaw/plugin-sdk/channel-core"; import { wechatAgentPlugin } from "./src/channel.js"; import { startInboundPoller, stopInboundPoller } from "./src/inbound.js"; export default defineChannelPluginEntry({ id: "wechat-agent", name: "WeChat (Agent Backend)", description: "Bridge a third-party WeChat HTTP bot into OpenClaw.", plugin: wechatAgentPlugin, registerFull(api) { // 启动轮询器(单账户 MVP) api.onReady?.(() => startInboundPoller(api)); api.onShutdown?.(() => stopInboundPoller()); }, }); ``` ### 5.4 `src/channel.ts`(ChannelPlugin 主体) ```ts import { createChatChannelPlugin, createChannelPluginBase, } from "openclaw/plugin-sdk/channel-core"; import { resolveAccount, inspectAccount } from "./config.js"; import { sendText } from "./outbound.js"; import type { ResolvedAccount } from "./types.js"; export const wechatAgentPlugin = createChatChannelPlugin({ base: createChannelPluginBase({ id: "wechat-agent", setup: { resolveAccount, inspectAccount }, }), security: { dm: { channelKey: "wechat-agent", resolvePolicy: (account) => account.dmPolicy, resolveAllowFrom: (account) => account.allowFrom, defaultPolicy: "open", // 私聊默认开放,客户可改 allowlist }, }, threading: { topLevelReplyToMode: "reply" }, outbound: { attachedResults: { sendText: async (params) => sendText(params), }, }, }); ``` ### 5.5 `src/client.ts`(wechat-agent HTTP 客户端) 纯 Node.js `http`/`https` 封装,无 npm 依赖,保证 tgz 体积小: ```ts export interface WechatAgentClient { checkOnline(): Promise; getMessages(opts: { limit: number; direction: "received" }): Promise; sendText(opts: { toWxid: string; content: string; ats?: string }): Promise<{ ret: number }>; } export function createClient(apiBase: string): WechatAgentClient { ... } ``` 对应客户后端的 3 个接口: - `POST /login/check-online` -> `{ ret: 200, data: { online: true } }` - `GET /messages?limit=N&direction=received` -> `{ ret: 200, data: [...] }` - `POST /message/send-text` -> `{ ret: 200 }` 接口契约参考当前 `@e:\workspace\openclaw-wechat-skill\wechat\wechat-get-messages\SKILL.md` 等 skill 定义。 ### 5.6 `src/inbound.ts`(轮询 + 分发) ```ts let timer: NodeJS.Timeout | undefined; let lastCheckTime = ""; const recentMsgIds = new Set(); // 去重窗口,最近 1000 条 export function startInboundPoller(api: PluginApi) { const cfg = api.config.channels["wechat-agent"]; const client = createClient(cfg.apiBase); const intervalMs = cfg.pollIntervalMs ?? 10000; async function tick() { try { if (!(await client.checkOnline())) return; const messages = await client.getMessages({ limit: 50, direction: "received" }); for (const msg of messages) { if (shouldIgnore(msg, cfg)) continue; if (recentMsgIds.has(msg.msgId)) continue; recentMsgIds.add(msg.msgId); await dispatchToAgent(api, msg); if (msg.timestamp > lastCheckTime) lastCheckTime = msg.timestamp; } trimDedupeWindow(); } catch (err) { api.logger.warn("inbound poll error", err); } } timer = setInterval(tick, intervalMs); void tick(); // 立即跑一次 } export function stopInboundPoller() { if (timer) clearInterval(timer); timer = undefined; } ``` `dispatchToAgent()` 是最关键也是最不确定的部分(见 §14)——调用 `openclaw/plugin-sdk/inbound-reply-dispatch` 或 `openclaw/plugin-sdk/inbound-envelope` 将 RawMessage 包装成 OpenClaw 的 inbound envelope,推给核心。**精确 API 形态需要去 `openclaw/openclaw` 仓库的 Microsoft Teams 或 Google Chat bundled plugin 看实例**(SDK 文档原话:"see a real example in the bundled Microsoft Teams or Google Chat plugin package")。 ### 5.7 `src/outbound.ts` ```ts import { createClient } from "./client.js"; export async function sendText(params: { to: string; // wxid text: string; account: ResolvedAccount; }): Promise<{ messageId: string }> { const client = createClient(params.account.apiBase); const resp = await client.sendText({ toWxid: params.to, content: params.text, ats: "" }); if (resp.ret !== 200) throw new Error(`send-text failed ret=${resp.ret}`); return { messageId: `wx-${Date.now()}` }; // 客户后端未返回 msgId,自造一个 } ``` ### 5.8 `src/session.ts`(会话映射) 微信消息的 `fromWxid` 对应 OpenClaw 的 conversation id。MVP 用最简单映射: - 个人聊天 wxid(如 `wxid_abc123`) -> OpenClaw conversation `wechat-agent:wxid_abc123` - 群聊 wxid(`xxx@chatroom`) -> MVP 不处理,直接过滤掉 v1.1+ 需要实现 SDK 的 `messaging.resolveSessionConversation` 处理群聊 + 回复 threading。 ### 5.9 `src/security.ts` 包住 SDK 的 `security.dm` 策略: - `open`:任何 wxid 都能触发 agent - `allowlist`:只有 `allowFrom` 数组里的 wxid 能触发(其他人消息静默丢弃或可选发提示) MVP 不做 pairing。 --- ## 6. Inbound 详细流程 ``` 每 10 秒 tick: 1. checkOnline() 失败 -> 本轮跳过 2. getMessages(limit=50, direction=received) 拿到 N 条 3. 对每条 msg: a. shouldIgnore(msg, cfg): - msg.type in ignoreTypes -> skip - msg.fromWxid 命中 ignoreWxidPrefixes / ignoreWxidExact -> skip - msg.content 为空 -> skip b. 去重:msg.msgId 已见 -> skip c. security check(DM policy): - policy=allowlist 且 fromWxid 不在 allowFrom -> skip + log d. dispatchToAgent(msg): - 构造 inbound envelope: { channelId: "wechat-agent", conversationId: session.fromWxid(msg), senderId: msg.fromWxid, senderName: msg.nickName, text: msg.content, timestamp: msg.timestamp, rawMessageId: msg.msgId, } - 调 api.messaging.deliverInbound(envelope) 或等效 SDK 函数 (精确 API 需看 bundled plugin 实例) 4. 更新 lastCheckTime + trim 去重窗口 ``` Cooldown 不在 channel 层做,交给 agent / OpenClaw 核心处理(如果需要)。 --- ## 7. Outbound 详细流程 Agent 决定回复后,OpenClaw 核心调用 `plugin.outbound.attachedResults.sendText({ to, text, account })`: ``` sendText({ to: "wxid_abc123", text: "您好~", account: { apiBase, ... } }): 1. POST {apiBase}/message/send-text body: { toWxid: "wxid_abc123", content: "您好~", ats: "" } 2. 解析响应: - ret=200 -> return { messageId: synthetic-id } - ret!=200 -> throw Error(加入 body.msg 作诊断) 3. 所有异常由 core 处理(重试 / 回填失败状态) ``` --- ## 8. DM Security / 身份策略 | 场景 | 行为 | |---|---| | dmSecurity=open(默认) | 所有 wxid 都能触发 agent | | dmSecurity=allowlist,wxid 在 allowFrom | 正常处理 | | dmSecurity=allowlist,wxid 不在 allowFrom | 丢弃,log 级别 debug | | 群消息(`@chatroom` 结尾) | MVP 阶段丢弃,log 级别 info | | 公众号(`gh_` 前缀) | 永远过滤(ignoreWxidPrefixes) | | 系统联系人(weixin/fmessage/...) | 永远过滤(ignoreWxidExact) | --- ## 9. 错误处理与可观测性 - **所有 inbound handler 外层 try/catch**:插件 bug 绝不冒泡崩 OpenClaw Gateway - **checkOnline 失败连续 N 次**:log WARN 并放缓轮询到 60s,恢复后回到正常间隔 - **sendText 失败**:throw 给 core,core 自己记失败 + 可选重试 - **日志走 `api.logger`**:OpenClaw 统一日志,客户在 OpenClaw UI 「基础设施 → 日志」看 - **关键指标**(后续可加): - `channel.wechat-agent.inbound.received.total` - `channel.wechat-agent.inbound.skipped.total{reason}` - `channel.wechat-agent.outbound.sent.total` - `channel.wechat-agent.outbound.failed.total` --- ## 10. 测试计划 ### 单元测试(vitest,无外部依赖) - `client.test.ts`:mock http.request,覆盖 checkOnline/getMessages/sendText 3 个接口的成功/失败/网络异常 - `channel.test.ts`: - resolveAccount 能从 cfg.channels["wechat-agent"] 读出 apiBase - inspectAccount 正确返回 enabled/configured/tokenStatus - dmPolicy=allowlist 时 allowFrom 生效 - `inbound.test.ts`: - shouldIgnore 对 system/emoji/公众号/系统联系人的过滤 - 去重窗口:同 msgId 不会 dispatch 两次 ### 集成测试(本地 OpenClaw dev 实例) 1. 本地开发机装一个 OpenClaw dev gateway 2. 用 `npm link` 或 `openclaw plugins install -l ./` 装本地插件 3. Mock wechat-agent 后端(写个 200 行 express 模拟接口) 4. 脚本发"模拟收到消息" -> 验证 agent 被唤起 -> agent 回复 -> mock 后端收到 send-text 请求 ### 真机验证 - 用客户的实际 `8.138.37.248` 后端(或他们给一个测试实例) - 装 tgz 到客户服务器,配 `apiBase`,发测试消息,看 agent 回复 --- ## 11. 开发计划(分阶段) ### 阶段 M0:项目骨架 + 客户端(1 天) - [ ] 创建仓库,tsconfig + esbuild + vitest 跑通 - [ ] `src/client.ts` + 单测 - [ ] 能 `npm pack` 产出 tgz ### 阶段 M1:最小可跑插件(2-3 天) - [ ] `src/config.ts`、`src/channel.ts` 完成 - [ ] `openclaw.plugin.json` config schema - [ ] `index.ts` + `setup-entry.ts` 注册 - [ ] 本地 OpenClaw 装上能在 UI 的「Channels」里看到条目 - [ ] 填 apiBase 后 inspectAccount 返回 configured=true ### 阶段 M2:Inbound 打通(3-5 天,**关键路径**) - [ ] 找到 OpenClaw SDK 的 inbound dispatch 确切 API(看 MS Teams / Google Chat 源码) - [ ] 轮询器 + dispatchToAgent 能把消息推进 agent 对话流 - [ ] 验证:在 OpenClaw WebChat 里能看到"收到了一条来自 wxid_xxx 的消息" ### 阶段 M3:Outbound 打通(1-2 天) - [ ] `src/outbound.ts` + 集成到 plugin.outbound.sendText - [ ] 验证:agent 回复后,mock 后端收到 /message/send-text 请求 - [ ] 真实后端联调 ### 阶段 M4:MVP 打包交付(1 天) - [ ] 修完联调 bug - [ ] README + 安装说明 - [ ] esbuild 产单文件,`npm pack` 出 tgz - [ ] 放入现有项目 `dist/` 目录作为 v1.3.0 一部分 **MVP 总工期预估:8-12 个工作日**(含 buffer,关键风险在 M2) --- ## 12. 风险与未决项 ### 未决技术点(需要在开工前/开工初期搞清楚) | # | 问题 | 解决路径 | |---|---|---| | R1 | Inbound dispatch 的精确 SDK API 是什么 | 扒 `github.com/openclaw/openclaw` 的 bundled plugins 源码(MS Teams / Google Chat / Feishu),找到调哪个函数 | | R2 | `registerFull(api)` 里 `api.onReady` / `api.onShutdown` 是否真存在 | 读 SDK 的 `defineChannelPluginEntry` 类型定义 | | R3 | 客户后端是否支持 webhook callback 推送 | 问客户,如支持可选用 push 模式替代轮询(更省资源) | | R4 | `messaging.resolveSessionConversation` 对私聊是否必须实现 | 看 BlueBubbles / Telegram 等 bundled plugin,极简情况能否省 | | R5 | OpenClaw Gateway 进程崩溃后 plugin 是否自动恢复 | 查文档 + 实测 | | R6 | 多账户:客户未来想挂第二个微信号如何组织 config | v1.1 再考虑,MVP 先单账户 | ### 非技术风险 - **OpenClaw SDK 不稳定**:处于早期阶段,API 可能在 `openclaw` 版本升级时 break。我们 peerDeps 锁最低版本并在每次官方大版本后回归测试 - **客户后端 API 变更**:`wechat-agent` 是第三方,接口契约可能变。把客户端解析做成宽松模式(未知字段忽略),降低冲击 - **冷启动时延**:插件首次启动 + 首次 tick 可能 2-5 秒。客户如果发消息立刻要回复,会感觉"慢了一下"——这是 OpenClaw gateway + LLM 首次唤起开销,无关插件实现 --- ## 13. 交付物 & 版本里程碑 ### MVP 交付(v0.1.0) - `@fmode/openclaw-wechat-agent-channel-0.1.0.tgz`(~100KB,esbuild 单文件) - `README.md`:安装 3 步、config 字段说明、故障排查 - 客户升级指南:从 v1.2.2 daemon 迁移到 channel 的步骤 ### v0.2.0 可能包含 - 群聊支持(需要 mention gating) - 图片 inbound 解析(显示给 agent "看到了一张图") - Webhook push 模式(如果客户后端支持) ### v1.0.0(生产就绪) - 多账户 - Pairing 流程 - 媒体 inbound/outbound(图/音/视/文件) - Approval capability(如果客户需要"agent 发重要消息前先请人审批") --- ## 14. 与当前 v1.2.2 并存策略 开发期间: - **客户继续用 v1.2.2 daemon**,不中断 - 我们在本地 + 一个灰度环境开发 + 测试 channel plugin - v0.1.0 交付时给客户一个「停旧启新」脚本: ```bash # 1. 停 v1.2.2 daemon kill $(cat ~/.openclaw/wechat-auto-reply.pid) 2>/dev/null rm -f ~/.openclaw/auto-reply-daemon.js ~/.openclaw/wechat-auto-reply-*.json # 2. 装 channel plugin openclaw plugins install ./fmode-openclaw-wechat-agent-channel-0.1.0.tgz openclaw gateway restart # 3. UI 填 apiBase -> save -> enable ``` **回滚方案**:如果 channel plugin 出问题,客户一条命令回到 v1.2.2: ```bash openclaw plugins disable wechat-agent bash /path/to/v1.2.2/quick-start.sh ``` --- ## 15. 决策点(等用户拍板) | 项目 | 选项 | 默认 | |---|---|---| | NPM scope | `@fmode` / `@your-company` / 其他 | 待定 | | 开源与否 | 开源到 GitHub / 私有 tarball | 私有 tarball | | License | MIT / Apache / 商业 | MIT(如开源) | | M2 调研策略 | 先读 bundled plugin 源码 / 直接问 OpenClaw maintainer | 先读源码 | | 是否做 webhook push 模式 | MVP 轮询 / MVP 就做 push | 轮询,v0.2+ 加 push | --- **下一步**:本文档确认通过后,我开新仓库 `openclaw-wechat-agent-channel`,按 §11 的阶段计划推进。第一周出 M1(骨架)给你看,确认方向对了再深入 M2。