HerTa-st/Herta-dsh ↗★ 21

dsh-herta

Installs Herta (黑塔) as a DeepSeek Harness agent — persona, memory, narrative layer, voice and UI. · DSH 插件:把 Herta(黑塔)作为一个 agent 装进 DeepSeek Harness —— 人设、记忆、语音与界面。

Package
dsh-herta
Compatibility
Unverified
Harness peer range
>=0.1.7-rc.1 <0.3.0-0
Version
0.1.11
License
MIT
Last updated
Oct 21, 2026

Install

$npx -p @deepseek-ai/dsh dsh plugin --profile web add github:HerTa-st/Herta-dsh

验完记得还原 patch(别让它进正式配置)。


> 这一条路是**踩出来的**:此前几轮都以为「没有 Key 就验不了真实链路」。
> 其实要分开看 —— **真实模型回什么**验不了,但**行为链路**能验。

### 其余缺口

- **五个工具从未在真实会话里被调用过** —— 只验证到「进了工具表」与纯逻辑单测。
  lab 里没有 API Key,所以一次真实调用的闭环还没走通。
- **构建脚本需要指向一份 DSH 安装**,因为 preset 的底本是官方随附的那份。
  0.1.7 起:`build-preset.mjs` 读 `dsh-web-app/presets/standard.patch.yml`,
  通过 `DSH_PACKAGES`(指到 `…/node_modules/@deepseek-ai`)定位;集成测试与
  `test-dream.mjs` 通过 `DSH_MODULES`(指到 `…/node_modules`)借 `@deepseek-ai/*`。
  **两者都不再写死本机绝对路径**,探测不到会明确报错并告诉你设哪个变量。
  `HERTA_SRC`(Herta 源码树)仍有一个本机默认值,可用环境变量覆盖。
  ⚠️ 桌面应用的包在 `app.asar` 里,构建脚本读不到 —— 构建请指向一份
  **解开目录**的 DSH 安装(npm 安装或便携版)。
- **她的设置页已删除,全部设置由 DSH 设置 ▸ 黑塔 统一管理**(2026-09-26)。
  `Herta-src` 里的 `SettingsModal` + 7 个分页 + 侧栏入口按钮整块移除,插件也不再往
  她自己的 `settings.json` 写任何字节(写回、种子、`↺ 跟随整机` 一并删除)。
  仍在「暂未接线」组里的 9 项**整机当前不读**,页面逐行标注了原因 ——
  详见 `docs/设置搬迁.md`。(`voiceEngine` 与 `realtimeVoice` 已在 2026-09-27 移出这一组,见下。)
- **整机页的 `submitText` 未实现** —— 她那套输入框既然已隐藏,这条路径日常碰不到;
  但若要恢复输入框,必须同时把 bridge 的 `submitText` 补上,否则还是发不出消息。
- **C 层客户端消费 cue 的那一半未端到端验证** —— cue 的**抽取**已有单测覆盖
  (`test-mapping.mjs`),但从一个真实的 `herta_speak` 工具结果触发播放,需要
  一次模型调用才能走通。
- **整机的 `listSessions` 返回空** —— 她自己的会话列表 / 开场白 / 设备卡还没接;
  整机目前只服务「当前这一个 DSH 会话」。
- **云端语音(MiniMax)已端到端接通(2026-09-27)**:她的回复正文 → 宿主分段 →
  MiniMax 云端合成(**认领**她账号上已有的克隆,不做上传/克隆)→ SSE
  `/herta-minimax-events` → 浏览器半侧 → `push("voice", …)` → 整机 iframe 播放
  (iframe 缺席时父窗口用 WebAudio 自己放)。边写边念;复核否决/要求重说时把还在流的
  那一段掐掉;每轮 800 字上限;云端不可用(没密钥/没认领到克隆/被拒/克隆被删)时
  **显式回落本地模型**,并在设置页写明"现在是谁在说话"。**仍未接**的:
  `voiceEngine = local / mimo` 这两条(本地模型目前只服务 `herta_speak` 与这条回落,
  `mimo-tts.js` 仍未实例化)—— 这是决定不是遗漏。
  单测:`npm run test:minimax`(324 项;`test-minimax-segment` 还与上游做了
  16164 次差分比对,0 处不一致)。
