dsh-time-watchdog
给 DSH 加一只计时看门狗:记录本轮对话从 turn/start 开始已经运行了多久,并据此做四件事:
- 模型侧:
clock_elapsed工具,随时读到本轮已用时间。 - 模型侧:
/set-time设定的分段提醒,在每步之前向模型注入一条隐藏提示词,催它收尾。 - 人侧:聊天气泡上方的卡片——周期显示「本轮已运行多久」,以及每条提示词发出时提示「提示词已发送」。
- 人侧:设置页里可编辑的 4 种提示词模板 + Toast 开关。
English (short): A turn timer for DeepSeek Harness. It folds
turn/startinto a session projection to answer "how long has this turn been running", exposes that as theclock_elapsedtool, and usesagent/pre-stepto inject wrap-up prompts on a/set-time <t1> <t2> <t3>schedule. Two cards above the composer show run-time and "prompt sent"; four prompt templates are editable in Settings. No network, no telemetry, config is one JSON file under~/.dsh/storages/.
安装
dsh plugin --profile <你的 profile> add dsh-time-watchdog
装完重启 DSH 桌面进程(宿主侧 lib/index.js 是 ESM,刷新页面不够)。
也可以直接从本仓库装(想跟 main 最新代码时):
dsh plugin --profile <你的 profile> add git+https://github.com/aawef/dsh-time-watchdog.git
本地开发:从源码目录接线# 1. 接到 profile(junction,不需要管理员权限)
powershell -Command "New-Item -ItemType Junction -Path \"$env:USERPROFILE\.dsh\profiles\<profile>\my-plugins\dsh-time-watchdog\" -Target \"<本目录绝对路径>\""
# 2. 在 ~/.dsh/profiles/<profile> 里加依赖并装
# package.json → dependencies: "dsh-time-watchdog": "file:../web/my-plugins/dsh-time-watchdog"
# package.json → dsh.profile.bundles: 追加 "dsh-time-watchdog"
pnpm install
它做什么
| 能力 | 说明 |
|---|---|
| 计时 | 折叠单元 watchdog 在 turn/start 记下时刻,在 turn/end 结算总耗时 |
| 读数 | 工具 clock_elapsed 返回本轮已用时间(进行中持续累计,已结束冻结) |
| 周期卡片 | /clock 设的间隔,弹卡显示已运行时长 |
| 收尾提醒 | /set-time <t1> <t2> <t3> 三段式排程,到点注入提示词 |
| 提示词定制 | 4 种模板(开始 / 未超时周期 / 首次超时 / 超时后周期),设置页可改 |
| 配置 | ~/.dsh/storages/dsh-time-watchdog.json |
计时口径
与 DSH Web 客户端右上角那个「深度求索中,用时 12s」完全一致:
- 起点 =
turn/start事件的时刻; - 只有轮处于 open 状态才继续计时,
turn/end之后读数冻结; - 向下取整到秒。
区别只在于取数方式:客户端读的是浏览器内存里的 timeline 投影(宿主侧不存在等价物),
本插件自己折叠一个 session projection 存时刻,因此读数 O(1) 且不碰
session.eventAt()(该 API 已标 @deprecated,注释写明 "new calls are prohibited")。
工具
clock_elapsed
无参数。返回:
{
"turn": 7,
"running": true,
"elapsedMs": 93000,
"elapsedSeconds": 93,
"elapsedText": "1m 33s"
}
模型读到的正文是「第 7 轮:本轮进行中,已运行 1m 33s(93000 ms)。」,JSON 作为第二段附在后面供解析。
running: false 表示本轮已结束,elapsedMs 是这一轮的总耗时。
收尾提醒:/set-time
/set-time <t1> <t2> <t3>
每段可写裸秒数 30,也可写复合时长 30s、2m、1h、1h20m30s
(单位大小写不敏感,顺序必须 h→m→s,允许跳级如 1h30s)。
| 参数 | 含义 |
|---|---|
t1 |
到 t2 之前,每 t1 提醒一次 |
t2 |
预期可工作时间(截止点) |
t3 |
超过 t2 之后,每 t3 提醒一次;t3 <= 0 关闭这一档 |
示例:/set-time 30s 5m 1m(等价于 /set-time 30 300 60)= 预期 5 分钟,
期间每 30 秒提醒一次;到 5 分钟发一次「首次超时」,之后每 60 秒再催一次。
长跨度直接写 /set-time 10s 1h20m30s 2m。
参数上限 86400 秒(24 小时)。
生效规则(只对一轮生效)
| 下达时机 | 行为 |
|---|---|
| 当前没有进行中的对话 | 挂起,等下一轮 turn/start 时才真正开始计时 |
| 当前有进行中的对话 | 从指令发出时刻起算,并立即注入「计时已生效」提示词 |
该轮 turn/end |
计划被清理,不会沿用到后续轮次;要再次生效必须重新执行 |
未指定的两份提醒(开始、首次超时)在每轮里各只发一次。
边界情况
t1 >= t2:跳过「未超时周期提醒」这一档(否则它永远不触发,等于骗人)。t3 <= 0:「首次超时」照发,「超时后周期」关闭。- 长工具执行期间不触发:提醒只在
agent/pre-step发生,工具跑完进入下一步时才会到点。 - 到达截止点只是注入提示词,不会中止模型或强制结束本轮。
实现要点
- 通道是
agent/pre-step(与官方dsh-time-context同一条路)。pre-step能替换进入该步的消息, 所以注入的提醒会在同一个模型请求里被读到,而不是等到下一个请求。 - 注入用
createUserMessage({ source: { kind: "dsh-time-watchdog", form: "notice", summary } })。source.kind !== "user"的user/message在客户端归为context节点、不渲染成聊天气泡—— 所以模型读得到、界面上看不到,正是「隐藏提示」要的效果。 - 计划(
t1/t2/t3、deadline、已发档位)存在内存的Map<sessionId, plan>里, 不写 session 事件:命令本身不改事件流,而计划的生命周期完全由 turn 边界决定。 - 排程是纯函数
stepPlan(plan, now),输入当前时刻,输出「该不该提醒 + 新计划」, 因此可以完全脱离时钟被测。 - 到达截止点后每
t3秒一条,一轮最多十几条user/message,是有意的取舍 (要「每次提醒都进模型上下文」就不可能不落历史)。
提醒卡片(toast)
两张卡片
| 卡片 | 触发 | 内容 |
|---|---|---|
| 已运行 | 本轮进行中,每 /clock 秒 |
本轮对话已运行 Xm Ys(+ 第 N 轮) |
| 已发送 | 宿主每注入一条提醒(投影 injectSeq +1) |
「提示词已发送」;在设置页打开「显示详细内容」后,卡片里还会带上本条提示词的正文 |
两张都停留 5 秒(HOLD_MS)后消失。
设间隔
| 命令 | 效果 |
|---|---|
/clock 30 |
每 30 秒弹一次 |
/clock 2m |
每 2 分钟弹一次 |
/clock off 或 /clock 0 |
关闭 |
/clock |
回显当前设置 |
允许 1–3600 秒。设为 off 时周期卡片完全不出现。
行为
- 只在本轮进行中计时;
turn/end之后不再弹。 - 第一次弹卡在「本轮开始后 t 秒」,之后每 t 秒一次,间隔从上次弹卡时刻算,不累积漂移。
- 换到新一轮时,弹卡基线清零,从新的
turnStartedAt重新算。 /clock改完配置,页面最迟 3 秒(CONFIG_POLL_MS)跟上,不需要刷新。
实现要点
- 卡片挂在
conversation.input.overlay(composer 卡片顶部的零高锚点data-composer-overlay),子元素用bottom:100%浮在输入框上方。 - 客户端读的是
useProjection("watchdog"),所以watchdog折叠单元必须带wire块; 没有wire的单元是 host-only,浏览器端读到undefined。 - 客户端拿不到宿主内存,配置走同源
/dsh-time-watchdog/api/config。 - 样式只用
--dsw-alias-*主题变量,不require任何 Harness Client 包。
设置页
设置 → 「计时与收尾提醒」(settings.section 槽位,id dsh-time-watchdog,order 92)。
- 编辑 4 种提示词模板,改动即时保存
- 「恢复默认模板」把四个文本框填回默认值(仍需点保存)
- 「发送提示词时显示 Toast」开关(关掉后不弹卡片,注入与计时逻辑不受影响)
- 「在『提示词已发送』卡片里显示详细内容」开关(默认关;开了就带上本条提示词正文)
- 说明区列出
/set-time的参数语义与「只对一轮生效」的规则
占位符
| 占位符 | 替换成 |
|---|---|
&t1& &t2& &t3& |
本次配置的三个参数,渲染成紧凑时长(30s / 1m30s / 1h20m30s) |
&t4& |
未超时时 = 剩余时间;超时后 = 已超时时间,格式同上 |
四个占位符都自带单位,模板里不要再写「&t2& 秒」,否则会渲染成「1m30s 秒」。
保存前不校验;注入前统一替换。渲染后会再扫一遍清掉残留的 &tN&,
所以就算模板里写了不存在的 &t9& 也不会把占位符漏给模型。
HTTP 端点
/dsh-time-watchdog/api/config
| 方法 | 行为 |
|---|---|
GET / POST |
返回 { ok, intervalSeconds, toastOnPrompt, toastDetail, templates, defaults, templateKeys } |
PUT |
局部更新(body 里给哪些字段就改哪些),返回更新后的完整配置 |
webServer 缺席(CLI / 无 UI 组合)时端点不注册,插件其余功能照常。
生效规则
| 文件 | 改完之后 |
|---|---|
lib/index.js / src/* |
重启桌面进程 |
lib/client.js |
刷新页面(客户端资源是 no-cache 的) |
package.json |
重启;改了依赖还要 pnpm install |
cordis.patch.yml |
重启 |
注意:~/.dsh/profiles/desktop/node_modules/dsh-time-watchdog 是 pnpm 装出来的副本,
不是软链。改完源目录要重新 pnpm install(或手 copy)才会进宿主。
配置文件
~/.dsh/storages/dsh-time-watchdog.json
{
"intervalSeconds": 30,
"toastOnPrompt": true,
"toastDetail": false,
"templates": {
"start": "…",
"before": "…",
"firstOverdue": "…",
"afterOverdue": "…"
}
}
intervalSeconds: null = 周期卡片关闭;toastDetail: true = 「已发送」卡片带正文。文件坏了/读不到时一律退回默认值,不影响插件加载。
设置页保存时会写全四个模板,所以文件里看到的一定是完整形状。
测试
node --test "tests/*.test.mjs" "tests/*.mjs"
| 文件 | 条数 | 覆盖 |
|---|---|---|
tests/watchdog.test.mjs |
14 | 折叠单元与读数口径 |
tests/timing.test.mjs |
24 | /set-time 解析、模板渲染、stepPlan 排程决策 |
tests/client.test.mjs |
16 | 客户端纯函数(vm 沙箱里求值 lib/client.js) |
tests/client.render.test.mjs |
13 | 迷你 React 渲染卡片,验证弹/不弹/撤/换轮 |
tests/config.test.mjs |
10 | /clock 参数解析与配置归一化 |
tests/host.test.mjs |
12 | 假 ctx 跑 apply(),验证注册、折叠、命令回执、pre-step 注入 |
前五个都不 import 宿主包,在插件目录内即可跑;host.test.mjs 需要能解析
@deepseek-ai/dsh-llm 与 dsh-tools(只在 profile 的 node_modules 下成立),
解析不到时整组 skip 而不是报错。
源目录跑 78 条(77 pass + 1 skip)——那 1 skip 就是 host.test.mjs 整组;
在 profile 的 node_modules 副本里跑同一套命令是 89 条全绿。
(npm 包里不含 tests/,所以用 npm 装出来的副本要跑测试得先把 tests/ 拷进去。)
No comments yet. Be the first to write one.