Kiru-Sama/kiru-progress-bar

kiru-progress-bar

DeepSeek Harness 进度汇报提醒插件:累计每 N 步或每 N 秒提醒 Agent 汇报一次进度,侧栏实时显示计数,聊天区横幅显示上次提醒时刻。附带余额低额提醒与 DeepSeek 峰谷时段指示。

未分類deepseekdeepseek-harnessprogress

kiru-progress-bar

让 Agent 在长回合里主动、简短地汇报进度的 DeepSeek Harness 插件。

安装 / 卸载 / 已知限制 → INSTALL.md。零依赖,不用装任何 npm 包。

除了进度提醒,它顺带做三件小事:

  • 余额监控 —— 读宿主自己的账户余额(不碰你的 API key),低于阈值就让 Agent

提醒你充值;充值余额与赠送余额分开显示,判定按两者之和(平台扣款先从赠金扣)。

  • 峰谷时段 —— 侧栏常驻显示 DeepSeek 官方的高峰/低谷时段,「梁文峰」= 高峰(红色发光)、

「梁文谷」= 低谷(绿色,半价)。

  • 可见性 —— 每次提醒会在聊天区弹横幅,告诉你「上次提醒是什么时候」;

越过阈值却还在等步骤边界时,也会明确写出来,而不是让你干等。

两个触发器,阈值都可调:

触发器 · 默认 · 含义 步数 · 10 步 · 自上次汇报起累计 N 次工具调用 时间 · 120 秒 · 自上次汇报起累计经过的真实时间

任一命中就提醒一次,然后两个计量一起归零(共用量表,避免「第 10 步刚提醒完、 2 秒后时间阈值又到」的连击刷屏)。

提醒不是弹窗,而是通过 tools/post-execute 决策的 additionalContexts 注入一条合成 user 消息——唯一能让模型真正看见并照做的通道。 消息带 source.kind = 'progress-nudge',所以不会被当成用户提示词污染历史。

计量规则

时钟按墙钟推进,running 决定它在不在走。停表只由两件事触发:暂停、回合结束。

情况 · 行为 暂停(面板上的暂停按钮) · 立刻冻结,秒数纹丝不动;干等多久都不涨。恢复后从冻结值继续,不补算暂停期间的时间 回合结束(agent/turn-stopping) · 归零并停表,面板回到 0 步 / 0 秒 新回合 / 新用户消息(agent/pre-step 的 turn 编号变化) · 归零并起表——用户刚发消息,接下来的思考时间要计 Agent 主动对你输出文本(agent/assistant-stream 的 text-delta) · 归零并继续走表——「它刚跟人说过话了」,计量从这句话开始重算

只有真正对人说话才触发最后一条:思考是 reasoning-delta、执行是 tool-call-delta、空白文本,都不会重置(有测试钉住这三条)。 不需要的话把 resetOnSpeech 设成 false。

为什么要"继续走表"而不是"停表"

最初的实现是「只统计两次工具调用之间」,并且说话后直接 anchorAt = null 停表。 后果是:Agent 说完一句话 → 停表 → 接着深度思考 3 分钟、期间一次工具都没调 → 界面上一直显示 0/10 · 0秒,看起来像坏了,这段时间的时间触发也永远打不到。

现在:说话只清零计量、时钟立刻从那一刻重新起表;思考时间照常累计, 并在下一次工具调用时参与时间阈值判断。另外 /status 的 live 是读取时现算 的(不是缓存的快照),否则两次工具调用之间接口只会吐旧值——这是同一个 bug 的另一半。

界面

两处界面。

元素 · 内容 侧栏按钮 · 实时显示「第 N/阈值 步 · 已过秒数」;暂停时变色显示「已暂停」。时钟停摆时秒数不做本地插值 侧栏展开面板 · 两条进度条 + 步数/时间阈值输入框 + 暂停恢复 + 恢复默认 + 已提醒与已汇报次数 聊天区横幅(常驻) · 只要提醒过至少一次,输入框上方一直显示「● 上次提醒 10月12日 12:27」 聊天区横幅(高亮) · 新的提醒到达时,换成「● 已提醒 Agent 汇报进度 · step 10 · #1」并高亮 12 秒,之后回落常驻样式

横幅上的时间是宿主返回的 `nudge.at`(服务器时钟),不是浏览器本地时间—— 这样它和会话日志、接口里的时间完全对得上。横幅右侧有个 ×,手动关掉后会一直 藏到下一次提醒为止(不是永久关闭)。

