Herta-dsh
yunmengyuan/herta-dsh
Installe Herta comme agent DeepSeek Harness : un preset portant ses sections de prompt de persona et de mémoire, six outils de mémoire et de voix, une couche narrative qui sépare sa réflexion de ce qu'elle dit, 80 clips vocaux fournis, la synthèse vocale hors ligne et MiniMax, et deux vues de conversation qui rendent la session avec ses propres composants.
Installer
dsh plugin --profile web add github:yunmengyuan/herta-dshREADME
dsh-herta
把 Herta(黑塔) 作为一个插件装进 DeepSeek Harness: 她的人格、她的记忆、她的声音、她的界面。
📌 来源声明
本项目是基于原作者项目所做的第三方改造,不是原创作品。
- 原作者项目:Herta —— THE SELF THAT USES THE AGENT
- 原作者官网:https://www.herta-ai.com/#research
- 本项目把 Herta 作为一个插件接进 DeepSeek Harness —— 人格、记忆、语音、界面四层 都来自 Herta 原作;插件侧的代码(
src/、scripts/、Cordis 配置)为本次改造新写。本项目由第三方独立开发,与 Herta 原作者无任何隶属、合作或背书关系。 如原作者认为本改造有不妥之处,请联系我,我会立即调整或撤下。
另:「Herta / 黑塔」是 HoYoverse《崩坏:星穹铁道》中的角色。Herta 原作者项目本身 即为非官方同人作品,本项目同样是非官方同人作品,与 HoYoverse / miHoYo / Cognosphere 无关联、未获其背书或赞助。
素材授权细节见
THIRD-PARTY.md。
🎨 相关仓库:主题
界面配色、开机 ASCII 开场、可换背景与「外观」设置页,是另一个包:
dsh-theme-herta —— 也可以单独安装
(没装本插件的人,照样能只下主题)。
0.1.7 起本插件自带它。 接线三件事都已落地:主题已发到 npm (
dsh-theme-herta@0.1.0)、本包把它列成依赖、cordis.patch.yml里多了一行 并列的ui-theme-herta。所以装了本插件就一起拿到主题,不用再单独装; 装完照例要关窗重开(bundle 是启动期挂载的)。主题包自己的
cordis.patch.yml不会被自动应用 —— loader 只读 profile 的dsh.profile.bundles里那些包的 patch,所以挂载点必须由本包给出。本仓库里也放了同一份源码(
theme/dsh-theme-herta/)。⚠️ 主题的NOTICE.md记着一条未决的授权边界:lib/opening/那 10 个文件移植自PersonaCLI/Herta,而那个上游未标注标准许可证(GitHub 读作NOASSERTION)—— 用之前请自己与上游确认。
四层,各自独立可验:
| 层 | 内容 | 落在哪 |
|---|---|---|
| A 身份 | HertaBio.txt 逐字作人格前缀 + 为 DSH 改写的处境/纪律 | agent preset |
| B 记忆 | 记忆货架(.herta/narrative/)作动态提示词段;四个记忆/做梦工具 | preset 行 |
| C 语音 | 80 条她本人的 .opus(开场白 / 语气词 / 自我收回 / 彩蛋)+ 发声工具 | 静态路由 + client |
| D 界面 | 乙:用她的展示组件渲染 DSH 会话;甲:iframe 装下她的整机 | conversation.view ×2 |
⚙️ 兼容性(先看这一节)
面向 DSH 0.1.7-rc.2。 本版本修的正是 0.1.5 → 0.1.7 之间三处破坏性 API 变更
(都是实测出来的,不是猜的):
| 变了什么 | 0.1.5 时的写法 | 0.1.7 的现状 | 本仓库怎么办 |
|---|---|---|---|
| agent preset 的载体 | $DSH_HOME/.agent-presets/<name>/agent.cordis.yml(一整棵 cordis 树)+ preset.yml | 该目录机制整个移除(运行时里已无任何代码引用 .agent-presets);preset 变成一条 @deepseek-ai/dsh-agent-preset loader 行,官方写成 dsh-web-app/presets/*.patch.yml | 生成 preset/herta.patch.yml,作为第二条 bundle patch 随插件一起装(dsh.bundle.patch 现在可以是数组) |
| 用户偏好的存放(设置域) | 宿主 ctx.settings.register(ns, schema) + 客户端 settingsScope.bind({namespace}) | 两者一起消失:SettingsProvider/SettingsScope/SettingsRegisterOptions 不再导出,客户端 settingsScope 服务不存在,事件 settings/updated、settings/document-updated 也没了;换成基于插件 Config 的 SettingsForms/configForms,没有第三方命名空间入口 | 用后者:插件自己的 volatile Config(落 profile 的 cordis.patch.yml),客户端挂 settings.section 一页。密钥另走凭据缝 ctx.remote.credentials(值进 $DSH_HOME/.credentials.yaml,不进明文配置)。2026-09-26 起黑塔的设置只有这一处 |
| 消息来源的 kind(会话格式 v4) | { kind: "plugin", plugin: "dsh-herta" } | v4 只认「生产者自有 kind」,kind: "plugin" 在写入会话时就被 assertV4SourceRowAdmission 拒绝(format v4 message requires a producer-owned source kind);第三方插件的合法形状是 { kind: "plugin:<包名>" } —— 这正是 DSH 自己的 v3→v4 迁移为旧行推导出的形状,两代读回同一个 kind | 全部改成 { kind: "plugin:dsh-herta" }(agent.steer 两处 + 两个 PLUGIN_SOURCE);新增 scripts/test-source-kind.mjs 钉住这条不变量 |
⚠️ 第三处症状最难定位:被拒的事件根本没进会话日志(写入前就抛了), 事后翻
session.v4.jsonl.zstd是干净的,只有 GUI 上显示「本轮运行失败」。 实测就是这么踩的 —— 详见 v0.1.3 版本历史。
其余用到的 API 在 0.1.7 上没变,实测可用:ctx.systemPrompt.section({name,order,text})、
ctx.tools.register(defineTool(...))、ctx.inject([...])、ctx.slots.inject/register、
ctx.uiConversation.binding(id).target('chat')、
ctx.on('agent/turn-stopping' | 'agent/error' | 'tools/result')、
@deepseek-ai/dsh-llm 的 BlockAssembler / createUserMessage、
webServer.register({ kind: 'prefix' })。
升级 DSH 后先跑这三条:
dsh --profile <p> --dump-config→ 应有- id: preset-herta,且其config.plugins末尾有plane: preset- 启动日志 → 应有
plane=host、plane=preset、叙述层依赖就绪、设置命名空间已就绪:herta(自带页面,不自动生成)、整机页面已挂:/herta-ui, 不应出现settings.register is not a function,也不应再出现从整机迁移设置:…(种子已删除)或设置写回已挂(写回已删除)npm test→ 1544 项(31 组);npm run test:integration→ 28 项
📌 素材权利声明(clone 后先看这一节)
本仓库包含《崩坏:星穹铁道》的角色素材,这些素材的权利不属于本仓库作者:
© 米哈游版权所有
【《崩坏:星穹铁道》素材的权利归米哈游所有,其他内容的相关权利、利益均归各自所有者享有】
| 素材 | 干什么用的 |
|---|---|
assets/voice/(80 条 .opus,2.55 MB) | C 层语音:开场白 / 语气词 / 自我收回 / 彩蛋 |
preset/herta.patch.yml(42 KB) | A 层人格正本(内含 HertaBio.txt 逐字,含引用台词) |
icon.png(384×384,173 KB) | 插件在 DSH 插件管理页里的图标(取自 Herta 上游桌面应用,缩放重编码以符合 DSH 的 ≤256 KiB 约束) |
上述声明依据米哈游官方 《崩坏:星穹铁道》同人衍生作品创作指引 V2.0(2024-04-18 生效)第三条放置。 素材权利未转让,收录不构成授权,使用仅限非商业用途, 且不得作为独立素材包再分发(官方指引将「纯搬运」排除在二创许可之外)。
完整声明、依据、使用者义务与移除方式见 NOTICE.md;
Herta 上游项目自身的授权范围见 THIRD-PARTY.md。
clone 后开箱即用,无需自备素材。
装
插件市场 / npm
已收录:条目在上游精选目录
awesome-dsh-plugin(PR
#5946,2026-09-29 合并)。
所以 DSH 内置的**插件市场(dshmarket)**里搜 herta 就能找到「Herta-dsh」并一键安装 ——
2026-10-01 实测:本机市场的目录(/dsh-market/registry)里已经有这一条,added: 2026-09-29。
📌 目录与版本是两条链:目录(简介、分类、下载量、能力扫描)由上游 CI 每日重建, 而装到哪个版本以 npm 的
latest为准 —— 市场里「有更新」的判据也是 npm 的latestdist-tag(dshmarket/lib/updates.js)。所以新版本发到 npm 之后用户立刻能装到, 目录卡片上的版本号要等下一次重建才跟上。⚠️ 市场搜索的候选集只有那份上游目录(2026-10-01 实测 4400 条),不含本机已装 的插件,也没有「自定义目录源」这个用户入口。所以
file:装进来的插件不会出现在搜索结果 里 —— 要能被搜到,就必须先被收录。条目里那条
install: dsh plugin --profile web add dsh-herta是上游 CI 按模板生成的通用值 (条目文件里作者只写url/name/category/description四项);桌面应用要把 profile 换成desktop。
也可以直接装 npm 包(dsh-herta,0.1.4 起有):
dsh plugin --profile desktop add dsh-herta
桌面应用(0.1.7-rc.2)
桌面应用的 DSH 运行时打包在 resources/app.asar 里 —— 那是 Electron 的归档格式,
普通 Node 读不到,所以 install-web.mjs 驱动不了它。走应用内置的插件管理器:
plugin_manager install_bundle file:<本仓库绝对路径>
# 然后关掉桌面应用窗口,重新打开
或者在有独立 DSH 安装(不是 asar 版)时用命令行:
dsh plugin --profile desktop add file:<本仓库绝对路径>
独立 DSH 安装 / lab(脚本可全程驱动)
$env:DSH_BIN = "<…>\node_modules\@deepseek-ai\dsh\lib\bin.js" # 必须能被普通 Node 启动
$env:DSH_HOME = "<目标 home>" # 可选
node scripts\install-web.mjs --dry-run # 只读:先看它会做什么
node scripts\install-web.mjs --profile desktop
# 然后关掉桌面应用窗口,重新打开
脚本做四件事:前置检查 → 备份 → 装(dsh plugin add,失败则走确定性兜底)
→ --dump-config 自校验(herta 行必须恰好一条、preset-herta 必须进合成结果、
其他已装插件不能消失)。回滚:--rollback。
preset 不再需要单独安装。 0.1.7 起它是本包自带的第二条 bundle patch
(preset/herta.patch.yml,见 package.json 的 dsh.bundle.patch 数组),
plugin add 会一并生效 —— 所以这个脚本里没有「拷 preset 到 $DSH_HOME」那一步了,
也不再有「preset 属于整个 home」那个副作用。
必须关窗重开:dsh.profile.bundles 的变更是启动期合成;桌面应用没有子进程守护,
直接杀掉 dsh web 不会自动拉起。
出问题回滚:node scripts\install-web.mjs --rollback(之后同样关窗重开)。
回滚按 home + profile 过滤备份(每份备份带 TARGET.json),不是取"全局最新"——
同一台机器上装过多个 profile 时,取最新会把别人的配置还原到这边来。
该路径已实测:还原后 bundles 与 dependencies 回到安装前、找不到匹配备份时
明确报错而不是乱还原。
⚠️ 桌面应用内置的插件管理器没有回滚。要改它真正在用的那个 profile 之前,
先手工备份 profiles/<name>/package.json 与 cordis.patch.yml。
装完之后,怎么让她出声
装好 ≠ 会说话。 默认引擎是「本地模型」,而模型要另行下载;不做下面这几步,她会 一个字都不念。以前这里连原因都不显示(2026-09-30 体检后已修:失败原因会写在 设置页那一行上)。五步:
- 打开设置:DSH 设置 ▸ 黑塔 ▸ 「语音」组。
- 二选一(要么下模型、要么填密钥):
- 本地模型(不花钱、不联网):到「整机动作」组点「本地语音模型」的下载,
约 72.7 MiB,装到
$DSH_HOME/tts/herta-best-e72;然后在「语音引擎」里选本地模型。只有 GitHub 这一个源(被墙时那个「安装包网盘」里没有模型)。 - 云端:「密钥」组里填 MiniMax 或 Fish 的密钥,再把「语音引擎」切到那一档。 ⚠️ MiniMax 只「认领」你账号上已有的克隆音色,不做上传/克隆 —— 新账号上还没有 那个克隆时,填了密钥也不会出声。
- 本地模型(不花钱、不联网):到「整机动作」组点「本地语音模型」的下载,
约 72.7 MiB,装到
- 点「试听」那一行:有声就说明这条链通了。没声就看同一行的状态行 —— 它现在会说 原因(没有密钥 / 模型没装 / 连不上且没配代理 / 认领失败…)。
- Fish 引擎要能连上接口:插件按
fishaudio.org(官方现在用的域名,国内可直连)→api.fish.audio(旧域名,国内会被按 SNI 重置,只作海外兜底)的顺序试。 两条都不通时才需要在「Fish 语音」组填 「Fish 代理」(例如http://127.0.0.1:7897), 或者设HTTPS_PROXY环境变量;留空 = 直连。都失败时状态行会直说原因。 ⚠️ 音色 id 跟域名绑定:默认的大黑塔属于fishaudio.org,id 是36e4d5f5-7654-43d4-b160-a1f15398116f(旧域名那个f9ede038…在新域名下会返回ERR_VOICE_NOT_FOUND)。手动换域名时,「Fish 音色」那一行要跟着换。 - 不想花钱 / 不想被吵到:「语音」组里
- 实时语音关掉 = 不再自动念回复(省钱;
herta_say与试听不受它管); - 静音只决定听不听得见,不决定要不要花钱合成(合成照旧发生)。
- 实时语音关掉 = 不再自动念回复(省钱;
架构
这个包同时挂在两个平面上,两件事不同
| profile bundle 行(宿主面) | preset 行(agent 面) | |
|---|---|---|
| 干什么 | 让 client 半侧进浏览器启动图 + 挂四条路由(语音 / 偏好 / 模型 / 整机) | 注册她的提示词段与五个工具 |
| 为什么不换 | dsh-client-modules 只扫 loader.entries(),preset 子树不在其中 | 放宿主面会把工具泄漏给所有会话 |
| 怎么区分 | 行上没有 config | 行上带 config: { plane: preset } |
| 住在哪 | 包自带的 cordis.patch.yml | 包自带的 preset/herta.patch.yml(0.1.7 起 preset 就是一条普通 loader 行;旧版的 $DSH_HOME/.agent-presets/ 目录机制已被 DSH 移除) |
两个实例的 apply 都会跑(模块只求值一次,fiber 是两个),所以模块级可变状态
必须与平面无关或按 key 索引 —— 缓存按 cwd 索引就是这个原因。
preset 行现在启动期就挂(0.1.7 实测:冷启动日志里 plane=host 与 plane=preset
一起出现)—— 旧版 0.1.5 是懒挂载,要等第一个属于该 preset 的会话才挂。
两种都正常,但别拿「没看到 plane=preset」当插件没装上的证据,以 --dump-config
为准。
两条界面路线
乙(conversation.view id herta)—— 用她的展示组件渲染 DSH 会话。数据走
ctx.uiConversation.binding(sessionId).target('chat'),组件拿到的是 DSH 的
ConversationNode[],映射成她的气泡。样式挂进 shadow root:她原版样式表的
:root 变量在 shadow 里不匹配任何元素,所以构建期把 :root 转成 :host、
删掉整页 chrome(见 scripts/build.mjs)。
甲(conversation.view id herta-full,页签显示「黑塔·整机」)—— iframe 装下她的整机(/herta-ui/)。
iframe 是独立文档,所以她的原版样式原样使用(:root / body 整页规则在这里
正好是对的)。数据经 postMessage bridge 从父窗口取;bridge 会把每个调用的最后一条
载荷缓存下来补给新订阅者 —— 不这么做,早期那次 reset 会在渲染层订阅之前丢掉,
现象是「界面起来了但会话是空的,而且不会恢复」。
整机页要去掉两处「宿主已经有的 chrome」(都落在薄层 src/herta-ui/herta-ui.css,
两条规则,上游 Herta-src/ 一行没动):
| 去掉的 | 为什么 |
|---|---|
.composer(她自己的输入框) | DSH 底部本来就有一整套输入区,两套并存;且她那套走 bridge.submitText,而 bridge 没实现这个方法,发出去的话只会丢在地上 |
.window-controls(右上角最小化/最大化/关闭) | 三个都是死按钮 —— bridge 把它们全实现成空函数(iframe 里也没有「窗口」可最小化,该最小化的是外层 DSH 窗口) |
两处都只圈元素自己的 class、不碰父级,所以布局自动让位:.workspace 的
grid-template-rows:1fr auto 里那个 auto 行会跟着子项自己塌,对话区直接长满
(实测 452px → 512px),不留空档也不出双重滚动条。
叙述调度层(她的「自我叙述」)
她的原版里有一层演员调度:说什么、怎么分拍、什么时候自我收回、开口之前先想什么。
这一层在上游骑在她自己的编码后端上(packages/herta/src/narrative/,46 个模块
1.29 MB)。本插件不移植那个后端(DSH 就是后端),只把调度逻辑接到
DSH 的 agent loop 上。
完整设计(已核实的 DSH API、机制映射、踩过的坑)见
docs/叙述调度层设计.md。要点:
她的叙述语法
(我 想)……(/我 想) 思考 —— 内心判断,不进用户视野
(我 说)……(/我 说) 说话 —— 真正说给开拓者的话
四组围栏在 zh/en 两种语言下都是中文(上游 thought-hint.ts:16-18:它们是语法
记号,不是指导语)。src/host/narrative-hints.js 把这套语法与上游的提示词资产
逐字移植进来(含 supervisor 否决模板、rethink/respeak 两阶段、三种分拍提示)。
四种行为,各自落在 DSH 的哪个钩子上
| 行为 | 落点 | 要 LLM 调用? |
|---|---|---|
| 分拍(干活中途补一句点评) | tools/result → 判据 → agent.steer | 否 |
| 自我收回 / supervisor 复核 | agent/turn-stopping(turn 关闭前被 await)→ 独立复核 → steer 让她重说 | 是 |
| thought tag | 输出里的围栏,由渲染层(乙 / 甲)呈现 | 否 |
| 做梦蒸馏 | herta_dream 的 distill: true → 宿主另起调用蒸馏候选 → 过门 → 落账 | 是 |
三条安全底线(缺一条都会出真问题):任何失败一律放行(复核坏了不该让她说不出话)/
配额到顶一律放行(steer 会让 turn 继续,持续否决她将永远说不完)/
拿不到模型路由就跳过(provider/model 是必填,不猜)。
一个必须知道的语义差距:DSH 的
agent.steer在下一个 step 边界生效, 不像上游能在后端事件发生当拍插话。所以分拍是「事件后一个 step 补评」—— 效果等价,时机晚一拍。
六条路由
前两条是白名单静态资源:启动时扫出文件索引,请求路径必须命中,否则 404。
不存在路径穿越的可能,也就不需要 ../ 过滤这类容易写错的代码。
/herta-voice—— 80 条语音 + 代码生成的index.json/herta-ui—— 整机页面(html / js / css / 开场段 / pdf worker)
后三条不是静态资源,是宿主自持的读写端点:
/herta-voice-model—— 本地语音模型(离线 TTS)的下载。GET状态、POST {"action":"download"|"cancel"|"remove"}点火即返回, 进度靠轮询GET拿。状态码与phase都用上游那套 (absent/downloading/ready/failed,失败带error键)。 详见下一节。/herta-minimax-events—— SSE:MiniMax 云端语音的 PCM 推给浏览器半侧。 三种帧:tts(一个单元的Int16PCM,base64)/ttsStop/state。 浏览器半侧转成push("voice", …)交给整机 iframe(iframe 缺席时自己在父窗口用 WebAudio 放)。走自建 SSE 而不是 DSH 的事件通道,理由见下一条注释。/herta-minimax-state—— 认领状态 + 手动动作:GET快照,POST {"action":"adopt"|"reset"}(设置页「重新认领」按钮)。
为什么 PCM 不走 DSH 的宿主→客户端事件:那条路是
ctx.remote.$on,事件名在@deepseek-ai/dsh-api-remotes里是硬编码白名单(第三方插件没有扩展点), 而且过线前要过isJsonValue——Int16Array一律被拒(不是真数组、原型也不是Object.prototype)。所以 PCM 只能走插件自持的 HTTP 路由,SSE 是dsh-client-hmr已经在用、本仓库已验过的形状。
/herta-settings已删除(0.1.7 那版曾用它自持语音偏好)。现在客户端直接走ctx.configForms,再留一条 HTTP 写入口就是第二个真相来源。密钥也不走 HTTP —— 它们走 DSH 官方的凭据缝ctx.remote.credentials。
本地语音模型(离线 TTS 的模型那半)
DSH 设置 ▸ 黑塔 ▸ 整机动作 里的「下载」现在是真的:宿主会去上游发布的地址取那一个归档。
固定参数从上游扒来,钉在代码里(src/host/tts-release.js)——不是运行时问服务端:
| 项 | 值 | 出处 |
|---|---|---|
| 归档 | herta-best-e72.tar.gz | Herta-src/…/tts/tts-release.ts |
| 地址 | https://github.com/PersonaCLI/Herta/releases/download/voice-herta-best-e72/herta-best-e72.tar.gz | 同上 |
| 体积 | 76,255,506 B(72.7 MiB) | 同上 |
| SHA-256 | ce993a6fab911e9a86328facc952120c21de54303652f648e96e9d14ee4f172e | 同上 |
| 解包后 | 115,897,197 B(110.5 MiB) | 同上 |
| 装到哪 | $DSH_HOME/tts/herta-best-e72 | 上游是 <userData>/tts,这里跟 DSH 的 home |
上游那段注释说明了为什么要钉:「A retrain is a new bundle id …, a new archive, new pins, and therefore an app release: the download never trusts the host, only this file.」 本插件不重打包也不转发这个归档,只是按上游发布的地址去取。
四段,任何一段失败都不会留下半个可用的 bundle(src/host/voice-model.js):
- 边下边算 SHA-256;先比字节数、再比哈希,任一不符即中止
- 解到最终目录旁边的
.installing/,带解压炸弹上限(只看普通文件与目录,拒绝路径穿越) - 拿 bundle 自带的
manifest.json逐个文件比 size + SHA-256,还要比release是不是这一版期望的 - 只有前三段全过,才
rename就位 —— 读到的要么是旧的完整版,要么是新的完整版
已经装好的旧 bundle 在任何失败下都活着(只在第 4 步被替换)。
✅ 运行时已随包分发(
assets/tts-runtime/,22 MB,sherpa-onnx 1.13.6 + onnxruntime 1.27.1)。宿主会真探测它(拉子进程把 addon 加载起来拿版本号, 结果进程内缓存),探测通过才报runtime: true—— DSH 设置页里「本地语音模型」 那行按它如实报「运行时未就绪」,写死 true 就是仓库别处修过的假绿。合成本身也实测过:
scripts/test-tts-runtime.mjs用真实模型合成 「你好。我是黑塔,天才俱乐部第八十三号。」→ 24 kHz / 5.08 s / 非静音 (峰值 25209、平均 2319),4.3 s 出结果。合成跑在子进程里 (src/host/tts-worker.cjs):sherpa 的 espeak 构建在 Windows 上处理不了 非 ASCII 绝对路径,而本机路径里就有中文 —— workerchdir到模型根再传相对 路径,与上游同一招;顺带也避免了同步阻塞宿主、原生件崩溃拖死宿主。⚠️ 还差最后一段:把她的回复自动念出来(触发 + 播放)还没接。 也就是说
runtime与bundle都是真的、引擎确实能合成,但「实时语音」开关 打开后她暂时不会自己开口。要接的是:回复文本 → 合成 → 推给 iframe 播放。
⚠️ 本机 TLS 提示。 这台机器对 GitHub 有中间拦截,普通 Node 的
fetch会直接fetch failed,加--use-system-ca才通。DSH 宿主是 Electron 拉起的普通 Node 子进程,所以要给宿主加NODE_OPTIONS=--use-system-ca再启动;下载失败时插件 会把这条提示写进日志(而不是只回一个network)。
六个工具
| 工具 | 作用 |
|---|---|
herta_narrative_list | 列出记忆货架,并如实报告哪份进了当前提示词、哪份被门拦下 |
herta_narrative_read | 读某一份的完整正文 |
herta_memory_save | 随手记一笔(过格式门 + 标题新颖性) |
herta_dream | 做梦:门槛更高(另加篇幅下限),并记进做梦账本 |
herta_speak | 让模型能主动发声;片段信息经 presentationMeta 落到工具结果的 meta 上,浏览器侧读取后播放。放的是她的录音片段(.opus),不经过任何合成 |
herta_say | 用她自己的克隆声音说一句话(MiniMax 云端合成,走 SSE 推给界面)。既是调试入口,也是语音链路的验收通道 —— 端到端哑掉时,先用它把「合成」与「推给界面」两段分开看 |
三道门
记忆内容会进她的系统提示词,所以写入必须过滤:
- 格式门 —— 与读取端同一道(逐字移植 Herta 的
few-shot-guard):对话栅栏必须 平衡、不嵌套、到 EOF 全关;首行必须是### 废案/### 记录头;单个文件不超过 1 万估算 token。检查的是结构不是词汇 —— 上游第一版用词表判断,把全部 18 份 活样本静默丢了 25 天。 - 标题新颖性 —— 逐字移植
novelty.ts(含「文件名消毒后同名」那个坑与连载篇豁免)。 - token 预算 —— 实测她真实货架折合 47445 tokens,全部注入不可接受。默认预算 12000,按「最新优先」挑选,但拼装时排回升序(拼接顺序与挑选顺序无关,否则 同样内容会产出不同前缀、打散 KV 缓存)。
开发
# 构建脚本需要一份「解开目录」的 DSH 安装(桌面应用的包在 app.asar 里,读不到)
$env:DSH_PACKAGES = "<…>\node_modules\@deepseek-ai"
node scripts\build.mjs # client 半侧(esbuild + 模块加载器包装)
node scripts\build-preset.mjs # agent preset 补丁层(以随附 standard 为底,只换 persona 行)
node scripts\build-herta-ui.mjs # 整机页面
node scripts\deploy.mjs # 三样都构建 + 镜像进 lab profile
node scripts\test-narrative.mjs # 货架逻辑(31 项)
node scripts\test-dream.mjs # 做梦逻辑(28 项)
node scripts\test-mapping.mjs # DSH↔Herta 映射(41 项)
node scripts\test-narrative-hints.mjs # 叙述语法与提示词资产(54 项)
node scripts\test-supervisor.mjs # 复核:判决解析 / 路由 / 否决配额(81 项)
node scripts\test-session-surface.mjs # 会话表面提取:候选回话 / 摘要 / turn(32 项)
node scripts\test-beat-policy.mjs # 分拍判据与配额(61 项)
node scripts\test-dream-distill.mjs # 蒸馏提示构造与解析(55 项)
node scripts\test-mimo-tts.mjs # MiMo 合成请求构造(40 项)
node scripts\test-voice-settings.mjs # 语音偏好的清洗 / 合并(58 项)
node scripts\test-voice-model.mjs # 模型下载:tar 解析 / 校验 / 状态机(50 项)
可选:用真实数据再跑一遍管线(需要本机已经有一份真的 bundle —— Herta 桌面应用 在「设置 → 语音」里下过模型的话就有):
$env:HERTA_TTS_REAL_BUNDLE = "$env:APPDATA\Herta\tts\herta-best-e72"
node scripts\test-voice-model-real.mjs
它把那份真实的 367 个文件 / 116 MB 自己打成 tar.gz、起一个本机 HTTP 服务、
再走完整的下载 → 校验 → 解包 → 再校验 → 换入,最后逐文件对账。
小归档验不了的东西(真实 manifest.json 的 366 个真 SHA-256、真实的
espeak-ng-data 目录树、真实长路径)都在这里过一遍。默认不跑,因为它依赖机器上
已有的那份模型。
可选:验证本地 TTS 运行时真能出声(同样需要一份真实模型):
$env:HERTA_TTS_MODEL_ROOT = "$env:APPDATA\Herta\tts\herta-best-e72"
node scripts\test-tts-runtime.mjs
22 项:运行时能被加载(拿到 sherpa-onnx 版本号)→ 真合成出 24 kHz 非静音音频 → WAV 头/采样数/字节数自洽 → 波形有起伏(不是一条平线)。
npm test 跑 31 组(核心测试链 + MiniMax 那一组,共 1544 项);另有 LLM 路径的集成测试
(28 项,用 mock 的 ctx.llm 把管道整条跑通):
npm run test:integration
# 等价于 node --import ./scripts/test-resolve-hook.mjs scripts/test-llm-integration.mjs
集成测试为什么需要那个 hook:本仓库刻意不带
node_modules,而supervisor-llm.js/dream-distill-llm.js静态 import@deepseek-ai/dsh-llm, 在仓库里直接 import 会ERR_MODULE_NOT_FOUND(在测试目录放软链接也没用 —— ESM 从被导入文件的位置解析)。hook 把它指向本机 DSH 运行时那份。
src/shared/mapping.js 是纯函数、不 import 任何东西,所以 Node 能直接测、
esbuild 也能原样打进 client bundle —— 一份代码两个消费者,不需要额外构建步骤。
之所以把它从客户端里抽出来:那是客户端逻辑最密的一段(11 种节点 → 3 种块),
埋在 .tsx 里就只能靠浏览器验证。
改动落在 dsh.profile.bundles 或 preset 上的,必须重启实例才生效。
环境变量:
| 变量 | 给谁用 | 含义 |
|---|---|---|
HERTA_SRC | build*.mjs、test-narrative.mjs | Herta 源码树(身份正本与渲染层从这里取) |
DSH_PACKAGES | build-preset.mjs | DSH 安装里的 …/node_modules/@deepseek-ai(preset 底本) |
DSH_MODULES | test-resolve-hook.mjs | DSH 安装里的 …/node_modules(借 @deepseek-ai/*) |
DSH_PROFILE_DIR | deploy.mjs、test-dream.mjs | 目标 lab profile |
DSH_BIN / DSH_HOME | install-web.mjs | 可被普通 Node 启动的 dsh bin、目标 home |
NODE_OPTIONS | DSH 宿主进程 | 本机对 GitHub 有 TLS 拦截时需要 --use-system-ca,否则模型下载会失败 |
HERTA_TTS_ARCHIVE_URL | tts-release.js | 开发用:覆盖模型归档地址(哈希 pin 照旧生效,内容不能换) |
已知缺口
⚠️ 叙述调度层:哪些验过、哪些没验
如实分开写,别把「代码写完了」当成「验过了」:
| 部分 | 验到了什么 | 没验到 |
|---|---|---|
| 提示词资产、语法解析、判决解析、配额闸 | 314 项纯逻辑单测(Node 里直接跑) | — |
| 挂载与依赖 | lab 冷启动日志:plane=preset + dsh-llm 可用 + llm 服务就绪 | — |
| 模型路由可读性 | lab 实测读出 {"provider":"deepseek-official","model":"deepseek-flash"} | — |
| LLM 路径的管道 | 28 项集成测试(mock ctx.llm):请求组装 / 流消费 / 判决解析 / 失败一律放行(无 llm、无路由、流抛错、finish 被截断) | — |
| supervisor 复核的真实闭环 | ✅ lab 里用假模型服务跑通了整条链(见下):她说没凭据的话 → 复核 veto → steer → 她重想重说 → 复核 pass → turn 才结束。日志:supervisor 否决 turn 1(第 1 次):她宣称写过笔记,但记录里没有任何写入工具调用 | 真实 DeepSeek 的判决质量(它到底会不会正确 veto)。假模型验的是链路与行为,不是判断力。 |
| thought tag 的真实渲染 | ✅ 同一次验证里,她重说的回复带 (我 想)…(/我 想) 与 (我 说)…(/我 说),页面正确呈现 | 上游那种「逐字揭示」的动画节奏(reveal-driver)没移植。 |
| 做梦蒸馏的真实行为 | ✅ lab 实测跑通整条链:她调 herta_dream {distill:true} → 两阶段(worthiness max_tokens=300 → generation 1200)→ 过 promoteFeian 的格式门 → 落盘 .herta/narrative/### 废案_01:….txt → 记账 manifest.json 记 promoted / tokens: 151 | 真实 DeepSeek 蒸馏出的候选质量(像不像她的语气)没验。 |
| 分拍的真实行为 | ✅ lab 实测:mock 发起一次失败的工具调用 → tools/result 判 tool-failed → 分拍注入。日志:tools/result #1 name=read isError=true → tool-failed / 分拍候选 turn=1 / 分拍 turn 1(tool-failed):cannot read …: not found | 验证类工具的成功分拍(verification-passed)没单独验。 |
| 空轮护栏(2026-09-30 新增) | ✅ 拿真实会话日志回放验过:session-96f68201 的 70 轮里判出 13 轮「用户在界面上什么都看不到」(54、58、60–70);而 52 / 56 / 57 这些「最后一步只出思考、前面有工具调用」的轮次正确地没判成空轮(那些轮次用户看得见卡片)。另加 40 项纯逻辑单测。✅ 且在生产里真触发过一次(2026-09-30):信标记到 silence: { "n": 1, "turn": 28, "attempt": 1 },那一刻她的正文没落地;下一轮她重说了 —— 通知 → 重说这条闭环实测走通。 | 模型持续把回话写进思考通道时,护栏只能让用户看见,治不了模型 —— 见下。 |
| 叙述层对子代理让过路(2026-09-30 新增) | ✅ 判据 11 项单测(用本机实测的 header 形状);外加 12 项源码级接线检查(三处钩子都在、且都问在动作之前、跳过都留痕)。 | 运行时行为没验 —— 「接线检查」证明的是源码里没漏、没挪位,不是子代理真的不再被打断。要真验得跑一次长子任务看它能不能自己交完。 |
一句话:调度逻辑、失败路径、两条 LLM 路径的管道都测了; 分拍 / thought tag / 自我收回 / supervisor 复核 / 做梦蒸馏的行为都在 lab 里 用假模型服务实测过;只剩「真实 DeepSeek 的判断力与语气质量」没验 —— 那需要真实凭据,且不属于代码正确性的范畴。
⚠️ 两种「她突然不会说话了」(2026-09-30 真事故)
session-96f68201 从第 58 轮起完全静默:用户只能一轮轮问「还在吗」,
每一轮再喂进去一条「只出思考」的先例。事后拆会话日志,静默有两条独立的路径:
| 路径 | 现象 | 谁的问题 |
|---|---|---|
| A. 只出思考 | 助手消息里只有 reasoning 块,没有 text 块也没有工具调用,turn/end 照样报 completed。模型在思考里循环「动手。/(做。)/(写。)」几十遍,最后把要说的那句写在思考末尾。DSH 只渲染 text 块 → 屏幕上什么都没有。 | 模型侧(DSH 的 EMPTY_RESPONSE 只拦「一个块都没有」,有思考就不算空)。插件管不了,但能让它看得见。 |
| B. 说了但被吞 | 回话确实在 text 块里,但整段被解析成思考 → speech 为空 → 界面上不显示。成因是 splitSurfaces 的形状 B(只有 (我 想) 没有说话围栏)把整段都当思考,连围栏之外那句正常发言一起吞掉。 | 插件的 bug,2026-09-30 修:splitThoughtFences —— 围栏之内是思考,围栏之外仍是说话。 |
处置:A 由 src/host/silence-guard.js(空轮护栏)兜住 —— 一轮结束前清点「用户到底能
看见什么」,什么都没有就注入一条看得见的通知并要求她重说;每轮最多 2 次,
且复核刚否决过的那一轮跳过(那条静默是 rethink 阶段故意要的)。
B 修在解析器里,半侧产物 lib/client.js 重建(npm run build)带上修复 ——
产物只经构建生成、不可手工修补(ADR-0003)。
护栏不会让模型停止犯错,它只是把「她不理我」变成「终端告诉你她这一轮只出了思考」。
叙述层该对谁说话:不当着工人的面喊她(2026-09-30)
叙述层那三样(复核 / 空轮护栏 / 分拍)都是人格行为 —— 它们对着「跟你说话的那个她」
说话。而 subagent 不是她:它是她在会话里派出去的工人,产出通过父级回话交回。
本机实测的形状(会话头,四个子代理会话逐个看过):
{
"id": "…",
"parentSession": "session-…",
"origin": "subagent",
"delegationDepth": 1,
"agentPreset": "herta",
} // 人机只有 delegationDepth: 0,没有 origin
注意最后那个字段:子代理继承人设,所以人格机制会一视同仁地打进它们。那天的代价是
两次收尾被吃掉 —— 一次子代理把一句「分拍点评」当成交付物(几千字的摘要没交出来,
只能靠 send_message 追回),一次报告交完了、最后一轮被复核拉去做「重想」。
判据是 isSubagentAgent(agent)(src/host/session-surface.js,纯函数):origin 是语义
字段、delegationDepth 是结构字段,任一说是子代理就认;两个都读不到才当人机 ——
默认站在「照常服务她」这一边,不误伤。三处钩子都问它,都问在动作之前,跳过都留痕
(marks + 信标 subagentSkip + 一行日志;分拍那处只在第一次打日志,工具结果太频繁)。
怎么确认叙述层真的在跑(启动信标)
叙述层在放行的时候不留痕迹:复核判 pass 就不注入、不写会话记录。所以在正式
环境里,光看会话记录区分不出「复核跑了并放行」与「复核压根没跑」。
src/host/narrative-beacon.js 把「跑到哪一步」写成一份可读证据,落在
$DSH_HOME/dsh-herta-narrative.json(lab 与正式环境各一份,互不覆盖):
{
"verdict": "叙述层在跑:复核执行过",
"phases": {
"install": { "pid": 123, "depsOk": true }, // 钩子挂上了
"llm": { "ok": true }, // llm 服务拿到了
"turnStop": { "n": 3, "turn": 3 }, // turn 边界钩子触发过
"review": { "n": 3, "verdict": "pass" }, // 复核真的执行过(pass 也记)
"veto": { "n": 1, "turn": 3, "stage": 1 }, // 复核否决过(rethink / respeak)
"beat": { "n": 1, "kind": "tool-failed" }, // 分拍判据执行过
"silence": { "n": 1, "turn": 60, "attempt": 1 }, // 空轮提醒注入过(只出思考的那一轮)
"subagentSkip": { "n": 3, "hook": "beat" }, // 叙述层对子代理让过路(诊断,不是推进)
},
}
verdict 可直接读作结论。只放阶段名、计数、时间与 pid,不放任何对话内容。
# 正式环境:桌面应用的 DSH_HOME 是 %USERPROFILE%\.dsh
Get-Content "$env:USERPROFILE\.dsh\dsh-herta-narrative.json"
# lab:$env:DSH_HOME 指到哪就在哪
Get-Content "$env:DSH_HOME\dsh-herta-narrative.json"
同一目录下曾经还有语音偏好 dsh-herta-voice.json —— 那条自持路线已废弃
(语音偏好现在是 DSH 设置里的字段)。旧文件若还在,只是一份没人读的历史残留;
插件启动时那次「读→改名 .imported」的一次性迁移也已删除。
turnStop那一条是「它活着」最硬的信号 —— 比「有没有人 veto」硬得多: 放行不留痕迹,而钩子被触发过就说明链路接上了。
怎么在没有 API Key 的环境里验这些
scripts/mock-llm-server.mjs —— 一个 OpenAI 兼容的 SSE 假模型服务,
按 system 提示的词判定阶段(复核 / 蒸馏 / 对话 / 第三方辅助调用),
并让第一次复核 veto、之后 pass,于是一次会话就能看到完整的自我收回往返。
它不碰任何真实凭据:DeepSeek provider 的配置里只放凭据的「名字」
(apiKeyEnv),密钥不进配置 —— 所以 lab 指向它时用的是假的环境变量名。
node scripts\mock-llm-server.mjs --port 8791
# 然后在**只属于 lab** 的 profile patch 里把 llm-deepseek 指向它:
# - id: llm-deepseek
# config: { baseURL: http://127.0.0.1:8791, apiKeyEnv: DEEPSEEK_API_KEY, models: [{id: deepseek-flash, …}] }
# 并用一个假值启动 lab:$env:DEEPSEEK_API_KEY='mock-key-for-lab-verification'
# 验完记得还原 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.mjs22 项)。 它现在是回落的音源: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;上游授权范围与收录依据见
THIRD-PARTY.md。
与 Herta 上游的关系
人格正本、格式门、新颖性判定、语音资产都来自 Herta(逐字或逐字移植,出处写在 各处注释里)。做梦的蒸馏环节被换掉了:原版是离线自主 pass(约 6700 行,用它 自己的 LLM 蒸馏候选),这里改由她在会话里写候选 —— 在 DSH 里模型就是她本人、 上下文就在眼前,再让宿主另起一次 LLM 调用去「蒸馏她自己」既贵又绕。 保留的是晋升门与做梦账本。
版本历史
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_v2vs/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:integration28 项全过。 - 仍未接 / 仍未决:
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 来自 注册),内容区被降级成一个空的<div data-slot-error="settings.section">。 - 顺手修掉的:无(这一版不放别的东西)。
- 验证:
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——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因此 必须不带--importhook(挂了 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里的值 (那正是「填了密钥也不出声」的根因)。
- 「Fish 密钥」走 DSH 凭据缝(
- 两条静默路径修掉:围栏之外的字算说话(「正常发言 + 内心话」不再被整段吞掉); 空轮护栏 —— 一轮什么都没说时注入一条看得见的通知(生产上已实测到它唯一的一次触发)。
- 叙述层对子代理让过路:分拍 / 复核 / 空轮护栏一律不拦子代理 —— 三处钩子都问在动作之前, 每次跳过都留痕。
- 设置页体检两批:记忆能取下、失败说人话、做梦账本不丢、模型下载失败说出原因、
计费字符上状态行;
theme与deviceScene经核实在 DSH 侧没有消费方, 从活字段降为「暂未接线」并各写清原因 —— 此前这两行会渲染两遍(既在普通分组里, 又在「暂未接线」里),现在只活在后者。 - 产物一致性上了测试:
test-artifact-sync.mjs守着「src/host的每个文件与lib/逐字节相同」, 也守着「发布目录里不许留补丁脚本的.bak备份」(那东西会跟着 npm 包一起发出去); 另加每个工具的「形状声明必须覆盖它的返回值」。 - 兼容性收口:
engines.dsh放宽上界,preset 底本改用自带的官方0.2.0。 - 许可证:GPL 分发义务写清,
THIRD-PARTY.md给出对应源码的获得方式。 - 测试:核心 23 组 906 项 + MiniMax 6 组 390 项 = 1296 项全过、0 失败。
v0.1.4
把 Herta 的 MiniMax 语音「拉过来」 —— 从"设置里存得下密钥"到"她真的用那个声音说话"。
- 新增
src/host/minimax/(TS 移植件,单独一条编译缝scripts/build-minimax.mjs, 用 Node 自带的stripTypeScriptTypes,不引 esbuild/tsc):api.ts—— HTTP 层,忠实移植上游minimax-api.ts;不含上传/克隆 (用户决策:只认领已有克隆,不把 8.36 MB 参考音频带进插件)。segment.ts——segmentSpeechUnits移植;与上游真身做了 16164 次差分比对,0 处不一致。voice.ts—— 只认领:按LEGACY_REFERENCE_TAG = "b1a43133"找她自己那个克隆 (那个 tag 正是herta-reference.wav的 SHA-256 前 8 位),失败冷却 10 分钟。synthesizer.ts—— refusal 三态 doom 整个 utterance、取消静默、超时算network、 并发上限 2、voice_missing交给宿主决定回落。pipeline.ts—— 纯状态机:边写边念、turn-stopping时掐掉还在流的那段、 每轮字符上限、子代理的文字不念。state.ts—— 克隆记录落$DSH_HOME/dsh-herta-minimax.json(机器状态,不进设置表单)。
src/host/minimax-voice.js—— 宿主接线:凭据缝读密钥、启动认领 + 凭据变更重认领、 SSE/herta-minimax-events+ 状态端点/herta-minimax-state、按voiceEngine分发、 显式回落本地模型并在状态里写明原因。- 新工具
herta_say(云端合成说一句,调试 + 验收通道);voiceEngine与realtimeVoice从wired: false翻成wired: true(那 11 项「暂未接线」因此变成 9 项)。realtimeVoice是自动念回复的总开关:关掉就不再自动送去合成(省钱), 而herta_say是明确要求说一句,不受它管。 - 密钥走凭据缝:
MINIMAX_API_KEY/MINIMAX_PLAN_API_KEY。没填就回落本地, 状态里说清是哪一种 unavailable。 - 测试:
npm run test:minimax= 388 项 (api 98 / segment 112 / voice+state 43 / synthesizer 37 / pipeline 43 / pcm 55)。 全量npm test= 20 组 1059 项全过、0 失败(14 组逻辑用例 671 项 + 上面 6 组 388 项)。
仍未接:voiceEngine = local / mimo 这两条(本地模型目前只服务 herta_speak 与
上面那条回落;mimo-tts.js 仍未实例化)—— 这是决定,不是遗漏。
同时按 DSH 插件清单规范收口(package.json,这一版才补齐):
dsh.manifestVersion: 1—— 清单格式标识,与包版本、会话格式版本都无关。engines.dsh: ">=0.1.7-rc.1 <0.2.0-0"—— 作者声明的兼容 DSH 版本区间。 要带显式下界:写成>=0.1.0-rc.1这种宽区间时,0.1.7-rc.2一类预发布版会被 semver 静默排除(预发布版只有在同major.minor.patch的比较子上才被认)。 插件市场的「按宿主发现」读的就是这个字段(也接受同版本线的@deepseek-ai/dsh-*peer 声明)。locale/en.json+locale/zh.json(形状{ "meta": { title, description } })—— 插件管理页与市场卡片不激活插件也能读到本地化的标题与简介; 配套exports补./locale/*.json、files收locale。files补NOTICE.md/THIRD-PARTY.md。npm 包会带上assets/voice/(80 条语音) 与preset/herta.patch.yml这些第三方同人素材,法律声明必须随包一起走; TTS 运行时的许可原文本来就在assets/tts-runtime/LICENSES/(含 espeak-ng 的 GPL-3.0 全文),随assets一起发,这一项此前已满足。description改中英双语、补keywords、补publishConfig.access: "public"。
刻意没加 peerDependencies。 规范允许「engines.dsh」与「同版本线
@deepseek-ai/dsh-* peer 声明」二选一,市场两者都读。这里只声明 engines.dsh:
本包当前的安装形态是 file: 本地路径,加 peer 会让 pnpm 在安装时去解析一批宿主包,
收益(市场兼容性展示)与 engines.dsh 重合,风险却不重合。等有真实 npm 安装反馈再说。
首次发到 npm:dsh-herta@0.1.4 —— 此前只在 GitHub 上,npm 上这个包名还是空的。
v0.1.3
首个把「0.1.7 兼容修复」真正发出去的版本。 v0.1.2 那批改动(preset 载体迁移、
语音偏好自持)此前只存在于仓库里、没有单独发版;这一版连同下面几项一起发布,
tag 为 v0.1.3:
- 会话 v4 的消息来源 kind(第三处破坏性变更,2026-09-26 实测补上):注入消息
原先写 0.1.5 时代的
{ kind: "plugin", plugin: "dsh-herta" },v4 在写入会话时 就拒(format v4 message requires a producer-owned source kind),症状是每一轮 都报「本轮运行失败」,而被拒的事件根本没进日志 —— 翻会话文件是干净的,极难定位。 三处agent.steer/PLUGIN_SOURCE全部改成{ kind: "plugin:dsh-herta" }(与 DSH 自己的 v3→v4 迁移为旧行推导出的形状一致),并新增scripts/test-source-kind.mjs把这条不变量钉住(已确认注入旧写法会变红)。 同一形状的坑dsh-mimo-coder也有,一并改了它的PLUGIN_SOURCE。 - 本地 TTS 运行时随包分发(
assets/tts-runtime/,22 MB):sherpa-onnx 1.13.6 + onnxruntime 1.27.1 + espeak-ng / piper-phonemize 的 fork,许可原文在LICENSES/。 宿主真探测它(子进程把 addon 加载起来拿版本号)之后才报runtime: true—— DSH 设置页里「本地语音模型」那行会如实报「运行时未就绪」而不是把按钮点亮,两个标志 都不是写死的。合成跑在子进程里(src/host/tts-worker.cjs):sherpa 的 espeak 构建在 Windows 上处理不了非 ASCII 绝对路径,而本机路径里就有中文。 ⚠️ espeak-ng 是 GPL-3.0-or-later 且静态链接,分发前需自行拍板 —— 见THIRD-PARTY.md。 - 本地语音模型的下载(
/herta-voice-model):归档参数钉死在src/host/tts-release.js(herta-best-e72/ 76,255,506 B / SHA-256),四段流程 —— 边下边算哈希(先比字节数、再比哈希)→ 解到最终目录旁边的.installing/(带解压 炸弹上限、拒绝路径穿越)→ 拿 bundle 自带的manifest.json逐文件比 size + SHA-256 → 前三段全过才rename就位。任何一段失败都不留半个可用的 bundle,已装好的旧 bundle 在任何失败下都还活着。 - 插件图标
icon.png(384×384 / 173 KB):满足清单对图标的两条硬约束 (≤256 KiB、必须位于 manifest 所在目录之内),生成过程留在scripts/make-icon.py。 - 构建与测试脚本去掉全部写死的本机绝对路径:preset 底本改从
dsh-web-app/presets/standard.patch.yml取,探测不到会明确报错并告诉你设哪个变量。
兼容性细节(三处破坏性 API 变更、修之前各自的症状)见上面「兼容性」一节与 下面 v0.1.2 一节 —— v0.1.2 的正文是前两处真正发出去的内容。
验证:npm test 13 组纯逻辑用例 622 项全过(test-narrative 31 /
dream 28 / mapping 41 / narrative-hints 54 / supervisor 81 / session-surface 32 /
beat-policy 61 / dream-distill 55 / mimo-tts 40 / voice-settings 50 /
voice-model 50 / herta-settings 92 / source-kind 7),另有 28 项 LLM 路径集成测试
(npm run test:integration)。
2026-09-26 收口:
herta-settings那一组被重写过。它原来测的是「把值写回 整机自己读的两份settings.json」(读-改-写、原子落盘、旧文件迁移、只写用户覆盖过 的字段)—— 那条链路连同src/host/settings-sync.js已整体删除,所以那些断言消失, 换成了字段表/Wired 注解/Config 同源 + 五组防回归(写回不得复活、语音偏好必须 真接 bridge、整机设置页组件必须不存在、Select必须存在、凭据缝必须用ctx.get而不是inject取)。数字从 78 变 92 不是「测多了」,是测的东西换了。
仍未接:回复 → 合成 → 整机 iframe 播放那一跳 —— 「实时语音」开关会亮, 但她暂时不会自己开口。
v0.1.2
兼容 DSH 0.1.7-rc.2。 这一版全部是兼容性修复 —— 0.1.5 → 0.1.7 之间有两处
破坏性 API 变更,旧版在 0.1.7 上「界面能开、功能静默失效」:
-
修 preset:DSH 移除了
$DSH_HOME/.agent-presets/目录机制,preset 改成一条@deepseek-ai/dsh-agent-presetloader 行。build-preset.mjs重写为生成preset/herta.patch.yml,并通过dsh.bundle.patch数组随包一起装。 修之前的表现是:--dump-config一切正常,但「黑塔」这个 preset 根本不存在 —— A 层人设、B 层提示词段与五个工具、叙述调度层全部静默缺席。 -
修语音偏好:DSH 移除了
SettingsProvider.register与客户端settingsScope(换成基于插件 Config 的SettingsForms/configForms,无第三方命名空间入口)。 改为自持:$DSH_HOME/dsh-herta-voice.json+ 白名单端点GET/PUT /herta-settings。 修之前的表现是:宿主打一行settings.register is not a function,客户端那个ctx.inject(["settingsScope"])永不回调 —— 面板能开能点,值永远是默认值。⚠️ 这一条后来又被推翻了(2026-09-26):
SettingsForms已经够用(插件自己的 volatileConfig+ 客户端settings.section),所以那条自持路线与/herta-settings端点一并删除。读上面的历史时别照它去做 —— 现在语音偏好就是 DSH 设置里的字段。 -
修构建/测试脚本:去掉全部写死的本机绝对路径;preset 底本从
dsh-agent-presets/presets/standard/*(已不存在)改为dsh-web-app/presets/standard.patch.yml;test-dream.mjs不再依赖某个 已部署的 lab profile(改用lib/+ resolve hook),test-narrative.mjs的临时目录改到系统 temp。 -
插件图标:加
icon.png(取自 Herta 上游桌面应用的resources/herta-icon.png, 由 1024×1024 / 1.16 MB 缩放重编码为 384×384 / 173 KB),并在package.json里声明"icon": "./icon.png"—— DSH 插件管理页按这个字段显示图标。 两条硬约束(见@deepseek-ai/dsh-package-manifest与dsh-app-boot的iconOf): ≤256 KiB、必须位于 manifest 所在目录之内;原图两条都不满足。 生成过程留在scripts/make-icon.py,便于换图时复现。 -
本地语音模型的下载:从上游扒来并钉死在
src/host/tts-release.js的 归档参数(voice-herta-best-e72/ 76,255,506 B / SHA-256),加上完整四段流程 (下 → 校验 → 解包 → 再校验 → 原子换入)与/herta-voice-model端点。 同时修了整机 bridge 里三个坏成员:downloadVoiceModel的兜底值曾是phase:"ready"(假绿 —— 盘上什么都没有却显示已就绪)、onVoiceModel从不订阅、cancelVoiceModelDownload是空函数。模型能下、能校验、能装;还发不出声(缺运行时)。 -
修「语音模型显示约 0 MB」(用户报,看截图发现的):iframe 那边的
onVoiceModel只登记本地监听、从不通知父窗口,于是父窗口那个onVoiceModel应答器永远不被触发 —— 它既不去读宿主的模型状态、也不推voiceModel事件。现在订阅会补一次call(与真实 preload 的语义一致), 父窗口另外主动推一次。实测:iframe 收到的unpackedBytes从0变成115,897,197,即面板显示「约 116 MB」。⚠️ 下载按钮仍然点不了:上游把它 gate 在
disabled={!runtime}上 (VoiceSettings.tsx:489/:506),而runtime指 sherpa-onnx 原生运行时 在不在。这是上游有意为之:没有运行时,116 MB 的模型下下来也用不了。 要让它亮起来,得先把那 22 MB 原生件接进来。 -
随包分发本地 TTS 运行时(应要求,22 MB):
assets/tts-runtime/(sherpa-onnx 1.13.6 + onnxruntime 1.27.1 + espeak-ng/piper-phonemize 的 fork, 许可原文见LICENSES/,espeak-ng 是 GPL-3.0-or-later 且静态链接,分发前需自行拍板 —— 见THIRD-PARTY.md)。宿主侧新增src/host/tts-runtime.js(真探测 + 合成入口) 与src/host/tts-worker.cjs(子进程执行体:非 ASCII 路径 / 同步阻塞 / 原生崩溃 三重隔离)。runtime从恒为false变成探测结果,于是「下载模型」按钮 与「实时语音」开关都会点亮 —— 而这两个标志这次都不是写死的。 实测:真实模型合成「你好。我是黑塔…」→ 24 kHz / 5.08 s / 非静音,test-tts-runtime.mjs22 项全过。仍未接:回复 → 合成 → iframe 播放那一跳。 -
验证:531 项单测 + 28 项集成全过(
test-mapping的实际项数从 31 更正为 41; 新增test-voice-model.mjs50 项,用现造的小归档覆盖哈希/体积/穿越/炸弹/取消/ 「失败不留半个 bundle」/「旧 bundle 在失败下活着」); 在真实 0.1.7-rc.2 运行时冷启动实测plane=host/plane=preset/叙述层依赖就绪/ 四条路由 200;无头 Edge 实测 client 半侧两个视图注册成功、voiceSettingsLoaded: true、控制台无错误; 模型下载走真链接实测(HTTP 进度按字节推进,content-length与 pin 逐字节一致)。
v0.1.1
叙述调度层(方案 B)落地,并修掉一个用户报的真 bug:
- 叙述调度层:把她的演员调度接到 DSH 的 agent loop 上(不移植上游 1.6 MB
后端)——
(我 想)/(我 说)叙述语法、分拍、thought tag、 自我收回、supervisor 复核、做梦蒸馏走 LLM。设计见docs/叙述调度层设计.md - 修 bug(用户报):她的思考围栏被当成发言显示出来了 ——
mapping.js原来 把 assistant 的整段文本无条件当speech,没解析围栏 - 可观测性:加启动信标
$DSH_HOME/dsh-herta-narrative.json,让「叙述层在不在 跑」变成可读证据(放行是不留痕迹的,光看会话记录分不出来) - 测试:314 项单元 + 28 项集成(mock
ctx.llm);另有假模型服务scripts/mock-llm-server.mjs可在无 API Key 的环境里验真实链路 - 验证边界:五种行为都在 lab 用假模型服务实测过;真实 DeepSeek 的判断力 与语气质量没验(需要真实凭据)。详见 README 的「验过 / 没验」表
v0.1
首个公开版本:四层(人格 / 记忆 / 语音 / 界面)+ 五个工具 + 两条静态路由。
Plugins associés
dsh-web (dsh-ssh)
zhu1090093659/dsh-web
dsh-web (dsh-remote-web-ui)
zhu1090093659/dsh-web
dsh-web-ui (dsh-ssh)
zhu1090093659/dsh-web-ui
dsh-purge
kingcharleslzy-ai/dsh-purge