DSH Plugins Marketplace

DSH Plugins

Plugins

/

dsh-task-timer

S

dsh-task-timer

Manifest valid

DeepSeek Harness plugin: a clock-led per-session working-time pill that subtracts the time spent waiting for your answers, whose dialog shows the span from session creation to its last activity (cappe

UI (client)hasBundlePatch

dsh-task-timer

DeepSeek Harness 插件:在对话最下方的仪表盘(composer dock)里,新增一个工作时长 pill, 展示本会话 agent 的工作时长;点开后给出创建对话至今;同时在左侧边栏、用户名那一行的 上方,显示全部对话的合计使用时长(1小时29分 · 对话总时长),紧接其下是同一批对话的 合计 token(3.3M · 对话总token)。侧边栏「工作区」里每条对话 右侧的时间补成 55分钟前,不再让人读成「这条对话跑了 55 分钟」。

它统计什么

工作时长 = 模型时间(llmMs) + 工具时间(toolMs) - 等待回答(awaitMs)。前两项直接取仪表盘 已在用的 sessionStats 投影,不再自己折叠一遍事件日志——仪表盘上「N轮N步」那个 pill 已经把模型时间和工具调用时间分开列出。

第三项是本插件唯一要自己折叠的东西。sessionStats 的工具时间是按 tool/call 到 tool/result 的真实跨度记账的,而 ask_user_question(以及 exit_plan_mode 的计划评审) 这类工具要等你回答才返回——你挂机两小时,它就诚实地记两小时工具时间。所以宿主侧另外 配对这几个调用,把它们的跨度单独累计成 awaitMs,再从读数里减掉。

因此天然不计入的还有:用户输入时间(user/message 与下一次 step/start 之间的空隙)、 轮次之间的挂机/空闲(tool/result 与下一次 tool/call、step/start 之间的空隙)——这些 时间从来不在 llmMs/toolMs 里,不需要减;跨轮挂机不需要额外处理,只有问句未答的那段 需要一个负数项,就是 awaitMs。

早于 1.4.0 的版本把「问了等你回答」的那段时间当成了工作时长:一次实测中,5 次提问的等待 合计 420.8 秒,占该会话读数 4.4%。口径修正见下,两侧(pill 与侧边栏合计)同步生效。

awaitMs 的配对规则与 sessionStats 逐字对齐,否则减掉的和记上的不是同一段:

  • 只有 ask_user_question / request_user_input / exit_plan_mode 这三种调用开等待; 带 MCP 前缀的副本(mcp__<server>__ask_user_question)按最后一段识别;
  • 等待在 tool/call 打开、在 tool/result 结算;未结算的等待不扣——那时仪表盘也还 没把它记成工具时间,提前扣会把读数扣成负数;
  • turn/end 时未结算的等待直接丢弃(取消/失败的轮次),sessionStats 同样是丢弃而不是 计时,所以这段本来就不在读数里。

计划评审(exit_plan_mode)走的是同一类等待,一并扣除。工具的审批弹窗没有对应事件, 日志里分辨不出来,因此仍按工具时间计——这是已知边界。

弹窗里的创建对话至今是另一个口径:最后一次对话活动 - 对话内容起点,是对话本身的墙钟 跨度,包含挂机和用户思考,但两端都钉在事件上:

  • 起点是第一条承载对话内容的事件(首条消息/首个 turn),不是会话 header 的 createdAt——桌面壳会在打开时就建好 Session 记录,第一条消息可能几小时后才来,按记录 起算会让几分钟大的对话读出 3 小时(实测一条 19:55 建记录、23:41 才首用的会话读出 3h46m)。日志里还没有内容事件时退回 createdAt;fork 继承的祖先日志时间都早于本 header,同样不会把起点拉长;
  • 终点是日志里最新一条推进对话的事件(消息、turn/step 边界、工具调用对),不是 Date.now()。因此一条对话没人再动、也没有新消息时,这个数停住不动,不会随着你挂着 窗口而一秒一秒往上涨;一旦有了新消息,终点前移到那一刻,跨度自然把中间的间隔算进去。

这是"只统计到本对话最后一条消息生成结束"的口径,与上面那个"纯 agent 工作秒数"互补:一个是 对话活了多久,一个是 agent 干了多久。

工作原理

  • 宿主侧 lib/index.js:通过 ctx.sessionProjections.register(...) 注册一个极简的 taskTimer 投影,发布四个端点——createdAt(建会话那一刻)、startedAt(第一条对话 内容事件的时间,0 表示还没有)、lastActivityAt (日志里最新一条推进对话的事件时间)、awaitMs(被问到答之间的累计跨度)。 createdAt 没有任何事件能改变;startedAt 由第一条白名单活动事件钉住一次、之后不动; lastActivityAt 只接受白名单事件(user/developer/system/message、turn/start|end、step/start|end、 assistant/attempt|message、tool/call|result),改名、改设置、归档这些元数据事件不算 活动,否则给会话改个名就能把空闲的时钟重新拨起来。取值点仍是 init(header, ...):这是 框架唯一把 header 交给投影的地方。
    • 游标单调:!(at > lastActivityAt) 直接丢弃回放里乱序/缺时钟的事件;游标初值就是 createdAt,所以 fork 继承来的那段日志(时间都早于本 header)不会把跨度拖成负数, 也不会抢走 startedAt(起点只接受不早于 createdAt 的事件)。
    • 等待表 awaitCalls 只在内存/检查点里维护({callId: 打开时刻}),不进 wire: 客户端需要的只是累计结果 awaitMs。
  • 浏览器侧 lib/client.js:向 conversation.composer.dock(对话底部仪表盘)注入 task-timer pill,排在官方 activity(TPS)和 usage(Token)之后。
    • 主标签沿用仪表盘的值在前、名称在后格式:51分24秒 · 工作时长;
    • 时长按需进位:不足 1 分钟显示 45.2秒,不足 1 小时显示 51分24秒,满 1 小时后 显示 1小时29分 并不再显示秒;分钟数不会溢出,不会出现 60分、89分 这类写法 (整小时显示 1小时);
    • 点击弹窗只给出仪表盘没有的两项:创建对话至今,以及被扣掉的 已扣除等待回答(后者 只在真的扣过数字时才出现)。模型时间/工具时间不在这里重复——「N轮N步」那个 pill 已经 分开列出,弹窗再列一遍是冗余;而扣减项必须写出来,否则 pill 的读数会比旁边的活动数字 略小,却没人解释差在哪。
    • 弹窗里的这一行打开时立刻刷新一次,打开期间每秒走一格;收起后在后台每分钟 轮询一次(OPEN_TICK_MS = 1e3 / IDLE_TICK_MS = 6e4),不常驻秒级定时器。 渲染值取 min(now, lastActivityAt) - (startedAt || createdAt):一旦追上终点(对话已空闲), 连定时器都不再启动,等新事件改写 lastActivityAt 再自然恢复。
    • pill 与弹窗标题前都带 primitives 的时钟图标(IconClockOutlineRegular,14px、与文字 间隔 6px),和旁边的 activity(仪表盘)、usage(数据库)两个 pill 一致;侧边栏 合计行仍为纯文字,不画图标。
    • 弹窗不自己复刻样式:运行时从 ui-chat 挂载的 stat-dialog.module.css 里读回它那套 带哈希的类名(.xxxx_panel / _title / _titleRule / _details),直接应用到本弹窗, 因此圆角、内边距、分割线、栅格、「N轮N步」弹窗与本弹窗用的是同一份 CSS 规则, 不存在两套近似值;拿不到那份样式表时(不带 ui-chat 的装配)才退回内置的等价内联样式。 唯一不复用的一条是底色:内置弹窗用 specific-menu 半透明菜单膜 + 背景模糊,本卡片 更宽更高,那层膜会把背后的壁纸/皮肤/输入框渐隐透成一片渐变,所以这里固定用仪表盘卡片 同款的 bg-layer-2 实心底色,并显式 backdrop-filter: none。
  • 侧边栏合计:向 sidebar.footer.action 注入两行——1小时29分 · 对话总时长,下面紧跟 3.3M · 对话总token——位置就在 Settings/用户名行正上方。那个 slot 是 display:flex 的横向 条,所以两行由插件自己的一个纵向容器摞起来,不是并排。两行都设了 user-select: none, 鼠标划过不会把数字选进选区。
  • 会话行的时间:侧边栏「工作区」里每条对话右侧原本只是一个距离(55分钟/55min), 贴在时钟旁边很容易被读成「这条对话跑了 55 分钟」。插件把它补成 55分钟前,与悬停卡 (本来就写 55分钟前)读法一致。文案不是硬编码的:后缀走 ui-workspace 自己的 time.ago 模板({t}前 / {t} ago),中英切换自动跟随。刚刚,以及同一格里可能出现 的状态词(待回答/待审批),都不动。理由与做法见下一节。

会话行的时间为什么是「前」

那格子是 ui-workspace 的 timeLabel 渲染的,没有对外 slot;而 locale 注册表对同一 命名空间的同一语言只允许注册一次(第二个 register("workspace", { zh, en }) 会直接抛 locale namespace "workspace" already has locale "zh"),所以也改不了它的词典。留给第三方 插件的只剩渲染结果本身:

决定原因
用 MutationObserver 盯 DOM没有 slot、没有可覆盖的词典,这是唯一不改宿主代码的位置
只匹配 _sessionRow 里的 _time两边的类名都带构建哈希(wnFoaW_sessionRow/wnFoaW_time),所以按后缀匹配;聊天消息与提问卡也有 _time,限定在会话行内就不会误伤
先比对模板再改模板用探针值 7 从 time.minutes 等键读回(7分钟),把探针两侧转义成锚定正则;状态词、刚刚 都对不上,自然跳过
只改文本节点的 nodeValue替换子节点会让 React 手里的节点脱钩,之后每次刷新都往已脱离文档的节点写值,行上的时间就永远停住
幂等改写本身会再触发一次观察,但此时文本已带「前」、不再匹配裸模板,于是停手,不会出现「前前」
宿主没有 MutationObserver 就整个不挂非浏览器装配(例如 SSR 冒烟)不该因为装饰器而报错

侧边栏使用时长怎么算

不额外读盘、不新开 RPC:直接复用侧边栏已经拿到的会话列表快照 (sessions.list 的 ids / byId / projectionsBySession),把每行的 sessionStats.llmMs + sessionStats.toolMs - taskTimer.awaitMs 加起来。

约定说明
数据来源会话行的 sessionStats 与 taskTimer 投影:已连接会话用实时值,冷会话用投影检查点缓存(session_projcache),因此不会为了这一个总数去解压历史日志
实时性当前会话推进时,projectionsBySession 的实时值优先于列表里的旧缓存,该数值随 agent 工作增长
扣减awaitMs 逐行先减再求和:等待属于某一条对话,先求和再减一个会话级总数在算术上不成立
排除项origin === "subagent" 的子会话行不计入——派发它的对话已经把这段时间算成工具时间,两边都算会翻倍
收起态侧边栏折叠成 56px 轨道时整行隐藏(轨道里放不下说明文字)
为 0 时一条会话都没记录过工作时长时整行不渲染,不留空行

侧边栏 token 合计怎么算

同一份列表快照、同一批会话,读的是 tokenUsage 投影:每行四个互不重叠的桶 (uncachedInputTokens / cacheReadTokens / cacheWriteTokens / outputTokens), 合计为提示侧三项加输出。

约定说明
口径提示侧三项 + outputTokens,与 ui-chat UsagePill 的「整份日志 token 总量」完全一致,两处不会各说一个数
数据来源会话行的 tokenUsage 投影:已连接会话读实时值,冷会话读投影检查点缓存
实时性与时长一样,projectionsBySession 的实时值优先于列表缓存
排除项origin === "subagent" 的子会话同样不计——父会话已经把这些 token 记在账上
数字格式紧凑写法,与内置 usage pill 同一套闸口:517 / 12.2K / 517K / 1.2M。不写全量数字,否则侧边栏宽度会把「对话总token」这几个字省略掉
为 0 时没有任何 token 记录时这一行不渲染;只有 token 没有时长时,token 行照常单独显示

目录结构

dsh-task-timer/
├── package.json        # 插件清单(dsh.bundle + dsh.client)
├── cordis.patch.yml    # bundle 补丁:insert 一条 task-timer 条目
├── install.ps1         # 本机 link: 安装脚本(不随 npm 包发布)
├── LICENSE             # MIT
├── lib/
│   ├── index.js        # 宿主侧:注册 taskTimer 投影(createdAt + lastActivityAt + awaitMs)
│   └── client.js       # 浏览器侧:工作时长 pill + 创建对话至今行 + 侧边栏时长/token 两行 + 会话行「N前」
├── locale/
│   ├── zh.json         # 插件页显示元数据(中文),必须是 {"meta":{"title","description"}}
│   └── en.json         # 插件页显示元数据(英文),同上结构
├── market/
│   ├── index.json      # format 1 索引(托管到 http(s) 后供「发现」读取,不随包发布)
│   ├── PUBLISH.zh.md   # 上架 / 自建索引源说明
│   └── PUBLISH-GITHUB.zh.md # 推到 GitHub 并进 dshmarketplace.dev 的流程
└── test/
    ├── projection.test.mjs # 投影契约测试(init 取 header、游标单调、state/view 引用稳定)
    └── render.test.mjs     # 客户端 pill 渲染冒烟测试(SSR,断言样式、结构与创建对话至今)

npm publish 只发布 package.json 里 files 白名单列出的文件,加上 npm 强制包含的 package.json / README.md / LICENSE,一共 8 个:lib/index.js、lib/client.js、 cordis.patch.yml、locale/{zh,en}.json、package.json、README.md、LICENSE。install.ps1、 market/、test/、node_modules/ 都不进包。

locale/*.json 的顶层必须是 meta 对象:插件页(设置 → 插件、插件管理器、发现)的标题与说明由 宿主 readPluginMeta() 读取 locale/<语言>.json 里的 meta.title / meta.description,缺这两项就 退回 package.json 的 name / description(于是中文界面显示英文说明)。文件名必须是语言 id (en、zh),en.json 必须存在,否则整个 locale/ 目录都不会被扫描。

安装

从公开仓库安装(<dsh> 指 DSH 安装目录下 resources/runtime/cli/bin/dsh.cmd,若已加进 PATH 则直接写 dsh):

& "<dsh>" plugin --profile desktop add github:ShuaihuiMou/dsh-task-timer

GitHub 与 Gitee 是同一份代码,直连 Gitee 等价:

& "<dsh>" plugin --profile desktop add "git+https://gitee.com/sunxiaochuan2588000/dsh-task-timer.git"

想锁死版本就在 # 后跟完整 40 位提交 SHA——插件市场的 git 源、本仓库 market/index.json 都是这么锁的, 下面这条锁的是 1.4.1:

& "<dsh>" plugin --profile desktop add "github:ShuaihuiMou/dsh-task-timer#d9d7fc0e2c5f49f71e671927e8d416e24390d3a8"

npm 上还没有这个包(尚未发布),等发布后 add dsh-task-timer 也等价。

调试本地源码时用 link: 装法,改动即时可见,不必重装;file:(不加 link: 前缀)则会把包 拷贝进 profile。在插件目录双击/运行 install.ps1,或手动执行:

& "<dsh>" plugin --profile desktop add "link:<插件目录绝对路径>"

install.ps1 会按 -DshCli <路径> → $env:DSH_CLI → PATH 上的 dsh 顺序查找 CLI。

安装完成后必须重启 DeepSeek Harness(新 bundle 补丁要重载才会注册投影与 pill)。

验证

宿主侧投影契约已用真实 zod 跑通(test/projection.test.mjs),输出如下:

projection: createdAt + startedAt + lastActivityAt + awaitMs published, cursor monotonic, waits paired, view references stable

它断言的是:init(header) 原样取 header.createdAt 并把游标起在同一个点(fork 的 inheritedEventCount 不参与)、起点由第一条内容事件钉住且祖先日志抢不走、只有对话类事件 推进 lastActivityAt 且乱序/缺时钟不会回退、 session/title 等元数据事件不动游标、wire.view 对同一 cell 复用同一个 view 引用(注册表靠 这个引用比较抑制推送)、view 里不再出现 llmMs/toolMs/workingMs。

等待的配对另有一组断言:普通工具(含 mcp__dsh__pwsh 这种带前缀的)不开等待、 ask_user_question 与 mcp__dsh__ask_user_question 开、exit_plan_mode 也开;未结算的等待 awaitMs 保持为 0(此时仪表盘也还没记账);结算后恰好等于 call→result 跨度;turn/end 丢弃未结算的等待且之后迟到的 tool/result 再也减不出数字;多次等待累加;awaitCalls 不出现在 wire 里。

复跑测试(需要 zod,用任意 Node 20+ 即可;DSH 自带的那份在 %USERPROFILE%\.dsh\dsh-runtimes\<runtime>\dependencies\node\bin\node.exe):

node test\projection.test.mjs

客户端渲染用 react/react-dom 做 SSR 冒烟(--no-save 装即可,不进 package.json):

npm install --no-save --package-lock=false react@19 react-dom@19
node test\render.test.mjs    # render smoke: closed-state and open-state assertions passed

弹窗断言把时钟钉在固定值上:createdAt = now - 30 分钟、且 lastActivityAt = now 时,弹窗必须 出现 30分0秒——证明这个数是从当前时间现算的;随后把 lastActivityAt 换到 createdAt + 10 分钟(即对话已空闲二十分钟),同一份渲染必须变成 10分0秒 且不再出现 30分0秒/20分0秒——这就是"没人动就不再累加"的回归测试。

扣减的回归测试同样在 test/render.test.mjs 里:llmMs 2分 + toolMs 7分、awaitMs 7分 时, pill 必须读 2分0秒 · 工作时长 且不得出现 9分0秒;弹窗多出一行 已扣除等待回答 7分0秒; awaitMs 大到把读数压成负数时整颗 pill 不渲染(而不是显示负值);宿主还是旧 view(没有 awaitMs 字段)时退回旧读数而不是 NaN。

侧边栏合计另有断言:某一行的 taskTimer.awaitMs 必须先减该行再求和 (1小时30分 → 1小时20分),且实时投影里的等待覆盖检查点缓存里的值。

浏览器侧 pill 的注入结构与官方 dsh-client-ui-chat 的 ActivityPill/UsagePill 完全同构 (同样的 conversation.composer.dock slot、同样的 useProjection keyed hook)。

侧边栏那一行用同一份 test/render.test.mjs 覆盖:wide=false 时不渲染、总量为 0 时不渲染、 子会话行被排除、实时投影覆盖列表缓存、1小时30分 · 对话总时长 的文案、user-select: none (含 -webkit- 前缀)、行内不画图标,以及 12px/18px 的排版。同一份测试还断言了第二行: 3.3M · 对话总token 排在时长之下、子会话的 token 不进合计、实时 tokenUsage 覆盖缓存值。 另一份断言还覆盖两条弹窗皮肤路径: 样式表缺席时用内置内联样式,样式表挂上后弹窗立刻改用 .xxxx_panel 等内置类名、并且不再输出 自己的背景声明。

会话行「N前」用一份极简假 DOM(matches/closest/querySelectorAll + 可回放的 MutationObserver)覆盖:已渲染的 55分钟 装载即变 55分钟前;后挂载的行在挂载的同一次提交 里被改写;刚刚、待回答 不动;聊天消息上同名的 _time 不受影响;改写回流到观察者时不追加 第二个「前」;切到英文词典后新行读 55min ago,已渲染的中文行保持 55分钟前;宿主没有 MutationObserver 时不抛错。

样式对齐(与仪表盘内置 pill 逐项一致)

字体/字号肉眼可见的差异只可能来自"自己发明一套尺寸"。这里不发明:所有数值都抄自 dsh-client-ui-chat 的 StatsPills.module.css 与 stat-dialog.module.css。

项目内置 ActivityPill/UsagePill本插件
字号/行高12px / 20px同
高度 / 内边距22px(1px 8px 内边距 + 20px 行盒撑开,不写死 height)/ 1px 8px同(gap 6px 留给前面的时钟图标)
圆角var(--dsw-radius-sm)(8px)同
前景 / 底色label-tertiary / 无底色(background: 0 0)同(只有 hover / 展开态才上 interactive-bg-hover)
悬停 / 展开态hover 与 [aria-expanded=true] 同为 interactive-bg-hover,文字转为 label-secondary同(并排选择器一次写死,展开不额外上一个"选中"底色)
图标每个 pill 一位 primitives 图标(activity 是仪表盘、usage 是数据库),尺寸由 .pill svg { 14px } 定时钟 IconClockOutlineRegular,pill 与弹窗标题各一位;侧边栏合计行仍纯文字
弹窗pad 16、specific-menu 面、12/18、6/16 明细栅格布局与栅格同一个类名:运行时读回 ui-chat 的 stat-dialog.module.css 哈希类名后直接套用
弹窗底色specific-menu(半透明菜单膜 + menu-backdrop-filter 背景模糊)不同:改用 bg-layer-2 实心底色并关掉背景模糊。这张卡片比 token 那张更宽更高,半透明膜会把主题壁纸/海洋皮肤/输入框渐隐透成一片渐变,实心底才是一个基础色
弹窗定位useAnchoredPosition,side top、gap 8、margin 12同(复用同一 primitive)
标签文案顺序数值在前、名称在后,· 分隔同(51分24秒 · 工作时长)

因此 lib/client.js 不再手写坐标钳制、不手绘 SVG、也不用 font: inherit(那会把继承来的 字号带进来,正是之前看起来"字体不一致"的原因)。

底色这条曾经抄错:早期版本给它上了 bg-layer-2 实心底。默认亮色主题下 dock 本身也是白底, 所以看不出来;换上海洋色这类半透明皮肤后,dock 透了、这颗 pill 仍然自带底色,就成了整排 仪表盘里唯一一块不透明的色块。现在与内置 pill 一样 background: 0 0,只在 hover 时才有底色。

改动生效范围(排查用)

文件生效方式
lib/client.js、locale/*.json渲染进程侧,重开窗口即读到新代码
lib/index.js主进程侧,只有整个 DSH 进程重启才会重新加载

判断宿主侧有没有真的换上新代码,看投影检查点缓存 %USERPROFILE%\.dsh\storages\session_projcache\sessions\<session-id>.json 里的 record.rows.taskTimer:换上新代码后应是 ver: 4 与 {"createdAt": ..., "lastActivityAt": ..., "awaitMs": ...}。

还有一种更直接的判据:ver 低于 4 的旧行会被注册表的版本闸门挡下、按 stateVersion: 4 自动重放日志重算,不需要手动清缓存;而主进程没重启时投影根本不会重算——此时 wire 里 没有 awaitMs,客户端只能按旧的 llmMs + toolMs 记账(问句的等待会继续被算成工作)。 所以看到读数还没扣掉等待,先确认托盘里的 DSH 进程是不是整只重启过。

Comments

Loading…

Similar plugins

dsh-plugin-session-clock

by msongw-design

A DeepSeek Harness plugin: rewrite the sidebar's relative session times as WeChat-style absolute times

Manifest valid

★ 0

MIT

JavaScript

Oct 31, 2026

dsh plugin --profile web add dsh-plugin-session-clock

by 2huy4n

A DeepSeek Harness plugin that gives a roleplay character a clock: sleep/wake, greetings, idle actions, proactive small talk, and outing photos.

Development & InfrastructureTerminal & ClientsManifest valid

★ 31

AGPL-3.0

JavaScript

Nov 2, 2026

dsh plugin --profile web add project-karen

by weibaohui

DeepSeek Harness plugin: cron-driven scheduled items with a full-page management UI. Each item carries a title, a prompt, and a croner expression; executions spawn a fresh agent session in a bound wor

Manifest valid

★ 0

JavaScript

Aug 19, 2026

dsh plugin --profile web add dsh-plugin-scheduled-items

DeepSeek Harness plugin: show a running / just-finished indicator on sidebar workspace rows

UI & ExperienceTerminal & ClientsManifest valid

★ 0

MIT

TypeScript

dsh plugin --profile web add dsh-workspace-activity

by zhu168

Save-money plugin for DSH (DeepSeek Harness) — define your own "pause / resume" time windows; at pause time running long tasks are paused (not stopped) automatically, and they resume when the window e

Workflow & AutomationManifest valid

★ 37

MIT

JavaScript

Aug 22, 2026

dsh plugin --profile agent add dsh-save-money

by cute-baobao

DeepSeek Harness usage recorder plugin: per-model hourly token usage (input/output/cache hits) with a stacked bar chart dashboard in the Web GUI.

Manifest valid

★ 3

MIT

TypeScript

Aug 14, 2026

dsh plugin --profile web add @dsh-usage-meter/usage