两个阈值输入即生效(防抖 350ms 后打 /progress-nudge/api/config),没有保存键。 改的是运行时覆盖:立即生效、不写 profile 配置、重启后回落到默认值。 输入框只在第一次拿到快照时同步一次,之后不会被轮询覆盖(否则会打断你正在输入的数字); 点「恢复默认」会重新同步。

设置页那张卡片已移除——侧栏面板就够了,没必要两处入口。

为什么必须有聊天区横幅

提醒是通过 additionalContexts 注入的合成 user 消息,只有模型看得见; 用户那一侧悄无声息——你能看到 Agent 之后有没有汇报,却看不到「提醒到底发出去没有」。 横幅把这件事补成可见的:Host 在每次提醒时把 nudge 快照(seq/时间戳/触发原因/步数/文案) 放进 /status,客户端靠 seq 递增判断「有新提醒」并高亮一次。 首次拿到快照只记基线,不会把历史提醒当新事件弹出来。

投递时机(硬约束,改不了)

提醒只能在步骤边界投递——additionalContexts 只能挂在 tools/post-execute 决策上。所以一个跑 4 分钟的命令,计量早在 120 秒就越界,但提醒必须等命令返回。

为什么不能"不等进程结束就提醒模型":工具执行期间 agent 循环是阻塞的, 模型根本没在运行,读不到任何东西。宿主提供了 agent.inject() / agent.steer() 这类"随时可调用"的注入通道,但看运行时源码就知道它们不会打断正在执行的工具:

inject(input) { this.send(input, 'next-step', false); }  // 塞进 next-step,不唤醒
steer(input)  { this.send(input, 'next-step', true);  }  // 塞进 next-step + 唤醒
wakeDriver(wakeAfterAbort) {
  if (this.phase.kind !== 'idle') {          // ← 工具正在跑
    if (… || wakeAfterAbort) this.phase.wakeRequested = true;
    return;                                   // 只记一个标记就返回
  }
  …起一个新的运行阶段…
}

唤醒只在 agent 空闲时才真的启动新步骤。唯一能强制打断的是 cancel(), 那等于杀掉用户正在跑的命令,不可接受。

所以能做的是把"它在等"这件事显示出来:越过时间阈值却还没到步骤边界时, /status 的 pending(due / overSeconds / blocked)让界面显示

● 已到期 224 秒 · 等当前步骤结束后提醒(上次提醒 10月12日 12:35)

而不是让你对着一根越过阈值的进度条和「已提醒 0」猜它是不是坏了。

配置

cordis.patch.yml 里的行配置(默认值见该文件):

字段 · 默认 · 说明 enabled · true · 总开关(暂停) stepThreshold · 10 · 累计多少步提醒一次 timeThresholdSeconds · 120 · 距上次汇报累计多少秒提醒一次 minGapSeconds · 45 · 两次提醒的最小间隔,兜底的连击保护 maxPerTurn · 20 · 单回合提醒上限,超过后静默 exclude · ["todo_write","ask_user_question"] · 不计步、也不重置计数的工具 includeSubagents · false · 是否也为 subagent 计数提醒 resetOnSpeech · true · Agent 主动输出文本时是否归零计量 locale · zh · 提醒文案语言(zh \ · en) balance.enabled · true · 是否开启余额监控 balance.thresholdCny · 1.5 · 低于这个金额就提醒 balance.scope · total · 判定口径:total = 充值 + 赠送;recharge = 只看充值 balance.checkSeconds · 300 · 多久查一次余额(最小 30) balance.repeatSeconds · 1800 · 仍在阈值以下时隔多久再提醒;0 = 只提醒一次 balance.includeInReport · true · 低额时在进度汇报末尾顺带提一句 tariff.enabled · true · 是否在侧栏常驻显示「梁文峰 / 梁文谷」 tariff.holidays · [] · 中国法定节假日(北京时间 YYYY-MM-DD),全天按低谷算

非法配置在插件激活时直接抛错,不会静默回退。

峰谷时段

DeepSeek 官方口径(Models & Pricing):

Off-peak rates are half of the peak rates. Peak hours are **01:00 - 04:00 and
06:00 - 10:00 UTC, Monday through Friday, excluding Chinese public holidays.**
All other hours are off-peak, including weekends and Chinese public holidays in full.

换算成北京时间正好是工作日 09:00–12:00 与 14:00–18:00,低谷一律半价。

· 高峰 · 工作日 09:00–12:00、14:00–18:00 → 梁文峰(橙) 低谷 · 其余全部:午休 12–14、夜间、周末、法定节假日 → 梁文谷(绿)

侧栏按钮常驻显示当前标签,悬停给出高峰窗口、下次切换时刻与倒计时; 面板里另有一行写明官方窗口与「下一次切换:12:00 → 梁文谷(还有 58 分钟)」。

