dsh-wechat-media
Manifest validPersonal WeChat and WeCom (企业微信) for DeepSeek Harness: QR pairing, one DSH conversation per chat, two-way text/image/video/document/archive transfer, and a two-way WeCom intelligent robot (long connection) that also receives images, files and videos
dsh-wechat-media
Personal WeChat for DeepSeek Harness (dsh) — scan a QR
code once, then talk to your agent from WeChat: one DSH conversation per contact or group, and two-way
text, images, video, documents and archives. Several WeChat accounts can be paired side by side.
给 DeepSeek Harness 的个人微信桥:扫一次码,之后就能在微信里和 agent 对话 —— 每个联系人/群各自 一个独立会话,文字、图片、视频、文档、压缩包双向互通,支持多个微信号同时在线(小号 + 大号)。
What it gives you / 它能做什么
| Capability | Detail |
|---|---|
| QR pairing | Cards in the Plugins panel. The login token is stored under $DSH_HOME, so restarting dsh does not need a new scan; re-pairing is one click. |
| One conversation per chat | Every WeChat contact (and group) gets its own DSH session with its own context, created or resumed automatically. |
| Text both ways | Your message becomes a turn; the answer is chunked back to WeChat. |
| Media both ways | Images, video, documents and archives. Inbound files are downloaded and decrypted to $DSH_HOME/wechat-inbox/<date>/<chat>/; the agent gets the path, so a text model can read images through read_image. Outbound files are uploaded and sent by the agent through its tools. |
| Multiple accounts | Pair a spare account and your main account; each keeps its own token, cursor and session map. |
| Persistence | Accounts, receive cursor, per-peer context tokens, chat→session routes, allowlist and preferences all live under $DSH_HOME/dsh-wechat-media/. |
Requirements / 环境要求
- dsh 0.2.0-rc.2 or newer with the standard agent loop and tools (the shipped
dsh web/ desktop profiles qualify), Node.js >= 22 (Node 24 tested), Windows / macOS / Linux. - A WeChat account that can scan a QR code. A spare account is strongly recommended: personal WeChat automation is against WeChat's terms of service and carries a ban risk, whichever library you use.
- Outbound access to
ilinkai.weixin.qq.comandnovac2c.cdn.weixin.qq.com.
Install / 安装
# from GitHub
dsh plugin --profile web add github:wjh-ux/dsh-wechat-media
# or from a checkout
dsh plugin --profile web add link:/absolute/path/to/dsh-wechat-media
Restart dsh, then open Plugins → dsh-wechat-media and press Add an account (QR code).
重启 dsh,然后在 插件 → dsh-wechat-media 点「添加账号(扫码)」。
Pairing / 配对
- The card shows a QR code (it polls its own status and refreshes an expired code automatically).
- Scan it with WeChat on the phone, confirm on the phone.
- If WeChat asks for digits, type them into the card and submit.
- The account appears with a status dot:
monitoringmeans the long poll is running. - Add an account (QR code) stays available at all times — scanning a different WeChat account adds it as another account, so a spare and your main account can run side by side. Scanning the account you already have simply refreshes its login. Each row also has Pair again and Unpair.
- Opened the QR by mistake? Press Cancel — the attempt disappears immediately instead of waiting for the code to expire. (A QR lives five minutes; an attempt gives up after eight, and at most three QR codes are issued per attempt.)
The QR encodes the landing-page URL the platform hands out — scanning it is what completes the binding.
Each account is stored under its normalized ilink_bot_id (b0f5860fdecb@im.bot → b0f5860fdecb-im-bot),
with 0600 permissions on the files that hold a token; accounts, cursors and conversation routes are keyed
per account, so several can poll at the same time.
Where inbound files go / 收件目录
By default inbound media is decrypted into $DSH_HOME/wechat-inbox/<date>/<chat>/. That directory is
configurable, and the change takes effect without a restart:
- From the card - the field under Inbound files are saved in submits a plain GET form (a navigation, not a fetch), so it works even in browsers where the card's requests never reach the plugin.
- From the control panel -
/dsh-wechat-media/panelhas the same field and a restore default button. - With the system folder dialog - Browse… / 浏览… (card and control panel) or
GET /dsh-wechat-media/pick-folderopens Windows' own picker on the desktop and saves the choice. A browser cannot hand over an absolute path (a directory input only exposes relative names), which is why the dialog is opened by the host (powershell -STA) rather than in the page. - From the plugin config -
inboxDir: "D:\\wechat-files"in the plugin options, used when no runtime preference is set.
The order is preference (card/panel) → config → $DSH_HOME/wechat-inbox. Only an absolute path is accepted: a
relative one is refused with a message instead of being resolved against the process working directory, because a
silently different directory is worse than the documented default. GET /dsh-wechat-media/state reports the
effective value as inboxRoot, with inboxRootDefault and inboxRootConfig for comparison.
Using it / 使用
Just message the bot from WeChat. A few things the agent can do from inside the conversation:
- answer through the normal reply channel (the bridge relays its final text automatically);
- send extra messages or files with its tools:
| Tool | Purpose |
|---|---|
wechat_send_text | An extra text message, or a message to a different chat (to), pre-empting the automatic relay for that turn. |
wechat_send_file | Send a local file back to WeChat. Images (.jpg/.png/.webp/.gif) travel as pictures, videos (.mp4/.mov) as videos, everything else (pdf, docx, xlsx, zip, 7z, …) as a file attachment. |
wechat_status | Which accounts are paired and which conversations exist. |
Every incoming message is prefixed with [微信 <name> <wxid>] so the agent can pass the right to when it
answers a specific chat. Group ids look like 12345678@chatroom.
Remote approval / 微信里远程审批
When a WeChat-driven session asks for approval — a tool call the permission preset wants confirmed — the prompt goes to the same WeChat chat, not only to the GUI:
⚠️ 需要你批准一次操作(远程审批)
工具:pwsh
原因:write outside the workspace
回 1 = 批准一次 · 回 2 = 拒绝
Answer 1 / y / 同意 / 批准 / /approve to allow that one call, or 2 / n / 拒绝 / /reject to
refuse it. The answer is consumed — it never becomes a new turn. If nothing arrives within
approvalTimeoutSec (default 120 s), or the prompt cannot be delivered, or the turn is aborted, the request is
handed back to the GUI, so an approval is never silently dropped. Set approval: "ignore" in the plugin config
to keep approvals in the GUI only.
Contact allowlist / 联系人白名单
Every chat that writes to the bot is remembered, and the card lists them under Contacts & groups / 联系人 / 群, with Allow / Ignore / Clear per row and an Only accept allowlisted chats switch.
- The switch is off by default — otherwise a freshly paired account would look broken to everyone who writes.
- With it on, only chats marked Allow reach the agent; Ignore and undecided chats are dropped and logged.
- A decision takes effect immediately (no restart) and is stored in
peers.jsonbeside the accounts.
Friends only / 只处理好友私聊
Only answer friends (1:1 chats) — the card's second checkbox, or /friends on from a chat. The protocol
carries no "is a friend" flag, so friends means 1:1 chats: when the switch is on, every group message
is dropped, whatever the allowlist says about that group. Direct chats are unaffected.
- Off by default: an upgrade must not change who the bot answers.
- Group messages are ignored silently (that is the point) but never invisibly: each one bumps
counters.groupsSkipped,/statusprints已静默忽略群消息 N 条, and the card shows the same number. - It is one preference (
prefs.json), so the checkbox and/friendscan never disagree, and it applies to personal WeChat and WeCom alike. - Switching it on while a group is waiting answers once in that group to explain the coming silence. Turning it off works from a 1:1 chat or the card — a group message is skipped before it could be read as a command, which is the honest consequence of "silent".
WeCom (企业微信) / 企业微信
WeCom works alongside the personal-WeChat bridge, outbound through two paths and inbound through a third:
| Path | What you need | Direction |
|---|---|---|
| Group robot webhook | one webhook URL from a WeCom group (群设置 → 群机器人 → 添加) | out: text, markdown, images |
| Self-built app | corpid + corpsecret + agentid of an app (应用管理 → 自建) | out: text, markdown, image, file, voice, video |
| Intelligent robot, long connection | the robot's BotID + Secret (智能机器人 → API 模式 → 长连接) | both: messages in, answers back in the same chat |
Configure any of them on the plain page http://127.0.0.1:19387/dsh-wechat-media/wecom (also linked from the card
and the control panel), which is where you send a test message from as well. The long connection needs no public
address: the plugin opens a WebSocket to wss://openws.work.weixin.qq.com and answers each message through the
frame the platform sent, so a WeCom chat becomes an ordinary DSH conversation - the same session machinery,
allowlist, approvals and reply relay as personal WeChat. Approval requests reach a WeCom chat as a proactive
long-connection message and are answered there with 1 (allow once) or 2 (reject); anything else stays in the GUI,
exactly as on the personal-WeChat side. The agent gets two extra tools:
| Tool | Purpose |
|---|---|
wecom_send_text | Send text or markdown through the configured robot, long connection or app (to overrides the recipients). |
wecom_send_file | Send a local file as image / file / voice / video. Over the long connection (or through a self-built app) any type works; with only a group-robot webhook, images do. |
Files travel the other way too: a file sent to the robot is uploaded as a temporary WeCom asset over the long
connection (init -> 512 KiB chunks -> finish -> media_id) and then sent, so no app credentials are needed. Uploads
are capped at 100 chunks (~50 MB) and each chunk is retried twice, which is what the platform tolerates; a failure is
reported back instead of being swallowed.
Settings live in $DSH_HOME/dsh-wechat-media/wecom.json with mode 0600 - they contain a webhook key, a corpsecret
and the robot secret, so they never go into prefs.json or a cordis patch. GET /dsh-wechat-media/state reports
whether a secret is set, never its value.
What stays impossible is the callback API. It insists on a public HTTPS URL that the enterprise registers, so a bridge running on your own machine cannot receive that way; the long connection above is what makes two-way WeCom possible without one.
Media arrives too. Images, files and videos sent to the robot are downloaded, decrypted and written to the same
inbox the WeChat side uses ($DSH_HOME/wechat-inbox/<date>/<chat>/), then handed to the model as a path plus a
[企业微信图片] …用 read_image 查看 hint, so a text-only model with a vision plugin can actually look at them. The
scheme is the official one for the long connection (@wecom/aibot-node-sdk): aeskey is base64 of a 32-byte AES-256
key, the bytes are AES-256-CBC with the iv taken from the key's first 16 bytes, PKCS#7 padded to 32 bytes, and the
file name comes from Content-Disposition because the payload carries none. Note this is not the personal-WeChat CDN
scheme (AES-128-ECB). A download that fails says so in the chat instead of dropping the attachment silently, and
/state counts both outcomes (counters.mediaDownloaded / counters.mediaFailed).
Chat commands / 聊天里的命令
A message that starts with / is addressed to the bridge, not to the agent - it is answered immediately and
never becomes a turn. This works in personal WeChat and in WeCom, and only in chats your allowlist lets
through (a blocked chat cannot run commands either).
| Command | Aliases | What it does |
|---|---|---|
/help | /h, /?, /帮助 | Lists the commands. |
/status | /状态 | Transport, account, chat, DSH session (and how many times /new restarted it), the WeCom long connection's state including its last error, the counters, approvals waiting, the last message time and the media directory. |
/new | /reset, /新会话 | Starts a new conversation: the chat moves to a new session id, so the old history is neither resumed nor carried over (the transcript stays on disk). The reply says which segment you are on. |
/friends | /friend, /好友 | Turns only answer friends (1:1 chats) on or off: /friends on, /friends off, or no argument to toggle. Group messages are then dropped silently and counted. |
/delivery | /发送方式 | How a reply is sent: whole (the finished answer, once) or stream (word by word, WeCom only - personal WeChat cannot edit a sent message). No argument shows the current mode. |
/auth | /授权 | Authorization level for approvals: /auth 严格 / 适中 / 完全权限 / 仅查看 (also strict, moderate, full, readonly). No argument lists them. |
/interject | /插话 | Turns interjection on or off: with it on, a message sent while the agent is still answering joins that turn instead of queueing its own. No argument toggles. |
/tokens | /token, /用量 | Token usage per account (and per chat inside it). /tokens reset zeroes it; /tokens 7 sets the clearing window to 1-30 days. |
/stop | /cancel, /停止 | Interrupts the turn running in this chat's session, and says so honestly when nothing is running. |
Anything else starting with / gets the command list back rather than a guess; a bare / or // is an
ordinary message. Commands are counted (counters.commands) and recorded in /state.recentRoutes.
Token usage / Token 统计
The bridge records what each account spends, per model call, from the usage the harness reports on the
assistant/message session event - the numbers the model adapter returned, so nothing is estimated. Inside an
account the spend is also split per chat.
- A window you choose, 1 to 30 days (default 30). The counters clear themselves at the end of the window.
The reset is applied lazily whenever the ledger is read or written, so a host that was switched off for a week
starts a fresh window on its next breath instead of reporting a stale one - and
resets/lastResetAtonly move when there was actually something to zero. - Three ways to see it: the card's Token usage section (one row per account, a window picker and
Clear now),
/tokensin a chat, and a single line inside/status. - Where it lives:
$DSH_HOME/dsh-wechat-media/tokens.json. Only counters are stored - no prompts, no replies, nothing the model said.
Interjection / 插话
A message that arrives while the agent is still answering can either join that turn or wait for its own.
- Off by default - the message queues as its own turn, which is what every chat already does.
- On (the card's checkbox, or
/interject on): the message is handed to the running turn with the harness's ownagent.steer(), so it lands in that turn'snext-stepinbox and the model sees it at its next step rather than after the answer. (followup()- the queueing path - puts it innext-turninstead.) - The switch only steers when there really is a turn in flight for that chat and a live agent to steer;
otherwise the message queues, because "interject" must never mean "dropped on the floor".
/statusshows the state and how many messages have been interjected so far. - Personal WeChat and WeCom behave identically, and the relay keeps a queue of waiters per session - a second message can no longer replace the first message's waiter (which is what used to lose a reply).
Authorization / 授权档位
What this plugin does with an approval request it receives is a plugin decision, and it has four levels - the
card's menu, or /auth:
| Level | What the plugin does |
|---|---|
严格 strict | Ask in the chat for every request, and treat silence as no - it never silently falls back to the GUI. |
适中 moderate (default) | Ask in the chat; if nobody answers, the GUI's own prompt stays in charge. This is exactly what earlier releases did. |
完全权限 full | Never ask here: approvals are left to DSH (with a session policy that asks nothing, that means actions simply proceed). |
仅查看 readonly | Ask nobody and refuse everything that needs an approval - fail closed. |
Honest scope. DSH's own session policy - the file sandbox (read-only / workspace-write / danger-full-access) and whether approvals are asked at all - belongs to the DSH client, not to a plugin. A plugin can only decide what it does with a request it is given. So with a session policy that never asks, 仅查看 has nothing to refuse: the card and this table say so plainly, and the real read-only switch lives in DSH's own session settings.
Delivery / 发送方式
How an answer leaves the bridge - the card's Sending menu, or /delivery:
| Mode | What the chat sees |
|---|---|
整段 whole (default) | The finished answer, sent once. What every earlier release did. |
逐字 stream | The answer as it is written: WeCom's stream message is incremental, so the same req_id is sent repeatedly with finish: false and the final send (which carries the whole answer) closes it with finish: true. |
Only WeCom gets the preview, and that is a platform fact rather than a shortcut: personal WeChat has no way to edit a
message that was already sent, so a WeChat chat keeps receiving whole answers whatever the setting says - /delivery
and the card both say so instead of leaving the option looking broken. Deltas are throttled (roughly 24 characters or
600 ms, whichever comes first) because one message per token would be absurd; a preview that no longer fits a single
stream body is skipped rather than truncated, so the final answer is never cut short. counters.streamChunks counts
what was sent, and /status shows the current mode.
Voice / 语音
Understanding a voice message. Both platforms transcribe voice themselves, and it is their transcript that reaches the model - labelled, so the model knows it is reading speech rather than something typed:
[微信语音(平台转写)] 帮我把这周的日程整理一下
[企业微信语音(平台转写)] 明天上午十点提醒我开会
When the platform sends no transcript, that is said plainly ((平台未提供转写)) instead of being papered over.
Personal WeChat's audio is saved as well, raw .silk - that is the format the protocol hands over, and nothing on a
normal machine can decode it.
Speaking. Two tools, one per platform:
| Tool | What it does |
|---|---|
wechat_send_voice | Sends audio through personal WeChat - a voice bubble only when the file is SILK, otherwise an audio file (see the rule below). |
wecom_send_voice | Sends a voice bubble through WeCom (uploaded over the long connection, or the self-built app). |
Either tool takes text - which is spoken on this machine - or path to an audio file that already exists. The
speech is made locally: the system's own engine (SAPI on Windows) writes a WAV, and ffmpeg converts it to a chat
format (mp3 by default, amr which is what WeCom specifies for voice, or wav which needs no conversion at all).
No cloud service, no API key, nothing leaves the machine. If either piece is missing the tool names it
(windows speech (System.Speech) or ffmpeg) rather than failing silently, and a message it delivered counts as the
answer being delivered (DELIVERY_TOOLS), so the relay does not repeat it.
A voice bubble needs SILK on WeChat - so the rule is explicit
WeChat renders a voice bubble only for Tencent's SILK codec, and its API additionally requires
voice_item.playtime (the duration in seconds). An mp3/amr/wav item is accepted by the API and then dropped by the
client - measured here (the text arrived, the voice never did), and corroborated by an independent implementation
with the same symptom (hermes-agent#9971). ffmpeg cannot
encode SILK at all, and this plugin deliberately bundles no dependencies - so voiceDeliveryFor() promises only what
the platform can actually show:
| Target | Audio | What arrives |
|---|---|---|
| WeCom | amr (default), mp3, wav | a voice bubble (verified by ear) |
| personal WeChat | anything | an audio file - it arrives and it plays |
A voice bubble is not deliverable on personal WeChat, and that is measured rather than assumed: Tencent's own
OpenClaw WeChat channel never sends the VOICE media type at all (image/video/file only), the equivalent issue in
another implementation has been open and labelled blocked for months, and two live attempts here - mp3 inside an
open conversation window, then SILK with playtime - were each accepted by the API and then dropped by the client.
So the tool says what actually happens instead of promising a bubble.
The vendored codec is used for the other direction, which does work: WeChat sends voice as SILK, and nothing
else on a normal machine can play it, so an incoming voice message is decoded to a playable WAV in the inbox
(silkToWav(), falling back to the raw .silk if decoding fails). That is the same use Tencent's own channel makes
of silk-wasm 3.7.1 (MIT), vendored under vendor/silk-wasm/ with its
licence and the hash it was verified against; see THIRD-PARTY.md.
playtime is read from the finished file with ffprobe (0 when it cannot be read, and the item then falls back to
its minimum - never an invented number). The tool's reply says which of the two happened, so "sent" never quietly
means "dropped".
Diagnostics / 排障
- The host logs one line per inbound message with the envelope's identifying fields:
inbound group chat=… session_id=… group_id=… room=… from=… type=…— for a group that settles which id the platform expects as the send target. node scripts/probe-group-send.mjs "<chatId>"sends a labelled test message to one chat, prints the platform'sretfor each addressing variant, and then lists the chats the bridge already knows.GET /dsh-wechat-media/state(loopback only) returns accounts, monitors, preferences, peers and live pairing attempts.state.wecom.inboundErrorandstate.wecom.sessionIssueanswer "why did the bot stop answering this chat?": the first is the last failure while handling an inbound WeCom message, the second records what had to be done when a stored conversation could not simply be resumed (resume,create,retryResumeand any replacement id). Both are also shown on the/dsh-wechat-media/wecompage.GET /dsh-wechat-media/wecom/inbound-test?chatId=…&text=…&chatType=grouppushes one synthetic callback through the real inbound path (allowlist,friends only,/commands, media, the agent).chatTypedefaults tosingle;groupis how a group-only rule is checked without a second person in a real group. The JSON answer echoes thechatTypeit used, plusinboundErrorwhen handling failed. The personal-WeChat twin isGET /dsh-wechat-media/debug/inbound?chatId=…&text=…, where a chat id ending in@chatroommeans a group and a paired account is required.
Where things live / 文件位置
$DSH_HOME/dsh-wechat-media/ accounts.json, prefs.json, routes.json, peers.json
$DSH_HOME/dsh-wechat-media/accounts/ <account>.json (token, 0600) · <account>.sync.json (cursor)
<account>.context-tokens.json (proactive-send windows)
$DSH_HOME/wechat-inbox/<date>/<chat>/ inbound media, decrypted, with the real extension
$DSH_HOME/wechat-inbox/frames/ frame grabs and other working files the agent creates
Delivery semantics / 发送语义
- The agent's final answer is relayed automatically, chunked at ~2000 characters with a short delay
between parts. If the turn already used
wechat_send_textorwechat_send_file, the relay stands down, so an answer is never delivered twice. - Replies settle on that turn's
turn/end, not on whole-agent idle: a session shared with the GUI never goes idle, which is the classic way a bridge "loses" replies it actually received. - Messages are deduplicated per account by
message_id, and the bot's own messages (message_type: 2) are ignored.
Implementation notes / 实现要点
Everything here was learned against the live platform; each item broke something first.
qrcode_img_contentis a landing-page URL, not an image. The QR has to be rendered locally from that URL — which is exactly what the official client encodes. This package vendorsqrcode-generator(MIT, © Kazuhiko Arase) to emit an SVG.- The media key has two encodings. Outbound
aes_keyisbase64(32-character hex text)— base64 of the hex string, not of the 16 raw bytes. Inbound accepts both that andbase64(16 raw bytes), so a client must handle both. - The upload is AES-128-ECB ciphertext in the body (
Content-Type: application/octet-stream), and the result is thex-encrypted-paramresponse header. The reportedfilesizeis the padded ciphertext size (ceil((rawsize + 1) / 16) * 16), whilefile_item.lenis the plaintext size as a decimal string. - A
getupdatesclient-side timeout is normal control flow, not a failure: the server holds the request for its full window. Only real errors back off.-14inretorerrcodemeans the token is stale: re-pair.HTTP 403means a second poller is using the same token — one consumer per token. ctx.webServerprefix routes must be registered without a trailing slash. Its matcher testspathname.startsWith(prefix + "/"), so/x/would look for a double slash and never match.- Tool parameters must not carry
required: false. The harness rejects the schema and the whole plugin fails to load; an optional parameter simply omits the field. - A
link:-installed plugin cannotimport "@deepseek-ai/…". Node resolves from the plugin's own directory, which has no such dependency; the Cordis loader can resolve it against the harness root (ctx.loader.internal.import(name, baseUrl, {})). - A session created by a plugin needs
agentPresets.mountandmeta.agentPreset. Without the mount the agent publishes against the empty global layer: no tools, no prompt, nothing it could act with. - The plugin-panel card needs a client module that registers the slots itself (
plugins.bundle.configkeyed by package name,plugins.row.configkeyed<package>#<row id>), wrapped inwindow.__ModuleLoader__.load({ id, factory }). Exporting a schema is not enough, and a missing wrapper fails the web boot.
Limitations / 已知限制
- Group sending is unverified but well-founded. Receiving from a group works today. Every reference
implementation addresses a chat through
to_user_idalone — the official client, the Go library (daemon365/weixin-clawbot, whoseTargetholds onlyToUserIDand whoseChatTypeis inbound metadata hardcoded todirect) and the Python port (SiverKing/weixin-ClawBot-API, which has nochat_typefield at all). This package sends the same way. What is not confirmed against the platform is which inbound field carries a group's addressable id —group_idorsession_id; the host logsinbound group chat=… session_id=… group_id=… room=…when a group first writes, which settles it, and if the answer issession_idthe fix is one line indescribeEnvelope. - Stickers and unknown item types are dropped, but visibly. A message that carries neither text nor a
downloadable file is counted (
counters.unusablein/dsh-wechat-media/state) and logged with its item types instead of vanishing silently. - Voice is not bridged as audio. WeChat's own transcript rides along as text when present; the
.silkpayload is downloaded but not transcoded. - Proactive sends need an open conversation window. Without a stored context token for that peer the
server answers
ret=-2; write to the bot first and the window opens. - One poller per account token. Two processes using the same token get
HTTP 403. - No message history API exists on this platform; only live messages are seen.
- Media is capped at 100 MiB inbound by default (
maxMediaMiB).
Security / 安全
- No secret is stored in this repository, and none is written into a cordis patch. The bot token lives in
$DSH_HOME/dsh-wechat-media/accounts/(mode0600where the OS supports it). - The plugin never puts the token in a URL. Traffic goes to the two WeChat hosts above and nowhere else.
- The card's HTTP routes refuse anything that does not arrive over loopback.
- Use a spare WeChat account if you can. Personal-account automation is at your own risk.
Prior art / 相关工作
This is not the first WeChat bridge for dsh, and the protocol work stands on Tencent's own MIT-licensed client. Credit where it is due:
@tencent-weixin/openclaw-weixin(MIT, Tencent) — the authoritative implementation of the iLink bot protocol this package'silink.jswas ported from.BiBoyang/dsh-im-bridge— turn/approval push, in-chat approval, persistent dedup.Jesse-njx/dsh-chatnode-wechat— conversation-node routing and inbound image handling.zhengjy01/dsh-wechat-clawbot(and its upstreamlubaiUwU/DSH-WeChatClawBot) — the floating-ball QR panel and the gateway split.
Compared with those, this package concentrates on media: the others move text (one of them images) and none send video, documents or archives. Pick one bridge and stay with it — two of them polling the same bot token will collide.
License / 许可
MIT — see LICENSE. The vendored QR encoder and the ported protocol client keep their own notices, listed in THIRD-PARTY.md.
WeChat and Weixin are trademarks of Tencent. This project is unofficial and is not affiliated with Tencent or DeepSeek.
Comments
Loading…