# 更新日志 本文件记录 dsh-status-rotator 的每个版本改了什么。格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/), 版本号遵循[语义化版本](https://semver.org/lang/zh-CN/);安装与配置见 [README_ZH.md](./README_ZH.md)。 ## [0.38.0] - 2026-10-27 ### 变更 - **常驻的 2 秒兜底轮询收掉了:默认间隔放宽到 10 秒、可配、可关,并且改成事件驱动为主**(issue #166,由 Oct1AtJoe 报告): 原来的 `rescanner = setInterval(rescanAll, 2000)` 是常驻的 —— 不管页面上有没有运行中的回合,每 2 秒都做两次全树 `querySelectorAll`(`scanTurnProcess` / `scanRunningRows`),再对已接管元素逐个读几何(`syncTruncation` 读 `scrollWidth` / `clientWidth`)。长会话下 DOM 有数千节点时,这是一笔恒定的周期开销,而且这个值和「实际有没有内容变化」无关。 现在按报告里的三档建议**取并集**落地: - **抽成配置项**:新增 `rescanIntervalMs`(默认 **10000**,范围 1 秒~1 小时,**0 = 关闭**),设置页「行为 → 基本设置」里有同族输入框, 浏览器半区 `normalizeConfig` 与 node 半区 `sanitizeConfig` 共用同一张范围表。**不写这个键的旧配置无需改动**。 - **降频 + 空闲调度**:到点后先等 `requestIdleCallback`(有 timeout 兜底,没有 rIC 的老宿主同步执行),不与流式渲染抢同一帧; **页面隐藏时不做**兜底重扫(切回来补一拍),标签页挂在后台不再付出全树扫描。 - **事件驱动替代**:常规路径本来就有 `MutationObserver` / `sourceObserver`;这次补齐三处原来只能靠轮询兜的: ① 溢出淡出改由 `ResizeObserver` 盯正文 span 的尺寸(窗口缩放、侧栏开合、字体加载都覆盖,不再每 2 秒重读一次布局); ② 宿主全屏遮罩关闭后恢复弹幕:命中后对遮罩元素做**定向属性观察**(`class` / `style` / `hidden` / `aria-hidden`), 宿主只改样式就关掉弹窗时也会立刻恢复,而不是等下一拍; ③ 服务晚于插件就绪(0.1.7 的 `sessions` 常晚一拍):从「每 2 秒无限期重试」改成**启动后 1 秒一次、最多 10 次的有限次重试**, 接上线就停,不再为「服务一直没起来」付周期开销。 兜底重扫本身保留(仍兜事件覆盖不到的边角),但默认频率降到原来的 1/5,且可用 `rescanIntervalMs: 0` 完全关掉。 ### 测试 - 新增 `scripts/issue-166-rescan-test.html`(驱动 `scripts/run-issue-166-rescan-test.cjs`,`npm run test:browser` 与 CI 都跑): 真浏览器 + 真 DOM,给 `document` / `Element.prototype` 的 `querySelectorAll` 计数,**只数旧 `rescanAll` 独有的两次全树查找**。 修前实测:关闭与默认间隔两个场景在 2.6 秒窗口内各 **2 次**(正是 2 秒硬编码的结果),修后 **0 次**;配 1000ms 时按配置发生。 另外钉住:关掉兜底后新增节点仍被 `MutationObserver` 接管、容器收窄/放宽时溢出标记由 `ResizeObserver` 跟上、 `sessions` 服务晚到约 4 秒仍被限次重试接上线(且接上后不再反复订阅),以及设置页字段默认 10000、改完整份落盘。 - `scripts/smoke-test.cjs` 补 3 条:客户端 `normalizeConfig` 的钳制 / 0 = 关闭 / 非数字丢弃;node 半区 `sanitizeConfigDocument` 同口径;`config.example.json` 的 `rescanIntervalMs` 与运行时默认一致。 ### 说明 - 既有配置与既有轮换行为不受影响:不写 `rescanIntervalMs` 时用新默认(10 秒 + 空闲调度),其余 `intervalMs` / `liveTickMs` / `reloadIntervalMs` 等节奏参数一字未动;`rescanIntervalMs: 0` 是显式关闭周期兜底,事件驱动路径照常工作。 - 这也是「`stable` dist-tag 由发版流水线自动跟随」链路稳定后的又一次常规发版;npm 的 `latest` / `stable` 都指向 0.38.0。 ## [0.37.0] - 2026-10-17 ### 新增 - **文案投稿搬进设置页:在 DSH 里写完、本地按机器人同口径校验、一键开好预填正文的 Issue**(issue #151,由 IThinkItsaName 提议): 以前投稿必须去 GitHub 填表单,写错了要等机器人跑完才收到 ❌ 原因。现在设置页多了 **「投稿」** 页签: 语种(zh / en / 两种都要)、分组(thinking / running / long / 全三阶段)、目标词库包(下拉直接读当前文档里的包)、 文案(一行一条)、可选署名、以及**与 GitHub 表单同一份提交须知**。校验规则**与 `scripts/phrase-bot.cjs` 完全一致** (分号、HTML、链接、控制字符、单条 ≤200 字符、≤60 行、展开 ≤120 条、与现有词库查重),当场逐条报原因; 另加一条只在设置页生效的结构检查 —— 行首 `#` / `---` 会被机器人的正文解析当注释或分隔线丢掉,提前拦下来。 点「**打开投稿页**」直接开一个**正文已填好**的 Issue(不打字、不粘贴),机器人照常校验、回复「立即试用」并开 PR; URL 装不下(超长)或浏览器拦弹窗时,「**复制正文**」把同一份内容放进剪贴板,正文本身也在页面上只读可见、可手动选中。 投稿草稿**不是配置**:它不进 `editorState`,填写过程一次盘都不写(回归页有断言钉住这一点)。 ### 测试 - 新增 `scripts/issue-151-submit-test.html`(驱动 `scripts/run-issue-151-submit-test.cjs`,`npm run test:browser` 与 CI 都跑): 真浏览器 + 真 DOM 走完整交互(页签与五个控件 → 空文案不就绪 → 打字出正文 → 勾选须知解锁按钮 → 「复制正文」拿到的就是正文 → 「打开投稿页」的 URL 里 `body` 与正文逐字一致、并带 `title` / `labels`), 还钉住分号 / 链接 / 全重复被拦下、部分重复只提示不阻断、以及**投稿交互不触发配置写盘**。 - `scripts/smoke-test.cjs` 新增一节,把"两边同口径"这件事做成**对齐测试**:同一批输入分别喂给设置页的纯函数与 `scripts/phrase-bot.cjs`,逐条比对归一化、语种/分组/目标包解析、以及**是否拒绝**; 并直接拿机器人自己的 `parseSubmission` + `validateSubmission` 解析设置页生成的正文(中文、双语×三阶段、ads 包), 证明"一键投稿"产出的正文确实会被机器人正确处理。为此把机器人的纯函数 `parsePackValue` 一并导出(行为未变)。 ## [0.36.0] - 2026-10-15 ### 修复 - **核心词库为空时,「文案」页的自动落点会选到未启用的词库包:打字确实写盘成功,状态栏却永远不显示,而且全程没有一句提示**(issue #144,由 qaz2325698718-dot 定位): `load()` 里的回落只认「包列表第一个」——`if (target === "" && coreEmpty && packs.length > 0) target = "pack:" + packs[0].id;`。 报告的配置是 14 个包只启用 `slacking`、顶层 `phrases` 全空,而 `packs[0]` 是未启用的 `reverse-proxy`:于是打开「设置 → 状态文案 → 内容」时编辑目标被静默改成 `pack:reverse-proxy`,用户打的字 400ms 后照常 PUT、也确实落盘(`saveState` 不进 error),但运行时只合并 `enabledPacks` 里的包 —— 写进去的文案永远不会进状态栏;又因为那个包本来就是空的,用户打开「文案」看到的是一片空列表,更像「我的 22 条文案被清掉了」。现在回落改用共享纯函数 `pickFallbackPack()`:**优先选启用中且有内容的包**,启用包都没内容时退回第一个启用包;一个包都没启用时只能落到 `packs[0]`,但设置页会立刻给出提示。同时补上缺的那层反馈:当前编辑目标不在启用列表里时,文案区显示「该词库包未启用,保存的文案不会出现在状态栏」(`role="status"`),「编辑目标」下拉把未启用的包标成 `未启用 · 包名`,并且已经选定的编辑目标只要仍然存在就不再被重读切走。文档没写 `enabledPacks`(= 全部启用)时行为与改动前一致。 - **状态行的行根新增公开白名单标记,别的皮肤/插件不必再用 `aria-hidden` 放行这条线**(issue #141,由 PerseusNana7mi 定位): [dsh-claude-style](https://github.com/Nwflower/dsh-claude-style) 0.13.2 的等待行写过一条「除我之外全隐藏」的规则 —— `[data-chat-running][data-dsh-claude-wait] > span > :not([aria-hidden="true"]):not(.dsh-claude-chat-wait){display:none}`。 `> span` 命中的正是 `span.runningContent`,而插件接管 0.2.0 运行行时把状态行 append 进同一个父母,于是整条线被 `display:none`:短语照轮换、时长照读、SSE 照响应,**只是看不见**。现在两条造线路径(`buildRunningLine` / `buildStatusLine`)的行根都带上 `data-dsh-status-rotator-line`,既有的 `dsh-status-rotator-line` class 同时作为公开 别名 —— 消费方可以按 `:not([data-dsh-status-rotator-line])` 放行,而不必给一条可见的文字行打 `aria-hidden`(那会丢读屏)。 插件**不**给标记加 `display`、也不与皮肤抢级联:标记是白名单口,不是自愈机制,别人不写 `:not(...)` 时结果不变(上游 claude-style 已在 [0.13.3](https://github.com/Nwflower/dsh-claude-style/releases/tag/v0.13.3) 把旧规则改成只隐藏它自己的 文案节点;装 0.13.2 的用户升级即恢复,本仓库这一改是为了让契约稳定、不再依赖对方的选择器写法)。 ### 测试 - 新增 `scripts/issue-141-coexist-test.html`(驱动 `scripts/run-issue-141-coexist-test.cjs`,`npm run test:browser` 与 CI 都跑): 真浏览器加载真实 `lib/client.js`,用真实 DSH 0.2.0 运行行 DOM 走一遍 `apply()`,再分别挂 0.13.2 旧规则 / 补了白名单口的 旧规则 / 0.13.3 修法,量 `getComputedStyle` + `getClientRects()`。同一页里既钉住「旧规则下状态行确实 `display:none`」, 也钉住「只加标记、消费方选择器不改仍然 `display:none`」,还钉住「0.13.3 修法下状态行可见且宿主文案仍被隐藏」。 - 新增 `scripts/issue-144-disabled-pack-test.html`(驱动 `scripts/run-issue-144-disabled-pack-test.cjs`,`npm run test:browser` 与 CI 都跑): 真浏览器加载真实 `lib/client.js` + 真 DOM,按报告人的文档形状(顶层 `phrases` 全空、`packs = [未启用包, 启用包]`、 `enabledPacks = [启用包]`)走一遍「打开文案 → 看自动落点 → 打字 → 追 PUT 落进哪个包 → 重读 → 手动选未启用的包」。 修前 3 个场景全挂(落点 `pack:reverse-proxy`、PUT 写进未启用的包、无提示、下拉无标记、无效 `enabledPacks` 原样写回), 修后 58 项断言全过。另外两个场景钉住「一个包都没启用时不许静默、且不该打开就是空编辑区」与 「`enabledPacks` 整份无效(含非字符串项)= 运行时口径的全部启用,界面不许把包标成未启用、也不许把无效数组写回」。 设置页夹具与 CDP 驱动同时抽成公共件 `scripts/settings-harness.js` / `scripts/browser-test-runner.cjs`,避免每个浏览器回归页各抄一份。 - `scripts/smoke-test.cjs` 新增「自动落点只选启用中的词库包」一节:覆盖 `pickFallbackPack()` 的各种文档形状 (只启用后面的包 / 两个都启用 / 都没内容 / 一个都没启用 / 缺 `enabledPacks` / 名单里有不存在的 id / 脏数据), 并直接核对新增文案 `packs.disabled`、`packs.disabledWarning` 中英都存在且占位符一致。 ## [0.35.1] - 2026-10-11 ### 修复 - **又大又居中的代码块被当成「画底色的面板」,弹幕层挂进了它自己的 `
`,复制代码会把弹幕文案一起复制走**(PR #139,由 NekoDD 定位并修复):
  `backgroundPanelOf()` 只问两件事 —— 「画了不透明背景」和「面积够大」;宿主会话列一窄,一个居中代码块就必然
  ≥ 80% 宽 / 50% 高,两条判据都满足,层于是被挂进了 `
