DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

ruqinhu /

ruqinhu/dsh-weixin-link

Verified

Weixin iLink channel plugin for DeepSeek Harness (DSH): bidirectional send/receive with delivery receipts

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@6bc42c24

@ruqinhu/dsh-weixin-link

English | 中文

The Weixin (iLink) channel for DeepSeek Harness: receives messages, and delivers turn results without the model having to remember to send them.

1. The constraint

Constraint Consequence
Every send must carry a context_token a token can only come from an inbound message
No token can be minted a quiet night parks the 07:00 report until the owner speaks
A fallback channel would hide failure no fallback; parked text is replayed, never rerouted
Over-long payloads can return a message id and still not arrive the 2,200-char cap is enforced in code, not trusted to the model

2. Which turns are delivered

source.kind Origin Delivered
schedule cron task ✅
weixin-link owner messaged from Weixin ✅
user typed in terminal / web UI ❌ answer already on screen
runtime-context / skill-catalog / time-context injected context ❌ not speech
compact-checkpoint / provider / fallback compaction, title generation ❌ internal noise

Only the first user message names the origin: every turn is followed by injected context, so the last one would classify every turn as context and deliver nothing.

3. Mechanisms

Problem Decision Rejected alternative Cost paid
Who delivers the channel subscribes to session/event and sends on turn/end telling the model "you must call weixin_push" — it answered in-session and delivered nothing, pushed meta-commentary, and shipped errors as reports the model cannot choose silence, except via [SILENT]
Duplicate sends identical text within 10 min counts as delivered; a manual weixin_push stands auto-delivery down an LLM "is this worth it" gate: a round trip per message, and a silent "don't send" is indistinguishable from a crash duplicates outside the window still go out
Empty turns 本次无产出(timestamp) silence: "ran and failed" looks like "never ran" one message that may say nothing
Failed turns ⚠️ 回合未完成(reason,timestamp):本次没有可投递的内容。, at most one per session per hour sending the partial text: a fragment reads like an answer; skipping: a crashed 07:00 report looks like a day with no schedule the hourly floor is in memory, so a restart can add one
Images downloaded from …/c2c/download?encrypted_query_param=…, decrypted with AES-128-ECB, saved to inbox/, and the message names the files for read_image dropping them silently: on 2026-11-04 an image was sent twice and neither the agent nor the log saw anything, so the owner gave up ("不改了就这样吧") one download per attachment inside a background task, and a decrypt failure degrades to the placeholder
Other media folded into a placeholder the agent can act on ((收到 1 个文件…)), with the wire field skeleton appended to inbound-media.log pretending to have read a voice note or a file: the model would answer about content nobody read content still unread — the placeholder says so
Files downloaded, decrypted, saved to inbox/, and the message names the path for read or bash dropping them: a document the owner sent would be invisible to the agent an unidentified type is saved as .bin instead of being guessed
Voice downloaded, then silk → 24 kHz wav (pilk) → faster-whisper base int8 with the official kwargs (beam_size 5, condition_on_previous_text False, VAD on with 500 ms min silence, no_speech_threshold 0.6, log_prob_threshold -1); the transcript becomes the message feeding audio to the model: this deployment rejects input_audio with 422, so a transcript is the only route 5–10 s per message on a 1.9 GB box; a failed transcription degrades to the saved path
Receive vs send replay on its own timer + single-flight; bootstrap does not await it awaiting replay inside the poll loop: a 30/60/120 s backoff means 3.5 minutes deaf replay is serial — one slow send delays the rest
Liveness rewrite state/heartbeat.json every round; judge every 60 s; restart when no round has been attempted for 3 budgets (≥ 90 s) judging by last success: a loop failing every round gets restarted every tick, aborting the retry in flight cannot tell "hung" from "paused"
Replay deletion delete only on a provably permanent failure deleting on "anything not NEEDS_INBOUND/RATE_LIMITED": ret:-2 prepare failed is transient, and that rule ate a 12:40 report queue keeps entries that may never send
Batching turns not done coalescing near-simultaneous turns: delays a decaying report, complicates the cap, harder to replay occasional burst of separate messages

4. Failure handling

Class Action Why not the alternative
stale token park, do not retry only a new inbound can mint a token
rate limited back off 30/60/120 s, then park dropping after the ladder looks like "never scheduled"
other one immediate retry, then give up more retries turn a blip into a queue stall
no context_token at all (auto-delivery) drop, do not park replaying a week-old report is worse than dropping it
replayed entry fails keep it, never re-park a second file for the same text means every copy is sent

5. Measured

Item Result
Unit tests 248 across 21 files; tsc --noEmit clean
Delivery round trip inbound 10:00:04 → turn completed 10:00:24 → ok msgid=7523202391886746000 in the same second
Watchdog heartbeat advances every round, restarts: 0 across a 14-hour run
Receipts one JSONL line per attempt, keyed by the operator's local day