- **C2(全量本地 TTS)已能合成**。现在:模型能按上游地址
  下载 / 校验 / 解包 / 装好,运行时(22 MB sherpa-onnx)随包分发且被**真探测**,
  合成本身用真实模型实测过(24 kHz / 非静音,`scripts/test-tts-runtime.mjs` 22 项)。
  它现在是**回落的音源**:MiniMax 不可用时由同一条 SSE 通道推给界面。
  注意回落需要先把模型下下来(`$DSH_HOME/tts/herta-best-e72`);没装时回落会
  明确报「模型还没装」,不会假装出声。
  **静音与音量走通了**(2026-09-26):它们是 DSH 设置里的 `voiceMuted` / `voiceVolume`,
  经 bridge 下发给整机的 `voice-prefs.ts`(`hydrateVoicePrefs`),播放路径真的读它们。
  换成独立版 / 官网 demo(没有那个 bridge 成员)时该模块退回自己的 localStorage。
- **整机页面占 20.1 MB**(16 MB JS + 3 MB 开场段)。开场段可以改成首次运行下载。
- **B 层的「知识」部分按计划推迟了**。现在实现的是**记忆**(货架 + 做梦账本 +
  四个工具);Herta 上游 `@herta/knowledge` 里还有一套 sqlite 知识库
  (canon / 自省提取 / 剧情摄入,数百行)。当初的分期决定是「先把
  `.herta/narrative/` 的文本货架打通,sqlite 留到 B2」。
  她的**身份**不需要它(那由 A 层的人格正本负责),所以不影响她现在的可用性;
  但如果你想要「她记得设定集里的细节」这类能力,那就是这块。

---

## ⚠️ 授权

插件代码(`src/`、`lib/`、`scripts/`、Cordis 配置)为本次改造新写,采用 MIT。

**语音资产(`assets/voice/`)与人设语料(`preset/herta.patch.yml`)不在 Herta 的
MIT 范围内**,权利归米哈游及各自所有者。本仓库已按《崩坏:星穹铁道》同人衍生作品
创作指引 V2.0 第三条放置法律声明后收录:**仅限非商业使用,且不得作为独立素材包再分发**。