`。宿主代码块的复制按钮读的正是 `pre.textContent`,
  它按文档序拼接全部后代文本 —— 排在最后的弹幕文案被追加到复制结果末端;屏幕上完全看不出来(层是
  `absolute` + `z-index: -1` + 默认 0.3 透明度,不参与排版)。现在回溯挂载点时跳过内容区
  (`pre` / `.md-code-block` / `[data-code-block-content]` / `[data-chat-flow-kind]`),继续往上找真正的底色面板;
  面积判据与兜底路径(无面板 → 主框架,无框架 → body)一字未动。

### 词库

- `star-ask` / `star-route` 两个 star 词库包随词库机器人刷新(PR #137):`star-route` 中英各 +34 条,
  全库合计 **1221 → 1289** 条。两个包出厂默认关闭,现有配置不受影响。

### 文档

- 中英文 README 按「效果展示 → 安装 → 配置与开发资料」重排,详细参数保留折叠入口。
- 更新状态行与四动作鲸尾预览,补齐 150 帧官方原版晃动及其 4 秒推荐周期;新增五种外观主题与实际弹幕展示。
- 补充 Desktop 从 npm 安装的图解,使用清晰的安装对话框原图;素材使用固定提交的绝对地址,兼容 GitHub 与 npm 包页。
- 修正文案计数、默认关闭的词库包说明,以及 JSONC 示例、字号范围和弹幕指针交互说明。

### 测试

- `scripts/danmaku-mount-test.html` 新增 `?codeBlock=1` 场景:面板中心放一个与宿主同构的代码块
  (`.md-code-block` + 不透明 `
` + 复制按钮,宽 100% / 高 70%,越过 80% / 50% 两条判据),
  断言弹幕层不落进 `
`,且「复制按钮读到的内容」逐字等于原始代码。

## [0.35.0] - 2026-10-04

### 新增

- 官方原版晃动默认采用推荐速度 0.25 次/秒(4 秒一轮),固定、tok/s、随机和工具触发统一按实际动作应用;其他动作保持用户速度。可关闭推荐开关。
- 官方原版晃动:保留用户提供 GIF 的 150 帧和原始时序,以 SVG 轮廓播放,并与现有动作通过 6 帧过渡衔接。
- 「参与切换的动作」:随机模式与工具调用触发共用可勾选的动作池,至少保留一个;单动作池持续播放。

### 调整

- 动作名称改为「翻转摆尾」和「官方原版晃动」,仅官方提供的晃动标注为原版。
- tok/s 摇速从等待时每轮 1 秒开始,默认最高 1.5 次/秒;默认 3 档约为 1.00 / 1.25 / 1.50 次/秒。
- 2–5 档改为速度预设,上限依次为 1.0 / 1.5 / 2.0 / 2.5 次/秒;手动修改上限后显示自定义。自定义上限可设为 1–10 次/秒,连续模式同样支持;分档边界保留缓冲。
- 保留旧配置已选档数和连续模式,但默认速度明显降低;固定速度保持原设置。
- 修复图标在循环边界被宿主重建时丢掉完成事件、使已排队切换一直等待的问题;恢复时只补实际错过的衔接。

### 测试

- `scripts/turn-process-017-test.html` 新增 `020-running-row-missed-junction` 场景:钉住「宿主在循环边界重建图标后,
  新请求不把**已经走完的圈数**当成新衔接」这条修复;同一会话重绑定场景的等待窗口改为按官方 4 秒一轮计算
  (默认 6 秒在「两条官方循环 + 0.48 秒过渡」下不够)。
- `npm test` 由 404 增至 **408** 通过;`scripts/verify-whale-tail-tiers.cjs` 随新的档位预设(1.0 / 1.5 / 2.0 / 2.5)同步更新,
  另有 `scripts/trace-official-tail.py` 与参考素材(GIF 的 SHA-256 与文档记录一致)一并入库。

### 说明

- **已知问题(测试脆弱,非用户可见缺陷)**:上面那个新场景在固定 `sleep(40)` 之后断言精确状态,对机器时序敏感。
  实测:合并提交上 CI 的 `browser` 失败过一次,**本地复现 2/3 失败**,而插入两行日志改变时序后 **2/2 通过**。
  必需的 `test` 检查始终通过,该场景只存在于 `scripts/`(不进 npm 包)。后续应改成等待条件而不是固定窗口。
## [0.34.1] - 2026-10-02

### 修复

- **`appearance.fontSize: 0`(跟随宿主)被服务端夹成 8,保存后回退**(issue #129、#131):
  `0` 是 `config.example.json` 的出厂默认,浏览器半边**刻意放行**它,而服务端半边的合并段把它
  当成「小于下限 8」夹掉了 —— 于是设置页选 0 当场生效、服务端落盘 8、刷新 / 重连回退成 8px,
  而且**全程没有任何报错**,用户只会以为设置没存住。现在两半同口径:`0` 属于「0 = 关闭」那一类,
  不参与下限钳制。其余取值一字未动(1~7 → 8,200 → 96,区间内原样),预设走同一段合并所以一并修好。
  顺带拆开了 README 中英那句会被读成「0 也在 8~96 里」的措辞。

### 测试

- 新增 `scripts/verify-appearance-zero.cjs`:对 15 个取值**逐点比对两半口径**、0 的语义与邻接钳制,
  以及**真 HTTP 往返**(GET 整份文档 → 改字号 → 整份 PUT 回去 → 再 GET,并直接读落盘的存储文件)。
  修前实测 **8 条失败**(含 `GET → 8`、`store → {"fontSize":8}`),修后全过。已接入 npm scripts 与 CI。
- `npm test` 补 1 条:0 保持 0、4 夹到 8、200 夹到 96、预设内 0 也是 0。

### 说明

- 值改回与出厂默认相同时,用户增量会**整块消失**,所以存储文件里不会留下字面的 `fontSize: 0`。
  这是既有增量机制的行为,也是对的 —— 契约是「不残留被夹过的 8」+「读回来是 0」,脚本按此断言。

## [0.34.0] - 2026-10-02

### 新增

- **鲸尾摇速分档(`whaleTailMotion.tpsTiers`)**:tok/s 模式下摇速原本是**连续**映射(tok/s ÷ 16,
  夹在 2–6 次/秒)。6 次/秒是参考摇速(1.5)的四倍,尾巴只剩糊影。现在可以把它量化成 **2–5 个离散档位**,
  等距铺在 **2–4 次/秒**之间:只在下一次跨过档位边界时变一次速,上限也从 6 压到 4,上限档仍看得清翻转。
  - `0`(默认)= 连续,行为与改动前**逐点相同** —— 旧配置零改动;
  - `1` 档没有意义(那等于固定速度)、非 number、越界值一律退回连续;
  - 设置页「外观 → 鲸鱼尾巴」在 tok/s 模式下多一个「摇速档位」下拉,与「固定摇速」互斥显示。
  来自小黑盒用户 **洛水之蔚** 的反馈(署名见 `CONTRIBUTORS.md` / `CONTRIBUTORS_ZH.md`)。

### 测试

- 新增 `scripts/verify-whale-tail-tiers.cjs`(30 条断言):档位取值集合、上限、单调性、等待档、
  非法值退回连续、`fixed` / `enabled=false` 分支不受影响、两半校验一致,并**逐点确认不带该键时
  等于改动前的连续行为**。已接入 npm scripts 与 CI。
- 设置页浏览器测试新增 3 条:tok/s 模式渲染出档位下拉(默认连续)、选择档位真的写盘、
  固定模式下该下拉消失。

### 工程

- `release.yml` 会在打出 GitHub Release 之后**自动把 npm 的 `stable` 标签指向本次版本**
  (dist-tag 属于包本身,所以 fork 上配好 `NPM_TOKEN` 即生效);缺 token 时软跳过并留一条
  `::warning` 注解 —— 不重演「没人察觉的一步没做」。发版顺序因此明确为**先 `npm publish` 再推标签**,
  `CONTRIBUTING.md` / `CONTRIBUTING_ZH.md` 已同步。

## [0.33.2] - 2026-10-02

### 词库

- **把词库投稿 #122 / #123 的 3 条文案带进发布**:`正在演奏春日影…`、`正在投掷保龄球…`、`正在升级LV6…`。
  它们随 #123 合进了 `main`,但 `v0.33.1` 的标签指向的是**合并之前**的提交 —— 那个标签已经在 fork 与 npm 上
  发布过,不便重指,所以默认词库当时比最新 Release 多这 3 条。本版把它们带进一个 Release。
- 文案计数随之从 **1218** 变为 **1221**(仍是 14 个包)。README 中英的标语与文件树注释、`package.json` 的
  description、`lib/index.js` 的注释都已对齐;分项按合并后的词库核对:zh **674** / en **547** / 合计 **1221**,
  开 **945** / 关 **276**。

### 说明

- 本版除上述词库与计数外**没有任何代码改动**。它存在的意义是让四处公开标识重新对齐到同一个版本号:
  仓库的 `package.json`、本 CHANGELOG 的这一节、`v0.33.2` 的 Release 产物、npm 上的 `latest`。

## [0.33.1] - 2026-10-01

### 工程

- **删除内部 roadmap**:那份 roadmap 一直放在**仓库之外**(工作区根目录),从未进入 npm 包或 Release 产物 ——
  `package.json` 的 `files` 白名单里没有它,仓库里也没有对应文件。所以这次删除不改变使用者装到的东西。
- **清掉指向它的条目编号**:代码注释、验证脚本、浏览器回归页与 CI 里把 `E1` / `E2` / `E4` / `V3` / `V4` /
  `N1` / `N2` 当标签用的地方,一律改写成自解释的说明(如「N2 并发控制」→「并发控制」)。
  这些编号来自那份 roadmap,删掉之后无从解析 —— 属于「指向不存在文件的存活引用」。
  `CONTRIBUTORS` 里的 `Deepseek V4.1 Pro` 是模型名,未动。

### 说明

- 已发布版本的 CHANGELOG 条目(0.31.0 / 0.32.0 / 0.33.0 里的 `(V3)`、`(V4)`、`E4` 等)保持原样:
  那是对外已经发布的记录,不改写历史;本节的说明即是这些编号的收口。

## [0.33.0] - 2026-10-01

node 半边的两笔**必修债**:**推送式热重载**与 **If-Match 并发控制**。既有配置零改动兼容,
既有的写入方(手写脚本、文档里的 curl 流程)也照旧可用。

### 新增

- **推送式热重载**:新增 SSE 事件路由 `/plugins/dsh-status-rotator/events`。页面打开时就连上这条通道,
  配置一有变更(设置页保存、手改 `config.json`、外部 / 上游词库更新)服务端立刻推一条通知,
  已打开的页面随即重读 —— **不再靠客户端每 `reloadIntervalMs` 轮询**。
  - 通道连着时轮询**自动停掉**(页面长时间开着近乎零开销);通道不可用或断开时轮询自动回来,老宿主行为不变;
  - 服务端在**每次(重)连时先推一次当前 `ETag`**,所以断线期间错过的变更会在重连那一刻补齐,
    那一端不会停在一个没人再推的旧值上;
  - 服务端自己每 2 秒做一次本地变更检测(只 stat / 比对,无网络往返),因此手改文件与上游词库更新
    同样会推出去,而不是只有设置页保存才推;
  - 为什么是 SSE 而不是 WebSocket:宿主 webserver 的 `register` 类型声明明确允许 handler
    「hold the response open, e.g. SSE」,而 `registerUpgrade` 要自己实现 RFC6455 的握手与帧;
    两者对使用者是同一个结果(服务端主动推、页面不再轮询),选风险小的那条。
- **If-Match 并发控制**:配置路由的每个 `GET` 都带 `ETag`;`PUT` 带上 `If-Match` 时,
  基于旧副本的写入被 **409** 明确拒绝,响应体带当前 `ETag` 供重读后重试。
  - 设置页**始终**带 `If-Match`,所以两个设置页标签页之间不可能无声互相覆盖;
    冲突时它明确提示「这次保存被拒绝」并把编辑器拉回服务端的最新配置,不再让你对着一份
    服务端从没接受的编辑继续改;保存成功后服务端回新的 `ETag`,续写不会自己撞自己;
  - `GET` 支持 `If-None-Match` → **304**(轮询兜底路径不必再传整个 body);
  - `If-Match: *` = 显式无条件覆盖;不带 `If-Match` 仍是**无条件写** ——
    HTTP 里前提本来就是客户端自愿的,既有脚本与文档化的 curl 写入流程因此不受影响,
    代价是它们拿不到并发保护(README 里写明了这一点);
  - 保存**串行化**:前提校验与落盘之间不允许另一个 `PUT` 插队,否则两个并发请求可以双双通过检查再先后覆盖。

### 测试

- 新增 `scripts/verify-push-and-conflict.cjs`(真 HTTP + 真 SSE 长连接):`ETag` / `304` / `409` / `*` /
  无条件写、两个标签页的时间线(A 先写、B 用同一份旧配置写被 409 拒绝且 A 的内容没被覆盖、
  冲突响应带当前 ETag)、保存后推送、**手改词库文件后也能推出去**、以及断线重连后 `hello` 带当前 ETag。
- 修掉三处会让验证脚本污染环境的问题(都是本轮跑测试时踩出来的):
  - `verify-push-and-conflict.cjs` 把插件的**临时包副本**复制到工作目录再 import ——
    保存路径会写包目录的兼容镜像 `config.json`,而那个路径相对 `lib/index.js` 硬编码,
    直接 import 仓库里的 lib 会把测试内容写进仓库;
  - `verify-bank-auto-update.cjs` 与 `verify-phrase-hot-reload.cjs` 之前**没有隔离用户配置存储**,
    会把测试内容写进使用者真实的 `$DSH_HOME/status-rotator/config.json`;现在各用自己的临时目录;
  - `verify-push-and-conflict.cjs` 成功路径补上显式退出:插件的变更检测定时器还挂着,
    等事件循环自己排空是等不到的(会被外层 `timeout` 杀掉并以 124 结束)。

### 文档

- README(中英)「自动加载」一节改写为「自动加载 + 推送」,并新增「从外部写配置(ETag / If-Match)」
  小节,列出四种请求各自的结果。

## [0.32.0] - 2026-10-01

表现层的两条:**视觉主题化**(V3)与**弹幕增强**(V4)。既有配置零改动兼容 ——
两个功能的默认值全是「什么都不改」,不打开观感与 0.31.0 完全一致。

### 新增

- **视觉主题化(V3)**:新增 `appearance` 配置块 —— 字体族、字号、发光(含发光色)、文字动画
  (`none` / `breathe` 呼吸 / `glitch` 故障风)与状态行活动指示(`none` / `ring` 环形 / `bar` 条形)。
  随包五个**主题包**(`classic` / `neon` / `terminal` / `candy` / `glitch`),设置页「外观 → 主题包」
  选一个会同时写入外观与渐变色板,选完每一项仍可继续手调。
  - 全部用挂在**插件自己的文本 span** 上的 CSS 类实现,不新增 DOM 节点、不碰宿主原文与时钟;
  - `prefers-reduced-motion` 下呼吸与故障风自动停掉;
  - `fontFamily` 会写进 CSS,因此走严格白名单(字母 / 数字 / 空格 / 逗号 / 引号 / 连字符),
    其余字符一律拒绝而不是转义;
  - 活动指示表达的是「正在跑」而非百分比 —— 宿主不暴露回合进度;打字机锁宽已把它的宽度算进去。
- **弹幕增强(V4)**:五个可选行为,**默认全关** ——
  `hoverPause`(悬停在滚动弹幕上把它冻住,移开按剩余距离折算继续飞)、
  `clickCopy`(点击复制文案;没有剪贴板 API 时不做假反馈)、
  `phaseColors`(按当前相位给弹幕上色)、
  `adaptDensity`(同屏越接近 `maxCount` 发得越稀,页面不可见时几乎不发)、
  `avoidPointer`(落点躲开指针所在的高度带)。
  - :warning: 前两项会把弹幕层变成**指针目标**,与「弹幕层永不拦截指针」的承诺相反,
    所以必须显式打开;而且只有弹幕条目本身接指针,层仍是 `pointer-events: none`。
- **`sendDanmaku(text)`**:让一句自己写的文案立刻作为滚动弹幕飞过。它是 E4 注册接口的
  **加法式**扩展(`api.version` 保持 `1`,只有破坏性改动才递增),设置页「外观 → 弹幕」也有输入框;
  设置弹窗遮罩期间弹幕层是暂停的,这时发的会**排队、等遮罩关掉后补发**,而不是无声丢掉。

### 测试

- 新增 `scripts/appearance-danmaku-test.html`(浏览器页 `--page=appearance`,4 档):默认值 = 什么都不改 /
  主题包落到真 CSS(字体、字号、发光类与颜色变量、动画类、`::before` 指示器真的渲染)/
  指针交互(层只在显式打开时接指针、悬停真的冻住且位置不变、点击真的进剪贴板、相位分色生效)/
  自己发一条。已接进 `npm run test:browser` 与 CI。
- `npm test` 里「设置页草稿集中在 editorState 一处」的断言跟随新增的 `appearanceDraft` 更新 ——
  它正是为抓这种「新增草稿却漏进唯一清单」而存在的。

### 文档

- README(中英)新增「外观主题」「弹幕增强」两节,并在功能总览、测试页表与 E4 接口表
  (`sendDanmaku`)补上对应条目。

## [0.31.0] - 2026-10-01

三个 P0 一起发:**反重复洗牌袋**、**条件句与稀有句**、**对外注册接口**。既有配置零改动兼容 ——
不想用新能力的用户什么都不必做,升级后设置页、词库与状态行表现与 0.30.4 一致。

### 新增

- **反重复洗牌袋**:状态行按「语言 + 相位 + 权重模式」维护一袋句子,袋里的句子抽完之前不会重复;
  袋子抽空重开时先剔掉最近 N 条,所以跨袋的接缝处也不会撞上一句。权重在袋内照常生效。
  新增 `antiRepeat.recentLimit`(0~50)与 `antiRepeat.persist`(把记忆写进浏览器本地存储,
  刷新页面后不会立刻又看到同一句),设置页在「行为 → 反重复」。
  顺带换掉了旧的「每个元素记上一句」:那套记忆挂在会被宿主回收的 DOM 元素上,既会漏也会丢。
- **条件句(`when`)**:文案条目可以只在它描述的状态下出现 —— `tool`(工具名,`*` = 任意工具,多个用 `+`)、
  `retry`(正在重试)、`pending`(宿主在等你作答,可写布尔或数字)、`phase`、`hour`(本地时段,可跨午夜)、
  `firstTurn`(本会话第一回合);写出来的条件必须**同时**成立。有命中的条件句时只从它们里抽
  (否则一句 `bash` 会被上千条无条件句按权重淹没);一条都没命中就退回无条件句,状态行不会空。
- **稀有句(`rarity`)**:取 (0,1] 的抽取概率,让一句只在这么多次抽取里参与一次,彩蛋用。
- **设置页行语法**:词库编辑器支持 `文案 | 权重 | when:tool=bash | rarity:0.05`,
  修饰段写在行尾、顺序随意,正文里的 `|` 不受影响;与 JSON 条目**双向可逆**,
  打开设置页保存一次不会把条件洗掉。
- **对外注册接口(E4)**:浏览器半边通过 `ctx.provide("statusRotator", api)` 暴露服务,第三方插件
  `inject: ["statusRotator"]` 或 `ctx.get("statusRotator")` 即可注册:
  - `registerPack({ id, label?, phrases })` —— 具名词库包,与文档里的包走**同一个** `normalizePacks`;
  - `registerPlaceholder(name, resolve, { live }?)` —— 自带 `{name}` 占位符(默认按动态字段每秒刷新);
  - `registerPhraseProvider(fn | { id, provide })` —— 动态文案来源,每次轮换现算;
  三者都返回注销函数。第三方内容与随包内容走**同一条**选句管线(同文本去重 / `when` / `rarity` / 洗牌袋),
  且**不进配置文档** —— 注册不写配置,设置页保存也不会把它持久化或弄丢。

### 修复

- 词条比较键(`lib/index.js` 的 `normalizeEntry`)补上 `when` / `rarity`:该函数用来判断
  「用户条目是否已经在随包词库里」,只按 text(+weight) 归一的话,一条与随包条目**文本相同、
  但带条件**的用户条目会被当成随包内容从差异里剪掉 —— 保存一次条件就没了。
- 词条校验收紧:`when` 里的未知键、越界的 `hour` 与 `rarity` 一律拒绝。键名写错(如把 `tool` 写成
  `tools`)的结果是「这句永远不出现」,属于这个仓库一贯要避免的静默失败,不如在写盘前明确报错。

### 工程

- **设置页从 `apply(ctx)` 闭包里抽出**,成为模块级工厂 `createSettingsPage(deps)`:八个依赖
  (`locale` / `effect` / `toolMotionStatus` / `toolMotionListeners` / `getRuntimePreset` /
  `getLastLocale` / `isDarkTheme` / `onSaved`)显式传入,组件可脱离 `apply` 独立实例化、渲染与卸载;
  `apply()` 因此从约 4973 行降到约 3059 行。槽位注册(`settings.section`)、四个 Tab、
  字段与读写落盘行为全部不变 —— 变的只是依赖从隐式变显式。
- 设置页三处「保存后热应用」的连续调用收敛成一个 `onSaved` 回调;自有样式表改由 `dispose()` 释放。

### 测试

- 新增 `scripts/verify-settings-page.cjs`:设置页的**结构契约** —— 只用八个显式依赖即可实例化、
  渲染四个 Tab、字典走注入的 `locale` / `effect`、`dispose()` 摘掉样式表;并有护栏:
  工厂源码里不得再出现 `apply` 作用域的裸引用,防止抽取被无声改回闭包捕获。
- 新增 `scripts/verify-phrase-selection.cjs`:洗牌袋(袋内不重复 / 跨袋不撞最近 N 条 / 按语言与相位分键 /
  池子换了就重开 / 快照往返)、`when` 六种条件的正反例与 `hour` 跨午夜、`rarity` 掷骰、
  选句池的三种退化、设置页行语法双向可逆(含「JSON 条目 → 行 → 条目不丢条件」),
  以及 node 半边的校验与比较键。这些都是**眼睛看不出来**的故障。
- 新增 `scripts/external-api-test.html`(浏览器页 `--page=external`):一个真的第三方插件站在宿主
  上下文上接入 —— 在**随包词库清空**的前提下,只靠第三方内容点亮状态行(排除「其实还是随包内容
  在起作用」的假阳性),并验证非法注册抛错、之后才坏的 provider 被记进 `status().failures`
  且不影响状态行、注册全程不产生任何 PUT。
- 浏览器测试运行器支持页面自报完成(`window.__RESULTS_DONE__`):无头下页面可能被判为后台、
  定时器被降频,固定等待时长会读到一份**被截断但全过**的断言清单(最危险的假阳性)——
  现在等页面宣告跑完,等不到按失败处理;没设该标记的页面行为不变。

### 文档

- README(中英)新增「反重复(洗牌袋)」「条件句(`when`)与稀有句」「从另一个插件扩展它」三节,
  并在功能总览与测试章节补上对应条目。
- 设置页词库编辑器补上新的行语法提示(中英各四条);「行为」页新增「反重复」分组。
- 变更日志补上遗漏的 `[0.30.4]` 版本链接索引。

## [0.30.4] - 2026-10-01

### 文档

- 审计变更日志:修正 `0.30.2` / `0.30.3` 的**顺序**(此前 0.30.2 排在 0.30.3 之前);把**版本链接索引**从 `0.19.2` 补齐到最新发布的 `0.30.3`(未打过 tag 的版本链到 npm 版本页)。
- 补齐开源社区配套:**贡献指南**(CONTRIBUTING.md / CONTRIBUTING_ZH.md)、**行为准则**(CODE_OF_CONDUCT.md / _ZH,Contributor Covenant 2.1 改编)、**安全策略**(SECURITY.md:私密上报通道、配置路由的写栅栏与残余风险、投稿机器人的权限面)、**PR 模板**与 issue 模板引导(config.yml);五份文档纳入 npm 包 `files` 白名单,两份 README 的贡献段补上链接。

## [0.30.3] - 2026-10-01

### 文档

- README(中英):Quick start 与安装清单补**桌面端**安装命令 `dsh plugin --profile desktop add dsh-status-rotator`。
- README(中英)末尾新增 **Star 趋势**一节,内嵌 [star-history.com](https://www.star-history.com/) 生成的图表(随 star 增长自动更新)。

最新发布见 [GitHub Releases](https://github.com/01Virex/dsh-status-rotator/releases);词库条数在每次发版时同步刷新。

## [0.30.2] - 2026-10-01

### 词库

- `coding` 包:`正在修改J-space…`(thinking 分组)→ **`正在指控j-space数据造假…`**,英文对应行同步为 `Accusing J-space of faking its data…`。
- `internet-memes` 包:新增自嘲条目 **`正在写错贡献者名字…`**(为这次把贡献者链接写错的事自嘲)。

### 文档

- 贡献者名单:修正 **A7m** 的 GitHub 链接(此前一直写成 `github.com/A7m`,正确是 [`A7m0spHere`](https://github.com/A7m0spHere),两份名单与 0.30.1 的发布记录一并改回);新增小黑盒用户 **子夜** —— 她反馈了「同时出现两条鲸鱼尾巴」的 Bug(未在 GitHub 提 issue,后由 PR #112 修掉);云贡献者(整活区)15 条按原话更新。

### 说明

- `coding` 包那条是**替换**不是新增:`正在修改J-space…`(thinking 分组)被 `正在指控j-space数据造假…` 顶掉,英文对应行同步改写。上一轮我误判为「仓库里没有该条目、按新增处理」并在 `running` 里多写了一条 —— 本轮已把重复项删掉,全库只剩这一条。

## [0.30.1] - 2026-10-01

### 修复

- 修复开启鲸鱼尾巴摇动后与宿主原生图标**重合成两条鲸鱼**(回归起点为 #104)。宿主 0.2.0-rc.2 的 `runningIcon` 里真正可见的是 ``(APNG mask),`` 只是被宿主 `@supports`/`@media` 关掉的静态回退;摇动原本只藏 `>svg`,命中的正是那个已经 `display:none` 的死元素,宿主图标留在原地与插件的 `tail-morph` 轮廓叠在一起。改为藏掉除 morph 外的**全部直接子级**(`>*:not(.dsh-status-rotator-tail-morph)`),对宿主后续更换实现也不再敏感。
- 浏览器回归夹具同步补上宿主的 APNG span 与那条 `@supports`/`@media` 级联(此前只摆一个 svg,回归根本复现不出来),并新增三条断言:宿主动画 span 被藏、静态 svg 被藏、插件自己的 morph 仍可见。冒烟另加一条静态契约,禁止把选择器改回只针对 svg 的窄写法。

## [0.30.0] - 2026-09-30

### 新增

- 鲸鱼尾巴新增 24 帧左右摆尾与 36 帧扭转摆尾,可与翻转摆尾随机切换。随机模式每 4–8 秒选择另一种动作,在匹配的衔接姿态处经过 6 帧轮廓过渡;三种动作都支持 tok/s 调速、固定速度和炫彩。设置页「外观 → 尾巴动作」可选择,旧配置仍使用翻转摆尾。关键帧按动作需要加载,暂停、回合结束及卸载时释放动画状态。

- 可选「工具调用时随机切换动作」:当前会话每次新的工具调用按设定概率触发,默认概率 35%(开关默认关闭),支持 0–100%。历史和重复通知不触发,连续命中合并等待,经 6 帧衔接切换;固定动作模式也可使用。本地预览加入模拟调用按钮。

- 参考 [dsh-whale-sway](https://github.com/asdnmy123/dsh-whale-sway) 的相位保持与图标重绘恢复思路:速度变化时保留动画进度,宿主替换图标后恢复当前动作、速度与炫彩,随机模式不重新抽取动作或重置切换计划。适配记录见 [参考说明](./docs/whale-tail-reference.md)。

- **默认词库新增「广告」模块**(第 14 个 pack,**默认不启用** —— 想看的用户在设置页把「广告」这一包勾上即可):三条对外展示用的第一人称进行时文案,每条都带功能与出处(仓库地址),方便顺着找到对应的插件 ——
  - `正在优化提示词 github.com/WestFox-AwA/dsh-prompt-optimizer…`([WestFox-AwA/dsh-prompt-optimizer](https://github.com/WestFox-AwA/dsh-prompt-optimizer))
  - `正在操控 blender github.com/sixtysevenlf/dsh-blender-plugin…`([sixtysevenlf/dsh-blender-plugin](https://github.com/sixtysevenlf/dsh-blender-plugin))
  - `正在切换工作人格 github.com/shenA2024/whale-persona…`([shenA2024/whale-persona](https://github.com/shenA2024/whale-persona))
  中英各三条;沿用词库房子风格(每条以 `…` 结尾、不含 `;`),与完整性契约和投稿机器人的校验口径一致。`enabledPacks` 白名单里**不含** `ads`,所以新装 / 未改配置的用户默认轮播不到这三条。

### 验证口径

- `whaleTailMotion`(PR #109)的新增键默认关(`toolSwitchEnabled: false`)、`animation: "wag"` 保持翻转摆尾、概率前后端各自钳到 0–1、枚举白名单两侧一致;生命周期有明确清理(无状态行/尾巴回归)。**保留**:「工具调用触发动作」目前只经单元/集成级断言(7 条)+ CI 验证,真实回合里的手感需用户自行开 `toolSwitchEnabled: true` 体验确认。

### 修复

- 修复工具事件早于图标挂载时丢失触发的问题:请求按会话保留,图标就绪后执行,并在回合结束、关闭功能、切换会话及卸载时清理。同一会话重新绑定不再重置工具选出的动作与请求;图标连续重建会恢复播放进度,正在进行的 6 帧过渡也能接着完成。

- 修复新版宿主在右侧栏关闭时无法定位当前会话、以及绑定尚未就绪后不再重试的问题,避免工具触发概率 100% 仍没有动作切换。通过主对话标记定位并检测绑定变化;设置页新增事件连接状态与调用/命中/切换统计,窗口隐藏期间的工具触发保留到恢复可见后播放。

## [0.29.0] - 2026-09-29

### 新增

- dsh 0.2.0 鲸鱼尾巴新增可选摇动:可按 tok/s 调速或设置固定摇速;启用此开关即明确选择播放摇动。
- 尾巴采用 24 帧矢量轮廓翻摆,支持炫彩;tok/s 模式最低每秒 2 次、最高 6 次,等待时保持最低速度。固定速度范围为每秒 0.25–6 次。

### 词库

- 词库投稿 [#102](https://github.com/01Virex/dsh-status-rotator/issues/102)(3 条,由 [@3418244301q-star](https://github.com/3418244301q-star) 投稿,合并为 #103);star 词库包(star-ask / star-route)随机器人刷新(#100)。

### 文档

- 贡献者名单补上鲸鱼尾巴摇动动画的作者 **[A7m](https://github.com/A7m0spHere)**(PR #104)与本次投稿人(见 [CONTRIBUTORS.md](./CONTRIBUTORS.md) / [CONTRIBUTORS_ZH.md](./CONTRIBUTORS_ZH.md))。

### 修复

- tok/s 调速接入新宿主的 `assistant/live-chunk` 事件,计入正文、推理和工具参数增量,避免旧快照字段缺失导致尾巴始终静止;历史重放不计入速度,切换会话重置采样。关闭文字实时刷新时,尾巴仍独立采样调速。

## [0.28.0] - 2026-09-28

### 新增 / 兼容

- **兼容 deepseek harness 0.2.0-rc.1**(npm `next` 档),并新增配置项 **`whaleTail`**:保留宿主的鲸鱼尾巴并让它跟着炫彩。
  - **0.2.0 的宿主变化**:运行中不再是折叠头 `button[data-turn-process]` 里的一段文本,而是会话流里独立的一行 `div[data-chat-running]`(隐藏读屏公告 span + 分隔条 + **DeepSeek 鲸鱼尾巴 SVG** + shimmer 文案);折叠头按钮改为**只在回合结束后**渲染(收起态摘要)。旧插件在 0.2.0 上因此**完全不会接管**(运行期间找不到 `[data-turn-process]`),状态行一直显示宿主原文。
  - **适配**:新增一条宿主路径 —— 认得 `div[data-chat-running]`,把插件那条线**挂进这一行的 `runningContent`**(鲸鱼尾巴之后),藏掉宿主自己的 shimmer 文案,时长照旧从宿主文本读(React 每秒整段重写它,插件只读不写)。刻意**不藏整行**:行里的 `role=status` 读屏公告必须留着(藏整行会让读屏用户失去「深度求索中」这个状态)。回合结束宿主撤行时,插件跟着释放,临时容器归零。
  - **`whaleTail`(默认 `false`)**:打开后把那一行的**鲸鱼尾巴留下**,与插件文案同一行(宿主 flex + `gap:6px` 布局天然对齐),并按「炫彩渐变」的色板给尾巴做流光 —— 尾巴 SVG 是 `stroke="currentColor"`,所以用 CSS 关键帧动画 `color` 即可;`prefers-reduced-motion: reduce` 下退回宿主原色。渐变关着时尾巴保持宿主原色。设置页在「外观」页(紧接着「炫彩渐变」一组),中英文案齐备。
  - **验证**:
    - 浏览器回归新增 3 档(**真实 0.2.0 DOM 逐字照抄的夹具**):`020-running-row`(接管那一行:线挂在 `#runningContent`、宿主文案被藏、读屏公告仍在、时钟与宿主 ±1s、尾巴默认藏)、`020-running-row-tail`(保留尾巴:类 + `animation-name = dsh-status-rotator-tail-rainbow`)、`020-running-row-finished`(宿主撤行 → 插件释放、`runningLines/runningLabels/runningTails` 归零)。三档全绿;泄漏档的前后容器规模一致(含三个新容器)。
    - 冒烟 +3 条(布尔接受 / 非法丢弃 / 默认 false)→ **381 通过 / 0 失败**。
    - **真实 0.2.0-rc.1 实例实测**(隔离 profile + 本插件,注入真实运行行):插件接管 ✓ 文案 = 插件短语 ✓ 时钟 23s ✓ 宿主文案藏起 ✓ **尾巴保留且 `animation: dsh-status-rotator-tail-rainbow` 在跑** ✓ 读屏公告完好 ✓ 控制台零报错 ✓;4 个 `