GitHub:https://github.com/blueziii/dsh-peakvalley · 作者 blueziii · 许可证 MIT
在 DeepSeek Harness 输入栏下方那一行的最右侧(上下文占用圆环的右边)加一个时段徽标:空闲时段的价格是高峰时段的一半。
高峰 · 1x · 距空闲 1h51m ← 高峰时段(橙色圆点)
空闲 · 0.5x · 距高峰 3h09m ← 空闲时段(绿色圆点)
悬停(title)可以看到完整说明:当前北京时间、当前时段、今天是周末还是哪个法定节假日、两条规则原文、下一次切换的倒计时,以及当前使用的模型。
只有当前模型走 DeepSeek 时才显示。 会话切到非 DeepSeek 的提供方或模型(例如 OpenRouter 的第三方模型)时,徽标完全隐藏。判断依据是提供方 id 或模型 id 里是否含 deepseek(不区分大小写):deepseek-account、deepseek-official、DeepSeek-V3.2 都会显示;模型信息读不到时按「未知」处理,照常显示。模型数据读自 ctx.get('modelDirectories') 服务(ui-model-selection 提供),组件本身不订阅,跟着每秒一次的时钟节拍读一次快照,所以切换模型后最多 1 秒生效。
| 北京时间 | |
|---|---|
| 高峰时段(1x) | 周一至周五 09:00–12:00、14:00–18:00,且当天不是中国法定节假日 |
| 空闲时段(0.5x) | 其余全部时间:周末全天、法定节假日全天、工作日 12:00–14:00、18:00–次日 09:00 |
- 判定一律按 UTC+8 北京时间,与本机时区、夏令时无关。
- 倒计时单位固定为
d/h/m(8d23h、1h51m、45m)。 - 节假日按自然日全天算空闲(不区分节假日里的具体钟点)。
- 时间显示精确到秒(每秒刷新一次),切换倒计时向上取整到分钟。
调休上班的周末(例如 2026-10-10 周六)仍按周末算,是空闲时段。
按需求原文「周末…均为空闲时段」,周末优先于调休。若你希望调休上班日按工作日计时,只需在 client.js 的 stateAt() 里改一处判断(把周末与「调休上班表」相抵),改完刷新页面即可。
插件就是一个普通的本地包目录,官方插件管理器原生支持「本地绝对路径」安装:
git clone https://github.com/blueziii/dsh-peakvalley.git-
打开 DSH,进入侧栏的 插件 页(设置 → 插件 里也有入口)。
-
点 添加插件。
-
在输入框里填入克隆下来的包目录的绝对路径,回车或点 安装:
<你克隆到的位置>\dsh-peakvalley -
安装完成后点 立即启用。
也可以让 Agent 代装:把同样的绝对路径交给插件管理器(install_bundle)即可。
插件管理器把输入内容当 pnpm spec 解析,所以也可以直接填
github:blueziii/dsh-peakvalley(未实测);上面那条「本地绝对路径」是实际验证过的方式。
安装后如果徽标没有立刻出现,刷新一次页面(F5)。这是新建的组合包,不是替换已装插件,所以通常不需要重启 DSH;只有将来替换这个包的代码时才要完全退出并重启。
开发机上这份目录已经以
link:形式装进了 desktop profile,也就是改完client.js刷新页面(F5)即可生效,不用重新安装。若刷新没反应,再完全退出并重启 DSH。
在 插件 → 已安装 里找到 dsh-peakvalley,卸载即可。卸载会删掉它在 profile 里的依赖声明,本项目目录(工作区里的这份源码)不受影响。
节假日安排在运行时自动获取,不需要手动更新内置表,也就不会出现「明年调休表还没内置」的问题。数据来自 NateScarlet/holiday-cn,内容取自国务院办公厅每年发布的节假日安排通知。
数据源与镜像
- 先取
https://cdn.jsdelivr.net/gh/NateScarlet/holiday-cn@master/<年>.json,失败再取https://raw.githubusercontent.com/NateScarlet/holiday-cn/master/<年>.json。 - 单次请求 8 秒超时(
AbortController),带cache: 'no-store',两个镜像都失败才判定为失败。 - 请求只带上年份(
…/2027.json),不带任何本地信息,也不经过本插件自己的服务器;除此之外插件不访问任何地址。
抓什么、多久抓一次
- 每次抓本年与下一年(2026 年的机器会同时拿 2026 与 2027)。
- 已公布的年份(数据里
days非空):每 30 天复查一次。 - 尚未公布的年份(文件存在但
days为空):每 12 小时重试一次。国务院通常在 11 月发布次年安排,公告一出,最迟 12 小时内插件就自动用上了,不用改代码、不用升级。 - 抓取时机是徽标每秒一次的时钟节拍里顺手检查(未过期时是个空操作),插件
apply阶段不联网。
缓存与降级
- 结果缓存在浏览器
localStorage的dsh-peakvalley.holidays({ v: 1, years: { '2027': { at, days } } }),刷新页面后先用缓存渲染、再联网复查。缓存损坏、版本不符、读写被拒(隐私模式)都只是静默降级。 - 三级降级:在线 → 内置表(2026) → 公历估算。tooltip 里会写明这一年用的是哪一种。
- 联网全部失败时保留上一次拿到的数据(连抓取时刻一起保留,下个周期再试),不会退回估算。
- 内置表仍保留(2026 共 33 天:元旦 1/1–1/3、春节 2/15–2/23、清明 4/4–4/6、劳动节 5/1–5/5、端午 6/19–6/21、中秋 9/25–9/27、国庆 10/1–10/7),只作为首次启动 / 断网时的兜底。离线且没有缓存时,农历节日(春节、端午、中秋)无法预知,只会按公历惯例估算并在提示里注明。
调休口径
只取 isOffDay: true 的条目,isOffDay: false 的「调休上班」周末(例如 2026-10-10 周六)会被忽略,与上文「调休上班的周末仍按周末算空闲」一致。
package.json dsh.bundle.patch → cordis.patch.yml;dsh.client.platform = web
cordis.patch.yml - insert: - id: peakvalley / name: 'dsh-peakvalley'
index.js Host 半边,空实现(可见内容全在客户端)
client.js 客户端 bundle:峰谷判定 + 徽标组件 + 插槽注册
locale/zh.json 插件列表里显示的名字与描述
locale/en.json 同上(英文)
icon.svg 插件列表图标
dev/check.mjs 开发期自检脚本(不随包发布)
- 徽标注册在宿主 slot
conversation.composer.dock(输入卡片下方那一行,即「会话统计」所在的工具栏),order: 20排在会话统计之后。 - 「最右侧 / 上下文右边」怎么做到的:宿主把
ContextMeter(上下文占用圆环)作为这一行 flex 容器的最后一个子项渲染,而插槽外壳带display: contents(Anchor的ANCHOR_STYLE),插槽条目的根元素因此就是该容器的直接 flex item。于是给根元素加 CSSorder: 1(宿主各条目与圆环默认都是0),就能越过默认顺序、排到圆环右侧。这一行justify-content: center,宽度收缩为内容宽,所以「最右」= 该行内顺序最末的条目。 client.js只require('react')(宿主模块表提供),不 import 任何 Harness Client 包;样式只用--dsw-alias-*主题 token(pkv_前缀类名),因此明暗主题都跟随宿主。- 颜色:高峰用
--dsw-alias-state-warn-primary,空闲用--dsw-alias-state-success-primary,次要文字用--dsw-alias-label-tertiary。 - 组件内
setInterval(1000)驱动每秒刷新,useEffect返回清理函数,卸载即停。
目标是让同一行的其它条目(上下文圆环、会话统计)不跳位:收起时宽度真正归零,而且几何变化必须逐帧过渡,不能一帧走完。
结构上分两层,各管一件事:
- 盒子(根元素)只管几何:
width/padding/margin/border-width一起过渡。退场时全部收到0,其中margin-right: -12px正好抵消宿主这一行的gap: 12px,所以徽标收起时宽度真正归零。只淡出不收宽是不够的——文字占的那段宽度会在卸载瞬间消失,同一行其它条目就会突然跳到空缺位置。 - 内容(
.pkv_inner)只管淡入淡出:进场时延迟 110ms 淡入(盒子先展开),退场时先 100ms 淡出干净、空盒子再收。文字在可见期间基本不会被盒子的overflow: hidden裁到一半,因此没有「文字被切着走」的生硬感。
时序(常规动效,ANIM_MS = 260):
- 进场:以
width:0+ 内容opacity:0挂载,两帧后转常态(等两帧是让浏览器先按初始状态画过一次,否则过渡会被跳过)。 - 退场:切到非 DeepSeek 模型时先保留节点播完动画,320ms 后(
UNMOUNT_MS = 260 + 60)才真正卸载;几何过渡额外延迟 40ms(EXIT_LAG),让内容先消失、空盒子后收。 - 半途切回:退场动画没播完又切回 DeepSeek,会取消卸载、直接回到常态,不会闪一下空。
这台机器的 Windows「显示动画」是关闭的:HKCU\Control Panel\Desktop\WindowMetrics\MinAnimate = 0,SPI_GETCLIENTAREAANIMATION 返回 False。Windows 上 Chromium/Electron 就是据此判定 prefers-reduced-motion: reduce,所以当前 DSH 里走的就是「减少动态效果」这条路径。
早期版本的 reduce 分支被写成了 transition: none,几何变化一帧走完 —— 那正是「其它组件突然移动到空缺位置」和「还是很生硬」的根因(那时无头 Chrome 也恒报 reduce: true,与用户实际看到的路径一致)。reduce 分支绝不能再把过渡关掉,现在只把它缩短为 140ms 线性(SHORT_MS = 140),内容仍然后显/先隐:
- 进场 140ms:盒子 0 → 145.91px,圆环每 10ms 最多移动 5.66px(实测
maxJumpPer10ms),140ms 就停稳,而卸载在 320ms —— 卸载那一刻位移早已归零,不再跳变。 - 退场同理:内容 60ms 内隐掉,空盒子 140ms 收完,圆环平滑回到原位。
想看到完整的 260ms 版本,打开 Windows 的「在 Windows 中显示动画」(设置 → 辅助功能 → 视觉效果)再重启 DSH;或把 client.js 里的 SHORT_MS 调到与 ANIM_MS 相同,让两条路径共用一套时序。
「auto ↔ 0 也能补间」靠根元素上的 interpolate-size: allow-keywords(Chrome 129+ 起支持;本机实测 Electron 44.0.0 = Chromium 152.0.7977.54)。常态宽度就是 width: auto,与不加动画时完全一致(零回归);万一该特性不可用,只是退化成瞬间收起,不会变形。内容子项带 flex:none; white-space: nowrap,宽度收缩时不会被压扁。
徽标整块就是按钮(role="button"、tabIndex=0、Enter / 空格同效、cursor: pointer、悬停与键盘聚焦给一层 color-mix 底色),点一下换一档,七档循环:
| 顺序 | id | 显示内容 |
|---|---|---|
| 1 | all |
时段 · 倍率 · 倒计时(默认) |
| 2 | state |
仅时段(高峰 / 空闲) |
| 3 | rate |
仅倍率(1x / 0.5x) |
| 4 | eta |
仅倒计时(距高峰 / 距空闲 d h m) |
| 5 | stateRate |
时段 + 倍率 |
| 6 | stateEta |
时段 + 倒计时 |
| 7 | rateEta |
倍率 + 倒计时 |
- 主次:时段也在显示时,倍率与倒计时压暗一档(
--dsw-alias-label-tertiary);只显示倍率/倒计时时用当前色(currentColor),单段更醒目。圆点恒显示,分隔符只插在相邻可见段之间。 - 记忆:所选档位存在
localStorage['dsh-peakvalley.layout'](存的是模式 id,不是下标);读写全部包在 try 里,隐私模式/被禁用/存了过期值都静默回退到「全部」。 - 换档也要平滑(否则换内容会改变徽标宽度,同一行的圆环、会话统计又要跳)分两拍:
- 点击后先挂
.pkv_switch,.pkv_inner用 90ms(SWAP_OUT_MS)淡出,盒子原地不动; - 淡完才换文案;同一次提交里用
useLayoutEffect把根元素钉在切换前的像素宽度(getBoundingClientRect().width),两帧后放开成width: auto——auto到auto之间浏览器看不到「变化」、不会插值,必须先钉出一个像素旧值,宽度才会真的做过渡,邻居才会平滑地跟着挪。放在useLayoutEffect里才能赶在本次绘制之前钉住。
- 点击后先挂
- 快速连点不会丢步:基准取「已经在路上的那一档」(
swapTo),新一轮会取消上一轮的计时器与 rAF。reduce 分支下换档淡出缩短为 60ms 线性。
node dev/check.mjs覆盖:index.js 可加载、client.js 语法、manifest 字段与 id 一致性、__ModuleLoader__.load 的 id 等于包名、插槽注册契约(含 t 与 modelStore 注入)、峰谷真值表(含 2026 全部节假日与调休日、窗口边界到毫秒)、切换倒计时、d/h/m 文案、模型可见性(非 DeepSeek 隐藏、服务缺失/current 为空/抛错时的回退)、组件在 React 桩下真实执行一次渲染、进出场动画时序(进场两帧转常态、退场计时器卸载、半途切回取消卸载、卸载后再切回能二次进场)、动画几何(interpolate-size、width:auto↔0、flex:none、reduce 分支仍要过渡 width 且绝不能是 transition:none、SHORT_MS < ANIM_MS、内容后显/先隐、UNMOUNT_MS 大于退场总时长)、点击切换显示内容(7 档矩阵与循环、tooltip 模式行、点击/Enter/空格、.pkv_switch 先淡出、换档时把旧宽度钉成像素值再两帧放开、连点不丢步、localStorage 存取与非法值/抛错/无 storage 三种回退、各档段数与主次类名、reduce 下换档淡出更短)、节假日在线同步(离线样本解析、放假日/补班日取舍、中文名映射、年份不符与脏数据、在线→内置→估算三级降级、两种 TTL、双镜像回退、全失败保留旧数据、在飞不重发起、缓存恢复与损坏/版本不符、写缓存失败静默、apply 阶段不联网、内置 2026 表与在线数据逐日一致性回归),以及「未 import Harness Client 包 / CSS 无 NaN / var() 只用主题 token / 根元素带 order:1」等硬约束。
当前状态:299 项全部通过。
还有一个做动画调试用的小工具(可选,不参与插件运行):先 node dev/css-dump.mjs,它会把 client.js 里真实的 CSS 抽出来写成 dev/_real.css,并额外生成一份剥掉 reduce 分支的 dev/_real-full.css;再用浏览器打开 dev/anim-real.html(?dir=enter|exit&series=1&full=1,需要先生成上面两个 CSS 文件,否则页面是没样式的)就能借 Web Animations API 把过渡钉在任意时刻逐帧采样,直接读出几何曲线(travel / maxJumpPer10ms / settledAt)。上面那些数字就是这么量出来的。
注意:这个脚本只做逻辑与契约校验。动效几何已经用无头 Chrome + 真实 CSS 逐帧量过了(就是上面那些数字),但装进 DSH 之后的实际观感(位置、颜色、间距、快慢是否顺眼)只能靠眼睛确认——DSH 的 Web GUI 需要鉴权,无法抓取或截图。