实现要点:

  • 时间**一律按北京时间算,再用 getUTC* 读**(加 8 小时后按 UTC 取值),

换机器、换 TZ 环境变量结果都不变。

  • 下一次切换用「枚举未来 8 天每个高峰窗口的起止,取第一个状态翻转的点」,

跨周末、跨节假日都不会漏。

  • ⚠️ 中国法定节假日无法离线判断,只能靠 tariff.holidays 手工列。

列表为空时只排除周末——这是唯一诚实的做法,不能假装知道; 面板上会提示「未配置节假日,仅按周末与时段判断」。

这是 DeepSeek 官方 API 的计费口径。如果你走第三方中转站,是否适用、
折率多少以对方说明为准;不需要的话把 tariff.enabled 关掉即可。

余额监控

余额不自己去打任何 API,而是读宿主自己的 deepseekAccount 服务 (设置页「账号与余额」显示的就是它):

const account = ctx.get('deepseekAccount')
const outcome = await account.getBalance({ version, locale, timezoneOffsetSeconds })
// → { status:'ready',
//     value:        [{ currency:'CNY', balance:'2.85033112' }],   // 充值钱包
//     bonusWallets: [{ currency:'CNY', balance:'91.7637864'  }] } // 赠送钱包
//   | { status:'failed' } | null(未登录)

这样做的好处:插件不碰任何 API key,也不关心中转站——你用哪个 provider、 key 存在哪、走不走中转,都和它无关。

为什么默认按「可用总额」判定

平台扣款先从赠金账户扣。所以「会不会用不了」看的是 充值 + 赠送, 只看充值余额会在「赠金还有一大笔」时误报:

充值 · 赠送 · 可用总额 · 阈值 1.5 下 ¥2.85 · ¥91.76 · ¥94.61 · total → 不提醒 ✅(recharge → 会误报) ¥1.00 · ¥0.00 · ¥1.00 · 两个口径都提醒 ✅

想只在充值余额低时提醒(比如担心赠金过期),把 scope 设成 recharge。 提醒文案的主语会跟着口径变——recharge 口径下说「充值余额只剩 ¥1.00(另有赠送 ¥92.52,可用合计 ¥93.52)」,不会写出「可用余额只剩 ¥93.52」这种误导话术。

行为 · 说明 轮询 · 每 checkSeconds 查一次,读数超过两个周期就算过期,宁可不报也不报旧数 打开面板 · 侧栏面板一打开就顺手刷一次余额(不然显示的是几分钟前的旧数),另有 ↻ 手动刷新 低额提醒 · 按 scope 判定命中时,在下一次工具边界投递一条「【余额不足】…」,带真实余额、来源明细、阈值与官方充值链接 去重 · 按 repeatSeconds 去重;0 表示只提醒一次 恢复通知 · 余额从「不足」回到「正常」时也投递一条「【余额已恢复】…」,让模型知道用户已经充值、不要再催 顺带提及 · includeInReport 开着时,低额期间的进度汇报末尾会附一句 未登录/服务缺失 · 记为 failed / unavailable,不提醒也不崩——不知道就不说 显示 · 面板两行:可用余额 ¥94.61 + 小字 充值 ¥2.85 · 赠送 ¥91.76;低额时横幅常驻警示;充值恢复后横幅闪一下绿色确认

金额显示统一收成两位小数(宿主给的是 2.85033112 这种高精度字符串)。

为什么「恢复」也要通知模型

只报「余额不足」会留下一个坑:用户充值之后模型并不知情,会继续按「余额不足」的 记忆行事,反复催充值。所以插件记录低额 ↔ 正常的翻转,恢复时也投递一条通知:

【余额已恢复】现在是 2026-10-24 19:52:03(周六)|可用余额已回到 ¥11.45(充值 ¥11.45),高于阈值 ¥1.5。
用户已经充值了。之前那条「余额不足」作废,不要再提、也不要再催充值;若刚才因此中断了什么,可以继续。

两个细节:

  • 恢复的同时会清掉 `repeatSeconds` 冷却——否则下一次真掉下去时会被 30 分钟

去重吞掉,提醒不及时。

  • 横幅上的数字跟判定口径一致:scope=total 显示可用总额、scope=recharge

显示充值余额。早期版本一律显示充值余额,有赠金时横幅上的数字是错的。

⚠️ 轮询用的 setInterval 必须 `unref()`,否则它会一直吊着事件循环,
插件卸载后进程也退不掉——测试直接挂死就是这个原因。

自适应退避(增强提醒)

侧栏面板上有「增强提醒」开关,默认开。它解决一个真机发现的问题: