# 更新日志
本文件记录 `dsh-greet-signoff` 的每个发布版本。格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
## [1.22.0] - 2026-09-20
> 发哥说:「给上下文、开场语、收尾分别做单独的开关,(苹果滑动开关)」。
> 这一版在设置页最顶上放了一排 **iOS 样式的滑动开关**,三个功能各管各的 ——
> 想要干净的输入框就关上下文卡,想只留开场不留收尾(或反过来)就单独关一行。
> 宿主半有改动(提示段按开关生成)→ **需要重启一次 DSH**;关掉/打开都在设置页里拨,拨完自动保存。
### 新增
- **三个独立总开关(苹果滑动开关)**:设置页最顶上新分区「总开关」,三格并排,每格一个 iOS 风格的滑动开关
(51×31 轨道 / 27 滑块 / 20px 位移,`cubic-bezier(.4,0,.2,1)` 过渡;深色皮肤用 iOS 深色档 #39393d / #30d158):
- 🚦 **上下文卡**:关掉后输入框上方那整块都不出现 —— 进度条、六格读数、上下文构成明细、到线红底横幅、
标签页标题的 🚨 前缀一起消失;纯页面显示,不影响模型。实现上走一层门组件 `GreetDockGate`
(开关一拨就整块挂上/卸下),**不是**在 `GreetDock` 里 early return —— 后者会让 React 的 hook
数量前后不一致,直接报错。
- 👑 **开场语** / 🏁 **收尾语**:关掉的那一行不再写进系统提示段,模型之后就不写它了;
文案、样式、文案池、场景快照都留着,随时开回来。
- 开关的真身是 ``(键盘 Tab / 空格可用、屏幕阅读器认得出),
轨道与滑块是 CSS 画的两层 span;系统开了「减少动态效果」时过渡一并停掉。
- **配置字段 `switches`**:`{ greeting, signOff, contextBar }`,缺字段 = 开。宿主半与浏览器半**只认明确写成
`false` 为关**,字符串 / `0` / 手改坏的值一律回落成开 —— 老配置升级后功能不会凭空消失。
- **诊断区认得「被开关关掉」这种状态**:上下文档位关着时,自检不再报「进度条未挂载(卡片没出现)」,
而是直说「已按总开关关闭,在设置页拨回来即可」。
### 变更
- **配置与附件默认跟着插件目录走**:数据目录从写死的 `$DSH_HOME` 改成三级解析——
`DSH_GREET_SIGNOFF_HOME` 环境变量 → **插件目录下的 `config/`** → 旧的 `$DSH_HOME`(兜底)。
插件搬到哪个盘哪个目录,配置就跟到哪;`config/` 已加进 `.gitignore`,不进版本库。
旧位置有配置、插件目录里还没有时行为与 1.21.0 完全一致(老用户不受影响)。
这次搬家:`D:\plugins\dsh-greet-signoff` → `E:\dsh-greet-signoff\dsh-greet-signoff`。
### 其他
- 单测 87 → 93 项:新增总开关清洗(缺字段 / 坏值 / 老配置一律为开)、`ruleTextWith` 按开关生成
(关一行只少一行、两个都关整段渲染成空串、关掉的行连文案池都不碰)、以及一条源码契约
(总开关区在跳转条第一位、三个 `masterSwitch` 都在、`role="switch"` / 滑块位移 / 门组件都在)。
- `client.js` 380.5 KB,仍在 400 KB 门禁内。
## [1.21.0] - 2026-09-20
> 发哥在桌面端(Chrome app 窗口)说「这个快捷方式不显示上下文的横幅」——
> 无头 CDP 抓到真凶:`GreetDock` 在**拿不到上下文读数**的客户端里裸读 `occupancy.capacity`,
> 抛 `TypeError: Cannot read properties of null (reading 'capacity')`,
> 整个 `conversation.input.dock` 槽位崩成一个空占位(`data-slot-error`)→ 进度条和红底横幅一起消失。
> **纯浏览器半修复 → 刷新页面即生效**;宿主半只是版本号跟着走(不重启只会多一行版本提示)。
### 修复
- **没有 `contextPressure` 读数时不再把整条进度条炸掉**:KPI 的 `limitText` 与占用格 `occupancyTip`
里的 `occupancy.capacity` 改成 `hasReading ? occupancy.capacity : null/—`。此前只要宿主投影还没送到
(新开的窗口 / 桌面端 app 窗口 / 刚连上的客户端),`occupancy` 就是 `null`,一读就崩,
连带把「到线红底横幅」一起拖下水——所以表现是"有的窗口有进度条、有的窗口什么都没有"。
## [1.20.0] - 2026-09-20
> 发哥指着进度条上一直是空白的那两格问「实测速率 / 到线约还有,怎么老是不显示?」——
> 这一版把它们从"只认本页亲眼看过的采样"改成**宿主半按本机请求记录算**,并在提示里写清数据是谁算的。
> 宿主半(`index.mjs`)有改动 → **需要重启一次 DSH**;浏览器半刷新页面即生效。
### 修复
- **「实测速率 / 到线约还有几轮」刚打开会话时不再空白**(三个根因一起修):
1. **改由宿主半算**:新增纯函数 `parsePace()`,从会话投影的 `contextTimeline.requests[]`
(每条带 `time / turn / step / prompt`)算「观察窗口内首末两条的 prompt 差 ÷ 时间差」,
随 `/api/greet-signoff/context` 的 `pace` 字段返回 —— 开页即有数、换标签页不丢、切会话各自独立。
浏览器半新增纯函数 `pickPace()`:**宿主优先**,宿主给不了(老版本宿主 / 投影缺失 /
这条会话还没有请求记录)才退回本页 45 分钟采样,最后才是跃升点斜率兜底。
2. **解开"相邻两次跃升只差几十毫秒"的死结**:token 占用只在每轮请求结束后更新,一个 5 秒刻度里
可能挤进好几步(实测相邻跃升只差 56~88 ms),旧的 `rateFromJumps` 只认"最近两次",跨度恒不达标
→ 兜底永远是 null。现在从最后一次跃升**往前找第一个跨度 ≥ 20 秒的点**,增量取这段区间内各次跃升之和。
3. **每轮涨量改成"轮末之差"**:轮内 prompt 会因为工具结果被裁而下降(实测同一轮 796967 → 785012),
旧实现按"每步涨量"取均值,把「约还能聊 N 轮」和「上一轮 ↑X 万」都算小了。
现在只取每轮的**最后一个点**做相邻轮差值,负数(压缩 / 裁剪)直接跳过。
- **"上一轮 ↑X.X 万"在饱和会话里也能出数**:上下文被 DSH 裁剪之后,轮末 prompt 可能比轮首还低
(实测同一轮 796967 → 785012),这时"轮末之差"是负数。现在负数时退回
"轮内峰值相对上一轮末的涨幅",仍为负才不显示;响应里同时给出 `lastTurnSteps`(最后一轮走了几步)。
- **只有一个轮次时直说原因**:全新会话第一轮还没结束时,"每轮涨量"本来就无从算起,
提示里会写明「每轮涨量要等这一轮结束(这条会话现在只有 1 轮)」,不再让人以为功能坏了。
- **速率读数写清来源**:悬停提示里明说这个数是「按本机记录算:这个会话最近 45 分钟的请求」/
「按本页采样算」/「按本页跃升算」;长时间没聊(窗口内记录不足两条)时退到最后两次请求并注明。
纯函数 `paceSourceText()` 产出文案。
### 其他
- 单测 78 → 87 项:新增 `parsePace`(窗口速率 / 轮末涨量 / 峰值兜底 / 脏数据 / 时间倒挂 / 窗口外回落)、
`pickPace`(宿主优先 / 混合来源 / 脏输入)、新版 `rateFromJumps`、`paceSourceText`,
外加一条锁住"进度条三格都从 pickPace 走、旧开关已删"的源码契约检查。
- `scripts/verify-manifest.mjs` 体积阈值 380KB → 400KB(本版 client.js 370.9KB,新增的全是核心诊断逻辑)。
- **文档重写**:README 补齐 1.11–1.20 的全部能力(预算口径与四档预算模式、六格读数、上下文构成明细、花费台账与对账、
交接包 `HANDOFF.md`、场景与文案池、活跃时长与实测速率的算法说明、长尾选项展开/收起),并把已知限制按"读数滞后一轮 /
新页面未知 / 宿主半旧版降级 / HANDOFF 覆盖"逐条写清。
- **预览图重拍**:`docs/preview-chat.png`(对话页 + 六格读数 + 红底预算横幅)与 `docs/preview-settings.png`
(设置页全部分区)按当前版本的真实类名与 CSS 规则重新渲染;旧图里的导航条还是三格、没有横幅。
## [1.19.0] - 2026-09-20
> 发哥一句「按照方案全部执行」。这一版把「提醒该换会话」这条链子补完整:**从"看见红线"到"把活交接出去"不再断在半路**,
> 同时让花费读数可校准、多标签不再重复计时、页面切走时不再空转。
> 宿主半有改动(`index.mjs`)→ **需要重启一次 DSH**;浏览器半刷新页面即生效。
>
> 2026-09-20 补完:原计划"留待下个版本"的两项(① 占用格的采样口径说明 ② 长尾选项「展开 / 收起」三处统一)
> 已在本版一并做完 —— 本版当时尚未发布(没 push、没打 tag),所以直接并进 1.19.0,不再另起小版本号;
> 这两处都只动浏览器半,刷新页面即生效。
### 新增
- **进度条显示「上一轮 ↑X.X 万」**:以前只有"token/分钟"这种匀速度量,看不出是哪一轮把上下文顶炸的;
现在 `.gs-dock-time` 行末尾直接给"上一轮涨了多少",涨幅 ≥ 5 万 tok 时变警示色(深浅主题各一套色)。
纯函数 `lastJumpRise` 只认"账本里 ≥2 次轮次跃升",数据不够就不显示,不猜。
- **交接摘要改走宿主接口落盘(省一次模型往返)**:以前点「总结要点」是把一段提示词塞进输入框、让模型自己
用 write 工具写 `HANDOFF.md` —— 要花一轮完整请求,还得赌模型听话。现在改成:提示词让模型只输出
`<<>>…<<>>` 包起来的摘要,前端观察整个 `document.body`(900ms 静默去抖、流式未写完先跳过、
同一段只落盘一次),抓到手就直接调宿主的 `POST /api/greet-signoff/handoff` 写文件并提示"已写入"。
接口失败自动退回复制到剪贴板。**宿主半那个接口从 v1.14.0 起就存在,但浏览器半一直没调用过 —— 这次接上了。**
- **多标签不再重复计时**:同一会话开两个标签时,两边各自累加活跃时长会让数字翻倍。现在每个标签有随机
`tabId`,靠 `gs.signoff.lock.` 锁(12 秒有效期)+ `BroadcastChannel` 辅助选出"记账的那个",
非主标签只读不写。
- **版本错配直接写在进度条旁边**:宿主半版本与浏览器半版本不一致时,`.gs-dock-note` 行直接提示
「⚠️ …不一致:重启一次 DSH 才会全部生效」,不用再翻设置页自检。
- **花费单价可配置 + 与台账对账**:宿主半 `/cost` 支持 `?priceIn=&priceCache=&priceOut=`(元/百万 token,
非法值自动回落内置的 1 / 0.02 / 4),响应新增 `pricing`(本次生效价来自自定义还是内置)与 `reconcile`
(算得金额 vs 台账 `ledgerCostCNY` 的差额比例);本会话、今日、近 N 天、Top5 全部使用同一份单价。
设置页新增「计价口径」三项输入与「恢复默认」,并显示对账差额(> 20% 给警示色)——
DSH 调价后不会再静默算错。
- **按历史峰值建议预算档位(只提示,绝不自动改)**:新账本 `gs.signoff.peaks` 记最近 30 次会话的占用峰值,
纯函数 `suggestBudget` 取 P90 给出建议的黄线/红线(样本 < 5 条不给建议);设置页显示建议 +「采纳」按钮,
到线横幅在"实际占用明显超过红线"时追加一句建议。
- **数据源拿不到时直说**:宿主半没响应、或本会话还没有投影文件时,进度条下方明确提示
「拿不到上下文明细:宿主半没响应或本会话还没有投影(重启一次 DSH 试试)」,不再只显示一个「—」。
### 优化
- **页面隐藏时暂停心跳**:标签页切到后台时跳过心跳的记账与重算,回到前台立即补算一次;隐藏时段不计入活跃时长。
- **「上下文占用」格补上采样口径说明**(原"本轮未实施"第①项,已做完):这一格以前只有一个数字,
看着像随手估的。现在鼠标停上去就说清楚三件事 —— ① 它是"**发下一条消息时预计要喂进去多少 token**",
不是屏幕上现在有多少字;② 数 = 上一次请求真正喂进去的 prompt(`pressureTokens`)+ 那次采样之后界面上新增的部分;
③ **只在每轮请求结束后刷新一次,天然滞后一轮**。预算档下顺带写出当前黄线 / 红线与本次读数,
超过红线还会提示"建议开新会话";切回"占模型窗口"口径则换成另一套说法。纯函数 `occupancyTip` 产出文案。
- **长尾选项的"展开 / 收起"三处统一**(原"本轮未实施"第②项,已做完):以前配色旁边挂一个「全部 37」按钮,
动效与车型是下拉末尾一条「▾ 显示全部(N 项)」,而且**展开之后就收不回来**(只能去设置页底部那个开关切)。
现在配色 / 动效 / 车型共用 `moreOptionLabel` 一套文案与同一个开关值:收起时「▾ 显示全部(N 套)」、
展开时「▴ 只看常用(M 套)」,从同一个下拉里就能收回精选;配色那个独立按钮删掉了
(净减一处重复控件,行为只多不少)。
- 单测:`moreOptionLabel`(量词 / 缺省 / NaN 兜底)、`moreOption` + `isMoreOptionValue`(`__more__` / `__less__` 与
正常选项不混淆、精选表里不许混进保留值)、`occupancyTip`(预算档 / 窗口档两套文案、空参与脏值不炸)、
`dockKpiCells` 把说明挂到占用格自己的 `title` 上(没传仍是空串,老调用方行为不变)。本轮共 78 项全绿。
### 其他
- **体积门禁 340KB → 380KB**(`scripts/verify-manifest.mjs`):这一版超限的主因是核心功能(读数、交接、互斥、对账),
不是长尾选项 —— 实测 40 种动效 + 32 个图标 + 37 套配色 + 形状/填充/阴影 + 风格预设全部加起来只占 client.js 的 5.9%,
门禁继续只用来防"长尾膨胀"。
- **被评估后否决的改动**:原本计划把长尾表拆成独立 JSON 懒加载 —— 实测只能省 ≤4.2KB(1.2%),却要引入
注入时序、旧配置回显、首屏竞态三类风险,判定不值得,不做。
## [1.18.0] - 2026-09-20
> 发哥三连问:「已聊时长别把上次的会话也记进来」「实测速率和到线约还有上一轮就没显示」
> 「截图里日常跟跟随默认档是不是重复了」。三件事都查实了,改动全在浏览器半(刷新页面即生效)。
### 修复
- **已聊时长不再串会话、不再吞断档**:以前直接拿宿主半给的 `session.header.createdAt` 相减;
而宿主半在"不知道你在看哪条会话"时会退回它自己记的上一个会话,于是把**上一次**的创建时间
递过来 —— 新会话就顶着上次的已聊时长。现在两级修:① 只有当宿主半答的 `sessionId` 与当前
会话 id 一致时才采信它的时间(不一致一律不用);② 已聊时长改成按会话 id 各自记账的
**活跃时长**:每 5 秒心跳累加,两次心跳间隔超过 5 分钟算断档、不计入;恢复旧会话时从
当前时刻重新起算,绝不把隔夜的断档算进来。账本存 localStorage,刷新页面接着累。
- **实测速率 / 到线约还有不再长时间显示 "—"**:① 速率门槛从 60 秒放宽到 20 秒(聊得快时
两轮间隔常常不到一分钟);② 时间轴采样不够时,退回"最近两次轮次跃升"的斜率兜底;
③ 采样账本(samples / jumps / jumpTimes)按会话 id 落盘,刷新页面立刻恢复,不用重新等一轮。
说明:DSH 的 token 占用本身只在**一轮请求结束后**才更新,所以这两格仍是"一轮跳一次"
而不是秒级实时 —— 但不再出现"上一轮什么都没有"。
- **预算档菜单不再自相矛盾("日常" vs "跟随默认档")**:查实确实重复 —— 当前档就是默认档时,
"日常"那一行被高亮、菜单最后一行又写"跟随默认档 · 当前就是这个",说的是同一件事。
现在:当前生效档只在**它自己那一行**标「✓ 来源 · 当前」(来源 = 本会话临时档 / 来自默认档 /
内置默认);「跟随默认档」只在**本会话设过临时档**时才作为还原操作出现,没设就不出现。
### 测试
- 新增纯函数用例:`activeElapsed`(首次种子 / 断档不累加 / 连续心跳累加 / 旧会话不从创建时间起算)、
`rateFromJumps`(跨度不足返回 null / 正常给斜率)、`tokensPerMinute` 的 `minSpanMs` 参数。
## [1.17.1] - 2026-09-20
> 发哥反馈:「箭头左右两侧还是没有跟输入框一样,有时候一样有时候不一样。」
> 只改浏览器半(刷新页面即生效):修"与输入框对齐"这件事的触发条件 —— 以前只有加载时量一次,
> 之后 hero↔对话态切换、侧栏收放、会话加载完这类**只改位置不改尺寸**的变化全都漏掉。
### 修复
- **对齐观察者原来是失效的**:`ResizeObserver` 观察的是 `el.parentElement`,那是宿主插槽层
(`display:contents`,宽高恒为 0),永远不会回调 —— 等于没观察者。现在改为观察真正会变尺寸的
composer 容器 / 输入框卡片 / 它们的父层 / 进度条自己。
- **补上 MutationObserver**(composer 容器子树:子节点、class、内联 style,触发合并到一帧):
位置变了但尺寸没变的情况也能立刻重测。
- **输入框selector放宽**:DSH 现在是 `contenteditable` 富文本(实测 textarea 数量为 0),
以前那条只认 `textarea` / `[contenteditable="true"]` 的路径会量不到;现在认
`textarea, [contenteditable], [role="textbox"]`。
- **量歪保护**:两条边加起来超过容器一半、或卡片比容器还宽,一律判为量歪并丢弃,
保留上一次的正确值 —— 宁可不动,也不会把进度条挤成一条。
- **挂载后补测**从 2 次(300ms / 1200ms)加到 3 次(+2600ms),覆盖会话内容异步长出来的阶段。
### 实测(无头 Chrome,实时页面)
- 基线:`rowPadL=16px`、进度条卡片 `left=493` = 输入框卡片 `left=493`(对齐)。
- 用内联样式把输入框卡片右移 40px(**不触发任何 resize 事件**)→ 进度条自动跟到
`rowPadL=36px`、`left=513` = 卡片 `513`(对齐);撤掉内联样式 → 回到 `16px / 493`(对齐)。
修之前这条路径不会重测,这正是"有时候一样有时候不一样"的来源。
## [1.17.0] - 2026-09-20
> 发哥要求:「到线约还有多少轮,也移动到实测速率后面,样式也一样。」
> 本版只改浏览器半(刷新页面即生效,不用重启 DSH):KPI 那一排从五格变六格,时间行只剩「上次读数 X 前」。
### 新增
- KPI 读数排新增第六格 **「到线约还有」**:显示 `N 轮`(玫红系,与状态色绿/黄/红和前面五格都不撞色),
位置就在「实测速率」右边,样式与其它格完全一致(上面 11px 小标签 + 下面同色淡底数字胶囊)。
轮数没算出来时写 `—`;设置里关掉「时间显示」时这一格不出现(沿用原开关语义)。
- 列数跟随格数(4 / 5 / 6 列),六格时 ≤1080px 退回 3 列、≤620px 退回 2 列。
- 单测 1 条(`test/client-pure.test.mjs`):六格顺序、玫红与淡底、暗色档、缺数据写 `—`、老调用方三格不变。
### 变更
- 进度条下的时间行不再显示「到线约还有 N 轮」(已搬进 KPI 排),只剩「上次读数 X 前」;没内容整行不渲染。
- 顺带修一处旧口径:以前只有"实测速率还没算出来"时才显示轮数,现在只要算得出轮数就一直显示(就在速率右边)。
## [1.16.0] - 2026-09-20
> 发哥要求:「把『日常』『实测』这两个功能放到(上下文占用那一排)里面,同一排、同样的样式。」
> 本版只改浏览器半(刷新页面即生效,不用重启 DSH):KPI 那一排从三格变成五格。
### 新增
- KPI 读数排新增第四格 **「预算档」**:显示当前档位(日常 / 大任务 / 省着聊 / 自定义),
样式与占用 / 花费 / 时长完全一致(上面 11px 小标签 + 下面同色淡底数字胶囊,紫色系);
这一格是**可点的**,点它弹出四档 + 「跟随默认档」的菜单(菜单改为挂在格子里向上弹,
不再是"fixed 不带坐标"——那种写法滚动后会跟着视口漂)。
- KPI 读数排新增第五格 **「实测速率」**:`~X/分`(青色系,避开状态色绿,免得和「上下文占用」混淆),
数据不足时写 `—`;它原来只是时间行里的一截小字,现在与其它读数平级。
设置里关掉「时间显示」时这一格不出现(沿用原开关语义)。
- 列数跟随格数(4 / 5 列),窄窗口由 media query 退回 3 列 / 2 列,避免"万/分"被挤成省略号。
- 单测补 1 条(`test/client-pure.test.mjs`):五格顺序、颜色、暗色档、`本会话`后缀、
以及"老调用方不给 showMode/showRate 时仍是最初三格"的向后兼容。
### 变更
- 紧凑 / 纯文字形态下没有 KPI 排,仍保留原来那个 🎯 小胶囊,保证任何形态都够得着换档。
- 时间行不再重复渲染速率,只留「到线还有几轮」与「上次读数 X 前」。
## [1.15.3] - 2026-09-20
> 发哥反馈:「截图蓝色框内的进度条线没有颜色,把(这些 token 是谁占用的)文字删除。」
> 本版只改浏览器半(刷新页面即生效):构成明细的每行进度条有自己的颜色,标题也短了。
### 修复(红卡里那排白条)
- 到线是红底白字的卡,而「上下文构成」的填充色此前被写成 `currentColor`(= 纯白),
红底上一排白条分不出哪块是哪块,看着就像"没有颜色"。
- 现在每行按行号取 `PART_COLORS` 里一个固定色(`--gs-part-color`):琥珀/天蓝/绿/粉/紫/青…,
浅底与红底(#d93026)上都够亮、够分得清;没到线时也不再跟着进度条色带一起变红变绿。
### 变更(标题瘦身)
- 展开按钮从「▸ 上下文构成(这些 token 是谁占的)」改成「▸ 上下文构成」;
那句解释挪到 `title` 悬停提示里,红卡里的加粗标题不再长得像一整句话。
### 验证
- `node --check client.js` 通过;`node --test` 59 项全通过。
- 无头 Chrome 渲染同款红卡 + 同款 CSS:6 行填充色分别为 rgb(245,158,11) / rgb(14,165,233) /
rgb(34,197,94) / rgb(236,72,153) / rgb(139,92,246) / rgb(6,182,212),无一行是白色。
## [1.15.2] - 2026-09-20
> 发哥反馈:「这些内容要放在同一个胶囊型里面,宽度也要一致,不要什么都自己另起一方,
> 很不美观又占位置。」本版把进度条区所有零件收进**同一个胶囊容器**(浏览器半,刷新页面即生效)。
### 变更(一个胶囊容器装下全部零件)
- 新增统一容器 `.gs-dock-stack`:三格读数(占用/花费/时长)、进度条、时间行、🎯 预算胶囊、
到线横幅、上下文构成、档位建议、提示文字**全部**装进同一张卡 —— 共用一条边框、一个底色、
一条左右边界;不再各自另起一方。
- 三格读数不再是自己一张卡:去掉边框/底色/圆角,只留一条细分隔线把它与进度条分区。
- 到线横幅也不再挂在容器外面:它现在是同一个胶囊里的一块(红底/黄底仍按线色)。
- 宽度一致性由容器统一保证:容器内容区与输入框**左右边界分毫不差**(缩进仍来自 `composerInsets()`)。
- 🎯 档位菜单改 `position:fixed`(卡片内定位会被圆角卡边界裁掉);打开时恒定在标题行正上方,
并自动夹在视口内。
### 验证
- `node --test` 59 项全通过;`node scripts/verify-manifest.mjs` 通过。
- 无头 Chrome 实测:容器内容区 841px == 三格/进度条/预算胶囊宽度 841px,三者 left/right 完全一致;
三格 `border-width:0`、背景透明;档位菜单 `position:fixed`、完整可见。
## [1.15.1] - 2026-09-20
> 发行版级的小修:把到线横幅下面**各自成行的两条提示并进横幅本身**,三块信息共用一张卡
> (浏览器半,刷新页面即生效)。
### 变更(一张卡,不再上下三条)
- 到线/超线时,「上下文构成(这些 token 是谁占的)」与「💡 档位建议」不再挂在横幅**外面**各占一行,
而是并进同一张横幅卡:卡内用一条细分隔线接在标题行下面,底色/文字色继承卡片(红底=白字)。
- 横幅改为纵向两层结构(`.gs-dock-banner` > `.gs-dock-banner-top` + 两个附加块),
标题行与按钮行仍是一行;卡内构成明细的进度条底色改用半透明白,红底上看得清。
- 卡内展开构成明细时停掉红卡的呼吸闪动(`.gs-dock-banner-open`)—— 一闪一闪没法逐行读数。
- 只有**没到线**时(没有卡片可依附),两条提示才恢复成原来的轻量小字各占一行。
## [1.15.0] - 2026-09-20
> 这一版只改**输入框上方这块进度条长什么样**(浏览器半,刷新页面即生效):
> 按发哥选定的「方案 7 · KPI 三格 + 贯通线」重排,配色取「A 语义状态色」+「C 数字胶囊底」。
### 变更(方案 7:KPI 三格 + 贯通线)
- **三格读数**:占用 / 花费 / 时长做成等宽三格(数字对齐成列),三格与输入框**同宽同左右边界**
(缩进仍由 `composerInsets()` 实测得出,随窗口与 hero→对话态切换实时同步)。
这三格取代了原来"三行灰字 + 一条粗条"的排布;下方贯通线只保留状态色实色填充。
- 三格拿不到数据的格子一律写 `—`,绝不写 0(写 0 看起来像坏了)。
### 变更(配色 A:语义状态色)
- **占用的数字颜色 = 状态本身**:安全=绿(配色方案第一色)、到黄线=黄、到红线=红,
不用比大小就知道"还能不能聊";进度线填充色跟着状态走(实色,不再走整条渐变;
未走到的部分仍保留淡色色带,越往后越危险)。配色方案(设置页那 37 套)依旧生效,用的是每套的三色。
- **花费固定财神金、时长固定蓝**;深色主题下自动换亮一档(`#b8860b → #e6b84d`、`#2563eb → #7aa2ff`),
跟着方案 C 的深浅判定(`html[data-gs-dark]`)走,dark-only 皮肤也能看清。
### 变更(配色 C:数字胶囊底)
- 每个数字后面垫一层**同色淡底胶囊**(`tintOf()` 现算 `rgba(...)`:浅色 0.14 / 深色 0.24),
颜色仍按语义走;底色用 rgba 而非固定色,所以占用那格换成任何配色方案都不会撞色。
### 变更(信息不再重复)
- 原来那一行"⏱ 已聊 X · 本条会话 ≈¥Y"搬进了三格,进度条下只留三格里没有的两个数:
**实测速率**(~X tok/分)与**读数新鲜度**(上次读数 X 前);两者都没有就整行不渲染,不留空行。
- 「紧凑」与「纯文字」两种形态未动(纯文字形态照旧把读数压在一行里)。
### 新增(测试)
- 新增纯函数 `dockKpiCells()`(三格读数与配色)与 `tintOf()`(同色淡底),并补 2 组单测:
三档状态色、深浅两档、坏值/空值一律 `—`、非十六进制颜色不炸。总用例 59 项全过。
## [1.14.0] - 2026-09-20
> 这一版的主线:**把"还剩多少"变成"花了多少钱、钱花在哪、接下来该干什么"**,
> 同时把设置页里那些"选项比需求多一个数量级"的地方收一收。
### 新增(花费看得见:这条会话花了多少钱)
- **为什么加**:进度条一直在说"用了 24.9 万 token",可 token 是个抽象单位 —— 看完还是不知道这值多少钱,
而单价早就标定过(未命中 ¥1/M、缓存命中 ¥0.02/M、输出 ¥4/M)。现在进度条那行直接写 `≈¥0.42`,
悬停提示里也有一份;设置页新增「花费与上下文」分区,给出今天 / 近 7 天 / 最贵几条会话的台账。
- 数据来自本机 `dsh-usage/usage-ledger.json`,**不联网**;宿主半是旧版就整段显示"读不到",不影响进度条本体。
- 顺手把这条反直觉的事实摆在设置页里:**输出价是缓存读的 200 倍** —— 少让我啰嗦,比省那点上下文更省钱。
### 新增(上下文构成明细:这些 token 是谁占的)
- **为什么加**:只说"用了多少",用户没法动手;能砍的只有占大头的几块(系统提示词、工具定义、记忆注入、
助手历史、工具结果)。现在进度条下面多一行「▸ 上下文构成(这些 token 是谁占的)」,点开是一列带占比条
的明细,按 token 降序,前几名就是能砍的地方。
- 数据由宿主半读本机上下文投影(`GET /api/greet-signoff/context` → 本机 `session_projcache`),默认收起、按需展开。
### 新增(档位建议 + 到线系统通知)
- **档位建议**:到线时不再只是报红,还会按客观计数说一句"这条会话已经超线 2.3 倍、还在涨 —— 是个没干完
的大任务就切「大任务」",按钮就在旁边。反向也管:挂着「大任务」档却只用到 45% 以下,会提醒可以降回
「日常」。**只提示,不自动改档**(换档是用户的决定),设置页可关。
- **系统通知**:页面在后台、标签页看不见时,到线可以发一条浏览器通知。设置页「花费与上下文 → 系统通知」
打开,会先向浏览器申请权限;拿不到权限就如实说"没打开",不留一个假开关。
### 新增(交接包:换会话时把摘要落盘)
- **为什么加**:以前点「总结要点」只是把"写摘要"这句话发出去 —— 摘要仍旧留在即将被换掉的那条会话里,
新会话开局还是空的,"提醒 → 换会话"这条链断在最后一步。
- 现在这句话多要一步:**要求把摘要用 write 工具写进工作区根目录的 `HANDOFF.md`**(宿主半的提示段里同时
约定:工作区存在 `HANDOFF.md` 时,开场先读它)。摘要从"对话里的文字"变成"新会话能直接拿到的文件"。
### 变更(换档入口:从"点一下轮转"改成"点开选")
- **为什么改**:旧版点一下胶囊在 `日常 → 大任务 → 省着聊 → 跟随默认` 里轮转 —— 轮到哪一档、下一步变成
什么、当前算"本会话临时档"还是"跟随默认",全得靠猜。现在点开就是一个列表:四档各自的线都写着,当前档
高亮,并注明它是哪来的。三个入口(胶囊 / 横幅按钮 / 设置页)都还在,走的仍是同一个 `applyBudgetMode`。
### 变更(精简:选项少一点,决定快一点)
- **长尾选项默认收起**:40 种动效 / 37 套配色 / 30 种车型,默认只列 6~8 个精选,下拉末尾挂一条
「▾ 显示全部(N 项)」;也可以去设置页「选项范围 → 全部」一次性放开。当前正在用的那一项**永远保留**,
不会因为收起而显示成别的档。
- **旧文案兼容 30 条 → 8 条**(换过固定行才需要的一次性兜底,30 条纯属用不到)。
- **「大小 + 朝向」合并成「尺寸」**(小 / 中 / 大):朝向固定"车头向右",天生朝左的车型自动镜像 ——
多一个"原样"开关只是多一格设置、多一个没意义的选择。
- **诊断分区默认隐藏**:它对日常使用是纯噪音;排障时在「花费与上下文 → 诊断」点一下就能显示。
### 删除(不再预测"还能聊多久")
- **为什么删**:那是拿最近 45 分钟的采样外推出来的,而 token 读数本身还要等上一轮请求结束才更新 ——
猜偏一次,这个数就再也没人信了。现在时间行只留两个"确实发生过"的数:**已聊多久** + **实测 ~1.2k/分**;
「约还能聊 N 轮」只在速率算不出来时作为兜底出现。
### 新增(测试)
- 单测 34 → **39**(浏览器半):长尾选项裁剪(精选 / 保留当前值 / 不共享引用)、金额写法(分、角、元、坏值)、
旧文案 30 → 8 的上限校验,外加一条 v1.14.0 源码级契约测试(花费 / 明细 / 档位菜单 / 诊断开关 / 通知都在)。
- 宿主半:新增上下文投影解析、花费聚合、交接落盘三组纯函数单测。
## [1.13.0] - 2026-09-20
### 新增(预算模式 = 「大任务模式」:三档预设 + 自定义)
- **为什么加**:预算线只有一组手填的数字时,日常小任务(7.5 万 tok 就该换会话)和一次大改造
(动辄十几万 tok)用的是同一条线 —— 大任务下会一直报红、提醒就变成了噪音。所以给它一个"档位"。
- **三档预设 + 自定义**:`日常` 7.5 万/11 万(默认)· `大任务` 15 万/20 万 · `省着聊` 5 万/7.5 万 ·
`自定义`(用设置页手填的两个数)。
- **优先级**:本会话临时档 > 全局默认档 > 内置默认。临时档存 `localStorage` 的
`gs.signoff.mode.`,默认档存在 `gs.signoff.ui.budgetMode`。
- **三个入口**:
1. **进度条行里的 `🎯` 小胶囊**(主入口,任何形态都在):点一下按
`日常 → 大任务 → 省着聊 → 跟随默认` 轮转,只改**本会话**;切完在进度条下方那行提示里
回一句"现在生效的两条线是多少"。
2. **到线横幅上的按钮**:红线/黄线横幅里新增「切大任务」(一键抬到 15 万/20 万)和
「跟随默认预算」(清掉本会话临时档)—— 正好放在"要不要开新会话"这个决策点上。
3. **设置页**「进度条外观 → 预算模式」:四个档位按钮,改的是**全局默认档**。
- 还没进会话(hero 态、拿不到 sessionId)时点胶囊会改默认档,免得点了没反应。
- 原来那两项输入框改名为「黄线(自定义档)/ 红线(自定义档)」,语义不变(选「自定义」时才生效)。
- 纯浏览器半改动:刷新页面即生效,不需要重启 DSH。
### 新增(测试)
- 新增 2 组纯函数单测(档位解析优先级 / 胶囊轮转顺序),并把 `resolveBudgetMode`、
`normalizeBudgetMode`、`budgetModeLabel`、`budgetModeHint`、`nextBudgetMode` 挂到 `__test`。
- 新增 1 组源码级防误删测试:三个入口的标签(`切大任务` / `跟随默认预算` / `gs-mode-pill`)必须还在。
## [1.12.0] - 2026-09-20
### 变更(进度条口径:从"占模型窗口"改成"占你自己的预算")
- **为什么改**:原来那条进度条的分母是**模型窗口(本机 100 万 tok)**,所以一条已经 24.9 万 tok
(每轮都要重读一遍、早该换会话)的会话,界面上只显示 **24%**,看着像很安全 —— 这个数回答的是
"离塞满模型窗口还有多远",不是"离你该换会话还有多远"。
- **新的默认口径 = 预算口径**:进度条 **100% = 你自己设的「必须换会话」线**(默认 **11 万 tok**),
黄线默认 **7.5 万 tok**。超了会显示"超 N 倍",条子封顶在 100% 并整条变红呼吸。
- 想回到旧口径:设置页「进度条外观 → 口径」切「占模型窗口」即可(两套随时可切)。
### 新增
- **一眼就知道该换会话(不用读数字)**:
- 到黄线/红线时,进度条下方弹**大横幅**:红底白字 + 呼吸的 `🚨 该开新会话了`,
副标题写明「已 ~24.9 万 tok(预算 11 万,超 2.3 倍)· 先点「总结要点」再开新会话」。
- **浏览器标签页标题**同时加前缀(黄线 `⚠️`、红线 `🚨`)→ 扫一眼标签就知道,不用盯着页面。
- 「总结要点 / 开新会话」两个按钮直接放进横幅里,顺手就能点。
- 设置页「进度条外观」新增三项:**口径**(预算 / 模型窗口)、**黄线**(默认 75000 tok)、
**红线**(默认 110000 tok),外加一段说明。改动即时生效、存浏览器本地。
- token 数改用中文直观写法:`248930 → 24.9 万`、`110000 → 11 万`。
- 进度条那行文字在预算口径下变成 `已用 ~24.9 万 / 11 万 tok · 超 2.3 倍 · 已聊 32 分`,
悬停提示里同时给出"占模型窗口 24%"(两个数都在,但主看预算)。
### 变更(其他)
- 单测 35 → **37**:新增预算口径读数(安全 / 黄线 / 红线 / 超线 / 无读数 / 红线填反的兜底)与中文 token 写法。
## [1.11.0] - 2026-09-20
### 新增
- **进度条的时间功能(真的在走,不是摆设)**:输入框上方那条上下文进度条现在多一行
`⏱ 已聊 42 分 · 预计还能聊 ~35 分 · 实测 ~1.2k/分`,并且**每 5 秒自己重算一次**(页面在后台时不折腾):
- **已聊多久**:宿主半新增 `GET /api/greet-signoff/session?sessionId=`,把会话的
`session.header.createdAt`(durable 创建时间,浏览器半看不到)交给页面半;拿不到时退回
「这个浏览器第一次看到该会话的时刻」(存 localStorage,刷新/重启不丢)。
- **还能聊多久**:按最近 45 分钟的**读数采样**算实测消耗速率(token/分钟)再推剩余时间;
采样跨度不足 1 分钟、或读数没有净增长时**宁可不给数字**,退回原来的「约还能聊 N 轮」估算。
- **读数新鲜度**:token 读数只在上一次请求结束后才更新,所以读数超过 2 分钟没变时会标一句
「上次读数 X 前」——看着时间在走、百分比不动时不会再误以为进度条坏了。
- 时间行只出现在「完整」形态;「紧凑 / 纯文字」形态把时间并进原来那一行文字里。
设置页「进度条外观」多了「时间显示」开关(默认开)。
### 修复
- **深色皮肤下固定行颜色认成浅色(方案 C 落地)**:以前只用 `body[data-ds-dark-theme]` 判深浅,
而 dark-only 皮肤(如 astral-choir / remiel-starlit / ice-princess)只改 CSS 变量、
在 `:root` 钉 `color-scheme: dark`,**不加**那个属性 → 深色档颜色(`colorDark`)永远不生效,
深橙字压在暗底上看不清。现在判定改成四条证据任一成立即算深色:
① `body[data-ds-dark-theme]`;② 根元素 computed `color-scheme` 含 `dark`;
③ 系统偏好 `prefers-color-scheme: dark`;④ 页面底色亮度 < 0.5(兜底)。
结果写到 ``,对话里那两行的深色档 CSS 同时挂
`body[data-ds-dark-theme]` 与 `html[data-gs-dark="1"]` 两条选择器(旧主题照旧生效)。
监听 html/body 属性变化 + 系统偏好变化 + 8 秒兜底对表,**换皮肤即时重算**,不用刷新页面。
- 设置页「诊断 → 健康自检」新增两行:**深浅判定**(把四条证据的实测值都列出来)与
**进度条时间行**(是否显示、当前文字),排"颜色不对 / 时间没出现"时不用猜。
### 变更
- 单测 30 → **35**:新增深浅判定四条证据、CSS 颜色解析与亮度、`chatCss` 双选择器、
时长/时刻格式化、消耗速率与剩余时间估算。
## [1.10.0] - 2026-09-18
### 新增
- **按工作区自动换文案**:宿主半从会话的 `Session.header.cwd` 取当前工作目录,按**目录前缀**匹配绑定表
(不区分大小写、斜杠随便写,多条命中取**最长**前缀)。优先级明确:**工作区绑定 > 文案池 > 固定文案**;
某一行没填就自动回落下一级。设置页「场景」分区里有绑定表(路径 + 该项目专用的开场/收尾),
还能点「填当前工作区」从页面里把路径猜出来填上。自检接口也支持 `?cwd=<路径>` 直接验证命中情况。
- **一键外观改成小样卡网格**:16 套外观不再是按钮列表,而是一张张卡片 —— 每张卡把**当前文案**按该预设
直接渲染出来(形状/填充/边框/阴影/颜色一眼可见),当前生效的那套高亮显示。
- **常用色板**:底色与边色各加一排常用色小方块,点一下即设好颜色并自动启用「自定义底色 / 边框」。
- **一键复制到另一边**:把当前行的**文案**或**外观**整包复制到另一行(两个按钮分开,互不干扰)。
- **图片支持拖拽上传**:把图片/GIF 直接拖进虚线框即可载入(拖进来时有虚线框反馈)。
- **撤销上次保存**:底部操作条新增「撤销上次保存」,一键回到上一次保存之前的那份配置(没保存过时置灰)。
### 修复
- **`section()` 只吃一个子元素(真 bug)**:多传的内容会被静默当成 `options` 丢掉 ——
「外观样式」分区里的**形状 / 填充 / 圆角 / 内边距 / 底色 / 边框 / 边色 / 阴影**整块**从未渲染出来过**
(1.4.0 把设置页改成折叠卡片布局时就埋下了)。现在 `section()` 支持可变数量的子元素,
只有当最后一项是"普通对象"(不是 React 元素)时才当作 options。
实测修复后这些控件与 12 个色板小方块全部回来了。
### 变更
- 单测 27 → **30**:新增按工作区绑定的清洗/匹配/优先级、`lineStyleSource`(复制外观不带走文案与图片)。
## [1.9.0] - 2026-09-18
### 新增
- **场景:一键整包切换(工作 / 生活 / 深夜)**。设置页多了一个「场景」分区:
每个场景就是**保存那一刻的整份设置快照**(文案、字体、外观、阈值、文案池),
点场景名=整包切过去,`✎`=用当前设置覆盖它,`✕`=删除;最多 8 个。
坏数据(id 不合法、重复、空名)一律在清洗时丢掉,绝不因为一个场景写坏而让整份配置读不出来;
场景内部不嵌套 `scenes` 自身(宿主半 `normalizeCore` / 浏览器半 `normalizeCore` 分离),彻底避免自我递归。
- **阈值提醒条 + 一键处理**:上下文占用到黄/红阈值时,输入框上方的进度条下面多出一行提醒,右边两个按钮:
**「总结要点」**把一段"交接摘要"要求交到你手上(能直接写进输入框就写;写不进去就复制到剪贴板;
连剪贴板也不可用就把整句话显示出来 —— 全程不自动发送,免得误触);
**「开新会话」**沿用原来的一键开新会话。
- **调试钩子 `window.__dshGreetSignoff`**:版本、连接自愈计数、贴样式器统计、输入框定位与写入、
阈值判断结果都挂在上面。出问题时在浏览器控制台敲一下就看得见,也是本仓库自动化验证的抓手。
### 修复
- **输入框定位错了整整一个版本**:DSH 现在的输入框是 `[data-slot="conversation.composer"]` 里的
`[role="textbox"]`(contenteditable),页面上**并不存在** `[data-dsh-part="composer-input"]` 这个更早版本的钩子。
受影响的是连接自愈的草稿判断("有草稿就不自动重载")——它此前恒为 `false`,等于这道保护没生效;
现在改成"钩子 → 插槽 → 可见的最后一个可编辑元素"逐级兜底。
- **「总结要点」改成回读校验 + 三级兜底**:实测 DSH 的输入框是 React 完全受控的 contenteditable,
直接写入会被立刻回滚(回读校验返回 false),所以只有真的写进去了才提示"已写进输入框",
否则退到剪贴板/直接显示。
### 变更
- 单测 25 → **27**:新增 `contextAlert`(阈值判断)与 `sanitizeScenes`(场景清洗、防嵌套、上限截断)。
- 设置页分区 7 → **8**(新增「场景」),顶部快速跳转条同步多一个胶囊。
## [1.8.0] - 2026-09-18
### 新增
- **本轮信息变量:`{model}` `{count}` `{elapsed}` `{tokens}`**。宿主半挂在 `llm/stream` 瀑布上**只读统计**
(不改请求、不改 chunk):这一次调用跑了多久、用了多少 token、哪个模型、本会话第几次调用。
于是固定行可以写成 `✅ 以上,本轮 {elapsed} · {tokens} · 第 {count} 次`,或者把 `{model}` 写进开场语。
**语义说明**:`{elapsed}` 与 `{tokens}` 报的是**上一次**调用的数值 —— 本轮的数字在回复之前根本还不知道;
`{count}` 把每次模型调用算一轮(含工具往返),重启后重新计数。
- **页面侧的"通配符匹配"**:这些值只有服务端知道,页面不可能自己算出同一个数。所以浏览器半把它们编译成
通配符正则(`^以上,本轮 .+? · .+?$`)来匹配 —— 数值换成多少、换成 `820ms` 还是 `12.4s`,样式都贴得上,
三种匹配模式(逐字/宽松/近似)都生效。
- **健康自检按钮**:设置页「诊断」区点「跑一遍自检」,一次问到底并逐项给结论:
配置读取、固定文案与文案池状态、贴样式器运行次数与命中行数、进度条是否挂载、连接自愈计数、
**宿主半与浏览器半版本是否一致**(不一致就提示需要重启)、**规则文本能不能生成**、
**会话识别是否精确命中**与上一轮统计。
- **新增自检接口**:`GET /api/greet-signoff/rule-text` 返回当前这一刻模型会看到的规则文本(运行时变量已解析)、
会话识别结果与上一轮统计;`GET /api/greet-signoff` 增加 `hostVersion` 字段。
### 变更
- 单测从 18 个扩到 **25 个**,并新增宿主半单测文件 `test/host-pure.test.mjs`(文案池挑句、运行时变量解析、
耗时/用量格式化、规则文本拼装、pool 字段清洗);`npm test` 现在一次跑两个文件。
## [1.7.0] - 2026-09-18
### 新增
- **文案池:开场语 / 收尾语可以"每轮换一句"**。设置页「文案」分区里多了一块「文案池」:
一行一句(最多 20 句,每句 200 字),开启后由宿主半在**每次组装提示词时挑一句** ——
选「随机挑一句」或「按顺序轮换」(轮换游标只在进程内累计,重启后从头开始,够用即可)。
开关是两行共用的,池子内容各存各的:只想让开场语轮换、收尾照旧固定也没问题;
某一行的池子为空时,那一行自动回落到固定文案。池子里的句子同样支持全部动态变量
(`{date}` `{weekday}` `{daypart}` `{time}` …),并且**页面样式对池子里每一句都生效**
(浏览器半会把池子里的每一句都编译成待匹配行)。旁边还有「把上面的文案加进池子」一键搬过去。
- **设置页顶部快速跳转条**:文案 / 字体 / 外观 / 提醒 / 进度条 / 旧文案 / 诊断 七个胶囊,
点一下=展开该分区并滚过去(卡片带 `scroll-margin-top`,不会被吸顶预览挡住),
右边还有「全部展开 / 全部收起」一键搞定。折叠状态照旧记在浏览器本地。
### 修复
- **底部吸底操作条不再压住最后一块内容**:面板底部留白从 58px 提到 74px(操作条 45px + 余量),
滚到底时「进度条外观 / 诊断」这类末段分区的控件完整可见、可点。
### 说明
- **「大屏两栏」这条没做**:实测 DSH 设置弹窗的面板宽度固定 **549px**(滚动容器 597px),
拆成两栏后每栏不到 300px,编辑控件会挤成一团,反而更难用。所以这一版改成了上面这套
"快速跳转 + 全部展开/收起 + 更紧凑的单列 + 吸顶预览",效果比硬拆两栏好。
## [1.6.3] - 2026-09-18
### 新增
- **连接自愈(重启后页面不再永远转圈)**:`dsh web` 每次启动都换访问令牌,已经打开的页面手里那份握手就永远续不上了,
界面上会一直停在**自动重连中**(官方文案本身就是让人手动重开)。现在插件带一个看门狗:
每 5 秒看一眼页面是不是在显示连接异常,**连续 30 秒**都异常、且探活 `GET /` 仍是 200(服务端是通的、只是这个页面的握手废了)、
且输入框里没有未发送的草稿时,自动重载一次页面 —— 重载会用 cookie 重新握手,页面自己就活了。
边界与安全阀:服务真挂了(探活失败)不动作,避免刷屏;页面登录态失效(401/403)也不动作(重载只会换一张错误页);
本标签页每滚动 1 小时最多自动重载 3 次、两次之间至少隔 60 秒;有草稿一律让路(宁可多等,也不弄丢你正在写的东西)。
设置页"诊断"区新增一行「连接自愈:检查 N 次 · 看到异常 N 次 · 已自动重载 N 次」方便自查。
## [1.6.2] - 2026-09-18
### 修复
- **「导入配置」「选择图片 / GIF」里的文字贴顶、不垂直居中**:这两个"按钮"其实是 `