法律声明原文见 [`NOTICE.md`](https://github.com/HerTa-st/Herta-dsh/blob/bab72f739855206e84cb967cef98e810cba1c733/NOTICE.md);上游授权范围与收录依据见
[`THIRD-PARTY.md`](https://github.com/HerTa-st/Herta-dsh/blob/bab72f739855206e84cb967cef98e810cba1c733/THIRD-PARTY.md)。

---

## 与 Herta 上游的关系

人格正本、格式门、新颖性判定、语音资产都来自 Herta(逐字或逐字移植,出处写在
各处注释里)。**做梦的蒸馏环节被换掉了**:原版是离线自主 pass(约 6700 行,用它
自己的 LLM 蒸馏候选),这里改由她在会话里写候选 —— 在 DSH 里模型就是她本人、
上下文就在眼前,再让宿主另起一次 LLM 调用去「蒸馏她自己」既贵又绕。
保留的是**晋升门**与**做梦账本**。

---

## 版本历史

### v0.1.11

**DSH 0.2.1 把 `cwd` 从内置提示词变量里删了** —— 黑塔 preset 的 persona 后缀里还挂着
`Your working directory is {{cwd}}.`,而新的插值是严格的:选黑塔 preset 的会话在发出任何
模型请求之前就抛错,整个会话起不来。除了删掉这一行,preset 底本也跟到了官方 0.2.1
(工具清单随上游),顺带把 MiMo 声音档接上线。

- **黑塔 preset 不再依赖 `{{cwd}}`**:那句后缀从 v0.1.5 起就在,一直靠 0.1.x 的
  `dsh-agent-loop` 注册 `cwd` 才没出事;0.2.1-rc.2 起它只注册 `provider` / `model`,
  未注册引用会让 `dsh-system-prompt` 在组装阶段直接抛错(官方 README 也是这么要求的)。
  症状很有迷惑性:标准 preset 没事、只有黑塔 preset 起不来,看起来像「插件坏了」。
  现在 suffix 里一个字都不再提工作目录 —— 目录信息由 0.2.1 的 working-directory 服务
  作为 runtime context 注入,信息没丢。
- **构建期兜底**:`scripts/build-preset.mjs` 新增 `assertPromptVariables()`,
  只放行 `provider` / `model`;以后谁再往 preset 里写未注册变量,构建当场失败,
  不会等运行到会话才炸。
- **preset 底本跟到官方 0.2.1-rc.2**(`scripts/baselines/` 新增逐字节导出):
  工具清单随上游 —— 新增 `time-context`(她终于知道当前时间了)、`tool-schedule`、
  `tool-plugin-market`(后两个跟官方一样 disabled),删掉 0.2.1 已移除的
  `tool-subagent-codex` / `tool-subagent-claude-code` / `tool-ralph` 行与
  `backgroundMode` 字段。`engines.dsh` 仍是 `>=0.1.7-rc.1 <0.3.0-0`。
- **MiMo 声音档接上了**(此前只有空壳):`createMimoAdapter` 改成真转接,宿主侧构造
  synthesizer(`MIMO_API_KEY` 每次现读、参考音读一次缓存),`mimo` 进
  `SPEAKING_ENGINES`。用不了的两种真原因分开报 —— `no_key` / `no_reference`,
  不再只有一句「未接线」。参考音要 **DataURL**、要归一化成 **24kHz 单声道**
  (实测不这么做服务端回 400);key 支持环境变量与 `C:/herta-ai/mimokey.txt` 文件兜底,
  参考音按上游 Herta 桌面版的实际落点去找(装过上游的人自带那份 174 秒素材)。
- **验证**:`npm test` 链 **31 组 1556 项全过、0 失败**;`npm run test:integration` 28 项全过;
  发布预检八项全绿。
- **仍未接 / 仍未决**:MiMo 缺 key 或参考音时只报原因、不会自己补;主题 `lib/opening/`
  的上游授权未决;「黑塔外观」的值存在浏览器 `localStorage`;`theme` / `deviceScene`
  两个字段 DSH 侧无消费方;SSE 没有断线重放。

### v0.1.10

**声音这条线一次收口** —— MiniMax 中转站可以配了、静音点了就闭嘴、语音工具条不再跟着滚;
外加两个「能跑但静默错」的真 bug:每一帧 tts 都在抛 `ReferenceError`(任何引擎都没声),
以及工具条那个静音键压根没接到音频链路上。

- **新功能:MiniMax 中转站可以配了**。四个可选字段 —— `minimaxBaseUrl`(地址)/
  `minimaxApi`(接口形状)/ `minimaxVoiceId`(音色 id)/ `minimaxModel`(模型名),
  **默认全空时与加它们之前逐字相同**(官方两条地址 + tag 认领 + `speech-2.8-hd`)。
  原先三处写死让中转站用户接不上:地址表没有配置入口;认领按 `voiceId.includes("b1a43133")`
  过滤(那个 tag 是作者账号上那个克隆的标记,中转站不可能有)→ 直接 `no_clone_key`
  终局失败;模型名是常量。更隐蔽的是 `available()` 要求「有 key **且** 已认领克隆」——
  中转站用户前一件为真、后一件永远为假,于是 router 在 `runAdapter()` 里就早退了,
  **静默回落本地、日志里连中转站的痕迹都没有**。
- **请求形状收进 `src/host/minimax/endpoint.ts`**:路径、请求体、响应解码、错误分类各一条记录,
  `api.ts` 只认这个 interface(加第三种形状不用碰 `synthesizePcm`)。官方与中转站的形状差异
  都是**实测事实**:路径 `/v1/t2a_v2` vs `/v1/tts/speech`;音色字段嵌套 vs **扁平**
  (嵌套写法实测回 502);回体 `data.audio`(hex)vs **裸 s16le PCM**,且回 PCM 时
  `content-type: audio/mpeg` 是**假的** —— 所以判形状按字节,不按头。
- **形状跟着地址定**(`shapeFor`):地址是官方那两条 → 官方形状;用户自己填的第三方 → 中转站形状。
  这是四个字段里**唯一一个「不改也能填完其余三格」**的,漏了它的症状是「看着连上了、每句都没声」
  —— 与音色无关,所以报上来像是「每个模型都连不上」。同一趟挖出并修掉三个真问题:
  `codeOf()` 靠中文字符串猜 code(音色 id 填错时只显示「失败」,新增 `SYNTH_CODES.voiceMissing`
  并把平台原因码留在中文里);`classifyStatus` 认不出没有平台码的错误(中转站回
  `{"error":{"message":"invalid api key"}}`,于是坏 key 既不显示也不进 `REFUSALS`,
  会拿着同一把坏 key 把剩下每句都试一遍);选了「中转站」形状却没填地址会「探通」一个
  根本发不出声的官方地址(404 在 `probeShape` 里算「地址活着」,那是给中转站地址定的规矩),
  现在直接报 `no_host` 并说清缺哪一格。
- **新增 `src/host/minimax/peak.ts`**:中转站回来的电平明显偏小(实测峰值 6231/32768),
  所以**只对明确偏轻的材料**(峰值 < 0.5)放大到 0.99,本来够响的原样返回。
- **静音点了就闭嘴**(用户报的):原先静音只被两道「下一次」的门读(排下一段 / 合成前),
  **已经排进 WebAudio 时间轴的照播不误** —— 听感就是「等她念完这句」。现在 `applyMuteNow()`
  做三件事:增益归零(对已在播的源一样有效)、停掉已排的源并清游标、给整机 iframe 推一条
  空 id 的 `ttsStop`(既有协议里「停全部」的写法)。解静音按当前音量重算,不是简单置 1。
  **没有** `suspend()` AudioContext —— 那要等一次 `resume`,而 `resume` 需要用户手势,
  静音一次反而把声音永久弄哑。触发点接在设置表单的 `subscribe` 上(只转发**真正变了**的字段,
  且首次不发通知 —— 页面刚打开、她正在念开场白时把声音掐掉是最招人骂的那种「修复」)。
- **工具条的静音键从来没接上音频链路**(用户第二次报的):它只改了一个组件内 `useState`
  (只喂给按钮标签、透明度、语气词判断),而真正拦声音的是设置里的 `voiceMuted`。
  现在这个键写的是同一个真闸门,并做双向镜像(设置页改的、整机 iframe 改的,工具条都跟着变)。
  `applyMuteNow()` 另记两条诊断(`minimaxMuteAppliedAt` / `minimaxGainNow`),
  把「调用压根没发生」与「调用了但听不见」分开。
- **语音工具条吸顶**:那四个键原来长在会话流里,上下文一长就滚上去(想关语音得先翻到最上面)。
  现在 `position: sticky` + 一层不透明底板 + `zIndex`。底板取 DSH 的 base 令牌、**不用
  `backdrop-filter`**:后者在这个壳里会强制新建合成层,滚动时和主题的背景图抢绘制(会抖)。
- **每一帧 tts 都在抛 `ReferenceError`**:`rememberSpokenAudio` / `awaitingSpokenTexts`
  在 `lib/client.js` 里**两个调用点、零个定义** —— 是 0.1.8 拆 region 时漏的一行 import
  (对照 0.1.7 产物:这个标识符从 3 次出现变 2 次)。症状是任何引擎的语音「点了没反应」,
  而且**连诊断标记都不留**(抛在 `markMinimax("minimaxAudioPlays", …)` 之前)。
- **新增四条测试**:`scripts/test-client-bundle-bindings.mjs`(issue #14 点名要的
  「打包产物级」判据:定义方必须 `export`、使用方必须 `import`、产物里必须有定义;
  摘掉那行 import 当场红 2 条)、`test-minimax-endpoint.mjs`、
  `test-minimax-relay-config.mjs`(假中转站 + 宿主真实装配的配置矩阵,接进 `test:minimax:e2e`)、
  `test-voice-mute-instant.mjs`。顺手修掉 `test-client-regions` 一直红着的那条
  (`./opus.js` → `./opus.ts`:`build.mjs` 的 jsToTs 认得,`client-test-hook.mjs` 不认)。
- **产物重建**:用 `Herta-src`(`build.mjs` 的默认来源)重建了 `lib/client.js`。
  与仓库里原来那份的差异只有两类:esbuild 的**源路径注释**(原先记的是本机已不存在的
  `../tools/_herta-src/…`)与上游 i18n 目录里多三条 **MiMo 引擎**的键
  (`voice.engine.mimo` / `voice.mimoKey` / `voice.mimoKeyDesc`,当前代码里没有调用点)。
- **验证**:`npm test` 链 **31 组 1544 项全过、0 失败**;`npm run test:integration` 28 项全过。
- **仍未接 / 仍未决**:`mimo` 合成器仍无调用点;主题 `lib/opening/` 上游授权未决;
  「黑塔外观」的值存在浏览器 `localStorage`;`theme` / `deviceScene` 两个字段 DSH 侧无消费方;
  SSE 没有断线重放(回复中途刷新会丢掉已推的帧)。

### v0.1.9

**设置页白屏修好了** —— v0.1.8 拆客户端时漏了三处 import,`设置 ▸ 黑塔` 那一页
打开是空的(本机与 npm 上那份都是)。这一版只有修复,没有新功能。

- **发布到 npm**:`dsh-herta@0.1.9`(N 个文件 / X MB)。
- **插件市场**:条目已收录,DSH 内置市场搜 `herta` 可一键安装。
- **这一版的主体**:**修 v0.1.8 的设置页白屏**。`b22ce9b`(#2「拆出 settings 层」)
  把代码搬进 `settings.ts` 时没把 `./machine.ts` / `./voice.ts` 的 import 带过来 ——
  `voiceModelState` / `miniMaxState` / `credentialStatus` 等 13 个名字成了自由标识符
  (`ui.ts` 同样漏 16 个、`voice.ts` 漏 1 个)。esbuild 不会为未绑定的标识符报错
  (它假设那是全局),所以构建期静默;设置快照变成 `ready`、渲染到语音那几行时抛
  `ReferenceError`,被 DSH 0.2.0 的**逐项错误边界**接住 —— 导航项还在(label 来自
  注册),内容区被降级成一个空的 `
`。
- **顺手修掉的**:无(这一版不放别的东西)。
- **验证**:`npm test` 链 **29 组 M 项全过、0 失败**;另有一条针对产物的探针(加载
  `lib/client.js` → 调注册进 `settings.section` 的组件 → 递归求值所有子组件):
  修前 3 处 `ReferenceError`,修后全部通过。
- **npm 上 0.1.8 已标记废弃**(`npm deprecate`):装 0.1.8 的请升到这一版。
- **仍未接**:同 v0.1.8 —— `mimo` 合成器仍无调用点;主题 `lib/opening/` 上游授权未决;
  「黑塔外观」的值存在浏览器 `localStorage`;`theme` / `deviceScene` 两个字段 DSH 侧无消费方。

### v0.1.8

**语音与界面各回各家** —— 架构审查的五个部分全部落地:四档语音引擎收成一个 interface,
`index.tsx`(3196 行)拆成五个零依赖的 region,取值域与叙述层的隐式 interface 各自收口;
顺手修掉三个真 bug,其中一个是「合成一失败,状态回执就断」。

- **语音合成器只剩一个 interface(candidate #1)**:新增 `src/host/synth-registry.js` ——
  四档引擎(`local` / `minimax` / `fish` / `mimo`)各成一个 adapter 工厂进一张表,
  选哪一档、失败要不要回落(**只有 `minimax → local`**)、取消转发给谁,全归一个 router;
  原先压在 `synthUnit` 里的 95 行 `if` 链退休(`synthUnitLegacy` 已删)。
  失败只给机器可读的 `code`(`no_key` / `network` / `refused` / …),中文在边界拼一次。
- **客户端 `index.tsx` 拆成四个 region(candidate #2)**:`machine.ts` / `voice.ts` /
  `ui.ts` / `settings.ts`,外加 `.opus` 播放那条路单独成 `opus.ts`(47 行);
  `index.tsx` 从 3196 行降到 364 行,只做装配。四个 region 之间**零依赖** ——
  改 PCM 播放不必再碰设置页的 diff。状态归它所在的层独占,外面只经显式入口
  (`bindMachineForm` / `stopVoiceModelTimer` —— 此前装配层直接给模块的变量赋值,
  拆分后 esbuild 会当场报 `Cannot assign to import`)。
- **测试从「抓源码文本」改成「穿过 interface」(candidate #3)**:`test-fish-proxy`
  的 11 条文本断言、`test-fish-key` 的几条,都改成读值或调用接口;
  `test-subagent-skip.mjs` 删除(它的三条断言由运行时测试覆盖)。
  新增 `scripts/test-client-regions.mjs` —— 靠 `scripts/client-test-hook.mjs` 把
  `react` / `react-dom/client` / `@gui/*` 指到最小桩,于是**未打包的客户端 module
  可以被裸 Node import**,客户端内部第一次有了可断言的行为。
- **引擎取值域收成一个声明(candidate #4)**:四档的合法值与可发声判定各自只写一处
  (`voice-engines.js`),设置页的选项、schema 的 enum、宿主的分发都从它派生;
  回落规则也变成一张表(表里没有的档就是不回落)。
- **叙述层的隐式 interface 收口(candidate #5)**:依赖通道(`host-deps.js`)与
  69 个诊断字段的总线(`host-marks.js`)拆开,子代理闸门收成一个 helper(三处调用)。
- **顺手修掉的三个真 bug**(都是「全面检查」抓出来的,不是五部分的内容):
  - **合成一失败,状态回执就断**:删旧代码时把紧邻的 `reasonText` 局部函数一起删了,
    而唯一调用点留着 —— 每次失败都抛 `ReferenceError`,被管线吞掉,于是试听没有回执、
    `engineNote` 不更新,fish 档(按设计不回落)彻底静音。已补回并加了断言。
  - **「没密钥」被静默说成「失败」**:`SYNTH_CODES` 漏了 `no_key` 这一项,
    `codeOf()` 那条判断求值成 `undefined`,被兜底逻辑吞成 `other`;
    现在每档自报「我为什么用不了」(`unavailableCode()`),没密钥就是 `no_key`。
  - **Fish 的文件兜底被掐死**:鱼档的可用性只认 DSH 凭据,于是把
    `fish_config.json` 里那条 keyFile 兜底也判成「不可用」;改问 `fish-tts.js` 自己。
- **取消现在真的取消得到人**:router 那句「转发给所有带 `cancel` 的 adapter」此前
  转发给零个(四档都没实现这个可选方法,而 minimax 合成器的 `cancel` 一直存在)——
  用户打断时在飞的云端请求不会被中断。现在接上了,并用**真工厂**加了断言
  (此前那组用的是假 adapter,只能证明「router 会转发」)。
- **`herta_say` 的引擎名不再写死 `minimax`**:成功返回的兜底改从当前档取,
  工具说明与 `output.engine` 的描述四档全列 —— 此前用 fish 或本地时它会报错身份(PR #12)。
- **新增 `scripts/test-synth-registry.mjs`**:router 的四档/回落/失败码第一次有行为覆盖。
- **验证**:`npm test` 链 **29 组 1446 项全过、0 失败**。
- **仍未接 / 仍未决**:`mimo` 合成器仍无调用点(选择器里点得动、会如实说明为什么不发声);
  主题 `lib/opening/` 的上游授权未决;「黑塔外观」那些值存在浏览器 `localStorage`,
  换机器不带走;`theme` / `deviceScene` 两个字段 DSH 侧无消费方。

### v0.1.7

**装完就有主题** —— 紫罗兰配色、开机 ASCII 开场、可换背景与「黑塔外观」设置页,
现在随插件一起装(主题包 `dsh-theme-herta@0.1.0` 成了本包的依赖,
`cordis.patch.yml` 多一行并列挂载)。另把「产物不再可手工修补」的构建收口做完,
并拆开叙述层的依赖通道与诊断总线。

- **自带主题**:三件事一次做完 —— `dependencies` 加 `dsh-theme-herta@^0.1.0`、
  `files` 加 `theme/`、`cordis.patch.yml` 里与 `id: herta` 并列插一条
  `id: ui-theme-herta`。loader 行**必须由本包给出**:主题包自己的 `cordis.patch.yml`
  不会被自动应用(loader 只从 profile 的 `dsh.profile.bundles` 里挑 bundle 的 patch),
  它只是本包的依赖、它的 patch 谁也不会去读。实测(dsh 0.2.0-rc.2):装载器能按名字
  解析到它、宿主半侧挂载成功;重复的 loader 条目 id 也只被覆盖、不炸启动。
  主题内容与**授权边界**见它的 [`NOTICE.md`](https://github.com/HerTa-st/dsh-theme-herta/blob/main/NOTICE.md)
  —— `lib/opening/` 那 10 个文件移植自 `PersonaCLI/Herta`,该上游未标注标准许可证。
- **主题接线后暴露的四处界面问题**:壁纸不再从左侧栏透出来(在那一栏下垫等价不透明底);
  「选项高光叠了两层」(侧栏底 × 选中行 × 通配悬停,三层半透明相乘)修掉;
  Windows 下品牌字不再被「收起侧边栏」按钮压住;会话里那条提示不再只剩 emoji 看得见
  (改取 DSH 的 `--dsw-alias-label-primary`,底与墨同源)。另把 `deploy.mjs` 的
  本地镜像集补上 `theme/`,否则 `file:` 安装「有主题却看不到主题」。
- **产物不再可手工修补(ADR-0003)**:esbuild **固定 0.25.12**(删掉候选列表,
  找不到就报错)、删除 8 个 `reapply-*.mjs`(靠字符串锚点给 bundle 打补丁,
  锚点对不上就静默漏改)、pre-commit 强制「`npm run build` 之后 `lib/` 无 diff」。
  代价写在 ADR 里:**没有 esbuild 的机器不能出产物**。
- **一个设置字段 = 改一个描述符(ADR-0004)**:展示元数据(`group` / `hint` /
  `enumLabels` / `widget`)住进字段描述符,`settings-groups.js` **整个删除**。
  此前加一个字段要同时改五处,漏一处就是渲染事故 —— 09-27「整组一行不渲染」与
  10-01「渲染两遍」两次都真发生过。测试从「两张名单对账」换成「单源自洽」+ 防复发断言。
- **叙述层:依赖通道与诊断总线拆开**:原先挤在同一个 `globalThis` 对象里(依赖
  `marks.ctx` / `marks.llm` 与 69 个诊断字段),外部无法区分谁写、何时就绪。
  现在拆成 `host-deps.js`(依赖通道)与 `host-marks.js`(诊断总线,那个对象只有这一处创建);
  顺带把 `{ kind: "plugin:dsh-herta" }` 收成唯一常量、设置命名空间收成单源。
- **测试不再 grep 源码,改为穿过 interface**:新增 `test-narrative-layer.mjs`
  (用假 ctx 记录 `on(event, fn)` 的清单与**顺序**,再真的调用那些 handler)、
  `test-http-json.mjs`(413 之后不再写第二份响应 —— 那是被 TCP RST 截掉的真事故)、
  `test-tar-extract.mjs`;`test-source-kind.mjs` 重写成断言常量形状;
  `test-subagent-skip.mjs` **删除**(三条断言已由运行时测试覆盖)。
- **仍未接**:主题 `lib/opening/` 的上游授权未决;外观设置存在浏览器本地
  (换机器不带走);MiMo 合成器仍无调用点;`theme` / `deviceScene` 两个字段仍未接线;
  本地合成常驻化未落地。
- 测试:**核心 26 组 1020 项 + MiniMax 6 组 390 项 = 1410 项全过、0 失败**。

### v0.1.6

**修掉「装完起不来」** —— 0.1.5 在宿主不给 `@deepseek-ai/schemastery` 时会整个插件导入失败
(`dsh: warning: 1 entry did not activate herta (dsh-herta): failed to import`);
这一版把入口改成动态导入加兜底,另把「只改产物、源码没跟上」的双向漂移收口。

- **入口不再硬依赖运行时的 schemastery**:入口第 22 行原先是**静态导入** `@deepseek-ai/schemastery`,
  而本包 `dependencies` / `peerDependencies` 全空 —— 它指望 DSH 运行时把那个包递过来。
  运行时不给时,**模块在解析阶段就抛**,静态导入又没法用 try/catch 兜,于是整个插件起不来。
  新增 `src/host/schema-compat.js` 做**动态导入 + 兜底**:
  - 拿得到真库 → 行为与从前**完全一致**;
  - 拿不到 → 退到一个**宽容**的最小实现(覆盖用到的 `object/string/number/boolean/union`
    与 `default/min/max/volatile`,未知方法名也返回可链式调用,且**保证不是 thenable**,
    免得被 `await` 挂住)。它**不做真校验** —— 这比「整个插件起不来」好。
  - `settings-schema.js` 的 `FIELDS`(字段名与默认值)不受影响,仍是唯一真相。
- **产物双向漂移收口**:`lib/client.js` 是打包产物、真源是 `src/client/index.tsx`;此前几批体检
  (`reapply-ux-texts*.mjs`,**已于 2026-10-03 整体删除**,见 ADR-0003)**只改了产物、没回填源码**,
  于是直接 `npm run build` 会**静默回退
  8 处已经生效的界面文案**。本次把 8 处按脚本记录的原文回填进源码后重建(措辞收口:不再断言
  「没有任何代码读它们」、MiMo 密钥行的实话、网盘行补「不含语音模型」、下载失败带上原因、
  语音状态行显示「已计费 N 字」等),并给 `voice` / `pipeline` 的内联类型补上
  `billedCharsTotal` / `lastCap`。
- **回归守卫**:新增 `test-schema-optional.mjs`(挂进 `npm test`)—— 只给 `dsh-tools` / `dsh-llm`
  造替身、**故意不给 schemastery**,要求入口能加载且走的是兜底;`test-schema-optional` 因此
  **必须不带** `--import` hook(挂了 hook 等于把真库递进去,测的就不是兜底了)。
- **发版工程**:新增 `RELEASING.md` 与 `scripts/preflight-release.mjs`(发布前八项预检 +
  `--verify-published` 发布后核验)与 `docs/templates/` 三份文案模板;预检**照 `package.json`
  的 `test` 链**跑测试,不再写死清单。
- 测试:**核心 24 组 913 项 + MiniMax 6 组 390 项 = 1303 项全过、0 失败**。
  ⚠️ **升级提示**:npm 上 0.1.5 是坏的(装完插件不激活),请直接升到 0.1.6。

### v0.1.5

**让她真的出声,并且每一处「没声」都说得清原因** —— Fish 引擎补齐成可用的一档,
两条静默路径修掉,叙述层不再对子代理说话,设置页按体检结果收口。

- **Fish 引擎(`voiceEngine: fish`)**:与 MiniMax 并存的一档。
  - 「Fish 密钥」走 DSH 凭据缝(`FISH_API_KEY`),`C:/herta-ai/fish_key.txt` 退化成兜底;
  - 「Fish 代理」是**可留空**的一行:接口按可达性排序 —— `fishaudio.org`(官方现用域名,
    国内可直连)优先,`api.fish.audio`(旧域名,国内会被按 SNI 重置)兜底。
    ⚠️ **音色 id 跟域名绑定**:默认大黑塔用的是新域名下的 id,旧 id 在新域名上会返回
    `ERR_VOICE_NOT_FOUND`;
  - 计费字符数收下并显示到语音状态行(「这一档花了多少」有答案了);
  - 代理不再写死本机端口,空串也不会冲掉 `fish_config.json` 里的值
    (那正是「填了密钥也不出声」的根因)。
- **两条静默路径修掉**:围栏之外的字算说话(「正常发言 + 内心话」不再被整段吞掉);
  空轮护栏 —— 一轮什么都没说时注入一条看得见的通知(生产上已实测到它唯一的一次触发)。
- **叙述层对子代理让过路**:分拍 / 复核 / 空轮护栏一律不拦子代理 —— 三处钩子都问在动作之前,
  每次跳过都留痕。
- **设置页体检两批**:记忆能取下、失败说人话、做梦账本不丢、模型下载失败说