🔔 dsh-serverchan-notify
A DeepSeek Harness (DSH) plugin that pushes a Server酱3 (ServerChan³) notification to your WeChat every time an agent turn finishes an answer — and every time the agent asks you a question. Codex Stop-hook parity for DSH, plus the case codex never covered: an agent blocked on you.
English · 简体中文
What it does
- Subscribes to the DSH session event stream (
ctx.on("session/event", …)).
- On every
turn/end (completed / error / blocked / max-tokens / aborted), pushes one Markdown notification to Server酱3 → your WeChat. Each notification carries: conversation title, model, project directory, git branch, turn status, finish time, session id, and the latest reply excerpt (truncated at 16 000 chars).
- Also pushes when the agent asks you a question. An
ask_user_question tool call means the agent is blocked until a human answers — the worst case to miss — so it gets a notification of its own, carrying every question, its options, and their descriptions. Disable with notifyQuestions: false.
- Fire-and-forget: a failed push only logs a warning and never blocks or interrupts the agent loop.
- Skips subagent sessions by default (no spam from internal subtasks).
| codex Stop hook | this plugin |
|---|
| Trigger | one per finished turn | one per finished turn (turn/end), plus one per ask_user_question call |
| Key source | env / ~/.codex/secrets/… | env / config / $DSH_HOME/secrets/… (see SendKey resolution) |
| Failure handling | never blocks the turn | never blocks the turn |
| Scope | global hooks.json | global $DSH_HOME/cordis.patch.yml (or per profile) |
🔐 This repository contains no SendKey. Keys come from the environment, a file, or plugin config — never from source code.
Requirements
- DeepSeek Harness (DSH) — verified against
0.1.1-rc.2 and 0.1.2-rc.1 (@deepseek-ai/cordis ^4.0.1). The plugin reads the session log through whichever accessor the running Harness exposes, so both API lines are supported.
- Node.js ≥ 18
- A Server酱3 SendKey (free account at https://sct.ftqq.com/)
Quick start
1. Get a SendKey
Log in at https://sct.ftqq.com/, open the SendKey tab, and copy your key — it looks like sctp<number>txxxx…. The plugin auto-derives your dedicated push domain (https://<number>.push.ft07.com/send/<key>.send); legacy keys without a channel number use https://sctapi.ftqq.com/<key>.send.
2. Save the key (recommended)
mkdir -p ~/.dsh/secrets
echo '你的SendKey' > ~/.dsh/secrets/serverchan_sendkey
chmod 600 ~/.dsh/secrets/serverchan_sendkey
3. Register the plugin
Edit your DSH patch layer and restart the harness:
# global — all profiles (like codex's global hooks.json):
# $DSH_HOME/cordis.patch.yml (default ~/.dsh/cordis.patch.yml)
# per profile:
# $DSH_HOME/profiles/<name>/cordis.patch.yml
- insert:
- id: serverchan-notify
name: 'dsh-serverchan-notify'
config:
sendkeyFile: '~/.dsh/secrets/serverchan_sendkey'
Plugin rows are only resolved at boot — restart the harness process after editing.
Installing the package
The package declares a dsh.bundle manifest, so npm installation is one command:
# recommended
dsh plugin --profile web add dsh-serverchan-notify
# fixed version
dsh plugin --profile web add dsh-serverchan-notify@1.0.2
# GitHub monorepo fallback
dsh plugin --profile web add 'github:nickhelion/dsh-plugins#main&path:/packages/serverchan-notify'
# local development
git clone https://github.com/nickhelion/dsh-plugins.git
dsh plugin --profile web add "$PWD/dsh-plugins/packages/serverchan-notify"
The bundled cordis.patch.yml inserts the plugin row with all-default config; override any option by addressing the row id serverchan-notify from your own patch layer.
SendKey resolution
The first non-empty value wins, in this order:
| # | Source | Example |
|---|
| 1 | env var SERVERCHAN_SENDKEY | export SERVERCHAN_SENDKEY=sctp… |
| 2 | inline config sendkey | config.sendkey: 'sctp…' |
| 3 | env var SERVERCHAN_SENDKEY_FILE (path to a key file) | export SERVERCHAN_SENDKEY_FILE=… |
| 4 | config sendkeyFile (supports ~) | config.sendkeyFile: '~/.dsh/secrets/…' |
| 5 | default file $DSH_HOME/secrets/serverchan_sendkey | ~/.dsh/secrets/serverchan_sendkey |
Configuration
| Key | Default | Description |
|---|
sendkey | — | Inline key (overridden by the SERVERCHAN_SENDKEY env var) |
sendkeyFile | $DSH_HOME/secrets/serverchan_sendkey | Path to a key file; ~ is expanded |
reasons | [completed, blocked, error, max-tokens, aborted] | Which turn/end reasons trigger a push (interrupted is never pushed) |
notifyQuestions | true | Push when the agent calls ask_user_question and waits for you |
notifySubagents | false | Also push subagent sessions (off by default to avoid spam) |
timeoutMs | 8000 | HTTP timeout in milliseconds |
maxResponseChars | 16000 | Reply excerpt / question text truncation length |
disabled | false | Disable without removing the row (no key read, no subscription) |
Sample notification
Turn finished:
DSH 完成:
- 对话标题:…
- 模型:deepseek-official / deepseek-v4-pro
- 项目目录:
/home/you/project
- Git 分支:
main
- 回合状态:完成
- 完成时间:2026-08-18T21:00:00.000Z
- 会话 ID:
session-12
DSH 最新回复
…the latest assistant reply…
The agent is waiting on you:
DSH 提问:
- 对话标题:…
- 模型:deepseek-official / deepseek-v4-pro
- 项目目录:
/home/you/project
- Git 分支:
main
- 提问时间:2026-08-18T21:05:00.000Z
- 会话 ID:
session-12
DSH 正在等待你的回答
1. Choose Mode
Which path should I take?
- Ship it (Recommended) — small and reversible
- Plan first — one extra review round
2. Scope
Troubleshooting
| Symptom | Fix |
|---|
| "未找到 Server酱 SendKey" warning at boot | Provide the key via one of the 5 sources above |
HTTP 403 / timeout in the log | Network / proxy issue; the push domain is derived from the key (<n>.push.ft07.com) |
| No push after restart | Confirm the row id is unique and the package resolves — dsh --profile web --dump-config | grep -A8 serverchan-notify |
| Too many pushes | Turn on notifySubagents: false (default), trim reasons, or set notifyQuestions: false |
| Question reminder shows "无法解析本次提问内容" | The model emitted malformed tool arguments; the harness still got the question — open DSH to answer |
| Temporarily stop | disabled: true, then restart |
Development
npm install # installs devDependencies (cordis) for the smoke test
npm test # smoke test — stubbed fetch, no real push
REPORT=1 npm test # smoke test + print the assembled payload
npm run test:live # send one real test push with the configured key
Repository layout
lib/index.js plugin entry — event subscription, message assembly, HTTP push (turn end + agent question)
test-send.mjs standalone real push (same key resolution order as the plugin)
smoke-test.mjs cordis in-process test with a stubbed fetch
package.json package metadata + npm scripts
README.md English docs
README.zh-CN.md 中文文档
Contributing
PRs are accepted in the canonical nickhelion/dsh-plugins monorepo. Two ground rules:
- Never commit a SendKey (or any absolute machine path) — keys flow through env / file / config only.
- The event listener must stay non-throwing and fire-and-forget — a notification must never affect the harness.
License
MIT