# 更新日志 本文件记录 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),`