Bridges WeChat (ilink bot) private chats into DSH agent sessions and streams replies back, with hot-plug and a Settings tab.
Install
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:NattoCB/dsh-plugin-wechat-bridge
Any plugin you install runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network, and tool approvals don’t sandbox it. GitHub-sourced plugins also run build scripts at install time — pnpm blocks those until you allow them, so an install can stop with ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED or ERR_PNPM_IGNORED_BUILDS; dsh prints the exact key to add under allowBuilds in your profile’s pnpm-workspace.yaml, and the install works on the next run. Allowing a build is a trust decision: only install sources you trust, and pin a commit (github:owner/repo#sha).
README
Language: 中文 | English
Put your DSH agent in WeChat. A DeepSeek Harness bundle plugin: it bridges WeChat (ilink bot) private-chat messages into a DSH agent session and streams the reply back as plain text. Install into the
webprofile, scan a QR code to bind abot_type=3WeChat account, and chat from WeChat directly. One session per peer per day, durable JSON-file state, crash-safe polling; enable/disable live from the Settings UI tab, thesettings.yaml— nodsh webrestart.
✨ Features
- 📱 WeChat private chat → DSH agent: polls the WeChat
ilink botAPI (getupdates, multi-account); private-chat messages drive an agent session, replies come back as plain-text chunks (4096 chars × max 5, truncated beyond). - 🔌 Runtime hot plug: three independent controls — Settings UI tab,
/wechatslash command,settings.yamlflag — start/stop take effect immediately, no process restart. - 🗓️ One session per peer per day: local-midnight rotation, lazily created on the first inbound message, titled
<YYYY-MM-DD>; a day without conversation never materializes a session, and a corrupt log can't block the next day. - 🛡️ Crash-safe by construction: cross-process poll lock (
~/.dsh/wechat-bridge/poll.lock), per-chat serialization, inbound dedupe (eachmessage_idat most once), corrupt logs quarantined as.corrupt-<ts>and rebuilt. - 🚪 Inbound allowlist (fail-closed): empty
allowedPeers= deny everyone; matching on the bot's internal peer id — an opaque string that is not the WeChat alias/nickname; comma-separated; editable in the Settings tab's allowlist card, with one-click chips for ids seen in conversation. - 📣 One-way session notifications (on by default): the end of EVERY top-level DSH session's turn pushes a fixed-template digest to the allowlisted WeChat peers (session name ≤15 chars + a 6-char distinctive id badge — constant prefixes like
session-are stripped so you never see a meaningless "sessio"; then the turn response ≤200 chars — no LLM summarization). Strictly outbound: sent straight through the WeChat API, never written into any session. Notifications that fail on a stale context_token are queued (≤20, 24h) and merged-delivered on the peer's next inbound message — a new day never requires a two-way message to "activate" anything. - 📤 Outbound media: the agent calls the
wechat_send_filetool to upload a local image/video/file to the WeChat CDN and send it to the current peer (routed by extension, optional caption). - 📥 Inbound media: images/files/videos/voice are downloaded from the CDN and AES-decrypted, parked under
WeChatSpace/inbox/<date>/and described by path; images are attached as native image content when the selected model declares image input. - 🧠 GUI-equivalent context: each day's session is created with the user-global
~/.dsh/AGENTS.mdand the available skill catalog (<available_skills>) injected up front, mounting the same agent preset as the GUI. - 🚫 Interactive option UI disabled (hang-proofing):
ask_user_questionand other interactive-option tools are denied in WeChat sessions — their answer channel is the DSH web GUI, unreachable from the phone; questions and options are inlined as plain text instead, and the user replies with a normal message. - 💾 Self-contained persistence: accounts,
context_tokens, and poll offsets live in one atomic JSON file (~/.dsh/wechat-bridge/state.json); no database. Sessions live under~/.dsh/wechat-bridge/WeChatSpace. - 🔁 Automatic migration: the legacy
weixin-bridgedata directory and settings section are renamed once towechat-*; an account pauses for 60 minutes onerrcode -14(session expired).
Quick Start
Prerequisites
- DeepSeek Harness installed (
dsh webruns). - A WeChat account with
ilink botpermission (bot_type=3). - Note: the harness resolves bundle deps from the flat
~/.dsh/profiles/node_modulesfallback, so do not symlink the package from outside the profile tree (ESM); copy it under the profile. (Afile:dependency +dsh.profile.bundlesentry is the canonical registration; the copy is the booted artifact.)
Install (into the web profile)
One-line install:
dsh plugin --profile web add github:NattoCB/dsh-plugin-wechat-bridge
Manual install steps follow.
# 1. copy the plugin under the web profile's node_modules
# (keep vendored deps: qrcode/pngjs/dijkstrajs live in the plugin's own node_modules)
SRC=/path/to/dsh-plugin-wechat-bridge
DST=~/.dsh/profiles/web/node_modules/dsh-plugin-wechat-bridge
rm -rf "$DST" && cp -R "$SRC" "$DST"
# 2. register in the profile manifest (~/.dsh/profiles/web/package.json)
# dependencies: add "dsh-plugin-wechat-bridge": "file:<SRC>"
# dsh.profile.bundles: add "dsh-plugin-wechat-bridge"
# 3. (re)start dsh web — the bundle patch mounts the `wechat-bridge` service
# and serves the client settings tab at /plugins/<id>/client.js
dsh web
Bind by QR code
Open Settings → "微信桥接" (WeChat bridge) in the bottom-left of the DSH web UI → click "扫码绑定账号" (bind account) → the QR code renders inline (PNG data URL) → scan status auto-polls every 2 seconds → once confirmed in WeChat, the account is saved and the bridge enabled.
Or use the CLI in any DSH chat: /wechat qrlogin starts a login (returns a sessionId) → /wechat qrstatus <sessionId> polls the status; on confirmed the account is saved and enabled.
Run
Message the bot ("what's on today") — the agent answers as if you were in the GUI, and the reply comes back as plain text. The service mounts at boot; if settings.wechat-bridge.enabled is true it starts polling immediately, otherwise it idles until enabled.
Configuration
Options
| key | default | meaning |
|---|---|---|
enabled |
false |
boot-time autostart when the settings flag is absent; re-applied live on every change |
mediaEnabled |
true |
accept inbound media (download / decrypt / park) |
defaultProvider |
'' |
provider override for bridged sessions (empty = follow global default; editable in the Settings tab). Re-pinned on every WeChat message — switching models inside the session does not affect WeChat replies |
defaultModel |
'' |
model override for bridged sessions (empty = follow global default; editable in the Settings tab) |
allowedPeers |
'' |
inbound allowlist: internal bot peer ids allowed to drive the agent (NOT WeChat aliases), comma-separated; empty = deny everyone (fail-closed); editable in the Settings tab |
notifyEnabled |
true |
one-way session notifications: every top-level DSH session's turn end pushes a fixed-template digest to the allowlisted WeChat peers; toggle in the Settings tab |
dataDir |
~/.dsh/wechat-bridge |
where state.json (accounts / tokens / offsets) lives |
defaultCwd |
'' |
working dir for new sessions (else ~/.dsh/wechat-bridge/WeChatSpace) |
enabled, mediaEnabled, defaultProvider, defaultModel, allowedPeers and notifyEnabled also live in the wechat-bridge: section of ~/.dsh/settings.yaml; editing and saving re-applies them live:
wechat-bridge:
enabled: true # live toggle; the service re-applies on every change
mediaEnabled: true
defaultProvider: '' # bridged-session provider (empty = follow global default)
defaultModel: '' # bridged-session model (empty = follow global default)
allowedPeers: 'NhatoCola_F, abCdEf_12345' # inbound allowlist (internal bot peer ids), comma-separated
notifyEnabled: true # one-way session notifications (see below)
Troubleshooting: errcode -14 "session timeout"
The ilink API reports business errors INSIDE HTTP-200 bodies
({"errcode":-14,"errmsg":"session timeout"}); the plugin now surfaces them as real errors:
- Polling: getupdates answering -14 means the bot session expired server-side — the poller pauses 60 minutes and the Settings status card shows a red warning. Recovery: re-scan the QR code (polling resumes automatically afterwards, no restart).
- Notifications/replies: proactive pushes are rejected once the peer's context_token expires. Failed pushes enter a backlog (≤20, 24h) that is merged-delivered automatically on the peer's next inbound token refresh; the send-test button (
POST /wechat-bridge/notify-test) verifies on demand and also flushes the backlog.GET /wechat-bridge/statusexposeslastNotify,notifyBacklog,peerTokenAgeHoursand per-accounthealth. Note: one-way notifications do NOT depend on the day's WeChat session existing — sessions are created lazily on inbound; the real gate is context_token freshness.
Inbound allowlist (fail-closed)
allowedPeers is a deny-by-default inbound gate: only the listed ids may drive an agent session.
- Empty means nobody (the safe default, not everyone).
- These are NOT WeChat aliases or nicknames. The protocol identifies peers by an opaque internal id (e.g.
NhatoCola_F) that cannot be looked up from a WeChat profile. - How to get one: message the bot from that WeChat account — even when denied, the bot replies with the id; and the id then appears as a clickable chip under 「已对话过的 ID」 in the Settings tab's allowlist card.
- The Settings tab's allowlist card edits the list directly (comma-separated; both ASCII and full-width commas accepted; normalized on save) and persists to
settings.yaml; the/wechat-bridge/configAPI also accepts anallowedPeersfield. - Hot-reloaded, no restart.
One-way session notifications (notifyEnabled, on by default)
When enabled, the end of EVERY top-level DSH session's turn (GUI sessions, automation sessions, ...) pushes a fixed-template digest to the allowedPeers WeChat peers — pure template concatenation, no LLM in the loop:
【会话通知:<first 15 chars of session name, ellipsized...>(distinctive id badge, e.g. abcdef)】
<first 200 chars of the turn's final response, ellipsized...>
- Strictly one-way, mutual non-pollution: notifications go straight through the WeChat API and are never appended to any session or injected into any agent — the daily bridge session never sees them; a reply you send from WeChat still drives that day's session as usual. The bridge's own
wechat-*sessions are skipped entirely (their replies already reach the peer directly; this also prevents notify → reply → notify loops). - Filters: subagent children (
origin=subagentordelegationDepth>0) never notify;interruptedturn closers appended while reloading crash-orphaned logs never notify; a turn with no assistant text falls back to fixed placeholders by end reason (e.g.⚠️ 回合失败: ...). - Delivery condition: the WeChat ilink protocol requires a
context_token(originating from the peer's most recent inbound message), so only allowlisted peers who have messaged the bot at least once can be notified. Sends happen only while the bridge service is enabled andnotifyEnabled=true. - The Settings tab renders a toggle for this flag (persisted via the
/wechat-bridge/configAPI into settings.yaml);/wechat statusshows it too.
Runtime enable / disable (hot plug)
- Settings UI tab: status card (running state + enable/disable button, effective immediately; red warning when an account's poll session expired, pointing at re-scan), session-notifications card (one-way notify toggle + 「发送测试」send-test probe button + context_token expiry hint), allowlist card (direct editing + seen-in-conversation id chips + how-to-get-an-id hint), default-model card (hints that every WeChat message re-pins this model, so in-session switches don't apply) (two dropdowns pick provider/model from DSH's registered models), accounts card (account id, token status, last login time + remove), QR bind.
- Slash command (in any DSH chat):
/wechat status— running? account count? notification flag?/wechat enable— start the poll loop now (also writessettings.wechat-bridge.enabled=true)/wechat disable— stop the poll loop now (writessettings.wechat-bridge.enabled=false)/wechat accounts— list configured accounts/wechat qrlogin— start a QR login; returns asessionId/wechat qrstatus <sessionId>— poll scan status; onconfirmedsaves the account and enables/wechat rm <accountId>— remove an account
- Settings flag (hot-reloaded): edit
wechat-bridge.enabledin~/.dsh/settings.yaml; saving re-reads the flag and starts/stops the loop.
The UI tab calls the plugin's own HTTP API (/wechat-bridge/*) served by the host webserver — no external service involved.
Session model
- Session id:
wechat-<chatId>-<YYYY-MM-DD>(local machine timezone, e.g.2026-08-15); created lazily on the first inbound message of the day, never pre-created at midnight. - Title:
<YYYY-MM-DD>, pinned with theusertitle source so automatic title generation never overwrites it. - Default cwd:
~/.dsh/wechat-bridge/WeChatSpace(created on boot; override withdefaultCwd). - Peer identity stays encoded as
weixin::<accountId>::<peerUserId>(protocol layer, shared with the CodePilot lineage); only the plugin's own naming useswechat-*.
Files
src/index.js WechatBridgeService: poll loop, agent-driving, per-day sessions, hot-plug, one-way session notifications, /wechat command, /wechat-bridge/* HTTP API (QR rendered server-side)
client/client.js Client bundle: registers the Settings "微信桥接" section slot (React)
src/weixin-api.js ilink bot protocol client (getupdates/sendmessage/sendtyping/getconfig/qrlogin)
src/weixin-media.js inbound media CDN download + AES decrypt, outbound media CDN upload
src/weixin-ids.js synthetic chatId encode/decode (weixin::<accountId>::<peerUserId>)
src/weixin-types.js protocol enums/constants
src/notify.js one-way session-notification pure helpers (template rendering, turn-text extraction, session-name/subagent filters; unit-tested)
src/store.js JSON-file persistence (accounts, context_tokens, offsets; legacy-dir migration)
cordis.patch.yml bundle patch (registers service `wechat-bridge`)
package.json declares dsh.bundle + dsh.client (web)
node_modules/ vendored qrcode/pngjs/dijkstrajs (QR data-URL rendering, no pnpm needed)
Notes / scope
- Outbound media is agent-initiated via the
wechat_send_filetool; inbound voice is parked on disk only (no transcription). - Private chat only; no group semantics.
- Requires a WeChat account with
ilink botpermission (bot_type=3). - Persistence is a single atomic JSON file (
state.json) — sufficient for one DSH process. - The per-chat queue serializes within one process; the cross-process poll lock and message dedupe cover the multi-process case (keep the port single-owned anyway).
Links
More in this category
zhu1090093659/dsh-web#packages/dsh-remote-web-ui★ 8370
Remote control of a dsh web workspace from phone or PC: QR-code pairing through a token-gated channel, SSE real-time sync, and separate mobile and full desktop GUI modes.
zhu1090093659/dsh-web#packages/dsh-ssh★ 8370
SSH ops panel for DSH: web terminal, SFTP transfer with progress, local port forwarding, and one-command cluster execution across hosts; agents share the same host config.
saya-ch/dsh-mobile★ 371
Access DeepSeek Harness from the Android app or a mobile browser with secure LAN and remote connections, persistent device pairing, and a customizable mobile interface.
ZSeven-W/dsh-ios★ 312
A live iOS Simulator or USB-connected iPhone inside the conversation: 22 agent tools for booting, building, driving the UI by accessibility identity or OCR text, list-row actions and SwiftUI preview hot reload, plus a streaming sidebar panel you can tap and drag on.
liguobao/ds-harness-remote★ 266
Multi-device remote access for DeepSeek Harness: continue an active session from your phone, tablet, browser, or another computer over an end-to-end encrypted channel (Noise IK + adaptive relay/WebRTC transport), with device authorization, ApiProxy-only remote capabilities, and read-only file preview via dsh-file-viewer — no shell, remote desktop, or write access.
wenbin-wb/dsh-bridge★ 184
Remote and mobile access for DeepSeek Harness: provides LAN QR code connection, Cloudflare/custom tunnels, WeChat, QQ, Feishu, Telegram bot integration, and security authentication.
Community comments
Comments are public GitHub Discussions. Loading them connects to GitHub and Giscus; a GitHub account is required to post.