# Changelog

本文件遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)，版本号遵循 [SemVer](https://semver.org/lang/zh-CN/)。

## [1.13.2] - 2026-11-05

### Added

- **允许继续事件日志**：新增 `~/.dsh/dupgard/allows.jsonl`，用户选择「允许继续」时追加一行；
  字段与 `stops.jsonl` 完全相同（`decision` 为 `allowed`，含 `heldMs`），两个文件共用 `persistStopLog`
  与 `DSH_DUPGUARD_STOP_LOG` 开关、同一个脱敏/轮转设置；自定义截停日志路径时允许继续日志固定在同目录 `allows.jsonl`。
- 设置页开关文案改为「记录截停/允许继续日志」，hint 说明两个文件。

### Tests

- 持久化日志套件 10 → 13 项：路径解析、`allows.jsonl` 字段、同开关控制、真实点击允许继续的端到端接线。

## [1.13.1] - 2026-11-01

### Fixed

- **系统通知重放历史截停**：宿主通知队列保留最近 20 条（含 `dismissed`），而客户端的系统通知去重集合只存在于当前页面实例；
  页面刷新/重开后若首次轮询处于后台，旧截停会被当成新通知再弹一次。现在系统通知只接收**页面 bundle 加载之后**产生的普通截停，
  历史条目（包括已 dismissed）不再参与；页面内浮层逻辑不变。
- **「发送测试通知」与真实截停难以区分**：测试通知改用独立标题/正文，明确标注「测试通知」，并说明没有发生重复或截停。

### Tests

- 客户端 58 项（新增“历史 stop 不进入系统通知候选”回归测试）。

## [1.13.0] - 2026-10-31

### Added

- **可选「命中时暂停显示」**（`pauseDecisionOutput`，默认关；仅常驻版 + 通知通道）：命中所在数据块先扣住不显示，等用户决定；
  「立即截停」只保留命中前内容并丢弃扣住块，「允许继续」放行；不操作时，达到「阈值 × 倍数」则在最短反应时间后自动截停，
  否则恢复显示并转入连续观察。**只暂停显示，模型端计算不会真正暂停**（Ollama/llama.cpp/vLLM 等无 pause/resume 协议）；
  凡经 DSH `llm/stream` 的本地模型都适用。
- **本地模型默认不强制截停**（`localNoForcedStop` 默认开，`localProviderIds` 默认覆盖常见本地服务）：provider 命中本地列表时
  只弹决策卡片提醒，**不自动截停**（用户仍可手动「立即截停」）；无通知通道时命中也不截停，只打印一次说明。
  本地模型只消耗电，不产生 API 费用，默认不再被自动切断；需要恢复自动截停可在设置页关闭该开关。
  schema 21 → **24** 个 volatile 字段（暂停显示、本地不强制截停、本地 provider 列表）。
- **设置页拆分为 3 个分页**（顶部 tab，布局参考 dsh-cost-meter）：重复检测 / 截停与决策 / 通知与日志；
  字段归属与顺序不变，只是不再全部挤在一个长页面里。
  新增 4 项引擎测试（扣住/允许/自动停/恢复）+ 2 项宿主集成测试（paused 卡片/恢复 watching）。

### Fixed

- **周期/旋转文本会让决策卡片秒结束、频繁刷卡片、自动截停无法触发**：决策阶段原先用固定 unit 的尾部连续次数判断“重复是否中断”，
  无限字母表等周期文本每来一个字符尾部相位就旋转，卡片会在毫秒内 `recovered`，点击时常已结束（409），自动截停也到不了倍数。
  现在改用「最近窗口内该 unit 的出现次数」判断延续/计数，一次重复事件只生成一张卡片；真正中断（足够长的非重复内容）才取消；
  自动截停同样按窗口出现次数计数。决策套件 27 项。

### Changed

- **决策卡片改为「输出继续」模式**：命中阈值不再暂停生成，只弹出卡片；自动截停改为
  「实际重复达到阈值 × `stopAtMultiplier`」**且**「距命中已过 `decisionTimeoutMs`（最短反应时间）」两个条件都满足时才发生
  （等价于取两个时刻中较晚者）。用户仍可随时「立即截停 / 允许继续」，动作在下一个数据块生效。
- 客户端空闲轮询 1000ms → 500ms，让卡片更早出现（决策卡片存在时仍为 300ms）。

### Removed

- 移除 1.12.2 的「允许继续后平滑回放」：新方案不再暂停，没有缓冲内容需要回放。

### Tests

- `tests/decision.test.js`（27 项）：连续输出不暂停、立即截停/允许继续、双条件自动截停的两种先后顺序、
  周期旋转去重/延迟操作、暂停显示（扣住/允许/自动停/恢复）、本地 provider 默认不自动截停（卡片/手动停/无通道）、
  重复中断、流结束、窗口不足回退、钩子异常回退、宿主集成（watching/paused/noForcedStop 卡片、动作、自动截停）。
- 客户端 57 项（新增暂停卡片、本地不自动停卡片文案/动作、设置页 3 分页切换）；Config schema 24 字段全部 volatile。

## [1.12.2] - 2026-10-25

### Added

- **「允许继续」后的平滑回放**：决策窗口暂停期间上游通常已生成完剩余内容，恢复后所有 delta 会瞬间到达；
  现在会按约 600 字符/秒、最多约 1.5 秒把后续 delta 摊开显示（仅文本/思考 delta，`text-delta` / `reasoning-delta`），
  避免整段内容一次性闪现；内容顺序与完整性不变，超时观察路径不受影响。新增 1 项引擎测试（决策窗口 14 → **15** 项）。

## [1.12.1] - 2026-10-24

### Fixed

- **设置页缺少「重复守卫」命名空间**：DSH 启动早期同步 `require('@deepseek-ai/schemastery')` 可能撞上
  cosmokit 的 require(esm) 竞态（`Cannot require() ES Module ... because it is not yet fully loaded`），
  旧实现会把插件 `Config` 一次性置空，导致设置页报「未读取到「重复守卫」的设置数据」、1.12.0 的三个决策设置不可见。
  检测与截停本身不受影响。现在 `Config` 改为**按需访问器 + 后台退避重试**：竞态结束后再次读取即自愈，
  设置页刷新后恢复命名空间，无需重启宿主。
- 新增 1 项启动期竞态回归测试（功能测试 106 → **107** 项）。

## [1.12.0] - 2026-10-23

### Added

- **阈值决策窗口（方案 B）**：新增 `stopDecision`（默认开）、`decisionTimeoutMs`（默认 4000ms，1000–15000，步长 500）、
  `stopAtMultiplier`（默认 2，范围 2–10），设置 schema 18 → 21 个 volatile 字段。命中阈值时先**暂停拉取上游**
  （最长 `decisionTimeoutMs`）并弹出决策卡片：可「立即截停」（立刻收尾）、「允许继续」（本次生成不再检测）或
  关闭卡片；无操作则超时后恢复生成并进入观察期，实际重复达到「阈值 × 倍数」时自动截停，中途重复中断则取消决策。
  观察期内仍可操作，在下一个流式块生效。
- **回退行为**：无 webServer/通知通道、`notifyOnStop` 关闭、`stopDecision=false` 或动态入口 `plugin/host.js` 时，
  保持原「阈值即停」；`DSH_DUPGUARD_DECISION=off` 可临时强制回退。
- **通知通道扩展**：新增 decision 卡片 item（`kind/phase/deadline/autoStopCount`）与动作 `stop-now` / `allow-repeat`，
  `dismiss` 对决策卡片只关闭卡片、不取消计时；旧客户端/旧动作保持兼容。
- **客户端**：新增决策卡片（暂停/观察两种状态、立即截停/允许继续/关闭卡片），通知轮询从 2.5s 改为空闲 1s、
  存在决策卡片时 300ms；设置页新增「阈值决策窗口」开关、等待时长与自动截停倍数。
- **测试**：新增 `tests/decision.test.js`（引擎状态机 + 宿主集成，14 项）；客户端 52 → **54** 项（决策卡片渲染/动作）；
  持久化日志新增 `decision`（user/auto/threshold）与 `heldMs` 两个数值字段。
- **隐私**：决策卡片仅存于宿主内存，不写日志、不持久化；只有最终截停才写 stop-log / 发系统通知。

## [1.11.0] - 2026-10-22

### Added

- **截停持久化日志（仅 npm/常驻版）**：每次截停追加一行 JSON 到 `~/.dsh/dupgard/stops.jsonl`
  （设了 `DSH_HOME` 时为 `$DSH_HOME/dupgard/stops.jsonl`）。记录包含时间、版本、会话 id/标题、工作区路径/名称、
  provider/model、命中单元（>200 字符截断）、次数/跨度/命中通道、是否代码区域，以及命中时的检测参数快照。
  单文件超过 2MB 自动轮转为 `stops.jsonl.1`；写盘失败静默降级、绝不影响截停；与 `notifyOnStop` 开关无关。
  设置页新增「记录截停日志」开关（`persistStopLog`，默认开启，18 个 volatile 字段），`DSH_DUPGUARD_STOP_LOG=<路径>`
  可改位置、`=off` 是更高优先级的总开关；动态入口 `plugin/host.js` 因不能 `require('fs')` 不写日志。
  新增 `tests/stop-log.test.js`（路径/字段/截断/轮转/失败降级/设置开关/脱敏/默认脱敏/apply 接线，10 项）与共用宿主测试桩
  `tests/helpers/host-harness.cjs`；其余测试套件统一关闭持久化，避免写开发机真实目录。
  **隐私**：日志只写本机、不联网；**默认脱敏**（不写重复内容、会话标题、工作区路径与白名单内容，只保留长度/次数）；
  POSIX 上目录/文件按 `0700`/`0600` 创建；需要完整内容排查时显式设 `DSH_DUPGUARD_STOP_LOG_REDACT=0`；
  README 隐私章节明确披露截停日志的数据范围、关闭与删除方式。

## [1.10.2] - 2026-10-22

### Fixed

- **周期/旋转文本会让决策卡片秒结束、频繁刷卡片、自动截停无法触发**：决策阶段原先用固定 unit 的尾部连续次数判断“重复是否中断”，
  无限字母表等周期文本每来一个字符尾部相位就旋转，卡片会在毫秒内 `recovered`，点击时常已结束（409），自动截停也到不了倍数。
  现在改用「最近窗口内该 unit 的出现次数」判断延续/计数，一次重复事件只生成一张卡片；真正中断（足够长的非重复内容）才取消；
  自动截停同样按窗口出现次数计数。新增 4 项测试（周期旋转去重/延迟操作/真正中断/宿主集成），决策套件 18 项。

### Changed

- **`lib/index.js` 按职责拆分为模块**：设置集成（schema / 读写 / 派生回写 / 旧版设置通道）移入
  `lib/settings.js`（工厂注入 engine 与 config，保证 detector 变体测试使用被改写的引擎时设置默认值也一致），
  截停通知通道（队列 / 元数据 / HTTP 路由 / 继续指令）移入 `lib/notify.js`；`lib/index.js` 从 1327 行降到约 330 行，
  只保留入口编排（`apply` / `Config` / 测试钩子）。纯代码搬运，无行为、日志与 schema 变化；`module.exports`
  导出形状（`name/apply/Config/__notify/__advanced/__derive`）不变，detector/notify 的源码级防回归断言随模块位置同步调整。
- **新增 `npm run verify` 统一验证门禁**：一条命令执行 `npm test`（含 `build-host --check`）+ `stress` +
  `sandbox:selftest` + `audit:markdown`，作为重构/发布前的统一检查；README 中英文同步说明。
- **检测引擎收敛为单源实现（移除 800+ 行重复代码）**：把原本在 `lib/index.js` 与 `plugin/host.js` 各存一份的检测引擎
  （12 个函数、约 826 行完全相同实现）抽到新文件 `lib/engine.js`；`lib/index.js` 直接 `require` 它，动态入口
  `plugin/host.js` 改由 `tools/build-host.mjs` 从「引擎 + `tools/host-entry.template.js` 模板」生成（动态 Cordis 代码体
  不能 `require` 依赖）。`npm test` 第一步执行 `build-host --check`，生成物过期会直接失败；两个入口的行为一致性仍由
  `tests/detector.test.js` 全量覆盖。**无运行时行为变化**：默认值、设置 schema（17 个 volatile 字段）、通知通道与
  检测协议均不变。
- **客户端/宿主派生公式加防漂移测试**：客户端设置页保留一份窗口派生与分段表解析的镜像实现（浏览器 bundle 不能
  require 宿主模块），`tests/client.test.js` 新增 120 组配置的跨实现等价矩阵（分段表解析 / 最大单元长度 / 检测窗口），
  并顺带加中英文案键集对称断言；客户端套件 50 → 52 项。
- **压力套件的报告辅助去重**：新增 `tests/helpers/report.cjs`（`ok` / `bad` / `metric` / `summary`），替换三个压力套件
  各自复制的实现，输出格式与退出码语义不变。
- **README 同步**：项目结构补 `lib/engine.js`、`tools/build-host.mjs`、`tools/host-entry.template.js` 与
  `tests/helpers/report.cjs`，测试与开发一节说明「改引擎后运行 `npm run build:host`」；英文版同步。
- **README 拆分为中英文两份**：新增独立英文版 `README.en.md`（完整覆盖原有中文 README 的各节，顶部带语言切换），
  `README.md` 去掉原来的 `English summary` 小节，并删除双语标题、英文简介、英文标签等所有英文正文，
  只保留中文内容与顶部指向英文版的 `[English](README.en.md)` 链接；内部锚点随中文标题同步更新。
- **插件页本地化元数据补英文**：`locale/zh.json` 与 `locale/en.json` 的 `meta` 增加本地化标题
  （「重复输出守卫」/「Repetition Guard」），描述更新到当前能力与实测版本（截停通知、可选的浏览器系统通知、
  DSH 0.2.1-rc.2 CLI 与桌面版实测、宿主 API ≥ 0.1.1）。元数据由宿主启动时读取，重启 DSH 后插件页生效。

## [1.10.1] - 2026-10-21

### Added

- **README 增加 Awesome DSH Plugin 徽章**：本插件已被 [awesome-dsh-plugin](https://awesome-dsh-plugin.com) 收录，
  README 顶部挂上官方徽章（`https://awesome-dsh-plugin.com/badge.svg`）；投稿文档的“合并之后”一节同步标注为已收录，
  并顺手把其中过时的测试计数（171 → 176）与发布门禁描述更新到当前实现。

### Fixed

- **周期/旋转文本会让决策卡片秒结束、频繁刷卡片、自动截停无法触发**：决策阶段原先用固定 unit 的尾部连续次数判断“重复是否中断”，
  无限字母表等周期文本每来一个字符尾部相位就旋转，卡片会在毫秒内 `recovered`，点击时常已结束（409），自动截停也到不了倍数。
  现在改用「最近窗口内该 unit 的出现次数」判断延续/计数，一次重复事件只生成一张卡片；真正中断（足够长的非重复内容）才取消；
  自动截停同样按窗口出现次数计数。新增 4 项测试（周期旋转去重/延迟操作/真正中断/宿主集成），决策套件 18 项。

### Changed

- **系统通知的触发条件放宽为「页面不在前台」**：原来只看 `document.visibilityState === 'hidden'`，于是只有最小化或
  切到别的标签页才弹；只是切到其他窗口（浏览器窗口仍露着、仅失焦）时 `visibilityState` 仍是 `visible`，收不到通知。
  现在 `hidden` **或** `document.hasFocus() === false` 都会触发；页面可见且聚焦（你正看着它）时仍然不弹。
  系统通知闸门与设置页提示文案同步更新，用例覆盖「可见但失焦」与「可见且聚焦」两种状态。
- **复核 DSH 0.2.1-rc.2 适配性并升级发布门禁**：网页端与桌面版升级到 0.2.1-rc.2 后，真实宿主 `e2e:mock`、压力四套
  （真实契约校验走 cordis 瀑布，`dsh-llm 0.2.1-rc.2`）、功能 106 / 客户端 50 / 截停通知 20、沙箱自检与 Markdown 体检
  全部通过；`dsh-llm` 仍无 `invariant`、`BlockAssembler` 仍在，宿主仍投影完整 17 个 volatile 字段。上游 rc.1 → rc.2 为全包
  同步版本号（`cordis` 仍 4.0.5），仅客户端模型标签插件更名（与本插件无接触）。发布门禁的 `DSH_GATE_VERSION` 同步升到
  `0.2.1-rc.2`；README 的已实测清单与兼容性报告新增 rc.2 复核记录与历史核对行。
- **适配 DSH 0.2.1-rc.1 的上游变更**：`@deepseek-ai/dsh-llm` 移除了 `lib/invariant.js`（包导出也没有 `./invariant`，
  `invariants` 服务一并消失）。`tests/stress-real-invariant.mjs` 改为按上游版本自动选择真实实现层：有 `invariant`
  （0.2.0 时代）就用它，否则用**真实 cordis Context 的 `llm/stream` 瀑布**（`new Context()` → `ctx.on` → `ctx.waterfall`，
  与 `dsh-llm` 内部调用形态一致），输出统一交给真实 BlockAssembler（优先包根导出，回退内部文件）；两者都不可用时
  打印 SKIP。7 个场景 + 50 轮压力循环在两条链路上复用同一套断言。插件运行时本身不依赖该模块（仍只在 `llm/stream` 瀑布注册）。
- **发布前门禁升级到 DSH 0.2.1-rc.1**（`publish.yml`）：固定版本收进 `DSH_GATE_VERSION`（当前 `0.2.1-rc.1`）；
  安装成功时设置 `DSH_GATE_REQUIRE_REAL=1`，此时**没有任何真实实现层可用会直接判失败**，避免门禁静默失效；
  安装失败仍沿用 best-effort 语义（SKIP，不阻塞发布）。新增诊断步骤打印门禁实际使用的 `dsh` / `dsh-llm` 版本。
  本地可复现门禁分支：`DSH_GATE_FORCE_UNAVAILABLE=1`（应 SKIP/exit 0）与再加 `DSH_GATE_REQUIRE_REAL=1`（应 FAIL/exit 1）。
- **文档同步 0.2.1-rc.1 复核结果**：兼容性报告新增复核记录与历史核对行（真实宿主 `e2e:mock` PASS、设置页读写与
  测试通知实测、压力四套 PASS）；README 的已实测清单、英文说明与发布门禁说明更新为 0.2.1-rc.1。

## [1.10.0] - 2026-10-17

### Added

- **可选系统通知**（`systemNotify`，默认关闭；设置页「截停通知」组）：截停时额外发一条**浏览器系统通知**
  （Windows 右下角 toast / macOS 通知中心），点通知聚焦窗口并跳到被截停的会话。四条设计约束：
  ① **只在页面不可见时**才弹（`document.visibilityState === 'hidden'`），你正看着屏幕时不重复打扰；
  ② 正文只有固定文案「DSH：模型输出已截停」，**不含**工作区路径、会话名与重复内容（系统通知会出现在锁屏/通知中心，
  隐私面比应用内浮层大）；③ 同一会话按 `tag` 替换上一条，不堆通知；④ 首次打开开关时在**用户手势里**请求浏览器授权，
  被拒或浏览器不支持时只是不弹，浮层不受影响。
  实现上纯客户端（`Notification` API，**不改宿主检测逻辑**、无新依赖）；宿主只多一个持久化字段（schema volatile 字段
  16 → **17**）。新增 5 项客户端用例（开关默认值/写入/权限请求、闸门三条件+去重+上限+非法输入容错、文案隐私、
  通知器创建/tag/点击激活/未授权安全返回、「发送测试通知」按钮），客户端套件 45 → **50** 项。
  桌面版（Electron）同样适用：桌面端的权限处理对非 media 权限直接批准，因此授权状态直接是「已授权」，
  通知署名是应用本身；窗口最小化或被完全遮挡时才触发（`document.visibilityState` 为 `hidden`）。
  说明：该能力需要 DSH 界面处于打开状态（后台标签页即可）；headless 运行没有浏览器进程，不发系统通知。
- **「发送测试通知」自检按钮**（设置页，打开系统通知开关后出现）：不依赖真实截停，点击即弹一条系统通知，
  用于当场确认「浏览器 → 系统通知」这条链路通不通；权限被拒时按钮下方显示失败原因。

### Fixed

- **周期/旋转文本会让决策卡片秒结束、频繁刷卡片、自动截停无法触发**：决策阶段原先用固定 unit 的尾部连续次数判断“重复是否中断”，
  无限字母表等周期文本每来一个字符尾部相位就旋转，卡片会在毫秒内 `recovered`，点击时常已结束（409），自动截停也到不了倍数。
  现在改用「最近窗口内该 unit 的出现次数」判断延续/计数，一次重复事件只生成一张卡片；真正中断（足够长的非重复内容）才取消；
  自动截停同样按窗口出现次数计数。新增 4 项测试（周期旋转去重/延迟操作/真正中断/宿主集成），决策套件 18 项。

### Changed

- **设置页截图更新**（`screenshots/05`）：新增「同时发送系统通知」开关、浏览器权限状态行与「发送测试通知」按钮，
  状态行显示 `构建 1.10.0`。`03`/`04` 展示的默认值（`代码内阈值倍数 4`、派生窗口 12000）不变，`01`/`02`/`06`
  不涉及版本号与默认值，均保持不变。截图仍按原规则打码（只截设置面板，不含工作区路径、会话名、余额与侧栏）。
- **新增第 7 张商店图** `screenshots/07-system-notification.png`（380×114）：可选系统通知在 **Windows 右下角** 的真实截图，
  取自**桌面版（Electron）**——点「发送测试通知」触发。桌面版的通知**署名是应用本身**（`DeepSeek Harness`），
  比网页版（由 Chrome 转发、署名浏览器）更适合展示；正文刻意只有固定文案，不含工作区路径、会话名与重复内容。
  README 的画廊与 `screenshots.json` 已同步为 7 张。

## [1.9.2] - 2026-10-16

### Fixed

- **周期/旋转文本会让决策卡片秒结束、频繁刷卡片、自动截停无法触发**：决策阶段原先用固定 unit 的尾部连续次数判断“重复是否中断”，
  无限字母表等周期文本每来一个字符尾部相位就旋转，卡片会在毫秒内 `recovered`，点击时常已结束（409），自动截停也到不了倍数。
  现在改用「最近窗口内该 unit 的出现次数」判断延续/计数，一次重复事件只生成一张卡片；真正中断（足够长的非重复内容）才取消；
  自动截停同样按窗口出现次数计数。新增 4 项测试（周期旋转去重/延迟操作/真正中断/宿主集成），决策套件 18 项。

### Changed

- **默认「代码内阈值倍数」由 3 改为 4**（`codeBlockMultiplier`；代码区域内的判定阈值因此从「阈值 × 3」变为「阈值 × 4」，
  默认参数下 30 → 40 字符，派生检测窗口 2400 → **3200**）。依据是误杀审计（`npm run audit:false-positives`，260 个真实文件）：
  默认倍数 3 时 2 个文件被误判，倍数 4 时 **0 个**，而往白名单里加任何符号字符都一处不减少（详见下方 Added 里的审计条目）。
  **影响面**：只影响**没有显式存过该值**的配置（已保存的设置优先，不会被改动）；代码区域**外**的阈值与行为完全不变。
  同步更新：宿主与 client 的默认值、设置页文案（中英）、README（默认值表 / 代码区域说明 / 检测窗口公式示例）、以及与
  "阈值 × 倍数"相关的断言（代码区内触发点 30/35 → 40/45，派生窗口 2400 → 3200）。

### Added

- **Markdown 渲染体检进 CI**（`npm run audit:markdown`，`tools/audit-markdown.mjs`，CI 新增 `docs` job）：检查五类
  "渲染后才暴露、肉眼与 diff 都很难发现"的问题——① 围栏配对（含"反引号围栏的 info string 不允许再含反引号"这条
  CommonMark 规则）② 表格列数（未转义的 `|` 会把单元格切开、整行错位）③ 反引号 span 的等长配对（落单的反引号会吞掉
  本行后续内容）④ **CJK 粗体 flanking**（`**` 的左右邻字符决定能否开启/闭合）⑤ 编程式区间写法 `1..64`。
  触发它的真实缺陷：README 里那句"通知注册在 DSH 的框架级浮层……"中，加粗范围的**闭合标记前是右括号、后是汉字**
  ⇒ 按 CommonMark 这条加粗**不能闭合**，于是粗体一路"跨句泄漏"到下一句、并与下一个加粗粘连——而渲染后**看不到任何
  可见的星号**，只数可见星号是查不出来的。用修复前的 README 反向验证过：该工具准确报出 `README.md:270` 并以退出码 1 失败。
  顺带修掉 README 另外三处同类问题（嵌套反引号示例、落单反引号、表格内未转义的 `|`）与 CHANGELOG 两处历史条目里
  用来表示围栏的字面反引号（改成"4 个反引号"这类文字表述，事实不变）。
- **市场展示截图**：新增 `screenshots/`（6 张 PNG，含商店展示顺序的 `screenshots.json`）。顺序按"它做什么 → 怎么演示 →
  怎么配 → 进阶能力 → 安全边界"编排：截停通知卡 → 被截停的那一轮 → 设置页（重复次数，分段表模式）→ 分段表特写 →
  设置页（忽略白名单 / 截停通知 / 诊断行）→ 高级模式风险确认。截图中的工作区路径已打码。
  这些文件**不进 npm 包**（`files` 白名单未包含），仅供 GitHub/商店读取。
- **截图重拍（2026-10-15，改用 Playwright 驱动浏览器）**：统一视口 1600×1000、按元素取景（设置页只截面板、会话只截对话区），
  并做隐私处理——工作区**绝对路径打码**（`D:\▮▮▮▮▮▮` 形式）、会话 id 由卡片自身截断或打码、画面不含侧栏其它项目、
  余额/账号区域一律排除在取景之外。演示用的重复内容刻意**只用白名单之外的字符**（`loop.`：`l/o/p/.` 均不在
  `- | # 1 a — 0 9 =` 与片段 `asdf` 中），因此卡片上显示的"重复字符串"就是字面原始串，不会被白名单剥离后改写。
- **高级模式的隔离测试环境**（三件套，均不进 npm 包）：
  - `tools/sandbox-threshold-module.mjs`：**沙箱试运行**——在受限子进程里加载候选模块并打印每个单元长度的生效次数。
    隔离手段为 Node 权限模型（实测：写文件 / 子进程 / 越界读取一律 `ERR_ACCESS_DENIED`）+ 墙钟超时 + 堆上限；
    模块本身的 `console.log` 与结果用哨兵行分离，越权被拒的证据会一并展示。**网络不在权限模型覆盖范围内**，
    已在输出与文档中显著标明。
  - `tools/sandbox-modules/` + `tools/sandbox-selftest.mjs`：六个样例（合法分段函数 / `exports.repeatCount` 形态 /
    非法返回值 / 加载期抛错 / 死循环 / 越权尝试）与结论自检，已挂进 `ci.yml`（Node 22）与 `npm run sandbox:selftest`。
  - `docs/advanced-mode-sandbox.md`：三层隔离的完整做法（沙箱试运行 → 独立 profile + 一次性工作区 → 独立 `DSH_HOME` /
    WSL / Windows Sandbox），含各手段"隔得住什么、隔不住什么"与实测可用性。
- 测试钩子 `__advanced`（`loadAdvancedModule` / `createThresholdPolicy` / `describePolicy` / `isValidNeed` /
  `parseThresholdByLength`）：让沙箱工具复用宿主的真实加载与判次路径，避免"测的是一套、跑的是另一套"。
- **高级模式的隔离环境已实测可用**：新增 `tools/run-advtest.ps1`（在干净环境里启动隔离 profile）。
  过程中定位并记录了一个容易误判的现象：从**正在运行的 DSH 会话终端**里启动另一个 profile 时，会继承
  `DSH_PROFILE_DIR` / `DSH_PROFILE` / `DSH_SESSION_ID` / `DSH_WEB_URL`，而插件的「免重启读设置」优先读
  `$DSH_PROFILE_DIR/cordis.patch.yml` ⇒ 于是读到**那个 profile** 的设置（实测：本 profile 的 patch 里写
  `thresholdMode: module` 不生效，日志显示 `模式=table` ＋另一个 profile 的 9 项白名单；连 `--patch` 覆盖层
  也压不过它）。清掉这些变量后：`模式=module｜覆盖长度 1000 种｜次数区间 3–20｜最长跨度 3000 字符` ✓
  ——正是样例模块 `repeat-count.cjs` 的返回值，即**模块已被真实宿主加载并驱动了判次策略**。
- **确定性端到端测试**：新增 `tools/e2e-mock-repetition.mjs`（`npm run e2e:mock`）。它在本地起一个极小的
  **Anthropic Messages-SSE 假模型**，让**真实适配器 + 真实 agent loop + 真实 `llm/stream` 瀑布**去消费脚本化的
  重复文本 ⇒ 无 API key、零 token、不受"模型愿不愿意复读"影响。实测两行对照：简单模式（阈值 10）假模型 400 份
  → 最终消息 **10 份**、策略＝固定阈值 10；高级模式（模块对长度 1 规定 20 次）→ 最终消息 **20 份**、策略＝`module`，
  两种情况回合都以 `completed` 结束（退出码 0）。切 module 模式时脚本临时改写目标 profile 的
  `cordis.patch.yml` 并在 `finally` 里逐字节还原。
  附带结论：官方的 `@deepseek-ai/dsh-llm-mock-server` 只服务 OpenAI 的 `/chat/completions`，而本版 DSH 的
  DeepSeek 适配器走 Messages 协议（`<base>/v1/messages`，`protocol` 不可配置）⇒ 直接 404，该包已移除。
- **完整的高级模式隔离测试（一键）**：`npm run e2e:isolated`（`tools/e2e-advanced-isolated.mjs`）在**完全独立的
  `DSH_HOME`** 里跑三个用例并核对预期，真实 profile 的设置/会话/日志一概不碰（每个用例临时切到高级模式，`finally` 里
  逐字节还原）。实测三项全 PASS：① 合法模块 → 策略＝`module`、最终消息 20 份；② 加载期抛错 → 宿主打印
  「高级模式不可用」并回退基础阈值、最终消息 10 份；③ 导出非函数 → 同样回退、最终消息 10 份。
  配套：harness 新增 `--dsh-home` / `--module`（后者要求同时给 `--dsh-home`，避免改写真实 home 的 profile）、
  新增样例 `tools/sandbox-modules/trap-not-a-function.cjs`。
  死循环与越权两类危险用例**故意不进这套件**（它们在真实宿主里会卡死事件循环 / 真的得手），仍由第 1 层沙箱覆盖。
  隔离 home 的一次性运行能力来自 profile 的 `dsh.profile.bundles` 里写上 `@deepseek-ai/dsh-headless`
  （bundle 从全局 DSH 安装解析；npm 上的 `latest` 是 `0.0.1-rc.1`，与 0.2.0-rc.2 不兼容，无需也不应安装）。
- **误杀审计工具** `tools/audit-code-false-positives.mjs`（`npm run audit:false-positives`）：把真实仓库的源码包进
  ```` ``` ```` 围栏后重放**发布版同一份检测器**，统计"代码块里被误判为复读"的处数与触发单位，并可逐个试加候选白名单字符
  与候选倍数做对照。**用它回答了一个具体问题：要不要把代码块里常见的非故障重复字符加进默认白名单？**
  实测（260 个文件，`D:\ZQH260619\repos`）：默认配置（白名单 `-`/`|`、倍数 3）误杀 **2 个文件**（`─` (U+2500) 框线一处、
  某会话快照里一段 `a` 一处）；把 `= * / . # _ ~ + 0 1` 以及它们的组合加进白名单**一处都没减少**（真实代码里这些字符
  不构成 ≥30 的长重复）；只把**代码内阈值倍数 3 → 4**（代码区阈值 30 → 40）即降到 **0**，且全局白名单一字未动。
  ⇒ 因此 **1.9.2 起把默认倍数改为 4**（见下方 Changed），README 的代码区域小节已补上"噪声大就调倍数、别塞白名单"的指引。
  结论：**默认白名单不需要扩**——白名单是全局生效的，会连带漏掉"模型刷符号空转"这类真实故障；代码块的噪声应该用
  `codeBlockMultiplier`（含 `0` = 代码区完全不检测）来调。README 的代码区域小节已补上这条指引。
  （另注：作者本机配置里白名单写的是 `—` (U+2014 em dash)，而真实出现的是 `─` (U+2500 box drawing)，两者不是同一个字符。）

## [1.9.1] - 2026-10-10

### Fixed

- **周期/旋转文本会让决策卡片秒结束、频繁刷卡片、自动截停无法触发**：决策阶段原先用固定 unit 的尾部连续次数判断“重复是否中断”，
  无限字母表等周期文本每来一个字符尾部相位就旋转，卡片会在毫秒内 `recovered`，点击时常已结束（409），自动截停也到不了倍数。
  现在改用「最近窗口内该 unit 的出现次数」判断延续/计数，一次重复事件只生成一张卡片；真正中断（足够长的非重复内容）才取消；
  自动截停同样按窗口出现次数计数。新增 4 项测试（周期旋转去重/延迟操作/真正中断/宿主集成），决策套件 18 项。

### Changed

- **设置页按用户任务重新分组**（仅界面，字段与宿主语义不变）：原先"模式"与"阈值"分居两组、白名单占据首屏，
  现在自上而下四组，顺序＝从最常改到最少改：

  | 分组 | 回答的问题 | 内容 |
  | --- | --- | --- |
  | **重复次数（何时截停）** | 次数怎么算 | 模式下拉 → 该模式的次数来源（简单：触发阈值 / 分段表：三列表格 / 高级：模块路径）→ 代码区域内阈值倍数 |
  | **检测范围与开关** | 检测什么、按多长算 | 最小 / 最大重复单元长度 → 忽略空白、检测思考、检测工具参数三个开关 →（必要时）窗口上限告警 |
  | **忽略白名单** | 哪些内容不算重复 | chips + 合并输入框（默认已覆盖 Markdown 表格分隔符，通常在误杀时才需要动） |
  | **截停通知** | 截停之后做什么 | 通知开关 → 继续指令内容 |

  配套：`thresholdMode` 与其结果字段仍保持"模式在前"的依赖顺序；`codeBlockMultiplier` 与阈值同组（同属严格程度）；
  **关闭截停通知时隐藏「继续指令内容」**（该字段此时永不使用）；组名改为「重复次数（何时截停）」「检测范围与开关」
  「忽略白名单」。字段分组由元数据派生（`THRESHOLD_FIELDS` / `SCOPE_FIELDS` / `CODE_MULTIPLIER_FIELD`），
  不再手写两遍列表。
- **设置页层级与文案**：主标题 15px → **18px**；新增分组副标题 `h3.dg-subtitle` **15px**（比设置项名称 13px 醒目、
  比主标题小一档，底部状态行仍为 `dg-note` 不受影响）。提示文案去掉设置页**无法渲染的字面 markdown**
  （`**强调**`、反引号），并整体压缩：白名单 312→122 字、代码倍数 245→116 字、模块路径 211→105 字、
  窗口告警 146→83 字；删掉的细节（等价关系、保留代价、扩展名规则、区域边界）在 README 中均有完整版本。
- 测试（客户端 42 → **45**）：新增**分组契约**断言（四个副标题的顺序、每个字段归属哪一组、触发阈值必须排在白名单之前、
  关闭通知后隐藏继续指令）；层级断言改为从 CSS 解析字号并保证"副标题 ≥ 设置项名称 + 2px 且 < 主标题"；
  新增字典不得含 `**`/反引号的断言；把顺序敏感的测试助手 `lastFieldError`（取页面最后一个错误行）换成
  按字段定位的 `errorOfField(tree, id)`，避免分组调整误伤用例。

## [1.9.0] - 2026-10-09

### Fixed

- **通知路由注册了却永远 404（第二个真 bug，同一个功能）**：宿主日志已打印「截停通知通道已注册」、
  五个服务自检全"就绪"，页面却仍然报通道不可用。**真因**：注册时用了带尾斜杠的前缀 `/dsh-dupguard/`，
  而 DSH 的 prefix 匹配规则是 `pathname === prefix || pathname.startsWith(prefix + '/')`
  （`dsh-host-webserver` 的 `match()`）⇒ 只有前缀自身与 `/dsh-dupguard//…` 能命中，
  `/dsh-dupguard/notifications` 直接落到 SPA 兜底并返回**空 404**。
  在**运行中的实例**上验证：`GET /dsh-dupguard/` 返回我们处理器的 JSON（说明路由已注册），
  而 `GET /dsh-dupguard/notifications` 是 SPA 的空 404（说明子路径没命中）。
  修法：新增 `NOTIFY_ROUTE = '/dsh-dupguard'`（**不带尾斜杠**）用于注册，请求路径常量 `NOTIFY_PREFIX` 不变；
  启动日志改为同时打印两者。回归用例复刻真实匹配规则并断言注册路径**不以 `/` 结尾**且能匹配列表 / 动作路径
  （旧断言「路由前缀应与客户端一致」正是把错误写法固定下来的那条，已替换）。
- **截停通知在真实宿主里完全没生效（路由未注册）**——由用户实测发现：浏览器里能看到新的设置分组，
  但右下角始终没有通知（页面给出了「宿主半体尚未加载」诊断）。逐层取证后确认**不是"没重启"**：
  宿主进程启动时间晚于新代码写入时间，路由却仍返回 404。**真因**：我用 `ctx.get('webServer')` /
  `ctx.get('sessions')` 等解析 root 服务，而 **loader entry 的 ctx 用 `get()` 拿不到 root 服务**
  （本插件既有注释与 DSH 官方插件 `dsh-client-modules` / `dsh-client-connection` 都要求在 `ctx.inject`
  回调里以**属性**访问，如 `webCtx.webServer`）。后果有三层：路由根本没注册（通知永不弹）、
  工作区 / 会话名称永远为空、`agents` 拿不到 ⇒「发送继续指令」必然失败。
  修法：改为 `ctx.inject([...], scope => { services.x = scope.x })` 捕获**服务对象**，
  `describeStoppedSession` / `deliverContinue` / `registerNotifyRoutes` 全部改为接收服务集合；
  路由注册按官方写法放进 `webCtx.effect(...)`（极简 ctx 无 `effect` 时降级为直接注册）；
  另加两条**启动自检日志**：`截停通知通道已注册：/dsh-dupguard/notifications` 与
  `截停通知自检：webServer=…｜sessions=…｜sessionTitle=…｜workspaceRegistry=…｜agents=…`。
  测试侧同步收紧：`tests/notify.test.js` 与 `tests/detector.test.js` 的 ctx 桩改为**忠实模拟该约定**
  （`get()` 一律返回 undefined、服务只能经注入属性访问、`effect` 立即执行），并新增**静态防回归断言**
  （宿主源码不得出现 `ctx.get('`，必须使用 `webCtx.webServer` 与服务对象）；截停通知套件 19 → 20。
  教训：测试桩若比真实运行时"更宽松"，就会掩盖这一类真实故障。

### Added

- **截停通知 + 一键继续指令**（npm 常驻版；宿主 + 浏览器两半）：
  - **通知一定显示**：浮层注册在 DSH 的 `shell.overlay`（root 作用域）框架级浮层里，**与用户当前打开哪个会话无关**；
    被截停的会话即使不在前台，通知照样弹出（`llm/stream` 的 `options.sessionId` 给出被截停会话的身份）。
  - **通知至少显示**：**所属工作区**（会话 `header.cwd` + 工作区注册表按路径匹配的名称）、**会话名称**
    （`sessionTitle` 服务的标题投影）、**重复的字符串**（命中单元原文，等宽 + 引号，>160 码点按码点截断并保留真实长度），
    另有重复次数、跨度、命中通道（可见输出 / 思考文本 / 工具参数）与「是否代码区域内」。
  - **由用户决定是否继续**：「发送继续指令」→ 宿主向**被截停的会话**注入一条用户消息
    （`agents.get(sessionId).followup(createUserMessage(...))`，内容 = `continuePrompt`）；
    「不发送」→ 只记录选择；「打开该会话」→ 客户端 `uiWorkspace.openSession`（服务缺失时不显示该按钮）。
    投递失败（会话已关闭等）**不静默**：卡片上显示原因并保留，供用户手动处理；重复点发送是**幂等**的。
  - **通道**：宿主注册同源 HTTP 前缀路由 `/dsh-dupguard/`（`GET notifications` / `POST notifications/action`），
    浏览器轮询（2.5 s + 切回标签页时立即刷新）。跨站请求被拒、请求体有上限、响应 `no-store`；
    宿主无 `webServer` 服务时只告警一次，**截停本身不受影响**。
  - **新配置**：`notifyOnStop`（默认 `true`）与 `continuePrompt`（默认「请从中断处继续，不要重复之前的内容。」，
    ≤500 码点），设置页新增「截停通知」分组。Config 字段 14 → 16，`CONFIG` 在两个入口同步（动态插件形式不使用
    这两个键，README 已注明）。注入的是普通用户消息，插件不做任何隐藏改写。
  - **测试**：新增 `tests/notify.test.js` **19 项**（队列上限与状态、元数据解析含 Windows 路径归一与缺服务降级、
    消息构造的工厂/回退双路径、路由的列表/动作/幂等/跨站/非法请求、开关 `notifyOnStop=false` 时不产生通知，以及
    「真实截停 → 通知 → 用户发送 → 宿主注入」端到端）；客户端新增 C15 **8 项**（空列表不渲染、显示三要素与操作、
    发送后移除、不发送只记录、失败保留卡片并显示原因、只显示待处理与通道/代码区域文案、宿主半体未加载时的可见诊断、
    设置页开关不重复渲染）。
    客户端套件 33 → 41，`npm test` 现为 106 + 41 + 19。
  - 顺带修掉一个真 bug：`settingsValueFromConfig()` 的键清单未包含新字段，会导致**设置页的开关与文案在
    DSH ≥ 0.1.7 的插件 Config 模型下不生效**（该路径是主路径）；由 `tests/notify.test.js` 的开关用例发现并固定。
  - 反馈修正（用户实测截图发现）：
    1. **`notifyOnStop` 开关曾在设置页重复渲染两次**（检测参数组与截停通知组各一次）——检测参数组的渲染列表改为
       显式常量 `PARAM_BOOL_FIELDS`（`skipCodeBlocks` / `notifyOnStop` 都不在其中），并加回归用例（按字段节点计数必须为 1）；
    2. **宿主半体未加载时不再静默**：通知列表请求返回 404 或非 JSON 时，浮层显示诊断卡「截停通知未生效：宿主半体
       尚未加载」并写明需重启 `dsh web`（可点「知道了」关闭，宿主重启后自动消失）。真实成因：宿主代码只在进程启动时
       加载，只刷新页面不会启用通知通道——插件此前在这件事上是静默的。

## [1.8.3] - 2026-10-07

### Fixed

- **风险确认弹窗的主按钮白底白字**（未发布改动引入，由用户截图发现）：两个原因——
  1. 填充色用了 `--dsw-alias-brand-primary`（那是开关/复选框的强调色，**不是按钮填充 token**）；
  2. `.dg-btn-primary` 与基础 `.dg-btn` **同优先级**，而基础样式定义在后 ⇒ 背景被覆盖成浅色，只剩白色文字。
  修正：改用 DSH 自己的按钮 token **`--dsw-alias-button-primary-fill` + `--dsw-alias-label-primary-foreground`**
  （hover 用 `--dsw-alias-button-primary-hover`），选择器提升为 `.dg-btn.dg-btn-primary`；同时把弹窗其余视觉与
  DSH 的 `RiskConfirmation` 逐项对齐：警示行文字 `label-secondary`、警示图标用 `state-error-primary`、
  卡片宽 `min(440px,100%)` 且超高可滚动、勾选框 16×16 + `accent-color`、按钮最小宽度 72px/136px。
  说明：所有 token 名已对照 DSH 的 Theme inspect provider 与 `@deepseek-ai/dsh-client-ui-primitives` 的真实 CSS
  校验（`--dsw-alias-bg-mask`、`--dsw-alias-state-warning-tertiary`、`--dsw-alias-state-warning-primary`
  这三个此前写错的 token 已移除或改名）。
  测试：C14 增加**防回归断言**（必须使用 button-primary-fill / label-primary-foreground / accent-color，
  且主按钮选择器带 `.dg-btn` 前缀），避免再次落到不可见配色。

### Notes

- **配置兼容性盘点（逐发布标签实测）**：配置键自 1.0.0 起**只增不减、从未改名**——
  1.0.0 七项（`threshold`/`minUnitLength`/`maxUnitLength`/`detectionWindow`/`stripWhitespace`/`monitorReasoning`/
  `monitorToolArguments`）→ 1.0.1 `fixStandingMountConflict` → 1.0.2 `ignoredChars` → 1.4.0 `skipCodeBlocks` +
  `codeBlockMultiplier` → 1.6.0 起有 `Config` schema（10 字段）→ 1.7.0 `ignoredSubstrings`（11）→
  1.8.0 `thresholdMode` + `thresholdByLength` + `advancedThresholdFile`（14，与 `CONFIG` 的 15 项差一个
  `fixStandingMountConflict`，它是代码常量而非 schema 字段）。
  取值范围**只放宽过**：`codeBlockMultiplier` 1–100 → 0–100（1.5.0），其余自 1.3.0 起未变
  ⇒ **当年通过设置页写出的值不会因升级变非法**；缺键取默认；当前 schema 未声明的键（`fixStandingMountConflict`、
  拼写错误）被**忽略**而非让整节失效。
  新增回归用例 **S23「历史配置形态兼容」**（1.0 / 1.4 / 1.7 / 1.8 四种样本 + `*` 旧写法 + 未知键忽略），功能套件 105 → 106。
- 唯一需要用户动作的迁移仍是 DSH 侧的命名空间变更：**0.1.7 起设置键从 `dsh-dupguard:` 变为 `dupguard:`**
  （插件侧两条通道都支持，但 DSH 文档里的旧键不会自动搬移，升级后请在设置页重新保存一次）。
- 已知边界（非版本迁移引入）：**手改配置**里的类型/范围/枚举错误（如 YAML 里 `threshold: "12"`）会让 DSH 在注册时
  拒绝整个命名空间 ⇒ 回落到代码默认值；已在 README 排查清单中写明。

### Fixed

- **周期/旋转文本会让决策卡片秒结束、频繁刷卡片、自动截停无法触发**：决策阶段原先用固定 unit 的尾部连续次数判断“重复是否中断”，
  无限字母表等周期文本每来一个字符尾部相位就旋转，卡片会在毫秒内 `recovered`，点击时常已结束（409），自动截停也到不了倍数。
  现在改用「最近窗口内该 unit 的出现次数」判断延续/计数，一次重复事件只生成一张卡片；真正中断（足够长的非重复内容）才取消；
  自动截停同样按窗口出现次数计数。新增 4 项测试（周期旋转去重/延迟操作/真正中断/宿主集成），决策套件 18 项。

### Changed

- **高级模式标注为实验性 + 风险确认弹窗**（设置页）：下拉项改为「高级模式（实验性）」，**从其它模式切入时弹出
  确认框**——对齐 DSH「完全权限」的弹窗模式（`RiskConfirmation`）：警示行 + 「我已了解风险，并愿意继续」勾选，
  未勾选时「启用高级模式」按钮禁用，取消则保持原模式且**不写入任何设置**。弹窗文案说明该模式会在 **DSH 宿主
  进程内执行**指定 JS 文件、与 DSH 同权限、插件无法校验文件内容、功能仍是实验性。已处于高级模式时重复选择不再
  弹窗，离开后再次进入会重新弹窗；取消后下拉自动回到原模式（确认前显示待选值）。
  实现：模式写入拆为 `applyMode()`（真正写入）与 `commitMode()`（高级模式入口先门控）；新增 `pendingMode` /
  `riskAcknowledged` 两个状态；弹窗样式沿用 `--dsw-*` 主题变量（遮罩 + 卡片 + 警示行 + 勾选 + 取消/主按钮）。
  测试：新增用例 **C14**（弹窗门控 / 勾选后才可确认 / 取消不写入 / 已在该模式不重复弹窗），客户端套件 32 → 33；
  原 C11 的高级模式流程改为先通过确认。
- **插件页文案更新为最新功能**：`package.json` 的 `description`（插件页与 npm 页的**回退文案**）改写了，
  并新增 **本地化显示元数据** `locale/en.json` + `locale/zh.json`（DSH 约定：`locale/<lang>.json` 内的
  `meta.title` / `meta.description`；`en.json` 必须存在才会启用本地化，其它语言文件从同目录自动发现）
  ⇒ 插件页按界面语言显示中文或英文说明，内容是当前功能（三类代码区域统一放宽、分段表 / 模块决定的次数、
  自动派生的检测窗口与写回、0.2.0-rc.2 实测）。
  配套改动：`exports` 增加 `"./locale/*"`——**不列出的子路径会被 Node 解析器挡住**，否则 DSH 读不到 locale
  （实测：无该映射时 `readPluginMeta` 只能回退到包描述）；`files` 增加 `locale` 以便随包发布（`npm pack`
  已确认 `locale/en.json` 与 `locale/zh.json` 入包）。
  验证方式：用 DSH 自身的 `readPluginMeta()` 读取已安装副本，返回 `description = { en, zh }`；`plugin_manager`
  的 `list_bundles` 也已返回新文案。
- **白名单输入框合并 + 自动分类**（仅设置页 UI，字段与宿主语义不变）：
  原先「忽略字符」与「忽略片段」两个输入框合并为一个；输入的每个条目按空白 / 逗号切分后自动分类——
  **1 码点且非空白 ⇒ `ignoredChars`**（如 `-`、`|`、`·`），**其余 ⇒ `ignoredSubstrings`**（多字符片段，以及只在
  「忽略空白字符」关闭时才有意义的单字符空白）。依据：对单个非空白字符两者**完全等价**（剔除以字符为单位，
  先剔片段还是先去空白不影响结果），因此「放哪边」本来就不该由用户决定。
  - 分隔规则显式化：`- |` 加两个字符条目，`-|` 是**一个片段条目**（不再像旧的字符框那样把多字符输入逐个拆开）；
  - 校验沿用片段规则（每项 ≤ 64 码点、≤ 64 项、重复跨两表判断），错误就地显示且不写入；
  - 写入按需拆分：只写真正变化的字段（两个都变时各写一次，界面状态只更新一次）；
  - 删除 chip 时按归属字段清理（同值同时存在于两表也能一次删净）；
  - 空白字符无法在输入框内输入（分隔符本身是空白），需要时直接改配置文件——提示里已写明。
- 测试：`tests/client.test.js` 的 C7b / C10 改为合并输入框语义（分隔切分、单字符→字符表、片段→片段表、
  跨字段去重、超长/重复拒绝、按归属删除）；`tests/stress-client-ui.js` 的 500 chip 与 1000 次混合交互用例改用
  单码点 token 并断言**合并视图**与快照一致（该套件专注渲染/交互/写序压力，分类由功能套件覆盖）。
- README：配置表两行标注「共用一个输入框 + 自动分类规则」，功能等价关系表同步。

## [1.8.2] - 2026-10-06

### Fixed

- **CRLF（`\r\n`）行尾处理**：两处真实缺陷（全功能验收测试发现，均有永久回归用例）——
  1. 围栏结束行 ```` ```\r ```` 因 `\r` 未被视为尾随空白而**无法闭合**，导致其后内容一直被当作代码区域（漏判复读）；
  2. CRLF 空行（`\r\n`）被判定成「非空行」，导致其后的 4 空格缩进代码块**不被识别**（按普通阈值误停）。
  修正：`\r` 计入空白（`isSpace`）、行首缩进只统计空格与制表符、结束行剥离尾随 `\r`。
- 新增回归用例：共享套件 **CRLF 行尾**（围栏闭合 / 空行后缩进代码块 / 段落续行）与**制表符缩进按 4 空格计**；
  设置套件新增 **`.json` 模块回退告警**、**`.mjs` 模块**（Node ≥ 22 生效、Node 20 回退）与
  **行内区 256 字符上限告警**（恰好一次、闭合时不告警）。功能套件 **100 → 105 项**。
- **CI 修复（Node 20）**：`.mjs` 用例原先按「Node < 22 必回退」断言，但 **Node 20.19 起已内置 `require(ESM)`**，
  因此在 CI 的 Node 20 上误报（CI 失败 ⇒ 发布流水线被跳过）。改为**运行时能力探测**（尝试 `require()` 一个
  临时 `.mjs`）后断言，Node 20.19 / 22 / 24 均通过；文档同步为「`.mjs` 依赖 `require(ESM)`：
  Node ≥ 20.19 / ≥ 22.12 已内置，更早版本回退并告警」。

### Added

- **三类代码区域统一判定**：围栏代码块、**行内代码**、**缩进代码块**合并为同一个「代码区域」类别，统一使用
  `threshold × codeBlockMultiplier` 判定（此前只有围栏享受放宽，行内与缩进一律按普通阈值）。优先级
  **围栏 > 缩进 > 行内**：
  - 行内代码：1–64 个反引号开启（1 个、2 个……最多 64 个），须在**同一行**内由**等长**反引号 run 闭合；
    换行 / 保留超过 **256** 字符 / 块结束仍未闭合 ⇒ 开启符按普通文本，内容照常参与检测，并在宿主日志告警一次；
  - 缩进代码块：行首缩进 ≥4（制表符按 4 计）**且上一行为空行**，且上一个非空行不是列表项 / 引用起始；
    块内空行不终止，遇到首个「非空且缩进 <4」的行退出（列表项之后的缩进与段落续行**不算**代码，属保守规则）；
  - 围栏规则与未闭合围栏的语义不变；围栏内的反引号/缩进不另开区域；4 空格缩进的三个反引号不算围栏；
  - 倍数 `0` 时三类区域内部都不再判定，且区域边界清空检测缓冲（两侧文本不会拼成人为重复）。

### Fixed

- **周期/旋转文本会让决策卡片秒结束、频繁刷卡片、自动截停无法触发**：决策阶段原先用固定 unit 的尾部连续次数判断“重复是否中断”，
  无限字母表等周期文本每来一个字符尾部相位就旋转，卡片会在毫秒内 `recovered`，点击时常已结束（409），自动截停也到不了倍数。
  现在改用「最近窗口内该 unit 的出现次数」判断延续/计数，一次重复事件只生成一张卡片；真正中断（足够长的非重复内容）才取消；
  自动截停同样按窗口出现次数计数。新增 4 项测试（周期旋转去重/延迟操作/真正中断/宿主集成），决策套件 18 项。

### Changed

- 实现：`createFenceFilter()` → `createCodeScanner()`（两个入口同一份实现）；块状态的 `fence` 字段改名 `code`；
  主机侧把逐片段循环抽成 `feedRuns(b, runs)`，并新增 `flushCodeRegion(b)` 接到 `block-end` 与 `usage`/`finish`，
  确保行内区 hold-back 的文本不会逃过检测。
- 两处实现细节（都由用例固定住，避免回归）：
  1. **闭合符与内容必须分段发射**——否则缓冲区尾部是反引号，行内代码内的失控复读会被漏判；
  2. **闭合 run 恰好落在块末尾**时 `flush()` 仍按已闭合处理（拆成「内容（代码）+ 闭合符（代码）」两段）。
- 客户端设置项标签改为「代码内阈值倍数（围栏 / 行内 / 缩进）」，提示写明三类统一、缩进进入条件、
  列表/引用不算代码、未闭合反引号的 256 字符保留上限（中英同步）。
- 测试：功能套件 **90 → 100 项**（新增三类统一、行内不外溢、未闭合/超上限、缩进进入与退出条件、围栏优先与
  跨增量切分不变性），倍数 0/1 的行内与缩进用例并入 S16；压力套件的「4 空格缩进不享受放宽」旧断言按新语义改写，
  并新增「三类代码区域统一」用例；`npm run stress` 四套全 PASS。
- README：新增「代码区域判定（围栏 / 行内 / 缩进）」小节（三行表格 + 边界与代价），更新配置表、特性表、
  已知限制、English summary 与测试计数。

## [1.8.1] - 2026-10-01

### Fixed

- **周期/旋转文本会让决策卡片秒结束、频繁刷卡片、自动截停无法触发**：决策阶段原先用固定 unit 的尾部连续次数判断“重复是否中断”，
  无限字母表等周期文本每来一个字符尾部相位就旋转，卡片会在毫秒内 `recovered`，点击时常已结束（409），自动截停也到不了倍数。
  现在改用「最近窗口内该 unit 的出现次数」判断延续/计数，一次重复事件只生成一张卡片；真正中断（足够长的非重复内容）才取消；
  自动截停同样按窗口出现次数计数。新增 4 项测试（周期旋转去重/延迟操作/真正中断/宿主集成），决策套件 18 项。

### Changed

- **功能级冗余清理：隐藏 `skipCodeBlocks` 开关（与「代码块内阈值倍数 = 1」完全等价）**。三条代码路径均可证等价：
  `codeBlockMode()` 在 `skipCodeBlocks !== true` 时直接返回 1、策略侧 `codeScale` 取 1、客户端派生窗口的
  `codeMode` 取 1。设置页改为只保留倍数（0 = 不检测块内 / **1 = 关闭分档** / ≥2 = 放宽），并在倍数提示里写明
  等价关系；老配置里**显式设过** `skipCodeBlocks` 时，倍数行下方提示一次，且用户提交倍数会先 `unset`
  该旧键再写入（best-effort），避免「隐藏却仍覆盖倍数」的陷阱。宿主语义不变，老配置在未编辑前行为完全一致。
- 新增 README 小节「功能等价关系与『该用哪个』」：`skipCodeBlocks:false ≡ 倍数 1`、单字符片段 ≡ 字符白名单、
  单行表格 ≡ 简单模式、常量模块 ≡ 简单模式、阶梯模块 ≡ 分段表、`*:<次数>` 仅兼容手改配置、
  `detectionWindow` 手填值会被覆盖；并给出「简单/表格/模块」的选择建议。
- 审计中确认**不再改动**的项（写入文档以免后人误删）：不在模块模式强推 `minUnitLength = 1`
  （表格模式首行起始结构上就是 1；模块模式下它是**函数入参下界**，强推会让原先不参与判定的短单元变成候选，
  属行为改变而非精简）；不删 `detectionWindow` 只读投影（仅省约 25 行却损失设置页可观测性）；
  不合并两个白名单（要改「片段先剔、字符后剔」的顺序语义）；保留三模式、`*` 旧写法、
  `fixStandingMountConflict` 与 ≤0.1.6 兼容通道。
- **代码级冗余清理（无行为变更，除一处显示修复）**：
  - 删除已无引用的客户端字典键（`detectionWindow` / `detectionWindowHint` / `detectionWindowAuto` /
    `detectionWindowAutoHint` / `detectionWindowModuleHint` / `maxUnitFromTable` / `errThresholdTable`）
    与 `.dg-auto-value` 样式；`maxUnitFromTableHint` 改名为 `tableMaxUnitHint`（它仍是表格下方的提示）；
  - 删除 `lib/index.js` 中赋值后从未读取的 `runtime.derivedWindow` / `runtime.derivedMinUnit`，
    以及 `deriveWindow()` 返回值里无外部读取的 `plainSpan`；
  - 动态版 `plugin/host.js` 删除从未使用的 `CONFIG.advancedThresholdFile`（该版不支持 module 模式）；
  - 抽出 `buildDerivedPatch(info)`，让常驻版的两条写回路径（≥0.1.7 的 `settings.update` 与
    ≤0.1.6 的 `scope.update`）共用同一段判断（此前是两份拷贝）；
  - 写回去重语义修正：由「记住上次写过的值」改为「记住 (写过的值, 当时读到的文档值) 对」——
    用户随后把该字段改成别的值时能正确重新写回，同时仍避免 fiber 不回流导致的写风暴；
  - 清理 `commitMode()` 里无人渲染的 `errors.thresholdMode` 写入。
- **修复：高级模式（module）下设置页底部诊断行显示 `检测窗口 undefined`**。窗口字段已不在表单里，
  改为直接读控制器快照的 `detectionWindow`（缺失时显示 `—`）；`DEFAULTS.detectionWindow` 因此重新有了用途。
- **测试**：
  - 新增「派生值跨实现一致性矩阵」（6 组：简单/倍数 0/夹上限/表格单行/表格三行×2）固定宿主侧期望值；
    动态入口通过新增的 `__derived` 测试钩子断言其派生常量，客户端侧对应数值已有断言 ⇒ 三处拷贝不再各自漂移；
  - 修复 S19 用例的 mtime 粒度偶发（同一毫秒内覆写模块文件会被 mtime 缓存视为未变化）。

### Notes

- **DSH 0.2.0-rc.2 兼容性核对通过（1.8.0 无需改配置或换版本）**：
  - 宿主：`Config` 目录显示 `include:dupguard`（schema 已投影，14 个 volatile 字段照常生效）；
    `settings` 服务面 `configure` / `prepareDocument` / `describe` / `update` / `replace` / `mutate` 与 0.1.7 完全一致；
    `settings/document-updated` 事件签名不变；组合补丁层仍是 `<profile>/cordis.patch.yml`（热读取照常）；
  - 客户端：`settings.section` 槽位注册为 `dupguard`（order 25、active），官方设置页仍在用
    `ctx.remote.settings` 写通道（与本插件一致）；
  - 协议：`llm/stream` 瀑布签名与 `StreamChunk` 联合类型未变；`stress-real-invariant.mjs` 明确以
    **真实 dsh-llm 0.2.0-rc.2** 校验本插件的截停收尾（8/8 通过）；全量功能 87 + 客户端 31 项、压力四套全 PASS；
  - 0.2.0 新增 `tool-addition` / `tool-removal` 块类型不携带增量 ⇒ 不可能在其打开期间触发截停，
    故不会走到「未闭合块兜底」路径（既有告警行为保留）。
- **DSH 桌面版（Electron）兼容性核对**：`D:\.dsh` 为应用安装目录，运行时 `runtime.json` 声明
  `desktopVersion 0.2.0-rc.2`（Node 24.21.0 / pnpm 11.7.0）⇒ 与 CLI 同版本，API 层结论一致；
  profile 为 `<DSH_HOME>/profiles/desktop`，组合方式与 web 相同（bundle 层 → `cordis.patch.yml`），
  免重启热读取按插件安装路径反推 profile，桌面版同样生效。
  **`desktop` profile 由 Electron 应用独占管理**：`dsh plugin --profile desktop …` 直接报
  `profile "desktop" is managed exclusively by the Electron application`，须在桌面应用「设置 → 插件」里安装。
- **桌面 profile 工件核验 13/13 通过**（1.8.0）：版本 / `dsh.bundle` 与 `dsh.client.platform=web` / `engines`
  / 14 个 volatile 字段与默认值 / 窗口与单元长度派生 / 三字段合并写回 / 组合补丁层热读取 / 表格控件 / 构建标记。
  另记录一个**解析差异（非缺陷）**：web profile 解析到插件内嵌套的 schemastery **3.18.2**，desktop 解析到
  profile 级 **3.18.4**；3.18.4 下 volatile 字段解析为响应式单元格（`{ get() }`），本插件宿主正是以 `.get()`
  取值，默认值（`'simple'` / `10` / `["-","|"]` / `true` 等）均正确。
- 新增 [docs/compatibility-report.md](docs/compatibility-report.md)：可直接复制到社区 Discussions 的兼容性报告
  （CLI × 0.2.0-rc.2、桌面版 × 0.2.0-rc.2 两份 + 历史记录表）。
- 审计备注（有意保留的"重复"）：三份派生公式（lib / plugin / client）与两个入口的检测实现因**运行时不共享**
  而必须各存一份，靠双入口共享用例 + 上述一致性矩阵守行为；客户端 `DEFAULTS` 与宿主 `CONFIG` 的镜像由
  drift 测试守；`thresholdByLength` 的 `*` 旧写法、`fixStandingMountConflict` 补丁为兼容老版本保留。
  压力套件自带的字段镜像刻意独立（改动面大、收益小），不做合并。

## [1.8.0] - 2026-09-27

### Added

- **高级重复设置（按单元长度决定重复次数）**：设置页新增模式下拉
  - `simple`（默认）：固定 `threshold`，不构造策略、走原有快路径（与 1.7.0 行为逐字节一致）；
  - `table`：分段表 `1:40, 2:30, 8:12, *:10`，未覆盖长度用 `*`，无 `*` 用 `threshold`；
  - `module`：指向本地 JavaScript 文件（同步导出 `repeatCount(length) -> count`，**扩展名不限**：
    Node 对未注册扩展名按 CommonJS 加载，`.mjs` 需 Node ≥22，`.json` 不能导出函数），
    mtime 变化才重载；返回值需为整数 2–1000，`< 2`/非整数 ⇒ 该长度不判定；
  - 加载失败、模块抛错、空分段表 ⇒ 回退基础 `threshold` 并只告警一次；
  - 窗口约束随之变化：`max(threshold × maxUnitLength, max(p × need(p)))`；宿主日志的「生效参数」行
    新增次数策略摘要（模式 / 覆盖长度数 / 次数区间 / 最长跨度）。
- 设置页新增「高级重复设置」分节：模式下拉 + 按模式条件显示分段表或模块路径（**路径不再限定扩展名**，
  只校验非空；加载结果以宿主日志为准），
  模式字段纳入「恢复默认」。

### Fixed

- **改设置必须重启 DSH 进程才生效**：DSH ≥ 0.1.7 的设置页把值写入**组合补丁层**
  （`<profile>/cordis.patch.yml`），但该层的改动不会被重新解析进正在运行的 entry fiber ——
  表现为「设置页读回仍是旧值、检测仍用旧参数，重启后才正常」。现插件自行读取该文件
  （`DSH_PROFILE_DIR`，缺失时按安装位置反推 profile 目录），把用户层值叠加到 Config 之上，
  每次流式调用即时生效；文件变化时（`fs.watchFile` 轮询 + `settings/document-updated` 事件）
  还会把合并结果推回自身 fiber，使设置页读回也显示新值。解析失败/文件缺失一律退回 Config 值，
  检测不受影响；宿主日志会打印「生效参数（热更新）」行便于确认。

### Fixed

- **周期/旋转文本会让决策卡片秒结束、频繁刷卡片、自动截停无法触发**：决策阶段原先用固定 unit 的尾部连续次数判断“重复是否中断”，
  无限字母表等周期文本每来一个字符尾部相位就旋转，卡片会在毫秒内 `recovered`，点击时常已结束（409），自动截停也到不了倍数。
  现在改用「最近窗口内该 unit 的出现次数」判断延续/计数，一次重复事件只生成一张卡片；真正中断（足够长的非重复内容）才取消；
  自动截停同样按窗口出现次数计数。新增 4 项测试（周期旋转去重/延迟操作/真正中断/宿主集成），决策套件 18 项。

### Changed

- **检测窗口改为插件自动推导（不再由用户填写）**：`detectionWindow` 成为派生字段，等于「最严格的重复跨度」
  ——简单模式取 `max(threshold, threshold × 代码块倍数) × maxUnitLength`；**策略模式（table/module）只取策略跨度**
  `max over p of max( p × need(p), p × need(p) × 代码块倍数 )`（次数完全由策略给出，不再叠加基础阈值——否则
  `*:3` + 最大单元 1000 会被算成 10 × 1000 = 10000，实际只需 3000）。结果夹在 `[64, 1048576]`。设置页移除输入框、改为只读派生值；宿主每次生效都把该值**写回设置文档**
  （`settings.update(ns, { detectionWindow })`，值未变化时不写，避免写回循环），使设置页与配置文件
  显示的都是真实生效值；手工编辑的窗口值被忽略。默认参数下派生值为 **2400**（10 × 3 × 80；倍数 0 时为 800），
  不再是 8192。需求超过 1,048,576 时夹上限并提示降低阈值/倍数/最大单元长度（窗口不再可调）。
  动态版（`plugin/host.js`）取 `max(CONFIG.detectionWindow, 派生值)` 作为代码级下限。

- **分段表模式改为三列表格输入**：起始（只读，首行 1、其后 = 上一行「终止」+ 1）/ 终止 / 次数，
  行数 1–16 可自由增删（到上限/下限时按钮禁用）；**末行「终止」= 最大检测长度**，因此该模式下
  `maxUnitLength` 不再作为输入项——它与检测窗口一样是派生值，由宿主在每次生效时写回设置文档
  （与窗口合并为一次 `settings.update`）。存储格式不变（仍为 `"<终止>:<次数>, …"`），旧配置里的
  `*:<次数>` 仍兼容：上界延伸到文档 `maxUnitLength`，设置页迁移为末行；非法编辑（终止小于起始、
  终止不小于下行终止、次数不合法、终止超过 8192）就地报错且不写入。

- **派生值不再单独占行**：检测窗口与（表格模式下的）最大/最小重复单元长度都是不可编辑的派生值，原先各占一行
  只读行；现改为并入设置页底部已有的诊断行
  （`设置通道：…｜构建 …｜自动派生：检测窗口 9000 · 最大重复单元长度 1000（末行决定）`），
  界面更清爽、信息不丢；被上限夹住时原有的 ⚠ 提示仍然单独显示。
  **表格模式下 `minUnitLength` 也由表格派生（固定 1）**：候选区间完全由表格给出，界面只保留
  「代码块内阈值倍数」一个可编辑数值项，三个派生值（窗口 / 最大单元 / 最小单元）合并为一次写回。
  顺带修正按钮版式：`.dg-btn` 加 `white-space:nowrap`（「添加一行」不再折行），并补齐禁用态样式。

### Notes

- **语义提示（已写入 README）**：检测按每个候选周期取次数，而周期性文本在其周期的整数倍上同样成立，
  因此仅抬高长度 1 的次数通常无效；要放宽短串需把所有可能命中的小周期一起抬高
  （如 `1:40, 2:40, 3:40, *:10`）。长单元只需更少次数即可单独生效。
- 动态版（`plugin/host.js`）支持 `simple`/`table`，`module` 回退固定阈值并告警（高级模式仅常驻版可用）。
- 设置页只显示与当前模式相关的项：**「触发阈值」仅在简单模式显示**（`table`/`module` 下改为提示当前兜底值）；
  `minUnitLength`/`maxUnitLength`/`detectionWindow`/`codeBlockMultiplier` 与各开关在所有模式下保留。
- 测试：功能 84 + 30 项、压力四套全 PASS。

## [1.7.0] - 2026-09-24

### Added

- **多字符片段白名单 `ignoredSubstrings`**（默认 `[]`，显式开启）：
  - 整段**字面量**匹配（区分大小写、不支持正则），命中时先整段剔除，再做去空白与逐字符剔除；
    用于 `|---|`、`<br>` 这类由多个字符组成的固定片段（逐字符白名单只能忽略单个字符）；
  - **长片段优先**匹配，避免 `---` 抢先破坏 `-----`；
  - **跨增量安全**：每块最多保留（实际最长片段 − 1）个码点，等下一增量拼回后再匹配；
    块结束/流结束时补投尾巴，尾部文本仍参与检测；
  - 上限：每项 ≤ 64 码点、最多 64 项；空值/超长/超量条目运行时丢弃并各告警一次（同样内容只告警一次）；
  - 片段表为空时走零开销快路径，默认行为与未提供该功能时**逐字节一致**；
  - 设置页新增「忽略片段」列表（整段加入、不按码点拆分，含空值/超长/重复/超量校验），
    与字符白名单共用写通道，可持久化并可被「恢复默认」清空。

### Notes

- 实测开销（10 万增量近失配流）：空表 2.6 µs/增量 → 配置 4 条不匹配片段 3.3 µs/增量（约 1.3×）；
- 双入口（`lib/index.js` / `plugin/host.js`）实现保持一致，共享用例分别驱动两边；
- 宿主启动日志新增一行**生效参数清单**（阈值 / 窗口 / 最大单元 / 字符与片段白名单项数），
  设置页底部常驻「设置通道 + 构建标记」诊断行——用于区分「宿主未加载新代码」「设置没下发」「实现有问题」三类故障。

## [1.6.3] - 2026-09-24

### Fixed

- **写入成功却被判为「保存失败」**：`remote.settings.mutate()` 返回的命名空间视图是**写入前**的快照
  （配置由 loader 异步重载后才更新），此前的实现拿它与本地期望值做回读比对，于是每次写入都
  必然不一致——面板报错、看起来「没保存」，而宿主其实已经落盘。现改为**只以宿主应答为准**
  （`response.ok === false` 才算失败），与官方 `ConfigForm.mutate` 的语义一致；恢复默认同样按
  「全部 unset 是否未被拒绝」判定。

### Added

- 设置页底部常驻诊断行 `设置通道：<状态>｜<诊断>｜构建 <标记>`，并在写入失败时附带宿主原始原因
  与构建标记；浏览器控制台同步打印 `[dupguard] mutate → …` / `mutate 被拒…`，用于快速区分
  「宿主拒绝」「通道未就绪」「回读不一致」三类问题。

## [1.6.2] - 2026-09-24

### Fixed

- **插件 Config 的全部字段标记为 volatile（设置页拿不到命名空间的真正根因）**：
  `dsh-settings` 的 `volatileForm()` 只投影 `meta.volatile` 为真的字段，一个 entry 若没有任何
  volatile 字段，其表单为空、`describe()` 会**整个跳过**它——正是宿主日志显示已走 Config 模型、
  Config 目录也有 `status: schema`，但 `describe()` 返回的 16 个命名空间里没有本插件的原因。
  另外只有 volatile 字段才会被 `resolveConfig` 包装成带 `get()` 的响应式单元格，本插件实时
  读取参数正依赖这一点。schemastery ≥ 3.18.4 的 `schema.volatile()` 内部即 `extra('volatile', true)`；
  本插件自带副本可能更旧（无该方法），实现里已按 `volatile()` → `extra()` → 直写 `meta` 退化。
- **命名空间匹配覆盖组合前缀**：0.1.7 的 entry id 实为 `include:dupguard`（bundle 插入的行 id 是
  `dupguard`，组合层会加 `include:` 前缀），而设置命名空间取自 entry id。此前只匹配
  `dsh-dupguard` / `dupguard` 两个精确名；现按 精确名 → 名称包含 `dupguard` → 字段签名 三级匹配。

### Added

- **面板级通道诊断**：不可用态也显示 `设置通道：<状态>｜<诊断>`，诊断内容包含
  `describe 返回 N 个命名空间 [ns…]` 或 `describe 未成功`；浏览器控制台同步打印
  `[dupguard] remote.settings.describe 返回命名空间：…`，便于一次截图定位问题环节。

## [1.6.1] - 2026-09-24

### Fixed

- **0.1.7 上设置页停在「加载中」**：动态注入必须**同时声明父服务与点号服务**
  （`ctx.inject(['remote', 'remote.settings'], …)`）。只声明 `'remote.settings'` 时，
  cordis 的注入作用域里没有 `scope.remote`（父服务未声明），1.6.0 的回调因此静默返回、
  既未接入通道也未发布状态，面板永远停在 loading/idle。

### Added

- 设置通道探测改为多路兜底：注入回调（父+点号）→ 服务面多形态解析
  （`scope.remote.settings` / `scope['remote.settings']`）→ `ctx.get('remote.settings')`
  有界轮询（400ms × 40）；旧版 `settingsScope` 接上后立即停止新版探测。
- 面板加载态显示通道诊断（`设置通道：<状态>｜<诊断>`）并在浏览器控制台打印
  `[dupguard] client apply：…`，下次若再异常可直接从截图/控制台定位。

## [1.6.0] - 2026-09-24

### Added

- **DSH 0.1.7-rc.1 支持**（0.1.7 移除了客户端 `settingsScope` 与 `ctx.settings.register`）：
  - 宿主导出插件 `Config`：0.1.7 起设置命名空间由 loader entry 决定（本 bundle 插入的行 →
    `dupguard`），用户层写入后 loader 实时把新 `config` 下发给 `apply(ctx, config)`；
  - 宿主在 ≥ 0.1.7 上调用 `settings.configure({ auto: false })`，避免与自定义设置页重复出现两个页面；
  - 客户端改用 typert remote 通道：`ctx.remote.settings.describe()` / `mutate(ns, ops, revision)`，
    并兼容 `mutate` 直接返回命名空间行（与 `describe` 的 `{ writable, namespaces }` 形状不同）；
  - 命名空间解析顺序：`dsh-dupguard`（旧注册名）→ `dupguard`（bundle 行 id）→ 字段签名兜底。

### Fixed

- **0.1.7 下客户端入口永久 pending**（页面报 `web boot: 1 entry did not activate`，诊断为
  `waiting for service: settingsScope`）：静态 `inject` 收敛为各版本都存在的 `slots` / `locale`，
  设置服务改用 `ctx.inject(...)` 动态接入；任一服务缺席时插件照常激活，仅设置页显示「设置服务不可用」。
- 设置页的本机/远程判断改为实时读取（`remote.$host.isLoopback` 与 `describe().writable`），
  不再停留在注册时的快照值。
- 参数每次模型调用重算时的重复告警去重（无效白名单条目、窗口偏小各只告警一次）。

## [1.5.0] - 2026-09-24

### Added

- **代码块内三档处理**：`codeBlockMultiplier` 现在同时承担「模式选择」——
  - `≥2`（默认 `3`）：块内按「阈值 × 倍数」判定（1.4.0 的放宽行为）；
  - `1`：块内与块外同样严格；
  - `0`：**完全不检测代码块内**——块内内容不参与重复统计，生成的代码再长也不会被截停；
    进入/离开代码块时清空检测缓冲，围栏两侧的重复不会被拼成一次命中。
  取值范围由 1–100 放宽到 **0–100**；`skipCodeBlocks=false` 仍可整体关闭该机制。

### Fixed

- **周期/旋转文本会让决策卡片秒结束、频繁刷卡片、自动截停无法触发**：决策阶段原先用固定 unit 的尾部连续次数判断“重复是否中断”，
  无限字母表等周期文本每来一个字符尾部相位就旋转，卡片会在毫秒内 `recovered`，点击时常已结束（409），自动截停也到不了倍数。
  现在改用「最近窗口内该 unit 的出现次数」判断延续/计数，一次重复事件只生成一张卡片；真正中断（足够长的非重复内容）才取消；
  自动截停同样按窗口出现次数计数。新增 4 项测试（周期旋转去重/延迟操作/真正中断/宿主集成），决策套件 18 项。

### Changed

- 窗口缺口提示与宿主告警在倍数为 `0` 时不再放大要求（块内完全不检测，窗口只需满足
  `阈值 × 最大单元长度`），设置页提示文本与 README 同步说明三档语义。

## [1.4.0] - 2026-09-23

### Added

- **围栏代码块内放宽检测**（`skipCodeBlocks` 默认开启 + `codeBlockMultiplier` 默认 `3`，均可在设置页调整）：
  代码块内的重复串（生成的测试夹具、表格、ASCII 图、内嵌数据）改用「阈值 × 倍数」判定，
  不再被误判为复读；块内的失控复读仍会被兜住（默认连续 30 次即截停）。倍数范围 1–100，
  设为 1 等价于不放宽；关闭开关则块内与块外同样严格；
- 围栏识别按 CommonMark：起始行为行首 ≤3 个空格 + 连续 ≥3 个 `` ` `` 或 `~`（其后为 info string），
  结束行需同字符且不短于起始长度、其后仅空白；未闭合围栏延续到该块结束；
  围栏标记被切进多个增量（如 `"``"` + ``"`js"``）时仍能识别；行内代码与缩进代码块不受影响；
- 截停日志新增 `code-block=true（放宽阈值命中）` 标记，便于区分命中来源。

### Fixed

- **周期/旋转文本会让决策卡片秒结束、频繁刷卡片、自动截停无法触发**：决策阶段原先用固定 unit 的尾部连续次数判断“重复是否中断”，
  无限字母表等周期文本每来一个字符尾部相位就旋转，卡片会在毫秒内 `recovered`，点击时常已结束（409），自动截停也到不了倍数。
  现在改用「最近窗口内该 unit 的出现次数」判断延续/计数，一次重复事件只生成一张卡片；真正中断（足够长的非重复内容）才取消；
  自动截停同样按窗口出现次数计数。新增 4 项测试（周期旋转去重/延迟操作/真正中断/宿主集成），决策套件 18 项。

### Changed

- 窗口缺口提示与宿主告警改为按「阈值 × 代码块倍数 × 最大单元长度」计算（关闭放宽时倍数按 1 计），
  设置页提示文本同步显示倍数。

### Fixed

- 长围栏不再被短围栏提前关闭：围栏长度取起始行的**完整** run，4 个反引号开启的代码块不会被
  内部的 3 个反引号行关闭（此前一见 3 个反引号就进入并记长度为 3）。

## [1.3.1] - 2026-09-05

### Fixed

- **白名单按「单个字符」匹配的语义两端不一致**：设置页会把整串输入（如 `ab`）当成一个条目，
  而检测按单个字符比对，该条目永远不会生效。现在设置页把一次输入拆成单个字符逐个加入
  （忽略其中的空白），宿主对存量或手改的多字符、空、非字符串条目在运行时丢弃并打印告警；
  白名单比对同时改为按 Unicode 码点进行，emoji 等代理对字符可作为单个条目正常生效。
- **未改动的数值输入在失焦时也会写入**，把默认值写进用户层（`settings.yaml` 里因此出现
  `threshold: 10`、`maxUnitLength: 80`、`detectionWindow: 8192` 这类无意义条目）。现在值与
  宿主一致时跳过写入。
- **跨字段约束只在「最大单元」一行提示**：从「最小单元」一侧提交违规值时只能拿到宿主拒绝后的
  泛化「保存失败」。现在两行都给出具体错误并在本地拦截写入。
- 截停收尾遇到未知块类型（协议新增 `ContentBlock` 类型）时会静默按 tool-call 兜底，
  现补充一次性宿主告警，便于定位协议漂移。

## [1.3.0] - 2026-09-05

### Added

- 全部检测参数可在设置页动态调整（此前仅白名单可改）：`threshold`、`minUnitLength`、
  `maxUnitLength`、`detectionWindow`、`stripWhitespace`、`monitorReasoning`、
  `monitorToolArguments` 均注册进 `dsh-dupguard` 设置 schema（含上下界与非整数拒绝），
  改动即时热生效并持久化到 `settings.yaml`；
- 设置页参数表单：数值输入（失焦或回车提交、范围与整数校验、跨字段校验）与原生风格开关，
  写入仍走 `settingsScope` 控制器并回读快照校验；
- **窗口缺口提示**：`detectionWindow < threshold × maxUnitLength` 时，设置页显示
  「⚠ 检测窗口长度需要提高：至少 N（当前 M = 阈值 × 最大单元），超过 K 字符的重复单元无法识别」，
  宿主日志同时打印一条同义告警（只提示不拒绝写入）；
- 「恢复默认」改为清空本页全部用户设置（逐字段 unset），回到代码默认值。

### Fixed

- **周期/旋转文本会让决策卡片秒结束、频繁刷卡片、自动截停无法触发**：决策阶段原先用固定 unit 的尾部连续次数判断“重复是否中断”，
  无限字母表等周期文本每来一个字符尾部相位就旋转，卡片会在毫秒内 `recovered`，点击时常已结束（409），自动截停也到不了倍数。
  现在改用「最近窗口内该 unit 的出现次数」判断延续/计数，一次重复事件只生成一张卡片；真正中断（足够长的非重复内容）才取消；
  自动截停同样按窗口出现次数计数。新增 4 项测试（周期旋转去重/延迟操作/真正中断/宿主集成），决策套件 18 项。

### Changed

- 宿主 `settings.register` 增加 `validate` 跨字段约束（最大单元长度不得小于最小单元长度），
  违规写入被拒绝而非静默存入；
- 运行时设置同步改为逐字段归一化（类型/范围兜底，绝不抛错），非法外部编辑回落到代码默认值；
- 客户端组件改为全部 hook 前置，消除「加载中 → 就绪」状态切换时的 hook 数量变化风险。

## [1.2.1] - 2026-09-02

### Fixed

- 设置页写入在 DSH 0.1.2 上完全失效：0.1.2 的客户端 `connection` 服务不再暴露
  `api`（wire 面改为 `ctx.remote.<namespace>`），此前的直连 RPC 写法在 0.1.2 上
  同步抛错——表现为点击「添加」后列表乐观更新、底部停在「保存中…」，写入从未落盘、
  关闭重开不显示、白名单也不生效。现改为经 `settingsScope` 控制器写
  （`set` / `unset`：自动携带最新 revision、串行化并发写、把宿主应答折叠回共享镜像），
  写完回读快照校验后显示「已保存」或失败原因；该控制器接口在 0.1.1 与 0.1.2 一致，
  因此同时兼容两版。

### Added

- `tests/client.test.js`：用最小 React / DSH 客户端桩直接驱动设置页组件，覆盖写路径
  （控制器 `set`/`unset`）、「保存中…→已保存」回显、重开显示持久化白名单，并断言组件
  不再依赖 `connection.api`；`npm test` 与 CI 改为同时运行两个测试文件。

## [1.2.0] - 2026-09-02

### Added

- 适配 DSH 0.1.2 的 bundle 约定：本包自带补丁层 `cordis.patch.yml` 并声明
  `dsh.bundle.patch`，安装只需一条命令 `dsh plugin --profile web add dsh-dupguard`
  （`dsh plugin` 会自动把声明了 `dsh.bundle` 的依赖加入 `dsh.profile.bundles`，
  DSH 按层应用补丁插入宿主行 `{ id: dupguard, name: dsh-dupguard }`，无需手改 YAML）；
- package.json 增加 `dsh.compatibility`（`dsh >= 0.1.1-rc.1`）。

### Fixed

- **周期/旋转文本会让决策卡片秒结束、频繁刷卡片、自动截停无法触发**：决策阶段原先用固定 unit 的尾部连续次数判断“重复是否中断”，
  无限字母表等周期文本每来一个字符尾部相位就旋转，卡片会在毫秒内 `recovered`，点击时常已结束（409），自动截停也到不了倍数。
  现在改用「最近窗口内该 unit 的出现次数」判断延续/计数，一次重复事件只生成一张卡片；真正中断（足够长的非重复内容）才取消；
  自动截停同样按窗口出现次数计数。新增 4 项测试（周期旋转去重/延迟操作/真正中断/宿主集成），决策套件 18 项。

### Changed

- 与 DSH 0.1.2-rc.1 逐项核对：`llm/stream` 瀑布事件与 `StreamChunk` 协议、`settings.register`
  （`base` 选项）、客户端 `settingsScope.bind` / `decode(view.value)` / `describe`、
  `settings.section` 槽位契约、`--dsw-*` 主题变量均未变化，插件行为无需改动；
- standing-mount 缺陷在 0.1.2-rc.1 仍未修复（上游保留 reclaim 代际的 TODO），
  兼容补丁继续默认开启；
- README 安装章节改为以 bundle 方式为主、手工补丁层为辅，并说明两者不可同时使用
  （重复 entry id 会抛 `duplicate loader entry id: dupguard`）。

## [1.1.5] - 2026-09-02

### Fixed

- **周期/旋转文本会让决策卡片秒结束、频繁刷卡片、自动截停无法触发**：决策阶段原先用固定 unit 的尾部连续次数判断“重复是否中断”，
  无限字母表等周期文本每来一个字符尾部相位就旋转，卡片会在毫秒内 `recovered`，点击时常已结束（409），自动截停也到不了倍数。
  现在改用「最近窗口内该 unit 的出现次数」判断延续/计数，一次重复事件只生成一张卡片；真正中断（足够长的非重复内容）才取消；
  自动截停同样按窗口出现次数计数。新增 4 项测试（周期旋转去重/延迟操作/真正中断/宿主集成），决策套件 18 项。

### Changed

- 明确不支持 Node 18（与 DSH 一致）：`engines` 提升为 `>=20`，CI 矩阵调整为
  Node 20/22/24，README 同步说明。

## [1.1.4] - 2026-09-02

### Fixed

- Node 18 兼容性：不再顶层 `require('@deepseek-ai/schemastery')`（其 CJS shim 内部
  require 纯 ESM 的 cosmokit，Node < 20.19 抛 `ERR_REQUIRE_ESM`，导致插件整体无法加载），
  改为设置注册时惰性动态 `import`（ESM 入口在 Node 18 可用，且与 DSH 共享同一模块实例）；
  对应 CI 的 Node 18 测试恢复通过。

## [1.1.3] - 2026-09-01

### Fixed

- 设置页白名单增删与持久化的完整修复：
  - **根因**：DSH 的 `SettingsScopeController` 以 `spec.decode(view.value)` 调用自定义
    decode，传入的是命名空间设置值本身（`{ignoredChars: [...]}`），而非 wire 视图；
    此前按视图解读导致永远回退默认白名单 `['-', '|']`，持久化值从不显示；
  - 写操作改为直连 settings RPC（models 页同款），界面提供保存中/已保存/失败原因反馈；
  - 列表本地自治，免疫镜像旧值回传导致的"闪现后回滚"；
  - 写成功后重新拉取镜像（`mirror.load()`），重开设置页即显示持久化的白名单；
  - 按钮显式 `type="button"` 防止默认提交行为。

## [1.1.2] - 2026-09-01

### Fixed

- 设置页「添加」按钮在亮/暗主题下的对比度问题：不再使用品牌主色做按钮背景，
  两个按钮统一为中性样式（次要表面色背景 + 主文字色），任何主题下文字都清晰可读。

## [1.1.1] - 2026-09-01

### Fixed

- 设置页「重复守卫」始终显示"加载中"的修复闭环：
  - host 端改为 DSH 宿主行约定的 `ctx.inject` 模式访问 root 服务（loader entry 的
    ctx 无法用 `ctx.get` 直接解析 `settings` / `cordisInspect`，此前两个服务均不可见，
    命名空间从未注册）；
  - 客户端 decode 兜底：wire 视图任何形状异常都回退默认白名单，保证视图到达即 ready；
  - 加载态补充镜像状态与错误详情，并区分远程访问与命名空间缺失提示。

## [1.1.0] - 2026-08-30

### Added

- 图形化设置页（npm 常驻版）：新增 `lib/client.js`（`dsh.client` 浏览器端入口），在 DSH
  设置面板注册与「通用设置 / 模型 / 插件 / Agent 预设」并列的「重复守卫」分节，可视化
  编辑白名单（增删字符、恢复默认），样式使用 `--dsw-*` 主题变量与原有设置页一致；
- 设置持久化与热更新：host 端通过 settings 服务注册 `dsh-dupguard` 命名空间
  （`ignoredChars` 字段，schemastery schema，新增 `@deepseek-ai/schemastery` 依赖），
  设置变更即时热生效（动态版保持零依赖，白名单固定取常量）；
- 测试新增 settings 集成套件（默认 base、热更新、清空、恢复默认），共 48 项。

## [1.0.3] - 2026-08-30

### Fixed

- 思考（reasoning）复读守卫：`monitorReasoning` 默认改为开启，思考中同一字符串连续重复
  10 次以上即截停；修复开启后暴露的 reasoning 文本双重累积缺陷（`b.text` 与 `feedText`
  各加一次，导致闭合块文本翻倍）；新增"关闭开关"变体套件验证 `monitorReasoning: false`
  时仍全量透传。测试套件双入口各 21 项，加变体共 44 项。

## [1.0.2] - 2026-08-27

### Fixed

- 修复 Markdown 表格误截停：新增 `ignoredChars` 白名单（默认 `['-', '|']`），
  表格分隔行（如 `|---|---|`）与长分隔线不再被误判为复读；夹带白名单字符的真实复读
  （如 `-ab-` ×10）仍会被识别。测试套件新增 4 项，双入口各 20 项。

### Fixed

- **周期/旋转文本会让决策卡片秒结束、频繁刷卡片、自动截停无法触发**：决策阶段原先用固定 unit 的尾部连续次数判断“重复是否中断”，
  无限字母表等周期文本每来一个字符尾部相位就旋转，卡片会在毫秒内 `recovered`，点击时常已结束（409），自动截停也到不了倍数。
  现在改用「最近窗口内该 unit 的出现次数」判断延续/计数，一次重复事件只生成一张卡片；真正中断（足够长的非重复内容）才取消；
  自动截停同样按窗口出现次数计数。新增 4 项测试（周期旋转去重/延迟操作/真正中断/宿主集成），决策套件 18 项。

### Changed

- 验证与 DSH `0.1.1-rc.1` 兼容：`llm/stream` 事件签名、`StreamChunk` 协议、适配器关闭语义
  （`consumer.abort`）、`llm-invariant` 校验、`BlockAssembler`、agent-loop 流消费、
  `CordisInspectRegistryService`（`providers` 字段与 `register` 语义）均无变化，插件无需改动；
  `dsh-agent-presets` 的 standing-mount 多代并存缺陷 `0.1.1-rc.1` 仍未修复，兼容补丁
  （`fixStandingMountConflict`）仍然必要（文档表述更新为"≤ 0.1.1-rc.1"）。
- 验证与 DSH `0.1.0-rc.7` 兼容：`llm/stream` 事件签名、`StreamChunk` 协议、适配器关闭语义
  （`consumer.abort`）、`llm-invariant` 校验、`BlockAssembler`、agent-loop 流消费、
  `CordisInspectRegistryService`（`providers` 字段与 `register` 语义）均与 rc.6 一致，
  插件无需改动；`dsh-agent-presets` 的 standing-mount 多代并存缺陷 rc.7 仍未修复，
  兼容补丁（`fixStandingMountConflict`）仍然必要（文档表述由"≤ rc.6"更新为"≤ rc.7"）。

## [1.0.1] - 2026-08-17

### Added

- DSH ≤ rc.6 兼容补丁（`fixStandingMountConflict`，默认开启）：幂等化 `cordisInspect.register`，
  修复 preset standing-mount 多代并存导致的 "Host Cordis inspect provider ... is already registered"
  （截停后模型操作报 resume failed、必须重启 DSH 才能恢复）问题。
- 测试套件新增第 16 项（幂等补丁行为），双入口各 16 项。

## [1.0.0] - 2026-08-16

### Added

- 首次发布：实时检测流式输出中的连续重复（默认同一字符串 ≥10 次）并立即停止生成。
- 双入口交付：`plugin/host.js`（动态插件）与 `lib/index.js`（npm/组合常驻），行为一致、同一测试套件防漂移。
- 端到端测试（15 项 × 2 入口）与 GitHub Actions CI（Node 18/20/22）。
