dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
Deepseek Harness在Temux环境中使用的通知插件,通过Termux:API给手机发送消息通知或TTS语音通知。
给 DSH 加一条通知通道:需要你选择、或一轮结果出来时,用 Termux 给手机发系统通知。
点一下通知,直接用系统浏览器回到 DSH 页面 —— 不用在通知栏上做任何操作。
在 Termux 里跑 DSH 时,你通常会切到别的 App 等结果。于是有两种尴尬:
装了这个插件,这两种时刻都会推到手机通知栏。
通知分成两类,处理方式完全不同:需要你操作的会弹横幅打断你,结果类只安静地进通知栏。
| 场景 | 悬浮标题 | 悬浮正文 | 语音 |
|---|---|---|---|
需要你选择(ask_user_question、计划审阅…) | ❓ 需要选择 | 问题摘要 + 选项 | 需要你选择,{摘要} |
| 需要确认工具调用 | ⚠️ 需要确认 | 工具:{工具中文名} | 需要你确认,{工具中文名} |
| 权限请求 | 🔐 权限请求 | {resource} | 请求权限,{resource} |
| 场景 | 悬浮标题 | 悬浮正文 | 语音 |
|---|---|---|---|
| 任务完成 | ✅ 任务完成 | 本轮对话已结束 + 会话标题 + 结果摘要 | 任务完成 |
| 长任务完成(默认 ≥ 60 秒) | ✅ 长任务完成 | 耗时 {duration} + 会话标题 + 结果摘要 | 长任务完成 |
| 出错 | ❌ 出错了 | {error 摘要} | 任务出错 |
| 被中止 | ⏹ 已中止 | 用户取消了操作 | 任务已中止 |
三条设计原则:
bash → 命令行、str_replace_editor → 编辑文件),
否则 TTS 念英文变量名很生硬;其余默认行为:振动 1 秒、点通知用浏览器打开 DSH 页面、通知栏上没有按钮。
Termux 环境里能跑 DSH Web(dsh web)。
安装 termux-api 包:
pkg install termux-api
安装「Termux:API」应用(APK,必须单独装,只装上面的包不够)。
插件强依赖 Termux API 一路通道:没有 termux-notification + Termux:API 应用就发不出通知
(插件会在启动时探测,缺失就停用并在设置页的「检测环境」里写清原因):
⚠️ 这一步不能省。缺少 Termux:API 应用时,
termux-notification不会报错,而是一直挂起, 所以插件会在启动时探测它,发现缺失就直接停用并写一条警告日志(避免每次通知都留下一个僵尸进程)。
顺手检查一下通道是否通(装完应用后跑):
PLUGIN_DIR="$HOME/.dsh/profiles/web/node_modules/dsh-termux-notify"
bash "$PLUGIN_DIR/scripts/check-channel.sh"
它会依次检查 termux-notification 命令、Termux:API 应用,并带 10 秒硬超时真发一条测试通知。
退出码 0/1/2/3 分别表示:可用 / 缺命令 / 缺应用 / 超时。
在 DSH 的对话框里粘上这一行,然后说一句「帮我安装这个插件」:
https://github.com/2966859746/dsh-termux-notify
DSH 会自己去看这个仓库、把它装进 web profile、并告诉你需要重启。你只需要在最后重启一次 DSH。
重启方式:在跑 dsh web 的那个 Termux 会话里按 Ctrl+C,然后照你平时的方式重新启动(如果用了一键启动脚本,就是 ~/dsh/start_dsh-terminal.sh)。
因为 profile 的
patchReload是startup,装完必须重启一次才会加载新插件。
如果不想让 DSH 自己动手:
# 如果 dsh 在 PATH 里
dsh plugin --profile web add github:2966859746/dsh-termux-notify
# Termux 上一般要显式用 node 启动(node_modules/.bin/dsh 的 shebang 在本机不可用)
node "$HOME/dsh/node_modules/@deepseek-ai/dsh/lib/bin.js" plugin --profile web add github:2966859746/dsh-termux-notify
这条命令会把这个包写进 $HOME/.dsh/profiles/web/package.json 的依赖和 dsh.profile.bundles。
装完同样要重启 DSH。
装完可以确认它确实被组合进了配置树:
node "$HOME/dsh/node_modules/@deepseek-ai/dsh/lib/bin.js" --profile web --dump-config | grep -A 25 dsh-termux-notify
重启 DSH 之后:
termux-notification 命令、Termux:API 应用、
点击行为和振动方式,每一项都给出状态和修复提示。三步都通过就装好了。任何一步有 ✗,按提示修即可(提示里会写清楚要执行什么命令)。
想更彻底地验证,可以跑一次完整自检 —— 它会检查 profile 组合、宿主半侧能否按包名 import、 客户端 bundle 能否被发现,以及(在源码仓库里)跑全部测试:
PLUGIN_DIR="$HOME/.dsh/profiles/web/node_modules/dsh-termux-notify"
bash "$PLUGIN_DIR/scripts/check-integration.sh"
安装副本里没有
test/,所以最后一步会提示「跳过测试套件」——这是正常的, 决定装不装得上的三项检查都会照常跑。
打开 设置 → Termux 通知。所有改动立即生效,不需要重启,也不需要按保存:
Esc 撤销本次输入;| 设置 | 默认值 | 说明 |
|---|---|---|
| 总开关 | 开 | 关掉后完全不发通知 |
| 需要你选择时 | 开 | ask_user_question、计划审阅等等待你回答的请求 |
| 需要授权时 | 开 | 敏感工具的一次性授权请求 |
| 结果出现时 | 开 | 一轮结束(turn/end) |
| 子 agent 轮次也通知 | 关 | 子 agent 的轮次默认不打扰你 |
| 最短耗时(毫秒) | 0 | 填 5000 就只通知耗时 ≥ 5 秒的轮次,过滤掉秒回的小轮次 |
| 长任务阈值(毫秒) | 60000 | 耗时达到该值的轮次用「✅ 长任务完成」的说法 |
| 附带结果摘要 | 开 | 把模型最后一段文本放进通知正文 |
| 摘要长度(字符) | 120 | 摘要截断长度 |
| 优先级(需要你操作时) | high | high / low / max / min / default;结果类固定 default |
| 提示音 | 开 | |
| 振动 | 1000 | 毫秒;0 = 不振动;也可写 500,1000,200 这种 pattern |
| 振动方式 | termux-api | termux-api = 调用 termux-vibrate(推荐,不受通知渠道设置影响);notification = 用通知自带的 --vibrate |
| 静音也振动 | 开 | 对应 termux-vibrate -f:系统静音/仅振动模式下也振 |
| 悬浮通知 | 开 | 「需要你操作」的通知用重要性 HIGH 的专用通道弹横幅;结果类不弹,避免打断 |
| 悬浮通道 | dsh-heads-up | 通道 id。改成新值(如 dsh-heads-up2)会建一个全新通道,用来修复被调低过重要性的旧通道 |
| 点击通知打开 | http://127.0.0.1:3080/ | 点通知时用 termux-open-url 打开这个地址;留空则改为打开 Termux 应用 |
| 通知分组 / id 前缀 | dsh | 同类通知覆盖上一条,三类各占一条 |
| 语音通知 | 关 | 额外调 termux-tts-speak 把通知念出来(走 NOTIFICATION 音频流) |
| 播报内容模板 | {speech} | {speech} = 该场景的短句(推荐);也支持 {title} {content} 自定义成完整句子 |
| 播报语言 | 空 | 例如 zh / en;留空则由系统 TTS 引擎自行决定 |
| 播报语速 | 1 | 1 = 正常。Termux:API 会覆盖手机里的语速设置,想和手机一致就填同一个值(见下面常见问题) |
| 播报音调 | 1 | 1 = 正常。同上,也会被 Termux:API 覆盖 |
| 去重窗口(毫秒) | 1500 | 同内容在这个窗口内只发一次 |
| 只写日志(dry-run) | 关 | 打开后只写日志、不真发通知,用来验证接线 |
| 启动探测 Termux:API 应用 | 开 | 缺失时直接停用,避免每次通知都留下挂起的进程 |
设置页写的是用户覆盖,存在 $HOME/.dsh/settings.yaml 的 termux-notify: 段。
想改部署默认值(对这台机器上所有会话生效、且不受个人覆盖影响),在
$HOME/.dsh/profiles/web/cordis.patch.yml 里按 id 覆盖:
- insert:
- id: dsh-termux-notify
name: dsh-termux-notify
config:
minTurnDurationMs: 5000
vibrateMs: 0
headsUp: false
patch 是整份 config 替换而不是逐键合并:没写出来的键会由插件内置默认值补齐, 所以只写你关心的几个键是安全的。
优先级:内置默认 < patch 部署配置 <
settings.yaml用户覆盖(设置页)。
收不到通知
scripts/check-channel.sh(见上文),它会直接告诉你是哪一层的问题。pkg install termux-api。acquire wakelock。振动不生效
0;termux-api,它会调用 termux-vibrate,不经过通知渠道,
比通知自带的 --vibrate 可靠得多(Android 8+ 上后者常被通知渠道设置忽略);notification,改回 termux-api;-f,系统静音/仅振动模式下也会振;500,1000,200 这种 pattern 时只能走通知渠道(termux-vibrate 只接受单个毫秒数),
检测页会说明这一点。悬浮通知不弹横幅
按这个顺序排查(前两条最常见):
dsh-heads-up)。
如果你(或系统)把这个通道的重要性调低过,程序改不回来 —— Android 以用户设置为准。
这时把设置页的「悬浮通道」改成新值(例如 dsh-heads-up2),插件就会建一个全新的 HIGH 通道。顺带说明一个已修的坑:Termux:API 把
--id当通知 tag 用,同一个 tag 重发是「更新」已有通知, 而 Android 不会为更新弹横幅。所以「需要你操作」的通知每次都换一个新 tag (代价是这类通知不再互相覆盖,会在通知栏里各占一条);结果类仍用固定 tag 覆盖上一条。
语音的语速 / 音调和我在手机「文字转语音」里设置的不一样
这是 Termux:API 的固定行为,不是插件改写你的设置。它每次播报都会调用:
mTts.setPitch(...); // 不传 -p 就是 1.0
mTts.setSpeechRate(intent.getFloatExtra("rate", 1.0f)); // 不传 -r 就是 1.0
也就是无论你手机里配的是多少,它都会按参数重设一遍,缺省值就是 1.0 —— 于是你手机里的语速被盖掉了。
而那个值在 Termux 里读不到(settings get secure tts_default_rate 需要 INTERACT_ACROSS_USERS 权限),
插件没法自动跟随。
想和手机一致,就在设置页把「播报语速」「播报音调」填成手机里的同一个值即可。
手机上的语速滑块一般会显示倍率(例如 1.0x / 1.3x),照着填就行。
语音通知不出声
先在 Termux 里手敲一次,这是最快的判断:
termux-tts-speak 测试
Ctrl+C 退出)→ TTS 引擎被占住或排队堵住了。常见于短时间内反复触发播报、
或上一次被强杀后残留了等待中的请求;重启 Termux(或重开 Termux 会话)即可恢复,实测恢复正常后
4 个字约 2.7 秒。插件侧也做了防护:超时按文本长度自适应(20–90 秒),卡住时会被杀掉且不会留下挂起进程;
上一条还在念时会跳过新的而不是排队;连续失败到阈值后本次运行自动停止播报并在日志里说明;termux-tts-engines 应输出 JSON;NOTIFICATION 音频流,所以系统静音时通常不出声 —— 这是 Android 的行为,不是插件问题;另外:上一条还在念的时候来了新通知,插件会跳过这次的播报而不是排队 —— 通知是即时提醒,排队念一串过期消息没有意义。
点通知没有打开浏览器
http://127.0.0.1:3080/);termux-open-url 可用(它随 termux-tools 提供,检测页会检查)。看不到「Termux 通知」这一页
patchReload 是 startup);scripts/check-integration.sh,它会指出是 profile 组合、宿主半侧还是客户端 bundle 的问题;想先确认接线对不对,但不想被通知打扰
把「只写日志(dry-run)」打开,然后提问一次或跑完一轮,日志里会出现
[dry-run] 【需操作】❓ 需要选择 :: … :: 语音「需要你选择,…」 这样的记录。
node "$HOME/dsh/node_modules/@deepseek-ai/dsh/lib/bin.js" plugin --profile web remove dsh-termux-notify
然后检查 $HOME/.dsh/profiles/web/package.json 的 dsh.profile.bundles 里是否还留着
dsh-termux-notify —— 有的话删掉那一行。最后重启 DSH。
如果之前在设置页里改过值,可以顺手删掉 $HOME/.dsh/settings.yaml 里的 termux-notify: 段。
插件挂三个只读旁路观测点,不改变 DSH 原有行为:
| 观测点 | 类型 | 用途 |
|---|---|---|
user-questions/request | Cordis waterfall | 需要人类回答(提问、计划审阅) |
approval/request | Cordis waterfall | 敏感操作的一次性授权 |
session/event 的 turn/end | 事件流 | 一轮结束、结果出现 |
两个 waterfall 监听器都是「先发通知,再 next() 委托」,所以下游的回答者/审批者行为完全不变;
通知全部 fire-and-forget,异常只写日志,永远不会影响 agent 运行。
几个实现上的选择:
prepend:Cordis 的 waterfall 里不调用 next() 就等于否决整条链。若追加在末尾,
就要等真正的回答者(Web 前端)先交出结果 —— 也就是用户已经答完才轮到发通知,通知就失去意义了。termux-notification 会一直挂起等待广播回应,
所以启动时先探测、缺失就停用;真发时用 spawn + detached 起子进程,超时对整个进程组发 SIGKILL
(只杀直接子进程会留下挂起的广播进程)。termux-vibrate:直接调用系统 Vibrator 服务,不经过通知渠道,
绕开 Android 8+ 上「渠道设置把 --vibrate 吃掉」这个常见问题。--priority。
Termux:API 建通道时从 intent 的 priority extra 读重要性,但随包的 termux-notification-channel
包装脚本不传这个 extra(建出来是 DEFAULT,不弹横幅),所以插件直接调底层的
libexec/termux-api NotificationChannel ... --es priority high 来建通道。
另一个坑:给 --channel 传一个不存在的通道 id,通知会被系统直接丢弃 ——
因此插件先确认通道建好,建不成就退回默认通道,绝不引用不确定存在的通道。setSpeechRate/setPitch,缺省 1.0,
所以「不传」不是「跟随系统」而是静默变成 1.0;插件显式传 -r/-p,让它由设置项决定。termux-tts-speak 会阻塞到播完才返回,所以它的超时按文本长度自适应
(30–90 秒),不能沿用通知的 8 秒超时,否则长句会被念到一半杀掉;上一条还在念时跳过新的,
避免排队念一串过期消息。--id 是通知 tag(notify(tag, 0, ...)),同 tag 重发是「更新」,
而 Android 不为更新弹横幅。所以需要横幅的通知每次用新 tag。同理,这类通知不传 --group ——
通知分组在不少 ROM 上也会抑制横幅。--priority 由场景决定(需要操作的用配置值,结果类固定 default),
这样「任务完成」不会用横幅打断你。ctx.settings.installSection() 注册运行时命名空间,浏览器半侧往
settings.section 槽注册一页,两者 key 相同才会渲染出来。改动经 settings 服务持久化到
settings.yaml,因此不重启就能生效。仓库里的客户端 bundle 是手写的(window.__ModuleLoader__.load + CJS 风格 require),
不需要 tsdown/vite 构建,改完刷新页面即可。
git clone https://github.com/2966859746/dsh-termux-notify
cd dsh-termux-notify
npm test # 三套一起跑
node test/run.mjs # 46 项宿主:配置、场景内容、工具名映射、argv 组装、节流、停用、真实超时与进程组清理、悬浮通道与 tag、语音播报、环境检测
node test/integration.mjs # 用真实 cordis 加载插件,验证 waterfall 委托与 agent scope 派发
node test/client.mjs # 假浏览器 + 迷你 React 加载 client bundle,验证设置页渲染与写入
test/integration.mjs 需要 @deepseek-ai/cordis 与 @deepseek-ai/dsh-scope,
所以测试只在仓库里跑得起来(安装副本不含 test/,由 scripts/check-integration.sh 自动跳过)。
其中有一项会真的起一个 sleep 30 子进程,验证超时兜底确实杀掉了整个进程组;
悬浮通知的用例还专门覆盖了「通道建不成时绝不引用它」(引用不存在的通道会让通知被系统丢弃)。
MIT