|
|
hace 4 meses | |
|---|---|---|
| .. | ||
| scripts | hace 4 meses | |
| src | hace 4 meses | |
| .gitignore | hace 4 meses | |
| LICENSE | hace 4 meses | |
| README.md | hace 4 meses | |
| index.ts | hace 4 meses | |
| openclaw.plugin.json | hace 4 meses | |
| package-lock.json | hace 4 meses | |
| package.json | hace 4 meses | |
| tsconfig.json | hace 4 meses | |
OpenClaw channel plugin that bridges a third-party WeChat HTTP bot (xbot / wcf / PC-hook class) into the OpenClaw agent pipeline.
Inbound messages arrive via polling, get normalized + filtered + batched,
then dispatched through OpenClaw's standard reply pipeline so the agent
can respond using its LLM + memory + skills. Replies are sent back via
the bot's send-text endpoint.
Ships as the successor to the standalone auto-reply-daemon.js that
lived in the companion skill pack (v1.x). The plugin supersedes that
daemon once installed — see "Migrate from the v1.2.x daemon" below.
v0.2.0. Private-chat + group-chat (mention/keyword trigger) text inbound are supported. Media and multi-account land in later versions.
| Feature | v0.2.0 |
|---|---|
| 1-to-1 text inbound / outbound | ✓ |
| System / official-account filter | ✓ |
Message dedupe (msgId) |
✓ |
| Self-message echo filter | ✓ |
| FAQ plugin-layer fast path | ✓ |
| Batched agent turns (merge rapid messages) | ✓ |
| Per-wxid reply cooldown | ✓ |
| Agent-driven replies (LLM + skills + memory) | ✓ |
| DM allowlist policy | ✓ |
| Pairing flow | ✓ (core-owned) |
Group chat + @mention/keyword gating |
✓ |
| Image / voice / file inbound | planned v0.2 |
| Media outbound | planned v0.2 |
| Multi-account | planned v0.3 |
| Webhook push mode | planned v0.3 |
2026.4.15 (see peerDependencies.openclaw). Older hosts are not supported — the plugin is built and tested against this exact version.engines.node).# on your build machine: typecheck + esbuild bundle + pack
npm install
npm pack # runs prepack (typecheck + build), produces fmode-openclaw-wechat-agent-0.1.1.tgz
# ship the .tgz to the customer server, then on that server:
openclaw plugins install ./fmode-openclaw-wechat-agent-0.1.1.tgz
openclaw gateway restart
The tgz ships a pre-bundled dist/index.js with zero runtime dependency on the host's openclaw package resolution tree — see Build & release. The customer does not need to run npm install or configure NODE_PATH / symlinks.
git clone <repo-url> openclaw-wechat-agent
cd openclaw-wechat-agent
npm install
npm run build # produces dist/index.js
openclaw plugins install --link .
openclaw gateway restart
openclaw plugins install @fmode/openclaw-wechat-agent
openclaw gateway restart
⚠️ Upgrading wipes your business config — back up first.
OpenClaw v2026.4.x's
openclaw plugins uninstall wechat-agentdeletes, per its own printed summary, *"config entry, install record, allowlist, channel config, directory"*. That includes everything underchannels.wechat-agent.*in~/.openclaw/openclaw.json—apiBase,faq,allowFrom,selfWxid,ignoreWxidExact, the lot.The
.bakfile the CLI creates during the nextinstallis a snapshot of the post-uninstall state, so it's not a recovery source for your business settings. You have to back up before uninstalling.
# 1) Snapshot the current wechat-agent business config into a dated file
cp ~/.openclaw/openclaw.json ~/.openclaw/openclaw.json.pre-upgrade-$(date +%F).bak
jq '.channels["wechat-agent"]' ~/.openclaw/openclaw.json \
> ~/.openclaw/wechat-agent.config.$(date +%F).json
# 2) Verify the snapshot actually captured something (not `null`)
cat ~/.openclaw/wechat-agent.config.$(date +%F).json
# 3) Uninstall old + install new (CLI will NOT overwrite an existing directory)
openclaw plugins uninstall wechat-agent
openclaw plugins install ./fmode-openclaw-wechat-agent-X.Y.Z.tgz
# 4) Verify install picked up the new bundle
openclaw plugins inspect wechat-agent | head -10
# expect: Version: X.Y.Z, Shape: plain-capability,
# Source: ~/.openclaw/extensions/wechat-agent/dist/index.js
# 5) Restore the business config you backed up in step 1
tmp=$(mktemp) && jq --slurpfile cfg ~/.openclaw/wechat-agent.config.$(date +%F).json \
'.channels["wechat-agent"] = $cfg[0]' \
~/.openclaw/openclaw.json > "$tmp" && mv "$tmp" ~/.openclaw/openclaw.json
# 6) Sanity-check
jq '.channels["wechat-agent"] | {enabled, apiBase, pollIntervalMs, dmPolicy}' \
~/.openclaw/openclaw.json
# 7) Restart gateway
systemctl --user restart openclaw-gateway.service # if the gateway is a user unit
# or: openclaw gateway restart
# 8) Tail the log for a fresh "starting poller" line
tail -f /tmp/openclaw/openclaw-*.log \
| grep --line-buffered -iE 'wechat-agent.*(starting poller|inbound|delivered)'
You should see [wechat-agent:default] starting poller apiBase=… interval=…ms
within a few seconds, then the first poll ok (N new) on the next
pollIntervalMs tick.
Plugins installed as 0.1.0 are the pre-bundle tsc-emit build and
fail at runtime on any customer host where OpenClaw lives outside the
plugin's node_modules tree (typical Linux installs; see
Build & release for the exact error). Everyone still
running 0.1.0 should upgrade to 0.1.1 using the flow above. The bundled
dist/index.js in 0.1.1 has no openclaw/plugin-sdk* runtime imports
left to resolve, so the Cannot find package 'openclaw' restart loop
goes away without any manual node_modules/openclaw symlink.
In the OpenClaw UI: Channels → WeChat (Agent Backend) → Edit.
Or edit ~/.openclaw/config.json (or wherever your config lives):
{
"channels": {
"wechat-agent": {
"enabled": true,
"apiBase": "http://8.138.37.248/api/wechat-agent"
}
}
}
{
"channels": {
"wechat-agent": {
"enabled": true,
"apiBase": "http://8.138.37.248/api/wechat-agent",
"pollIntervalMs": 10000,
"batchWindowMs": 3000,
"replyCooldownSecPerWxid": 5,
"dmPolicy": "allowlist",
"allowFrom": ["wxid_customer1", "wxid_vip_lead"],
"ignoreWxidPrefixes": ["gh_"],
"ignoreWxidExact": [
"weixin", "fmessage", "medianote",
"filehelper", "floatbottle"
],
"faq": [
{
"id": "greet",
"keywords": ["你好", "您好", "在吗", "hello", "hi"],
"reply": "您好~我是 AI 助理,请问有什么可以帮您?"
},
{
"id": "price",
"keywords": ["价格", "报价", "多少钱"],
"reply": "价格请参考官网 https://example.com/pricing ,或稍候由人工跟进。"
}
]
}
}
}
| Field | Default | Description |
|---|---|---|
enabled |
true |
Master switch. |
apiBase |
http://127.0.0.1:8138 |
Root URL of the wechat-agent HTTP bot. |
pollIntervalMs |
10000 |
How often to pull new messages (3s-120s). |
batchWindowMs |
3000 |
Wait this long before dispatching, so rapid-fire messages from one wxid merge into ONE agent turn. |
replyCooldownSecPerWxid |
5 |
Min seconds between two replies to the same wxid. Prevents reply bursts. |
groupEnabled |
true |
Enable group-chat handling. |
groupKeywords |
["帮我","请问"] |
Group reply trigger keywords (substring match). |
groupMentionTokens |
["@bot","@助手"] |
Extra text tokens treated as mention triggers in group messages. |
groupReplyAtSender |
false |
When true, group replies @-mention sender wxid via ats. |
dmPolicy |
"open" |
"open" | "allowlist" | "pairing". |
allowFrom |
[] |
Whitelist wxids (used when dmPolicy="allowlist"). |
ignoreWxidPrefixes |
["gh_"] |
Drop messages from wxids starting with any prefix (e.g. gh_ = official accounts). |
ignoreWxidExact |
system list | Drop messages from system contacts (weixin, filehelper, etc.). |
ignoreMessageTypes |
["system","emoji","voice","image","video","location","file"] |
MVP only handles text; others are ignored. |
faq |
[] |
Plugin-layer FAQ fast path. If a message contains any keyword, reply without calling the agent. Each rule: { keywords: string[], reply: string, stop?: boolean }. |
defaultTo |
— | Default wxid when agent replies without an explicit target. |
selfWxid |
auto-detect | Bot's own wxid. If unset, plugin probes /login/check-online. |
FAQ rules are evaluated in order; first keyword hit wins. stop: false
means: send the FAQ reply AND still hand the message to the agent
(useful for instant acknowledgement + follow-up reasoning).
polling loop (pollIntervalMs)
├── checkOnline()
├── GET /messages?direction=received
└── for each new msg (dedupe by msgId):
├── drop: system contacts / official accounts / self-echoes
├── drop: non-text (MVP) / empty
├── if group: require @mention / keyword trigger
├── drop: not in allowlist (when dmPolicy=allowlist)
│
├── FAQ match? → sendText(reply) directly ← fast path, no LLM
│ cooldown.noteReply()
│ (if stop=false, also fall through to agent)
│
└── cooldown.enqueue(msg)
│
└── (after batchWindowMs, or once cooldown expires)
→ dispatchBatchToAgent(batch):
resolveAgentRoute → activity.record
enqueueSystemEvent
formatInboundEnvelope → finalizeInboundContext
updateLastRoute
createReplyDispatcherWithTyping + deliver cb
dispatchReplyFromConfig ← LLM runs here
└── deliver cb → POST /message/send-text
The deliver callback is where the LLM's reply actually lands on
WeChat — it chunks long markdown, calls the bot's send-text endpoint,
and bumps the cooldown timestamp for this wxid.
The companion skill pack (openclaw-wechat-skill-v1.2.2.zip) ships a
standalone auto-reply-daemon.js and three management skills
(wechat-auto-reply-start/-stop/-status). This plugin replaces all
four of them.
# 1. Stop and remove the old daemon
pid=$(cat ~/.openclaw/wechat-auto-reply.pid 2>/dev/null || true)
[ -n "$pid" ] && kill "$pid" 2>/dev/null || true
rm -f ~/.openclaw/auto-reply-daemon.js \
~/.openclaw/wechat-auto-reply.pid \
~/.openclaw/wechat-auto-reply-config.json \
~/.openclaw/wechat-auto-reply-config.template.json
# (Optional) remove the 3 daemon-control skills so the agent stops
# seeing "start/stop/status" options.
rm -rf ~/.openclaw/skills/wechat-auto-reply-start \
~/.openclaw/skills/wechat-auto-reply-stop \
~/.openclaw/skills/wechat-auto-reply-status
# 2. Install the plugin
openclaw plugins install ./fmode-openclaw-wechat-agent-0.1.1.tgz
openclaw gateway restart
# 3. Port your config
# Old daemon's ~/.openclaw/wechat-auto-reply-config.json →
# channels.wechat-agent.* in your OpenClaw config (see "Configure" above).
# The FAQ array copies over 1:1 into channels.wechat-agent.faq.
If something misbehaves, disable the channel and bring the daemon back:
openclaw plugins disable wechat-agent
# ... reinstall the v1.2.2 daemon from the old skill pack ...
bash /path/to/v1.2.2/quick-start.sh
Nothing is destructive — config and the plugin files live separately.
After startup, the OpenClaw status page shows:
running — is the poller aliveonline — last probe of /login/check-onlinelastPollAt — timestamp of last successful pollpollsOk / pollsFail — cumulative countersinboundCount / outboundCount — messages seen / sentfaqHits — how many replies came from the FAQ fast pathselfWxid — the bot's own wxid (used for self-echo filter)Logs flow into OpenClaw's unified log stream under the [wechat-agent]
prefix.
"no messages are being picked up"
Check channels.wechat-agent.apiBase is reachable from the OpenClaw
host (not just from your laptop). Inside a container, 127.0.0.1 is
the container itself — use the host's IP.
"online=false but the bot service is up"
Our checkOnline expects { ret: 200, data: { online: true } }. Some
forks return { code: 0 } or { status: "ok" } instead. Either
normalize in the upstream service or open an issue.
"agent never replies to real messages, only to FAQ keywords"
FAQ matched on a very short keyword (e.g. "在" matches any message
containing "在"). Tighten keywords or set stop: false so the agent
also gets a chance.
"agent replies once then goes silent"
replyCooldownSecPerWxid may be too high. Drop it to 0 during
debugging.
"plugin fails to load with 'runtime not initialized'"
Something in the channel object referenced getRuntime() at
module-evaluation time (not inside a function). All usages must be
lazy — see outbound.ts and monitor.ts for the pattern.
channel exited: Cannot find package 'openclaw' imported from .../dist/src/sdk.js / health-monitor: restarting (reason: stopped) loop
Symptom of an older build that was shipped as per-file tsc output. The
customer's ~/.openclaw/extensions/wechat-agent/ tree is not a descendant
of the host openclaw package tree, so Node's ESM resolver cannot find
openclaw/plugin-sdk/* subpaths at load time. Fixed in the current build
pipeline — the whole plugin ships as a single self-contained dist/index.js
with no runtime openclaw/* imports (see Build & release).
If you hit this, you are running an old tgz; rebuild with npm pack and
re-install.
npm install
npm run typecheck # tsc --noEmit
npm test # vitest run
npm run build # esbuild -> dist/index.js
npm run pack:tgz # typecheck + build + npm pack
Run against a real OpenClaw instance (after npm run build):
openclaw plugins install --link $(pwd)
openclaw gateway restart
openclaw plugins list | grep wechat-agent
Source layout mirrors @openclaw/zalo and
knowall-ai/openclaw-msteams:
channel-plugin/
├── index.ts entry — register() hook
├── openclaw.plugin.json manifest (config schema)
├── package.json npm + openclaw.channel metadata
├── scripts/
│ └── build.mjs esbuild bundle config
└── src/
├── channel.ts ChannelPlugin object (all adapters)
├── config.ts config readers + normalization
├── client.ts HTTP client for wechat-agent backend
├── monitor.ts polling loop + dispatch-to-agent
├── outbound.ts ChannelOutboundAdapter
├── faq.ts FAQ fast-path matcher
├── cooldown.ts per-wxid batch window + cooldown
├── runtime.ts PluginRuntime singleton
├── sdk.ts SDK barrel (subpath re-exports)
└── types.ts shared types
This plugin ships as a single self-contained dist/index.js produced by
esbuild (scripts/build.mjs), not by tsc.
OpenClaw installs third-party plugins into ~/.openclaw/extensions/<plugin>/.
That directory is not a descendant of the host's openclaw package tree
(which lives under /usr/lib/node_modules/openclaw on a typical Linux host),
so Node's standard ESM resolver cannot resolve bare specifiers like
openclaw/plugin-sdk/channel-plugin-common from inside the plugin code.
Per-file tsc output therefore fails at runtime with:
channel exited: Cannot find package 'openclaw'
imported from /home/<user>/.openclaw/extensions/wechat-agent/dist/src/sdk.js
health-monitor: restarting (reason: stopped)
…in a 5-second restart loop, and the channel never actually starts polling.
Symlinking openclaw into the plugin's node_modules works around it on a
developer box but is not something a Linux customer should have to do.
Bundling at build time against the dev-time openclaw install folds every
openclaw/plugin-sdk* runtime symbol into the plugin tgz. The host's
openclaw package is no longer required on the module resolution path;
only the PluginRuntime object that OpenClaw passes into register(api)
is — and that does not go through package resolution.
index.ts — our own sources plus every runtime
import of openclaw/plugin-sdk (root) and openclaw/plugin-sdk/*
(subpaths: channel-plugin-common, status-helpers, channel-reply-pipeline).import type { … }) are erased by esbuild's TS loader
and contribute zero runtime weight.node:*).@napi-rs/canvas,
sharp, node-llama-cpp, playwright-core, etc.) that are only reachable
via SDK side-effect imports we never execute from this channel. They are
left external so esbuild does not try to parse .node binaries or
platform-specific install stubs. If a future feature brings us into those
code paths, the resulting Cannot find package will be immediate and loud.scripts/build.mjs greps its own output for any surviving
from "openclaw…" / require("openclaw…") and fails the build if it finds
one. This is the single strongest regression guard for the bug above.
MIT © fmode
@openclaw/zalo and
knowall-ai/openclaw-msteams.