Cloudto1/dsh-approval-chime ↗★ 0
dsh-approval-chime
宿主申请权限时播放自定义审批提示音 适合需要声音提醒审批请求,且希望在设置中调节音量和音色的用户。
同名包的其他仓库
安装
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:Cloudto1/dsh-approval-chime说明文档
阅读完整 README ↗dsh-approval-chime
DSH 审批提示音插件:宿主向你申请权限的那一刻响一声,音量、音色、开关都在**「设置 → 通知提醒」**里调。 内置风铃、铃铛、蜂鸣三种合成音色,支持试听与恢复默认,还能导入本地音频(≤5MB、最多 50 个)当音色; WebAudio 现场合成、不加载音频文件,配置持久化到宿主设置文档,另附已触发次数与上次触发时间, 静音环境下也能自证是否生效。
上面这段同时是「插件市场 → 已安装」那一行灰色介绍位要显示的文字。市场只对精选目录里的插件 自动显示作者简介;本插件是本地
link:安装、未上架,那个位置要用市场自己的「添加备注」粘贴 (≤200 字符)。原因见 CHANGELOG 的「rev-7 · 市场简介」条。
当宿主向用户申请权限(审批请求)时播放一声音,并在**「设置 → 通知提醒」(独立分区,rev-7 起)提供一页设置: 启用开关(苹果式拨动开关 38×22,开启色与音量条同一个令牌;rev-9 起)、音量进度条(0..100)、音色选择(chime / bell / beep)、试听、恢复默认, 以及已触发次数 / 上次触发时间**等可见指示(无声环境下也能自证)。
- 提示音是 WebAudio 现场合成(正弦/三角/方波 + 包络),不加载任何音频文件。
- 设置持久化到宿主设置文档(默认
$DSH_HOME\settings.yaml的approval-chime:段), 写入走平台既有的 revision-fenced 通路(浏览器ctx.settingsScope.bind({namespace}).set/unset→remote.settings.mutate→ 宿主文档)。插件自己不碰任何浏览器存储。 - 完全不参与审批瀑布:不注册任何
approval/request监听,内置审批面板的决策路径一个字节都没变。 - 按会话独立(rev-10):会话标题行挂一个小铃铛(开=实心铃铛 / 关=带斜杠),点一下只静音这个会话;
铃铛旁的 caret 还能给该会话单独指定音色与音量(默认跟随全局)。这些覆盖不进设置文档、不进浏览器存储,
而是由宿主半写进插件自己的文件
$DSH_HOME|~/.dsh\approval-chime\sessions.json(原子写、上限 200 条、按updatedAt淘汰)。 多个会话同时待审批时各响各的(不再合并成一声):同一批按快照顺序逐个响、相邻 180 ms。 - 写入收敛(rev-11):同一会话的两次点击是两个 POST、两条 socket,应答被消费的顺序不保证等于宿主写盘的顺序, 所以"最后一个应答落地"并不足以决定本地表。最后一次在飞的写入落定后,插件会重读一次宿主文件, 让本地表最终等于文件 —— 因此铃铛显示的状态不会再与宿主的记录长期相反(rev-10 的 F-01 已修,见 §9 H17)。
本 README 是 t2 的交付说明,也是 t5 挂载/重启验收 的操作手册。
实现依据:docs/契约调研.md(t1 的宿主契约调研,全部结论带 文件:行号;rev-10 的会话槽/待审批表/home 规则见 §L)。
1. 包结构
dsh-approval-chime/
package.json name/type=module/exports{./client,./cordis.patch.yml}/
dsh.bundle.patch=./cordis.patch.yml, dsh.client.platform='web'
cordis.patch.yml insert 一行 {id: dsh-approval-chime, name: dsh-approval-chime}
lib/index.js 宿主半:注册 approval-chime 设置命名空间(默认 enabled=true, volume=70, tone=chime)
+ 音频路由 + 按会话覆盖路由与 sessions.json(rev-10)
lib/client.js 浏览器半:审批监听 + WebAudio 合成 + 「通知提醒」设置分区 + 会话头部小铃铛
+ 写入落定后的收敛重读(rev-11)(经典脚本,只有 factory 闭包)
verify/_harness.mjs 自测脚手架(vm 经典脚本加载器 / 模拟 React / 模拟 AudioContext / 模拟服务与 locale / 假 HTTP 请求-应答)
verify/host-half.test.mjs 自测 1:包结构 + 宿主半注册与降级 + 按会话存储与路由(rev-10)
verify/client-half.test.mjs 自测 2/3:浏览器半装载、两个槽注册、分区页、铃铛两态与 popover、响铃语义、按会话有效值、
写入收敛(rev-11:应答反序仍与存储一致)、batch 间隔、自动播放策略
verify/waterfall.test.mjs 自测:审批瀑布零注册(静态 + 运行时 + 真 cordis 对照实验)
verify/custom-audio.test.mjs 自测 4/4:导入音频(路由、413、名册、自定义音色播放)
node_modules/@deepseek-ai/schemastery 指向上游自愈副本的 junction(见 §2,必须存在)
docs/契约调研.md t1 产出(本插件所有宿主契约的唯一依据;rev-10 的 §L 见下)
docs/挂载与验收.md t5 的挂载/验收手册(§7.1 = rev-10 手工步骤)
2. 必须存在的 junction(t5 预检项)
工作区插件是以 link: 依赖挂载的,Node 会把链接解析到真实路径
(D:\...\dsh-approval-chime\lib),因此它的裸导入不会去 profile 目录找包。
本包自带一个 junction,指向上游每次启动自愈的依赖闭包:
# 已由 t2 创建;预检/重建用
$ns = '\node_modules\@deepseek-ai'
New-Item -ItemType Directory -Force -Path $ns | Out-Null
New-Item -ItemType Junction -Path "$ns\schemastery" `
-Target '$DSH_HOME\profiles\node_modules\@deepseek-ai\schemastery'
预检命令(应输出 True 且两个 realpath 相同):
Test-Path '\node_modules\@deepseek-ai\schemastery\package.json'
(Get-Item '\node_modules\@deepseek-ai\schemastery').LinkType # Junction
lib/index.js 故意不用顶层静态 import '@deepseek-ai/schemastery':缺链接时静态导入会让整行
loader entry 导入失败。它按 import.meta.url(junction)→ ctx.baseUrl(profile 目录)→
$DSH_HOME/profiles(宿主自愈镜像)的顺序尝试解析,再退到动态 import(),全失败就打印警告并保持惰性——
绝不抛出、绝不阻断 dsh web 启动。自测 1 用子进程 + 空 DSH_HOME + 包外副本实测了这条降级路径。
3. 设置项
| 字段 | 类型/范围 | 默认值 | 含义 |
|---|---|---|---|
enabled | boolean | true | 关闭后任何声音都不产生(包括试听,且会计入 suppressedDisabled);审批触发路径连 AudioContext 都不创建 |
volume | number 0..100 | 70 | 音量。主增益 = volume / 100 × 0.6(MASTER_GAIN),volume = 0 时不发声 |
tone | chime | bell | beep | custom: | chime | 音色。前三个为现场合成(bell 为三角波+双分音,beep 为两声方波短鸣);custom: 前缀指向一个导入文件 |
custom | [{ id, name }] | [] | 导入的音色名册。顺序即含义:按导入先后排列,渲染时排在三个内置音色之前 |
- 命名空间:
approval-chime(匹配/^[a-z][a-z0-9-]*$/,dsh-settings强制校验)。 - 入口(rev-7):设置 → 通知提醒(
settings.section的独立分区,idapproval-chime、order 16)。插件页不再出现本插件的行。 - 落盘:
$DSH_HOME/settings.yaml→approval-chime:段;applies: 'live',改完立即对插件生效。 - 「恢复默认」= 对
enabled/volume/tone依次scope.unset()(不动custom:导入的文件是用户的素材库,不是一项偏好)。 - 音量/开关与「试听」的边界(rev-5 更正措辞):审批触发在
volume = 0或关闭时既不建音频节点也不建 AudioContext;但**「试听」是显式用户动作**,会先解锁音频上下文(刻意设计:让你第一次点击就能解除浏览器自动播放限制),只是同样不发声。原文档"不创建任何音频节点"的说法过宽,已按实测更正。 - 非 loopback 页面(远程浏览器)平台只把写入留在内存(
persistence = isLoopback ? 'host' : 'memory')。该模式下分区页渲染为空(导航行仍在):命名空间拿不到status === 'ready',分区组件返回null,因此不会出现"内存保留"之类的提示行(该提示分支不可达,属历史措辞)。验收请用http://127.0.0.1:3080。
3.1 导入自定义音频(rev-4)
- 操作:分区页「音色」右侧的 导入音频 → 选择本地文件 → 浏览器把字节
POST到/api/approval-chime/audio→ 宿主半写入插件目录的audio/.,并把{id, name}追加进custom名册,随后自动选中并试听。 - 两处分开存是刻意的:名册(顺序、显示名)随设置文档走,音频字节落在插件目录 —— 设置文档要保持小、可读、可合并,而浏览器存储是按浏览器且会被清理的。
- 上限 5 MB;允许扩展名
mp3 / wav / ogg / oga / opus / m4a / aac / flac / webm。超限 →413(先把剩余请求体读干再应答,否则 Node 会在请求未读完时销毁 socket,413 根本到不了客户端),扩展名不支持 →415,拒绝而非截断(被截断的音频会解码成怪声,比明确失败更糟)。 - 名册上限 50 个:满了以后「导入音频」直接拒绝并说明原因,不会先把文件传上去再把它显示成"(文件缺失)"。
- 文件名处理:先
trim,扩展名按最后一个点判断;没有扩展名时按 MIME 兜底(audio/mpeg→.mp3)。id 是大小写敏感的小写 uuid。 - 选中导入音色时右侧出现 移除:删除文件并从名册去掉该项;「恢复默认」不会删文件。
- 音量对导入音色同样生效:样本经与合成音同一个主增益播放,
lastGain仍然是volume/100 × 0.6。每个文件只 fetch+decode 一次,之后复用解码后的 buffer。 - 弹出列表显示 3 行,第 4 项起滚动(
select::picker(select)的max-height,依赖appearance:base-select,见 §8 的浏览器前提)。 - ⚠️ 改
lib/index.js(宿主半)必须重启dsh web:音频路由和custom字段的 schema 都活在宿主进程里;只改lib/client.js时刷新页面即可。 (rev-10 起宿主半还多了按会话覆盖的路由与sessions.json,所以 rev-10 及之后的验收必须先重启宿主,见 §3.2。)
3.2 按会话独立(rev-10,写入收敛 rev-11)
- 入口:会话标题行(
conversation.session.header.actions,本插件 idapproval-chime、order 30,已取证的官方占用是 -10/10/20) 里的一个小铃铛 + 更小的 caret。铃铛 开=实心铃铛 / 关=带斜杠铃铛,title与aria-label同时给中英两种文案 (本会话审批提示音:开 · Approval chime for this session: on/…:关 · … off)。 - 铃铛语义(①A:只可能更安静,不可能更响):点击 = 切换该会话的
enabled覆盖 —— 当前会响 → 写enabled:false(只静音这个会话);当前被静音 → 清除enabled覆盖(回到跟随全局)。 不提供「全局关时给单个会话强制打开」。全局关时每个未覆盖会话都静默,铃铛也显示为「关」。 - 有效值:
enabled = 会话覆盖 ?? 全局、volume = 会话覆盖 ?? 全局、tone = 会话覆盖 ?? 全局(逐字段)。 会话覆盖的音色是custom:而名册里已经没有这个文件时,回退到全局音色并在 popover 里说明(不报错、不静音)。 - caret popover(自绘、只有 react 依赖):音色(跟随全局 / 导入的音色 / 风铃 / 铃铛 / 蜂鸣)、
音量(勾选「跟随全局音量」= 清除覆盖,取消勾选后 0..100 滑杆即为该会话的音量)、恢复跟随全局。
position:fixed+getBoundingClientRect自定位(贴到视口底部时向上翻),外部 pointerdown 与 Escape 关闭。 写入是乐观更新:先本地生效,宿主拒绝则回滚并在 popover 里显示错误行。 - 写入收敛(rev-11):每次写入 +1 一个"在飞写入"计数;计数归零时重读一次宿主文件,
让本地表最终等于文件 —— 因此两次点击的应答即使被反序投递(rev-10 的 F-01),铃铛也不会与宿主的记录长期相反。
被拒绝的写入仍然回滚并保留错误行(这次重读只重读表、不覆盖该次写入的结论)。
代价是每次写入多一个
GET(连点两次只多一次,因为它是"归零才读");重读本身失败时退化为"无覆盖"并保留原因。 - 落盘:
$DSH_HOME\approval-chime\sessions.json(未设DSH_HOME时~\.dsh\approval-chime\sessions.json, 规则与宿主@deepseek-ai/dsh-home-paths一致:纯空白的DSH_HOME视为未设置)。 形状:{"version":1,"sessions":{"":{"enabled"?:bool,"volume"?:0..100,"tone"?:string,"updatedAt":number}}}。 三个字段都可缺省;POST里字段值null= 清除该字段(回到跟随全局);三个字段全缺省的记录不落盘。 - 两个 HTTP 路由(沿用既有
webServer注册与「没有 web 服务器就只是不做路由」的降级姿势):GET /api/approval-chime/sessions→{ok:true, revision:, sessions:{…}};POST /api/approval-chime/sessions,体{sessionId, patch}→ 应用后返回同样的结构。 非法sessionId(空/非字符串/超长 >200)、非法volume(非 0..100 整数)、非法tone(非chime|bell|beep|custom:)、 未知 patch 字段一律 400 且不落盘;未知路径 404;非 GET/POST/HEAD 方法 405。 - 上限与淘汰:最多
MAX_SESSIONS = 200条,按每条记录的updatedAt淘汰最旧;文件缺失/损坏/非对象时退化为空表并告警, 绝不抛错中断插件(下一次成功写入会把文件重写成合法内容)。 - 原子写:同目录临时文件(
.sessions. ..tmp)+rename()覆盖目标;写失败会清掉临时文件并回500。 - 多会话各响各的:同一批快照里 N 个可响会话各自响一次,按快照顺序、相邻 180 ms(
BATCH_GAP_MS); 被本会话静音的不响并计入suppressedSession(设置页抑制行显示「因本会话提示音关闭而静音 ×N」)。 全局关导致的静默仍计suppressedDisabled(语义不变)。 - 跨机器说明(②B 的固有属性,不修):覆盖表在宿主所在的机器上,因此换机器/换 profile 不会跟着走; 设置文档里的全局值照旧按平台通路同步。详见 §9。
- ⚠️ rev-10 改了两半:新路由与新文件都在宿主半,必须先重启
dsh web;只刷新页面会出现「铃铛在、写入报错」。
手工验收判据(摘要;逐步操作见 docs/挂载与验收.md §7.1):
| 判据 | 通过的样子 |
|---|---|
| 铃铛存在且两态 | 会话标题行有 20px 小铃铛 + 更小的 caret;开=实心、关=带斜杠;hover 同时显示中英文案 |
| 只静音一个会话 | 把 A 的铃铛点成「关」后 A 不响、B 照响;设置页出现「因本会话提示音关闭而静音 ×N」;全局开关不变 |
| 「开回来」= 清除覆盖 | 再点一次铃铛,sessions.json 里 A 的 enabled 字段消失(不是写入 true) |
| 多会话各响各的 | 两个未静音的会话同时待审批 → triggers 连续 +2(相邻约 180 ms),不是一声 |
| 每会话音色/音量 | caret → 选 bell、音量 30 后,该会话 stats().lastTone === 'bell'、lastGain === 0.18,其它会话与设置页不变 |
| 数据落点 | $DSH_HOME\approval-chime\sessions.json 出现该会话记录;settings.yaml 无会话数据;浏览器无任何存储项 |
| 上限 | 最多 200 条,最旧的 updatedAt 先被淘汰(自测用 205 条种子文件断言) |
| 写入收敛(rev-11) | 同一会话连点两次后,sessionWrites().outstanding === 0 且本地表与 sessions.json 逐字段一致(应答顺序无关) |
| 零回退 | 设置页与本轮之前逐项一致;审批面板按钮行为不变 |
4. Headless 自测(无需浏览器、无需宿主、无需装包)
cd ''
node --check dsh-approval-chime/lib/index.js
node --check dsh-approval-chime/lib/client.js
node dsh-approval-chime/verify/host-half.test.mjs # 124 项
node dsh-approval-chime/verify/client-half.test.mjs # 302 项
node dsh-approval-chime/verify/waterfall.test.mjs # 22 项
node dsh-approval-chime/verify/custom-audio.test.mjs # 74 项(导入音频)
全部打印 [PASS] 并以退出码 0 结束(合计 522 项);任何一项失败会打印 [FAIL] 并置退出码 1。
各自证明什么:
- host-half:package.json/cordis.patch.yml 的挂载契约(
exports['./client']、dsh.bundle.patch、dsh.client.platform、只 insert 一次、id=name);junction 存在且指向宿主镜像;ctx.settings.register('approval-chime', schema, {applies:'live'})注册了真的 schemastery schema (默认值{enabled:true,volume:70,tone:'chime'},拒绝volume=200 / -1 / tone='nope' / enabled='yes'); 重复注册会被跳过;ctx/settings缺失、describe()抛错、register()抛错都不外抛; 在包外副本 + 空$DSH_HOME的子进程里,schema 不可解析时 apply 依旧不抛且什么都不注册; rev-10 新增:按会话覆盖的存储与路由 —— 路径规则($DSH_HOME\approval-chime\sessions.json;纯空白$DSH_HOME视为未设置)、 目录递归创建、GET空表/POST合并与null清除、12 种非法补丁(含非整数音量、大写 custom id、未知字段)一律 400 且文件字节不变、 413/404/405/HEAD、上限 200 与按updatedAt淘汰最旧(205 条种子文件 → 读回 200 条)、 5 种损坏形状退化为空表并告警、损坏后写入可修复、原子写(临时文件同目录 +rename,写失败清理临时文件并回 500、目标不变)。 - client-half:
lib/client.js以vm经典脚本方式装载(等同 ``),id='dsh-approval-chime'、只require('react')、factory 返回{name, inject, apply}; apply 后:绑定{namespace:'approval-chime'}、注册settings.section一项 (id='approval-chime'、order=16、label是随 locale 重读的 thunk、locale=NS), 并断言从未注册/等待/提及插件页那张卡片的槽; rev-10 新增:注册conversation.session.header.actions一项(id='approval-chime'、order=30、locale=NS, 断言 id 不撞官方四个占用者);铃铛两态(实心/带斜杠的差异断言在 SVG 上)、title与aria-label同时含中英两种文案、 点击写POST {sessionId, patch:{enabled:false}}、再点写{enabled:null};caret popover 的内容(跟随全局 + 3 内置音色)、 自定位(视口底部向上翻、水平收进视口)、Escape 与外部 pointerdown 关闭、拒绝写入回滚 + 错误行; 有效值真值表(未覆盖/仅静音/音色覆盖/音量覆盖 × 全局开与关)、缺失 custom 音色回退全局音色、 4 条 pending(其中 1 条被本会话静音)→ 3 响、相邻 ≥180 ms(120 ms 时仍为 1 响)、逐会话音量与快照顺序、suppressedSession +1且设置页显示该原因;诊断对象既有键一个不删 + 新增sessions()/sessionSettings(id); rev-11 新增:一个"POST 应答可被扣住、由测试决定投递顺序"的存储桩 —— 同一会话连点两次后先放第二个应答、再放第一个, 断言迟到的旧应答不能在本地表里留下记录、铃铛仍跟随全局、本地表与桩里的存储逐字段相等、只重读一次、无在飞写入; 另测单次点击仍收敛、被拒写入仍解析为false且错误行不被重读抹掉(这条新自测在 rev-10 的字节上会报红——已实测); 零浏览器存储断言(localStorage/sessionStorage/indexedDB/caches.均不出现); 注册 zh/en 词典(含nav/title/intro与 rev-10 的会话文案)、订阅uiSession.pendingInteractions、remote 订阅数为 0; 分区页渲染出唯一的「通知提醒」(与导航行文案一致)、一行 intro,以及 range(0..100)/checkbox(rev-8 起role=switch,绘制为苹果式拨动开关)/select(3 音色)/导入+隐藏 file input/试听/恢复默认/计数器文本/抑制原因行/ 只读与已覆盖徽标/错误行/bundleRevision 徽标(条件行各有一条断言); 拖动在释放时写set('volume',…)(一次鼠标拖拽会先后触发pointerup与mouseup, 因此同一值可能写两次;平台的写队列串行化并携带pendingRevision,语义等价于一次), 开关/音色立即写,恢复默认走unset; 一次审批 = 一个连到 destination 的主增益且其值 等于volume × MASTER_GAIN, 同 key 重复发布不再响、替换请求(新 key)再响一次、非 approval 条目不响,volume=0/enabled=false时音频节点数为 0; 首个用户手势解锁 AudioContext 并释放监听、解锁前/不支持的浏览器只计数不抛异常, 全程无 unhandled rejection。 - waterfall:静态断言
lib/client.js/lib/index.js里没有审批事件名、没有$on、没有.on(、 没有waterfall(;运行时断言 apply 后 remote 订阅为 0; 用宿主真实 cordis 复跑 t1 的实测:不调用next()的监听器切断整条链、其后注册者永不执行、 决策返回值原样返回、{prepend:true}在ctx.on上确实生效(所以问题只在$on只转发两个参数, 并由dsh-api-gateway/lib/client.js的$on(event, listener)源码签名佐证); 最后在这种"敌对环境"里证明本插件照样响铃。
自测会在
%TEMP%下建一个临时目录(包外副本 + 空 DSH_HOME + 子进程脚本), 结束时删除;不写工作区其它位置,不碰 profile,不装任何包,不联网。
5. 挂载 + 重启验收步骤(交给 t5)
前置:§2 的 junction 存在;自测全绿。
-
改 profile(唯一需要动 profile 的一步) —
$DSH_HOME\profiles\web\package.json:dependencies增加"dsh-approval-chime": "link:";dsh.profile.bundles追加"dsh-approval-chime"(追加即最后一层;本插件对行序无要求)。
-
让 profile 的
node_modules出现该 junction: 推荐(t5 实测路径,等价于手改):node dsh-approval-chime/deploy/mount.mjs—— 它先备份、再幂等地写入上面两处 JSON 改动并建 junction;--dry-run只看不改。⚠️ CLI 语义更正(t5 实测源码,
@deepseek-ai/dsh/lib/plugin-Ddi42qoW.js):dsh plugin --profile只是把参数转发给 profile 目录里的 pnpm (spawnSync('pnpm', args, { cwd: profileDir })),成功后再按「已安装依赖」重算dsh.profile.bundles。 因此dsh plugin --profile web install只是pnpm install:它本身不会新增依赖; 只有当package.json已声明该link:依赖时,它才会建立 junction(并补上缺失的 bundles 项)。 想用一条命令完成「依赖 + junction + bundles」,正确形式是dsh plugin --profile web add "link:"; 该形式本任务未实测(会跑 pnpm、改写pnpm-lock.yaml并动node_modules,风险大于收益), 本插件采用的是上一条的等价手工/脚本路径。详见docs/挂载与验收.md§8。退化路径(连脚本也不想跑时):
New-Item -ItemType Junction -Path "$DSH_HOME\profiles\web\node_modules\dsh-approval-chime" -Target ""。 ⚠️ 不要再往profiles/web/cordis.patch.yml里写同 id 的insert:所有层的 patch 会合并成一个数组, 重复 id 直接抛duplicate loader entry id→ 启动失败。临时停用请用 id 定向覆盖:- id: dsh-approval-chime+disabled: true。 -
重启宿主:宿主半不会热加载(宿主把 bundle 字节读进内存)。 停掉当前
dsh web进程,重新dsh web,然后浏览器刷新一次页面。 -
验收信号(可观测):
- 宿主日志无
failed to import loader entry dsh-approval-chime、无client-modules: … failed to compose; - 浏览器控制台:
window.__DSH_BOOT__.entries里出现{id: 'dsh-approval-chime'}; - 控制台:
window.__DSH_APPROVAL_CHIME__存在(见 §7 自证接口); - 设置导航里出现独立一行 「通知提醒」(在「插件」之后、「Agent 预设」之前),进入后是标题「通知提醒」+ 一行说明 + 本插件的全部控件,右上角带构建戳(当前
rev-11 · per-session chime (race fix)),且「启用提示音」是苹果式拨动开关(开启=与音量条同色的蓝底白钮、关闭=灰底); - 反向判据:设置 → 插件 → 插件配置里不再出现本插件的行(rev-7 起不再注册那张卡片);
$DSH_HOME\settings.yaml出现approval-chime:段(点一次试听/改一次音量后)。- rev-10 的新信号(rev-11 起写入收敛):任意会话标题行出现小铃铛 + caret(开=实心 / 关=带斜杠,hover 中英双语);
控制台
await fetch('/api/approval-chime/sessions').then(r => r.json())返回{ok:true, revision:…, sessions:{…}};$DSH_HOME\approval-chime\sessions.json在点过铃铛/caret 后出现;连点两次后__DSH_APPROVAL_CHIME__.sessionWrites().outstanding === 0(写入已落定并重读过)。 手工步骤见docs/挂载与验收.md§7.1。 - 分区页空白时先看宿主日志:搜
dsh-approval-chime。若是could not load @deepseek-ai/schemastery; settings namespace "approval-chime" is not registered, 就是 §2 的 junction 缺失(重建后重启即可,导航行仍在但页面为空);若是settings service unavailable,说明 profile 里没有dsh-settings(本插件对它是硬依赖,只影响本插件)。
- 宿主日志无
-
功能验收(不依赖听力):
- 分区页「已触发」计数在审批出现时 +1,「上次触发」显示时间;没有声音时下面的提示行会说明原因 (被开关关闭 / 音量为 0 / 浏览器自动播放策略 / 不支持 WebAudio);
- 先点一次页面任意处或「试听」解锁 AudioContext,再触发一次审批(让宿主申请一次权限), 应能听到提示音;把音量拖到 0 或关掉开关后,计数只能看到"静音原因",不应有任何声音;
- 刷新页面后音量/音色/开关保持(值来自宿主设置文档)。
-
回滚:删掉
dsh.profile.bundles里那一行与dependencies里那一行 → 重启; 可选删除 profile 里的 junction 与settings.yaml里的approval-chime:段。三层独立可回滚。