Skip to content
dsh.fish
Bundle

@dnalec/dsh-message-push

DeepSeek Harness 消息推送插件:会话停下(任务完成 / 中断 / 出错 / 阻塞 / 待审批 / 待回答)即推送到 QQ / Telegram / 飞书 / 微信,支持扫码接入与设置页

Source
DNAlec
License
MIT
Updated
Updated 2 days ago

Readme

# dsh-message-push

[English](README.en.md) | 中文

[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 的 Cordis 插件:**除了子代理以外,任何会话停下来都把消息推到配置好的消息平台**(QQ / Telegram / 飞书 / 微信),于是人不用一直盯着屏幕。

覆盖所有「停下来」的情形:

| 场景 | 触发点 | 推送文案 |
|---|---|---|
| 任务完成 | `turn/end(completed)` + `agent/status(idle)` | ✅ 任务完成 |
| 任务中断 | `turn/end(aborted)` | ⏸ 任务中断 |
| 任务阻塞 | `turn/end(blocked)` | 🚫 任务阻塞 |
| 任务出错 | `turn/end(error)` | ❌ 任务出错(附错误信息) |
| 达到长度上限 | `turn/end(max-tokens)` | ↯ 达到长度上限 |
| 需要审批 | `approval/request` | ⚠️ 需要你审批 |
| 需要回答问题 | `user-questions/request` | ❓ 需要你回答问题 |

每条推送带上会话名(标题 / 目录 + 会话尾号)、最后一段回复的预览,以及「打开网页处理」的链接。

本插件是**推送层**:不做审批决策、不在聊天里批复、不把入站注入 agent 会话。决策仍在网页;推送只负责把人叫回来。其他插件可以直接调用 `ctx.messagePush.notify({...})` 复用同一套渠道与去重。

## 安装

从 GitHub(钉 tag 更稳):

```sh
dsh plugin --profile web add github:DNAlec/dsh-message-push
```

从本地检出安装(开发时用):

```sh
git clone https://github.com/DNAlec/dsh-message-push.git
dsh plugin --profile web add "$PWD/dsh-message-push"
```

需要 `pnpm`。装完**重启 `dsh web`**:profile 组合在启动时解析,重启后设置页才会出现「消息推送」。
可选依赖(QQ 扫码创建机器人 / 飞书 SDK)随包声明,`dsh plugin add` 时由 pnpm 安装;缺了只影响对应渠道,插件照常工作。

从 npm 安装(发布后):

```sh
dsh plugin --profile web add @dnalec/dsh-message-push
```

卸载:`dsh plugin --profile web remove @dnalec/dsh-message-push`,重启后生效。`~/.dsh/message-push/` 下的配置与凭据不会删除。

## 配置

打开 设置 → **消息推送**。

1. **选一个渠道并连接**(至少要有一个「已启用 + 已绑定目标 + 已连接」的渠道):
   - **QQ**:点「扫码创建机器人」用手机 QQ 扫码,或手填 AppID / AppSecret。扫码成功后会自动把扫码者设为推送目标。
   - **Telegram**:填 @BotFather 给的 Bot token,保存即开始长轮询。
   - **飞书 / Lark**:填开放平台 App ID / Secret(需要可选依赖 `@larksuiteoapi/node-sdk`)。
   - **微信**:点「扫码登录」(官方 iLink,仅私聊,建议专用小号)。
2. **绑定推送目标**:未绑定时,用手机私聊机器人发一条消息,然后在设置页「最近入站」里点选;或者直接在未绑定时回复 **`是`** / `确认` / `yes` / `ok`,该聊天即被设为推送目标。
   > 这是**私人 bot** 设计:谁先回复「是」,谁的聊天就成为推送目标。不要把机器人放到陌生人能私聊的地方。QQ 群聊必须 @ 机器人,并在设置里填发言人的 `userId`。
3. **点「发送测试推送」**:收到消息说明通道打通。
4. 按需调整**通知规则**:总开关、`webUrl`、预览字数、合并窗口、各类停止原因的开关、以及「子代理会话也推送」(默认关闭)。

## 触发与去重

- **只看主会话**:`session.header.origin === 'subagent'` 或 `delegationDepth > 0` 的会话默认跳过(可在设置里打开)。
- **同一 turn 只推一条**:`turn/end` 与 `agent/status(idle)` 到达顺序不保证;先到的先把「停下来」定型,等 `turn/end` 带来原因后再发(最多等 `reasonUnknownDelayMs`,默认 8s,超时按「会话已停下」发)。
- **待审批 / 待回答优先**:它们比「停下」更具体,立即推送;同一段停顿里紧跟的 `idle` 会折叠进这一条(追加「另有 N 条待处理」),不会再多发一条「任务完成」。
- **合并窗口**:同一会话的同类事件在 `repeatWindowSecs`(默认 60s)内合并,只在计数变化时补发一条更新的提醒,避免刷屏。
- **发不出去不影响宿主**:没有任何可用渠道时记 `PUSH_SKIP` / `PUSH_FAIL` 审计并按 60s 限频告警,会话照常运行。

## 服务 API(给其他插件)

插件启动时通过 `ctx.provide('messagePush', …)` 在 host plane 提供服务,任意插件可消费:

```js
// 方式一:可选消费,缺了也能跑
const push = ctx.get('messagePush')
if (push) {
  push.notify({
    kind: 'needs-approval',      // needs-approval | needs-question | custom | turn-stop
    sessionId,                   // 会话 id(用于去重与显示尾号)
    title: '修复推送插件',        // 会话名(可选)
    body: '需要批准:删除 3 个文件',
    toolName: 'bash',            // 可选
  })
}

// 方式二:硬依赖(服务不在就等它出现)
export const inject = ['messagePush', 'timer']
export function apply(ctx) {
  await ctx.messagePush.broadcast('自定义通知')
}
```

| 方法 | 说明 |
|---|---|
| `broadcast(text, opts?)` | 发给所有「开着 + 有目标 + 已连接」的渠道,返回 `{ sent, ok, failed, skipped }` |
| `send(channelId, chatId, text, opts?)` | 直发指定渠道/目标 |
| `notify(event)` | 走观察器管线(去重 + 窗口合并)推送一条通知 |
| `onInbound(fn)` | 订阅入站文本(只读)。返回 `{ dispose() }`;回调返回 `true` 表示已认领,宿主不再处理 |
| `channels()` | 当前真正可推送的渠道 id 列表 |
| `state` | 最近推送时间 / 结果 / 错误 |

设置页有 `overwriteCorrupt` 兜底;配置文件损坏时插件用内存默认值运行且**绝不覆盖磁盘**。

## 数据

全部在 `$DSH_HOME/message-push/`(默认 `~/.dsh/message-push/`,权限 `0600`)。不要提交。

| 文件 | 内容 |
|---|---|
| `config.json` | 渠道开关、推送目标、通知规则 |
| `secrets.json` | QQ AppID/Secret、Telegram token、飞书 AppID/Secret |
| `wechat.json` | 微信 iLink 登录态(botToken / 上下文 / 游标) |
| `audit.log` | `PUSH` / `PUSH_FAIL` / `PUSH_SKIP` / `BIND` / `INBOX` / `CONFIG` / `WARN` |

没有审计行,就说明这次没触发推送。

## 故障排查

| 现象 | 处理 |
|---|---|
| 设置页看不到「消息推送」 | 确认 `dsh plugin --profile web add` 成功并**重启** `dsh web` |
| 测试推送报「没有任何渠道可推」 | 渠道没启用 / 没绑 `chatId` / 没连上——看渠道卡片的状态行 |
| QQ 状态「未配置凭据」 | 扫码或填 AppID + AppSecret 后点「保存并连接」 |
| QQ 扫码报「未安装扫码依赖」 | 在插件目录执行 `npm install`(需要 `qrcode` 与可选的 `@tencent-connect/qqbot-connector`) |
| 飞书状态「缺少依赖」 | `npm i @larksuiteoapi/node-sdk` |
| 收到消息但内容为空 | 会话还没产生任何 assistant 文本(例如空回复);`previewChars = 0` 也会关闭预览 |
| 推送里没有「打开网页」链接 | 设置里填 `webUrl`,或让进程环境有 `DSH_WEB_URL` |
| 群聊回复没人应答 | QQ 群必须 @ 机器人,并在设置里填该发言人的 `userId` |
| 子代理任务没有推送 | 默认就是关闭的;需要时打开「子代理会话也推送」 |

## 开发

```sh
npm test      # node --test tests/*.test.mjs
npm run check # node --check 全部源文件
```

结构:

```
src/index.mjs         宿主:事件接线、渠道装配、RPC、provide('messagePush')
src/watcher.mjs       会话停下观察器(去重 / 窗口合并 / 原因兜底)
src/format.mjs        推送正文渲染(纯函数,zh/en)
src/service.mjs       服务对象(broadcast / send / notify / onInbound)
src/inbound.mjs       入站:绑定目标 + 回执
src/replies.mjs       入站文本判定(纯函数)
src/store.mjs         配置与凭据读写(区分缺失与损坏)
src/audit.mjs         审计与限频告警
src/channels/         枢纽 + QQ / Telegram / 飞书 / 微信 适配器
src/provisioning.mjs  QQ 官方扫码创建机器人
client.js             设置页(React createElement,无 JSX)
locales.mjs           Client zh/en 文案
```

纯 JS、零 `@deepseek-ai/*` 依赖:只用 cordis 的注入名(`timer`)与运行时自带的 `fetch` / `WebSocket`。

改代码前先读 [AGENTS.md](AGENTS.md)。

## 许可

[MIT](LICENSE)

Install

dsh plugin --profile web add github:DNAlec/dsh-message-push

Profile: web

  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source