channel-plugin-design.md 23 KB

OpenClaw WeChat-Agent Channel Plugin - MVP 技术设计

版本:draft-0.1(方案阶段,未开工) 作者:Cascade + @fmode 依据文档:


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

{
  "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」的表单字段:

{
  "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(插件入口)

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 主体)

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<ResolvedAccount>({
  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 体积小:

export interface WechatAgentClient {
  checkOnline(): Promise<boolean>;
  getMessages(opts: { limit: number; direction: "received" }): Promise<RawMessage[]>;
  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(轮询 + 分发)

let timer: NodeJS.Timeout | undefined;
let lastCheckTime = "";
const recentMsgIds = new Set<string>();  // 去重窗口,最近 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-dispatchopenclaw/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

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 linkopenclaw 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.tssrc/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 交付时给客户一个「停旧启新」脚本:

    # 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:

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。