版本:draft-0.1(方案阶段,未开工) 作者:Cascade + @fmode 依据文档:
- OpenClaw Channel Plugin SDK
- OpenClaw Plugins
- Chat Channels
- 客户微信后端:
http://8.138.37.248/api/wechat-agent(xbot/wcf 类 PC hook)
将客户现有的微信 HTTP 后端(wechat-agent)包装成一个 OpenClaw 原生 Channel Plugin,让:
outbound.sendText 钩子调用 wechat-agent 的 /message/send-text 发出openclaw plugins install ./xxx.tgz 一条命令装好,在 OpenClaw UI 的「通信 → Channels」里配置 wechatApiBase 即可上线MVP 覆盖:
/messages) ┌─────────────────────────────────────────────┐
│ 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 |
独立新仓库 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
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.3.22"
}
}
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"]
}
}
}
}
}
}
}
}
}
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());
},
});
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),
},
},
});
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 定义。
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-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")。
src/outbound.tsimport { 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,自造一个
}
src/session.ts(会话映射)微信消息的 fromWxid 对应 OpenClaw 的 conversation id。MVP 用最简单映射:
wxid_abc123) -> OpenClaw conversation wechat-agent:wxid_abc123xxx@chatroom) -> MVP 不处理,直接过滤掉v1.1+ 需要实现 SDK 的 messaging.resolveSessionConversation 处理群聊 + 回复 threading。
src/security.ts包住 SDK 的 security.dm 策略:
open:任何 wxid 都能触发 agentallowlist:只有 allowFrom 数组里的 wxid 能触发(其他人消息静默丢弃或可选发提示)MVP 不做 pairing。
每 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 核心处理(如果需要)。
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 处理(重试 / 回填失败状态)
| 场景 | 行为 |
|---|---|
| dmSecurity=open(默认) | 所有 wxid 都能触发 agent |
| dmSecurity=allowlist,wxid 在 allowFrom | 正常处理 |
| dmSecurity=allowlist,wxid 不在 allowFrom | 丢弃,log 级别 debug |
群消息(@chatroom 结尾) |
MVP 阶段丢弃,log 级别 info |
公众号(gh_ 前缀) |
永远过滤(ignoreWxidPrefixes) |
| 系统联系人(weixin/fmessage/...) | 永远过滤(ignoreWxidExact) |
api.logger:OpenClaw 统一日志,客户在 OpenClaw UI 「基础设施 → 日志」看channel.wechat-agent.inbound.received.totalchannel.wechat-agent.inbound.skipped.total{reason}channel.wechat-agent.outbound.sent.totalchannel.wechat-agent.outbound.failed.totalclient.test.ts:mock http.request,覆盖 checkOnline/getMessages/sendText 3 个接口的成功/失败/网络异常channel.test.ts:
inbound.test.ts:
npm link 或 openclaw plugins install -l ./ 装本地插件8.138.37.248 后端(或他们给一个测试实例)apiBase,发测试消息,看 agent 回复src/client.ts + 单测npm pack 产出 tgzsrc/config.ts、src/channel.ts 完成openclaw.plugin.json config schemaindex.ts + setup-entry.ts 注册src/outbound.ts + 集成到 plugin.outbound.sendTextnpm pack 出 tgzdist/ 目录作为 v1.3.0 一部分MVP 总工期预估:8-12 个工作日(含 buffer,关键风险在 M2)
| # | 问题 | 解决路径 |
|---|---|---|
| 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 版本升级时 break。我们 peerDeps 锁最低版本并在每次官方大版本后回归测试wechat-agent 是第三方,接口契约可能变。把客户端解析做成宽松模式(未知字段忽略),降低冲击@fmode/openclaw-wechat-agent-channel-0.1.0.tgz(~100KB,esbuild 单文件)README.md:安装 3 步、config 字段说明、故障排查开发期间:
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
| 项目 | 选项 | 默认 |
|---|---|---|
| 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。