5.1 An outage, as the trace recorded it. Quota exhaustion (429 AccountQuotaExceeded), two consecutive builds on the same channel:

Time (UTC) Build Behaviour
10:40:29 old skip … turn=304 reason=not-completed
10:42:31 old skip … turn=305 reason=not-completed
10:44:34 old skip … turn=306 reason=not-completed
10:46:33 new send … turn=307 kind=failure len=44 → ok msgid=7522972414050248000
10:46+ new further notices held back: skip … reason=failure-notice-rate-limited

Four minutes of silence became one line on the phone saying the run did not finish.

6. Open questions

Question Current state
Token refresh cannot be done by the plugin; ~12 h without an inbound message parks sends
Recipients one poll loop, one recipient; multi-account needs a routing layer
Replay concurrency serial; per-entry locking would be needed
Failure-notice floor in memory; persisting costs a write per failure
Media / rich cards none — text is the only payload whose failure is observable here
Watchdog false positives a debugger pause or long GC looks like a stall; restart is harmless but recorded
Outbox validation none at write time; bad entries are skipped on read and logged

7. Compatibility

Item Value
DSH 0.2.2-rc.1 (peer dependencies pinned)
Runtime Node 22+, no native modules
Required services tools, credentials, sessionController; agents optional
Payload text out; inbound text, images, files and voice
Voice transcription needs python3 + faster_whisper + pilk on the host; sttCommand overrides the helper

8. Install, credentials, configuration

dsh plugin add @ruqinhu/dsh-weixin-link

The token is read through the DSH credential store (ctx.credentials.resolve()), not process.env: the store already layers $DSH_HOME/.credentials.yaml → environment → project/user .env, and reading the environment directly would shadow it.

Key Default Meaning
tokenEnv WEIXIN_BOT_TOKEN credential name for the bot token
accountIdEnv WEIXIN_BOT_ACCOUNT_ID credential name for the account id
baseUrl https://ilinkai.weixin.qq.com iLink endpoint
dataDir ~/.dsh/weixin-link tokens, outbox, receipts, trace
poll true run the inbound long-poll loop
pollTimeoutMs 40000 client budget; must exceed the server's 35 s hold
defaultUserId "" recipient; empty = most recent inbound sender
echoOwnMessages false surface the bot's own messages as inbound
sessionId "" session inbound is routed to, resumed when cold
replayIntervalMs 300000 outbox retry interval, own timer
dedupeWindowMs 600000 identical texts inside this window count as delivered
notifyTurnFailures true announce a turn that ended without completing
watchdogIntervalMs 60000 liveness check interval; 0 disables
enableStt true transcribe inbound voice locally; off keeps the file only
sttCommand "" override the bundled helper; {file} is substituted
sttTimeoutMs 90000 give up on one transcription after this long
sttPrompt simplified-Chinese hint initial prompt; without one base answers Mandarin in Traditional characters

9. Tools and files

Tool Purpose
weixin_push send now; parks automatically when it cannot deliver
weixin_status login state, token age, last delivery, outbox depth, poll liveness
File Content
receipts/YYYY-MM-DD.jsonl one line per send attempt, keyed by the +08:00 day
delivery-trace.log one line per turn decision, including skips and why
outbox/ parked messages awaiting a token
state/heartbeat.json last attempt/success, failures, inbound count, restarts
inbound-media.log one line per inbound non-text message: item types, field names/skeleton, and the image save outcome (no URLs, keys or bytes)
inbox/ inbound images, files and voice, named <messageId>-<kind><n>.<ext>, mode 0600
send session=... turn=12 kind=turn len=63
ok   session=... turn=12 msgid=7521135910923441000
skip session=... turn=13 reason=source-not-deliverable
skip session=... turn=14 reason=duplicate
watchdog-restart reason=stalled lagMs=142000 failures=0

10. Inbound routing

Situation Behaviour
session configured, cold start resume via sessionController.resolveAgent() (bare Agent, { agent }, or { error } — all three handled)
no session configured route to the first live root agent
no agent at all token cache still refreshed; that is what unblocks delivery

sessionController must be in static inject: cordis throws on an undeclared service rather than returning undefined, and a try/catch turns that into "messages arrive but nothing ever answers".

License

MIT

10. Release

Every version bump must produce a release; the tag alone is not enough. 0.4.3–0.6.4 shipped without one, so the server ran tarballs that GitHub never had.

node scripts/release.mjs --name "vX.Y.Z - summary" --notes notes.md

The script builds and packs when the tarball is missing, creates the tag and release, uploads the .tgz, and pushes the tag. An existing release of the same tag gets the asset attached instead of a second release. It reads GH_TOKEN / GITHUB_TOKEN, falling back to the github.com entry in ~/.git-credentials (credential.helper=store).

—/ 5

No ratings yet

Verified DSH bundle

Commit 6bc42c2463b6

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout