Перейти к основному содержимому
A

wechat-clawbot

alphainfix/wechat-clawbot

💬 A DeepSeek Harness plugin that brings your DSH agent into WeChat — chat with it from anywhere: native photo understanding, two-way file transfer, long-term memory, scheduled reminders, and permission approvals answered right in the chat.

Установка

dsh plugin --profile web add github:alphainfix/wechat-clawbot

README

wechat-clawbot

Connect a DeepSeek Harness (DSH) profile to your WeChat through the official WeChat ClawBot bridge (Tencent iLink API). Once bound, you can message your DSH agent from WeChat while you are away from the computer — ask it to look at files, edit them, run commands — and it replies (including permission/approval questions) in the WeChat conversation.

Status: v0.9.0 — text messages both ways; native vision (an inbound photo rides in the model's own message, and look_at_image hands a local one to it directly — no separate vision model, no second round trip); agent → WeChat file/image delivery; quoted (引用) message resolution; long-term memory; scheduled reminders; a settings card on 设置 → 插件 whose fields apply live; and a per-plugin model pin so the web UI's model picker no longer moves the bot. Group chats are not supported.

How it works

Phone WeChat ──► Tencent iLink cloud ──► this plugin (long-poll monitor)
        ◄────────────────────────────────  (text replies, approval questions)
                          │
                          ▼
              one fixed DSH agent session ("wechat-main")
              (persisted, auto-compacted like any DSH session)
  • The plugin reuses the protocol client from the official, MIT-licensed @tencent-weixin/openclaw-weixin package (see NOTICE); only the OpenClaw integration layer was replaced with a DeepSeek Harness integration.
  • The WeChat conversation maps to one fixed session id (default wechat-main), so context is shared, history survives restarts, and automatic context compaction keeps token usage low.
  • The WeChat session's approval policy is forced to ask: permission requests (file access, command execution, sandbox escalation) are forwarded to WeChat and you answer with 同意 / 拒绝 (or yes / no).
  • File/image delivery: the WeChat agent has a send_wechat_file tool — just ask it (e.g. "把 README.md 发给我" or "生成一张图表发给我"). Files travel through Tencent's CDN with the official AES-128-ECB encrypted upload pipeline; images, PDFs, Office documents, archives etc. are supported.
  • The plugin runs inside the profile process: as long as the profile is running, the bridge is up. Login is manual (scan a QR code), once per binding; the token is stored under $DSH_HOME/clawbot and reused on restart.

Requirements

  • Node.js >= 22
  • A DSH profile (e.g. the web profile) with a configured model provider
  • The WeChat mobile app with the official 微信ClawBot plugin available (设置 → 插件; WeChat may prompt you to update the app the first time you scan)

Install

# 1. Install the plugin into a profile (pnpm is required)
dsh plugin --profile web add wechat-clawbot

# 2. Restart the profile
#    (stop `dsh web`, then start it again)
dsh web

The plugin starts automatically at boot (autoStart: true). To change its configuration, add a clawbot row to the profile's cordis.patch.yml:

- id: clawbot
  config:
    allowFrom: []          # allowlist of WeChat user ids; [] = only the binder
    sessionId: wechat-main # the fixed DSH session for all WeChat messages
    forwardQuestions: false
    approvalTimeoutMs: 0   # 0 = wait indefinitely for approval answers
    logLevel: info

Connect (one-time, manual)

# From the profile directory (or anywhere; the state dir is shared):
clawbot login

A QR code is printed in the terminal. Scan it with WeChat → 扫一扫 and confirm. Afterwards:

  • WeChat shows a new contact 「微信ClawBot」 — open it and start chatting.
  • The running profile picks up the credentials automatically within a second (it watches the state directory) — no restart needed.

Other commands:

clawbot status   # show the bound account
clawbot logout   # unbind and delete credentials

Environment:

VariableDefaultMeaning
CLAWBOT_STATE_DIR$DSH_HOME/clawbotcredential/state directory
CLAWBOT_LOG_LEVELinfodebug / info / warn / error

Security

  • Allowlist: by default only the WeChat user who scanned the QR code may message the agent. Add more users via allowFrom (their WeChat user ids appear in the logs / clawbot status).
  • Approvals: the WeChat session runs with approval policy ask, so sensitive operations still require your explicit reply in WeChat. Other sessions (e.g. the local web GUI) keep their own policy.
  • Unknown senders are ignored silently.

Configuration reference

KeyTypeDefaultMeaning
autoStartbooleantruestart the monitor at boot when a bound account exists
allowFromstring[][]allowed sender ids; empty = binder only
sessionIdstringwechat-mainfixed DSH session for all WeChat traffic
cwdstringprocess.cwd()workspace for a freshly created session
forwardQuestionsbooleanfalseforward ask_user_question / plan-review to WeChat (see note)
approvalTimeoutMsnumber0approval answer timeout; 0 = wait forever
botAgentstringDSH-ClawBot/0.1.0 …bot_agent reported to iLink
maxImageEdgenumber2048long-edge pixel cap for compressed images
imageQualitynumber80JPEG quality for compressed images
compressThresholdBytesnumber1048576only compress images larger than this
apiBaseUrlstringhttps://ilinkai.weixin.qq.comiLink API base (override for testing)
logLeveldebug|info|warn|errorinfoprotocol log level
stripEmojibooleantruestrip Unicode emoji from outbound text
noticeMinBytesnumber2097152announce a file before upload only above this size; 0 = always
attachImagesbooleantruegive images to the model directly when the route accepts them
providerstringpin this session's provider; empty = follow the deployment default
modelstringpin the model; empty = follow the deployment default
reasoningEffortstringpin the effort; empty = the model's own default
mcpBridgebooleantrueanswer the /plugins/clawbot/mcp/* routes (see The Claude bridge)

forwardQuestions: only one userQuestions provider can exist per DSH context. The Web UI registers one while a browser is attached, so this option only takes effect in deployments without an active Web provider. Approval forwarding (approval/request) is independent and always on for the WeChat session.

Settings

Most of the table above is editable at 设置 → 插件 → 微信 Bot, and a change lands on the next message with no restart and no dropped WeChat connection: the plugin diffs the committed settings and copies the hot fields into its live config, which every collaborator holds by reference and reads at the moment it is used.

The card writes into the settings document's clawbot section, which is an override layer over the plugin's cordis rowcordis.patch.yml stays the default, and the file only ever stores fields you actually changed. Each row shows a 已覆盖 badge with a 重置 that drops back to the composed value.

Cold fields (sessionId, autoStart, forwardQuestions, botAgent, apiBaseUrl, cwd) are deliberately absent from the card. They are stored but not applied in place — apiBaseUrl would redirect a connected client mid-flight and sessionId would point the bridge at a session it is not bound to — so they live in cordis.patch.yml, where changing them already means a reload.

Pinning the model

Leaving provider/model empty means the session follows the deployment default, which is also why picking a model in the web UI used to move the bot: that picker writes through to the global agent-default-model. Setting them here breaks the coupling. A model id that will not resolve is refused rather than applied, so a typo cannot brick the next message.

The Claude bridge

Two directions, and they are separate mechanisms with separate failure modes.

Outward: /plugins/clawbot/mcp/* — Claude Code drives DSH

Four routes, consumed by dsh-mcp-bridge (an MCP server, not a DSH plugin), which is what gives a Claude Code session tools to list/read/drive DSH sessions and to reach the owner on WeChat.

RouteMethodDoes
mcp/sessionsGETlist sessions: id, title, cwd, live, running
mcp/readPOSTtail one session's user/assistant turns
mcp/sendPOSTsteer or follow up a live agent
mcp/notifyPOSTone WeChat message to the owner
mcp/statusGETis the bridge on, has anything called it

The privileged half lives here rather than in the MCP server for a concrete reason: ctx.agents holds the live agent objects, and steer()/followup() are in-memory methods an out-of-process caller cannot reach. The WeChat send needs the account token plus per-peer context tokens, so duplicating it outside would mean a second copy of the credential handling.

Three properties are load-bearing and covered by tests:

  • notify takes no recipient. It sends to account.userId — the id linked by QR login — so messaging anyone but the owner is unrepresentable, not merely discouraged.
  • send does not reach WeChat. Injected messages carry source plugin clawbot-mcp, and the bridge's session listener treats any non-schedule plugin turn as GUI-only (leaves currentSender unset). So a message pushed into wechat-main is answered in the web UI only. notify is the sole route to the phone. Changing that source string breaks the isolation.
  • Auth. Every route needs Authorization: Bearer <token> from <state>/mcp-token (0600, minted on first boot). mcp/status is deliberately unauthenticated — a browser page cannot hold a secret — so it reports the token's path, never its value.

mcpBridge: false makes all four return 403 while leaving them registered: the flag is read per request, so it is a live kill switch in both directions.

Inward: three tools — DSH sees and steers Claude Code

Registered on the WeChat agent (restrictTools is a deny list, so they need no allowlisting): list_claude_sessions, read_claude_session, send_to_claude_session.

Listing and reading go straight to files, which is the stable half: ~/.claude/sessions/<pid>.json is world-readable, and ~/.claude/projects/<slug>/<sessionId>.jsonl is the transcript (found by globbing for the id — deriving the directory name flattens both separators and dots, which is easy to get subtly wrong).

Sending is the interesting half. There is no documented CLI for reaching a live session: claude has no send/message subcommand, and --resume -p starts a new run from history instead of the session the user is watching. The real mechanism is peerProtocol 1 over /tmp/cc-socks/<pid>.sock with a 0600 key, and claude.exe is a native binary. So rather than reverse-engineer it, this shells out to a short-lived

claude -p "<deliver X to session Y>" --allowed-tools ListAgents,SendMessage --model haiku

and lets Claude Code do the socket work with its own implementation — protocol changes stay Anthropic's to keep working. Verified end to end.

Four things learned the hard way, all of them now pinned by tests:

  • Session names drift. One session was observed renaming itself harvard-96harvard-35harvard-ef inside a day. So the name is resolved fresh on every send, never cached, and an ambiguous handle is an error rather than a guess.
  • Success needs an anchored match. A bare /sent/i scores Claude Code's own "Session X is not reachable" and the plausible "was not sent" as a delivery.
  • State the run state and the queue explicitly, including when zero. The first version omitted both, and the bot answered 「都没有正在运行的任务」about a session that was mid-tool-call. An absent field does not read as unknown to a model — it reads as nothing, and the gap gets filled by invention.
  • Queue accounting has three operations, not two. enqueue adds, dequeue delivers the head, and remove withdraws a specific item by content. A real session measured 15/9/6 — counting only enqueue/dequeue calls that 6 pending when it is 0.

Run state is an inference: a turn is in flight when the newest main-thread entry is an assistant message holding a tool_use, or a user entry that is nothing but tool_result. Gated on a 180s freshness window, because a session killed mid-tool-call keeps an unfinished last entry forever.

Known limit: a message currently waiting in the queue does not appear in the transcript. Verified, not assumed — a session had one queued while enqueue/dequeue/remove balanced to zero, with the file 7s fresh and writes shown to be prompt. A live answer would need the peer socket.

Timestamps in the transcript are UTC. Render them through toLocaleTimeString, never by slicing the ISO string — "…T20:33:05Z" .slice(11,16) reported a 16:33 message as 20:33.

Limitations

  • Group chats are not supported (official channel behaviour).
  • Voice messages require a silk transcoder and are not supported.
  • One bound WeChat account per state directory. Multiple accounts are tracked in the store but only the first is monitored.
  • Scheduled reminders are session-local: one that comes due while the profile is not running is delivered, late, at the next start.
  • The bridge runs only while the DSH profile process is running. For 24/7 operation keep dsh web up (a login item or launchd unit).
  • WeChat's own bracket emoji codes ([捂脸]) are stripped unconditionally from outbound text: they are WeChat-internal notation and arrive through this bridge as literal brackets.

Development

npm install
npm run typecheck   # tsc --noEmit
npm run build       # tsc → lib/, then copies src/client.js → lib/client.js

node test/regression.mjs     # 55 offline checks, no model calls, no WeChat
node test/claude-peer.mjs    # 29 checks against the real ~/.claude
node scripts/bot-sim.mjs     # reply-style harness against the real prompt

Both suites are offline and cost nothing: no model calls, no network, no WeChat. They exit non-zero, so either can gate a build.

test/regression.mjs covers the things that used to fail silently: a strip rule that ate a markdown link, a settings field declared "hot" but only read at boot, an output schema that rejects a field the attachment store just started returning, and the bridge invariants above (that notify reads no recipient, that injected messages keep the clawbot-mcp source tag, that mcp/status never echoes the token).

A hot field must appear as config.X in one of the source files that suite greps — a field only read inside apply() is not hot, whatever HOT_FIELDS claims, and the check enforces that. When adding a source file that reads settings, add it to that list too.

test/claude-peer.mjs runs the peer tools against the real ~/.claude, because the failure mode there is not "it crashed" but "it silently returned nothing" — a renamed registry key, a changed transcript shape, a stale pid counted as live. Queue accounting, the idle path and the clock are asserted against a synthetic transcript with known answers, so they do not depend on what the machine happens to be doing. send_to_claude_session is registered but never invoked by the suite: it spawns a process and delivers into a real live session.

scripts/bot-sim.mjs drives the real system prompt and tool list against scripted turns and prints what WeChat would have received — so a prompt change can be judged without messaging yourself and without a session reset. It flags empty-response endings, stacked vision calls, and bracket emoji codes. Add --allow-emoji to exercise the stripEmoji: false branch.

The vendored iLink protocol layer lives in src/ilink/ (MIT, Tencent); the DSH integration is src/bridge.ts, src/inbound.ts, src/approvals.ts, src/monitor.ts, src/questions.ts, src/index.ts.

License

MIT. The vendored iLink client is MIT © Tencent — see NOTICE.

Похожие плагины