@huo15/wechat-service
v2.2.2
Published
OpenClaw 微信服务号(公众号)插件 v2.2.2:注册 ClawHub plugin tag 让 `openclaw plugins install @huo15/wechat-service` 不带版本号也能装;首次显式声明 openclaw.compat.pluginApi=>=2026.3.23(之前历史版本未设)。继承 v2.2.1 角色权限 + AI 对话护栏 + 自动回复。消息收发、菜单管理、模板/订阅消息、客服消息、素材草稿与图文发布、二维码/标签/JS-SDK/数据分析,支持多账号
Readme
@huo15/wechat-service
📖 正文内容
OpenClaw 微信服务号(公众号)渠道插件:把微信公众号变成你的 AI 协作入口。 一粉一会话隔离 · 12 个 agent tool · 覆盖 60+ 个微信公众平台官方 API · 多账号矩阵路由 · 知识库双写
@huo15/wechat-service v2.1.2
└─ 12 个 agent tool / 60+ 个 WeChat MP API / 架构按 @huo15/wecom 同构✨ 能力总览
| 维度 | 说明 |
|------|------|
| 🚀 一粉一会话 | 动态 Agent 派生:每个 openid 自动一个独立 agent;管理员名单可旁路 |
| 📨 消息全栈 | 客服消息(10 类)/ 模板消息(CRUD + 公模板库)/ 一次性 + 长期订阅通知 |
| 📰 内容发布 | 素材管理 + 草稿箱 + freepublish 流水线 + 群发(按标签/openid,预览,撤回) |
| 🔐 网页授权 | OAuth2.0 全流程(snsapi_base / snsapi_userinfo) + JS-SDK 签名 |
| 📊 数据分析 | datacube 17 项指标(用户增减、图文阅读分享、消息分析、接口调用) |
| 🤖 智能开放 | OCR 7 类(身份证/银行卡/驾驶证/行驶证/营业执照/车牌/通用) + 图像处理 3 项 |
| 🎫 卡券精简 | create / get / batchget / delete / consume / decrypt encrypt_code |
| 🧠 多账号矩阵 | accounts.<id> 隔离 webhook 路径、access_token、agent 路由 |
| 🛡️ 权限控制(v2.1.0+) | permissionMode=admin-only:写操作仅 main agent / adminUsers |
| 💾 知识库双写 | 本地 markdown(Karpathy 风格)+ Odoo knowledge.article 同步 |
📦 安装
# 通过 OpenClaw 安装(推荐)
/install @huo15/wechat-service
# 或直接 npm
npm install @huo15/wechat-service🚀 初始化
方式 A — 交互式向导(推荐)
/setup wechat-service向导收集:AppID / AppSecret / 服务器 Token / EncodingAESKey / 加密模式 / 原始 ID / 账号名。完成后会回显 webhook URL 和公众号后台需要填的字段。
方式 B — CLI 非交互式(v2.0.0+,适合 CI / Docker)
export WECHAT_SERVICE_APP_ID="wx1234567890"
export WECHAT_SERVICE_APP_SECRET="xxx"
export WECHAT_SERVICE_ENCODING_AES_KEY="x_x_43_chars_x"
openclaw channels add --channel wechat-service --name "我的公众号" --token "MY_TOKEN"非 default 账号用 WECHAT_SERVICE_<UPPER_ACCOUNTID>_APP_ID 这种带 accountId 前缀的 env:
export WECHAT_SERVICE_SHOP_A_APP_ID="wx_shop_a"
openclaw channels add --channel wechat-service --account shop-a🔌 Webhook URL
主路径:
https://你的域名/plugins/wechat-service/{accountId}兼容路径:/wechat-service/{accountId}。填到「微信公众平台 → 基本配置 / 开发者中心 → 服务器配置 → URL」,Token / EncodingAESKey 与配置保持一致即可。保存时公众平台发的 GET echostr 校验,插件自动响应(明文 + 安全模式都支持)。
⚙️ 配置 Schema
channels:
wechat-service:
enabled: true
defaultAccount: default
# ① 多账号矩阵
accounts:
default:
enabled: true
name: 我的公众号
appId: wx1234567890abcdef
appSecret: ${WECHAT_SERVICE_APP_SECRET}
token: ${WECHAT_SERVICE_TOKEN}
encodingAESKey: ${WECHAT_SERVICE_AES_KEY}
encryptMode: safe # plain | compatible | safe
originalId: gh_xxxxxxxxxx # 可选
replyMode: async # async(默认)/ passive
replyPlaceholderText: "收到,正在为你处理..." # async 模式占位
welcomeText: 欢迎关注!
# ② 静态事件路由
routing:
defaultAgent: wechat-agent
events:
subscribe: onboarding-agent
CLICK: menu-agent
TEMPLATESENDJOBFINISH: webhook-agent
# ③ 知识库双写
knowledgeSync:
enabled: true
localPath: ~/knowledge/huo15
odoo:
url: https://huo15.com
db: huo15
username: [email protected]
password: ${ODOO_PASSWORD}
articleParentId: 123 # 可选
# ④ 🆕 v0.2.0 动态 Agent 派生(与 @huo15/wecom 同构)
dynamicAgents:
enabled: true
dmCreateAgent: true # 每个 openid 一个 agent
groupEnabled: false # 公众号无群聊;保留是为了与 wecom schema 对齐
adminUsers: # 管理员 openid:旁路动态路由 + admin-only 模式可执行写操作
- oABC123xyz
# 🆕 v2.1.0 权限控制
permissionMode: admin-only # "open"(默认)/ "admin-only"
network:
egressProxyUrl: ''
timeoutMs: 15000Agent ID 命名(动态 Agent):wechat-service-{accountId}-dm-{sanitized_openid},例:wechat-service-default-dm-oabc123xyz。
🔗 顶层 bindings(必须配!)
⚠️ 不配 binding,agent 跑完后回复消息会被静默丢弃(OpenClaw 找不到 channel→agent 的反向路由)。这是最常踩的坑。
bindings 在 OpenClaw 顶层(不在 channels.wechat-service 里),把 channel + accountId 映射到 agent:
// ~/.openclaw/openclaw.json 顶层
{
"bindings": [
{
"agentId": "main",
"match": { "channel": "wechat-service", "accountId": "default" }
}
]
}多账号 / 多渠道场景:每个 <channel>:<accountId> 都要单独一条 binding:
"bindings": [
{ "agentId": "main", "match": { "channel": "wecom", "accountId": "default" } },
{ "agentId": "main", "match": { "channel": "wechat-service", "accountId": "default" } },
{ "agentId": "shopa-agent", "match": { "channel": "wechat-service", "accountId": "shop-a" } },
{ "agentId": "support-agent", "match": { "channel": "wechat-service", "accountId": "support" } }
]agentId: "main" 是 OpenClaw 默认 agent,不需要在 agents.list 显式注册。其他自定义 agent 要先注册:
"agents": {
"list": [
{ "id": "main" },
{ "id": "shopa-agent" },
{ "id": "support-agent" }
]
}📨 回复模式详解(replyMode + replyPlaceholderText)
公众号 webhook 协议要求 5 秒内返回响应,但 LLM 通常要 5–30 秒才能产出回复。Plugin 提供两种模式应对:
模式 A:async(默认,推荐)
accounts:
default:
replyMode: async # 默认值
replyPlaceholderText: "收到,正在为你处理..." # 默认值行为:
- webhook 收到消息 → 立即 返回被动回复 XML 含 placeholder 文本 → 粉丝立刻看到 "收到,正在为你处理..."
- 后台异步跑 agent → agent 产出回复 → 通过客服消息接口(
customservice/send)push 第二条给粉丝 - 粉丝在微信里看到两条消息:先是 placeholder(瞬间到达),然后是 agent 真回复(几秒后)
自定义 placeholder:
replyPlaceholderText: "🤖 AI 助手收到啦~ 正在思考中,请稍候 5-10 秒"关掉 placeholder(回到 v2.1.0 之前的行为):
replyPlaceholderText: "" # 空字符串 → 不发占位,立即返 success模式 B:passive(5 秒内必须出结果,否则降级)
replyMode: passive行为:5 秒内如果 agent 已经产出 text,则把整段回复打包成被动回复 XML 直接返回(粉丝只看到一条消息,无延迟);超时则降级到 async 模式(同上)。
适合:纯模板回复 / 关键词路由 / 缓存命中 等确定能 5 秒内完成的场景。LLM 推理建议留在 async。
Event 类回调不会发 placeholder
关注/扫码/菜单点击等 event 类回调始终返回 "success",避免微信侧把被动回复 XML 当事件确认从而触发额外重发。日志区分:
acked(placeholder) ← user message + async + 占位生效
acked(success) ← event 类回调 / passive 模式 / 占位关闭⏰ 客服消息「48 小时窗口」硬性约束
微信平台规则(不是 plugin 限制):
- 粉丝主动给公众号发完消息后,公众号有 48 小时窗口可以用
customservice/send主动回复 - 超过 48 小时禁用客服消息接口(
errcode: 45015);想发就要走模板消息或订阅消息(且粉丝事先订阅过) - 关注事件 / 扫码事件 / 点击菜单 也开 48h 窗口
实务建议:
- agent 回复走
async模式 + 客服消息(48h 内绝对够用) - 主动通知(超 48h、或粉丝从未交互)必须用模板消息(
wechat_service_message send_template)或长期订阅通知(send_subscribe)
🛠️ Agent Tools(12 个)
| Tool | 主要 action |
|------|-------------|
| wechat_service_menu | create / get / delete / create_conditional / delete_conditional / try_match |
| wechat_service_message | 客服消息(10 类)+ 模板消息 CRUD + 公模板库 + 一次性订阅 + 长期订阅通知(共 25 个 actions) |
| wechat_service_material | 临时/永久素材上传、图文素材、列表、删除 |
| wechat_service_article | 草稿箱 CRUD + freepublish 发布流水线(含 describePublishStatus) |
| wechat_service_user | 用户信息 / 粉丝列表 / 标签 CRUD / 黑名单 / 备注 |
| wechat_service_qrcode | create(temp_id/temp_str/perm_id/perm_str)+ gen_shortkey + fetch_shortkey |
| wechat_service_mass_send | 按标签 / openid 列表 / 预览群发 + 速度控制 + 撤回 |
| wechat_service_jssdk | sign(JS-SDK config)/ get_ticket / invalidate_ticket |
| wechat_service_oauth 🆕 v0.4.0 | build_authorize_url / code_to_token / refresh_token / userinfo / validate |
| wechat_service_analytics 🆕 v0.4.0 | list_metrics + query metric:<name> —— 17 项 datacube 指标 |
| wechat_service_intelligent 🆕 v1.0.0 | list_visions + run vision:<name> —— OCR 7 类 + 图像 3 项 |
| wechat_service_card 🆕 v1.0.0 | create / get / batchget / delete / consume / decrypt(卡券精简) |
所有 tool 都支持 accountId 参数;不传时使用当前 agent 绑定的账号或 defaultAccount。未配置账号会直接返回结构化错误(isError=true)。
🎯 v0.2 → v2.1 演进路线(已落地)
v0.1.0 初始版本(消息/菜单/素材/草稿/用户/标签/二维码/JS-SDK/群发)
v0.2.0 ✅ Phase 0 动态 Agent 框架(模仿 @huo15/wecom)
v0.3.0 ✅ Phase 1 通知能力补全(模板消息 CRUD + 长期订阅通知)
v0.4.0 ✅ Phase 2 网页授权 OAuth + 数据统计 datacube
v1.0.0 ✅ Phase 3 智能开放 OCR/图像 + 卡券精简版
v1.0.1 🩹 Bugfix channel id / config key kebab-case 对齐
v2.0.0 🏗️ 架构升级 src/ 按 @huo15/wecom 同构 + account-runtime + setup.applyAccountConfig
v2.0.1 📝 docs 刷新 SKILL.md description
v2.1.0 🛡️ 权限层 permissionMode=admin-only:写操作仅 main agent / adminUsers
v2.1.1 🩹 UX async 模式立即返 placeholder 占位回复
v2.1.2 📚 docs README 大扩 4 节配置 SOP(沉淀踩坑) ← 当前 latest详细变更见 CHANGELOG.md。
🛡️ 权限模型(v2.1.0+)
公众号的 12 个 agent tool 共 80+ 个 action,按副作用分两类:
| 类别 | 例子 | admin-only 模式下谁能调 |
|------|------|-------------------------|
| read(读) | list_templates / get_info / OCR / OAuth flow / analytics.query | 所有 agent(主 agent 和粉丝的动态 agent 都可) |
| write(写) | send_text(给任意 openid)/ mass_send.* / menu.create / card.create / article.publish / 模板&订阅消息发送 | 仅 main agent / adminUsers 列表 / OpenClaw owner |
重点:粉丝跟公众号的"自然对话"不受影响 —— agent 收到入向消息后的回复走 dispatcher 直接调客服消息接口,不经 tool。admin-only 模式只 block 粉丝在自己的动态 agent 里主动调 tool 越权的场景(譬如试图用 wechat_service_message.send_text 给别人发消息)。
# 启用方法(推荐生产配置)
channels:
wechat-service:
dynamicAgents:
enabled: true
adminUsers: [oABC_admin1, oXYZ_admin2]
permissionMode: admin-only被拒绝时 tool 返回结构化错误:
{
"ok": false,
"isError": true,
"action": "send_text",
"permissionMode": "admin-only",
"agentId": "wechat-service-default-dm-onormal",
"requesterSenderId": "oNORMAL",
"error": "[wechat-service] action \"wechat_service_message.send_text\" 是 admin/write 操作..."
}📐 多 Agent 路由
routing.events 允许把不同事件类型路由到不同 agent:
subscribe/unsubscribe— 关注 / 取消关注CLICK/VIEW— 菜单点击 / 跳转SCAN— 带参二维码扫描LOCATION/location_select— 位置上报TEMPLATESENDJOBFINISH/MASSSENDJOBFINISH— 模板 / 群发回调
未命中的事件走 routing.defaultAgent。设置 failClosedOnDefaultRoute: true 时,默认 agent 未配置会拒绝消息。
动态 Agent 优先级:当 dynamicAgents.enabled=true 时,路由先走静态 routing.events,再被动态 agent 覆盖(除非 senderId 在 adminUsers 里)。
💾 知识库双写
启用 knowledgeSync.enabled 后,每条入站消息 + agent 回复会同时写入:
- 本地 markdown:
{localPath}/wechat-service/{accountId}/{openid}/{YYYY-MM-DD}.md一天一个文件,首次写入带 YAML frontmatter,后续追加## HH:mm:ss段落。 - Odoo
knowledge.article:按 title[wechat-service] {name} · {openid} · {date}去重;存在则write追加 body,不存在则create(可选articleParentId)。
两路独立 best-effort:任一失败不影响另一路,也不会 throw 到消息处理链。
🔒 加密模式与签名
plain— 仅测试用,webhook 明文 XMLcompatible— 同时接受明文和加密;推荐只在迁移期使用safe(推荐)— 强制加密。插件用encodingAESKey+appId解密Encrypt字段并校验msg_signature
服务器 URL 校验(GET echostr)兼容 signature + msg_signature 两种签名方式。
📡 公众号后台必做的 6 件事(缺一不可)
登录 https://mp.weixin.qq.com 找管理员账号,按顺序操作:
| # | 后台位置 | 字段 | 注意 |
|---|---------|------|------|
| 1 | 设置与开发 → 基本配置 → 服务器配置 | URL | 填 https://你的域名/plugins/wechat-service/<accountId>,accountId 跟 ~/.openclaw/openclaw.json 一致 |
| 2 | 同上 | Token | 跟 plugin 配置 accounts.<id>.token 一字不差(无空格、大小写敏感) |
| 3 | 同上 | EncodingAESKey | 43 位完整字符串,跟 accounts.<id>.encodingAESKey 一致 |
| 4 | 同上 | 加密模式 | 选「安全模式」对应 plugin 的 encryptMode: safe(推荐) |
| 5 | 设置与开发 → 基本配置 → IP 白名单 | 加入 OpenClaw gateway 的出口公网 IP | ⚠️ 不加无法调任何 wechat API(包括 access_token / 客服消息 / 模板消息),报 errcode: 40164 |
| 6 | 设置与开发 → 接口权限 | 确认开通:客服消息 / 模板消息 / 用户管理 / 素材管理 / 群发接口 / 自定义菜单 / 数据分析 等 | 未认证号 / 个人号 / 订阅号有些接口禁用,需要服务号或已认证 |
取出口 IP 的方法:
# 在 gateway 跑的那台机器上
curl -s ifconfig.me ; echo如果 gateway 跑在本地 mac、用 frpc 反代到公网,出口 IP 是 mac 这边的公网 IP(因为 outbound 不走隧道,是 mac 直连 wechat API)。建议把 gateway 部署到固定公网 IP 的 VPS,避免每次 IP 变都要改白名单。
🧰 故障排查(常见错误对照 + 日志关键字)
错误码速查
| 现象 / 错误码 | 含义 | 排查 |
|--------------|------|------|
| 后台「请求失败,HTTP 返回非 200」 | webhook 不可达 | curl https://你的域名/plugins/wechat-service/<id> 看 200/404;查 frpc 隧道;查 gateway 是否在跑 |
| signature_invalid | Token 不一致 / 大小写 / 前后空格 | 重新对照 plugin 配置和后台 Token 字段,完全一致 |
| route-failure 但 signed=true | 签名算到了但结果不一致 | 同上,Token 校对 |
| errcode: 40164 | IP 不在白名单 | 加 mac 出口 IP 到后台白名单(上面第 5 步) |
| errcode: 45015 | 48 小时窗口已过 | 客服消息超 48h 不能发,改用模板消息 / 订阅消息 |
| errcode: 45047 | 客服消息超出每日上限 | 等次日重置或降发频 |
| errcode: 48001 | 接口权限未开通 | 后台「接口权限」页申请相应能力 |
| errcode: 40001 | access_token 失效 | 不用动,plugin 自动刷新;持续报错检查 AppSecret 是否被重置 |
| unknown channel id: wechatService | 老配置 key 不对 | 升级到 v1.0.1+,把 channels.wechatService 改成 channels["wechat-service"](kebab-case) |
| Channel does not support add | plugin v1.x 没暴露 setup adapter | 升级到 v2.0.0+ |
| ~/.openclaw/openclaw.json.clobbered.<ts> 不断生成 | 配置 validator 拒收,自动备份 → 还原 last-good | 看 .clobbered.* 里写了啥 → 修配置 → 重启 gateway |
| 粉丝发消息没反应 | bindings 缺 wechat-service 反向路由 | 顶层 bindings 里加 {agentId, match:{channel:wechat-service,accountId:...}} |
日志 grep 模板
# 实时跟踪 wechat-service 全链路
tail -f /tmp/openclaw-gateway.log | grep -E "wechat-service|customer_service|errcode"
# 看一条消息从 inbound 到 outbound 全流程
grep -E "reqId=<具体id>|wechat-service-outbound" /tmp/openclaw-gateway.log
# 单看 outbound 是否真的发出去
grep "wechat-service-outbound sent" /tmp/openclaw-gateway.log
# 看签名/解密失败
grep -E "signature_invalid|decrypt_failed" /tmp/openclaw-gateway.log
# 看是不是 IP 白名单问题
grep "40164" /tmp/openclaw/openclaw-*.log
# 看配置是否被 clobber
ls -lt ~/.openclaw/openclaw.json.clobbered.* 2>/dev/null | head -3期待看到的成功链路
粉丝发消息后日志按顺序应出现:
[wechat-service] inbound(http): reqId=xxx ... method=POST signed=true
[wechat-service] acked(placeholder) reqId=xxx accountId=default msgType=text from=oXXX
[wechat-service] inbound dispatch accountId=default agent=main from=oXXX
[wechat-service-outbound] sent text to openid=oXXX accountId=default (len=NNN) ← 关键少最后一条 = agent 没产出回复 / outbound 路由错 / 客服消息发送失败。按错误码对照排查。
🚦 完整最小可用配置(一键复制粘贴)
下面这段直接覆盖 ~/.openclaw/openclaw.json 即可(替换 4 处 __REPLACE__):
{
"agents": {
"list": [
{ "id": "main" }
]
},
"bindings": [
{
"agentId": "main",
"match": { "channel": "wechat-service", "accountId": "default" }
}
],
"channels": {
"wechat-service": {
"enabled": true,
"defaultAccount": "default",
"accounts": {
"default": {
"enabled": true,
"name": "我的公众号",
"appId": "__REPLACE_WX_APP_ID__",
"appSecret": "__REPLACE_APP_SECRET__",
"token": "__REPLACE_TOKEN__",
"encodingAESKey": "__REPLACE_43_CHAR_AES_KEY__",
"encryptMode": "safe",
"replyMode": "async",
"replyPlaceholderText": "收到,正在为你处理..."
}
},
"dynamicAgents": {
"enabled": false
}
}
},
"plugins": {
"entries": {
"wechat-service": { "enabled": true }
}
}
}填好 4 个 __REPLACE__ 字段(来自公众号后台「基本配置」+ AppSecret),然后:
# 重启 gateway 加载新配置
pkill -9 -x openclaw-gateway && sleep 2
nohup openclaw gateway run --bind loopback --port 18789 --force \
> /tmp/openclaw-gateway.log 2>&1 &
# 验证
sleep 5
openclaw channels list | grep 微信服务号
# 期待:微信服务号(公众号) default: configured, enabled生产上线前再加:dynamicAgents.enabled: true + adminUsers + permissionMode: admin-only(一粉一会话隔离 + 权限控制)。
🧪 脚本
npm run typecheck # tsc --noEmit
npm test # vitest run(13 个文件 184 个用例)
npm run build # tsc → dist/CI 在每次 publish 前会跑 prepublishOnly: typecheck。
📚 资源
- 微信公众平台官方文档:https://developers.weixin.qq.com/doc/service/guide/
- OpenClaw 文档:https://docs.openclaw.ai/zh-CN
- 本地知识库:
~/knowledge/huo15/2026-04-29-wechat-service-v2-wecom-mirror-refactor.md - 公司 Odoo 技术知识库:https://www.huo15.com/odoo/knowledge/
公司名称: 青岛火一五信息科技有限公司 联系邮箱: [email protected] | QQ群: 1093992108
📄 License
ISC © jobzhao([email protected])
