dsh-theme-endfield 1.2.3 → 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -103,7 +103,7 @@ node scripts/build-sounds.js --check # 校验生成物与定义一致
103
103
 
104
104
  | 平台 | 播放器 |
105
105
  | --- | --- |
106
- | Windows | `pwsh.exe` → `powershell.exe`(`-EncodedCommand` + `Media.SoundPlayer`) |
106
+ | Windows | `powershell.exe`(5.1)→ `pwsh.exe`(`-EncodedCommand` + `Media.SoundPlayer`) |
107
107
  | macOS | `afplay` |
108
108
  | Linux | `paplay` → `aplay` |
109
109
 
@@ -188,7 +188,7 @@ body:not(.theme-endfield-round) [class$='_iconButton'] { border-radius: 50% !imp
188
188
 
189
189
  唯一需要 JS 介入的表面是**等高线画布**——canvas 描边不能是 CSS 变量,所以它由一个监听 `<body>` class 的 `MutationObserver` 触发重绘(顺带让「别的标签页改了配色」也能跟着变)。
190
190
 
191
- 配色切换后的实测像素占比(`test/verify-shots.js` 解码真实截图统计):
191
+ 配色切换后的实测像素占比(`test/tools/verify-shots.js` 解码真实截图统计):
192
192
 
193
193
  | 截图 | 黄色像素 | 青色像素 |
194
194
  | --- | --- | --- |
@@ -62,7 +62,7 @@
62
62
 
63
63
  ### Host ↔ Client 数据流
64
64
 
65
- 1. **声明**(Host `index.js`):0.1.7-rc.1 导出 `Config`(`z.object({ 每个字段: z.string().default(...).volatile() })`);同时用 `ctx.inject(['settings'], sctx => sctx.effect(() => sctx.settings.configure({ auto: false }, ctx.fiber)))` 关掉自动生成的设置页(本插件自带四组页面)。旧宿主则在 `settings.register` 存在时执行 `register('dsh-theme-endfield', schema, { applies:'live' })`。schema 每个字段都是**字符串**字段并带 `.default(...)`:default-ON 存 `'1'`(读作 `!== '0'`),default-OFF 存 `'0'`(读作 `=== '1'`);palette/radius/fps/speed 各存一个文档里写明的字面量。字符串化让三代存盘点位的值模型完全一致,client 的键表、面板与测试都不必随 transport 改动。
65
+ 1. **声明**(Host `index.js`):0.1.7-rc.1 导出 `Config`(`z.object({ 每个字段: z.string().default(...).volatile() })`);同时用 `ctx.inject(['settings'], sctx => sctx.effect(() => sctx.settings.configure({ auto: false }, ctx.fiber)))` 关掉自动生成的设置页(本插件自带五组页面)。旧宿主则在 `settings.register` 存在时执行 `register('dsh-theme-endfield', schema, { applies:'live' })`。schema 每个字段都是**字符串**字段并带 `.default(...)`:default-ON 存 `'1'`(读作 `!== '0'`),default-OFF 存 `'0'`(读作 `=== '1'`);palette/radius/fps/speed 各存一个文档里写明的字面量。字符串化让三代存盘点位的值模型完全一致,client 的键表、面板与测试都不必随 transport 改动。
66
66
  2. **写**(浏览器 `client.js`):设置面板每个 toggle 调用内部 `prefsSet(field, value)` → 只有当快照是 **durably served**(`status:'ready' && writable`,且 `mode` 不是显式的 `'memory'`)时才调用 transport 的 `set(field, value)`,Host 收到后原子写盘。`mode` 只是 **0.1.7-rc.\* 才有的可选字段**:`@deepseek-ai/dsh-client-ui-settings` 0.2.1-rc.2 连同 `persistence` 概念一起删掉了它,快照只剩 `{ status, value, base, user, revision, writable }`,写一律经 `remote.settings.mutate()` 落到 Host 文档,所以**缺省 `mode` 必须按可持久化处理**,只有显式的 `mode:'memory'`(0.1.7 非 loopback 页面)才截断写入。这正是 v1.2.2 在新版桌面「改了不保存」的根因:闸门要求 `mode === 'host'`,而新版恒为 `undefined`,于是每次 toggle 都只 held + 有界重试,约 10s 后丢弃,刷新即丢(启动报告即 `boot report … status= ready mode= undefined writable= true`)。只凭 `writable` 判写是另一个坑:host 模式的快照即便本命名空间**尚未被 served** 也会返回 `writable:true` 与 `status:'unavailable'`(`commit radius = round … status= unavailable` 就是这么打出来的)——旧代码照写不误、清了脏标记但什么都没落盘,刷新即丢。现在这种写被**拦下并标脏**(页面内仍生效),等快照在后续 ready 回相(Host 文档提交、镜像重载)时由 subscription 自动补写;`configForms.set()` 明确返回 `false`(Host 拒绝/跳过)时同样重新标脏等待下次回相,而不是假装写成功。
67
67
  3. **读 / 生效**:transport 的 `getSnapshot().value` 就是已由 schema 校验并合并默认值的整段(client 再经 `prefsResolveSection()` 归一化到 34 个声明字段);主题层的 `isEnabled()` 与其它 getter 每次调用都 `prefsGet(field)` 现读本段,天然随值变化。
68
68
  4. **订阅同步**:`form.subscribe(...)` / `scope.subscribe(...)` 在每次落盘/镜像变化时唤醒,client 再跑一遍 `reconcileFromPrefs()`,把主题开关(enabled → mount/unmount token+样式表)、圆角/配色 class、水印、等高线、雷霆大字重新对齐。这样同 profile 里**另一个窗口/设备**编辑落盘文件(或本轮写入被 Host 回相确认)都不需要刷新即可热生效。订阅返回的 disposer 现在会被保存并在 run 拆除时调用:`ConfigForm` 是 provider 拥有、跨插件共享的实例,漏掉它会把这一个监听器泄漏给同页面的下一次 run。
@@ -283,7 +283,7 @@ body{font-family:var( --dsw-font-family, -apple-system, … )}
283
283
 
284
284
  ### 等高线 worker 是特例
285
285
 
286
- `src/contour-worker.js` + `src/contour-webgl.js` 由 `npm run build:worker` 把 9 个 kernel 函数抠出来,嵌成 `src/client/21-contour-worker.embed.js` —— **那是生成物,不要手改**。它整块是一行 `JSON.stringify` 出来的字符串,所以 `check.js` 的样式表护栏看不见它;改了 `src/contour-worker.js` 就必须重新生成。
286
+ `src/contour/contour-worker.js` + `src/contour/contour-webgl.js` 由 `npm run build:worker` 把 9 个 kernel 函数抠出来,嵌成 `src/client/21-contour-worker.embed.js` —— **那是生成物,不要手改**。它整块是一行 `JSON.stringify` 出来的字符串,所以 `check.js` 的样式表护栏看不见它;改了 `src/contour/contour-worker.js` 就必须重新生成。
287
287
 
288
288
  ### 门禁与报错
289
289
 
@@ -470,11 +470,11 @@ client.js is stale (first difference at line 4096); run npm run build:client
470
470
 
471
471
  上面所有常量——21 条高度带、22 个凸起——都是**按桌面窗口的观感**调出来的,却按「每面积」施加:窗口缩到手机宽度,同样的线数与岛数挤进一小条竖带,嵌套环密到读作乱线。375×812 实测(暗色):**62 条路径 / 23,890px 墨长**,单屏叠出 8~9 层同心环。
472
472
 
473
- 杠杆选的是**层级数**:线距 = `span*2/LEVELS`,直接按比例缩放屏幕上的线间距。短边 **<640px** 时层级数从 20 向 10 线性递减(320px 触底,下限 6),凸起数从 22 向 12 同步递减(让岛屿保持分离而不是叠成靶心);**≥640px 逐字节回到出厂值**,桌面窗口输出不变。640 这个锚点取在「手机宽(360–430)与窄分屏不再主导短边」之上,320 触底保证最小可用窗口仍是一幅完整地形。
473
+ 杠杆选的是**层级数**:线距 = `span*2/LEVELS`,直接按比例缩放屏幕上的线间距。短边 **<640px** 时层级数从 20 向 10 线性递减(320px 触底,下限 6),凸起数从 22 向 10 同步递减(让岛屿保持分离而不是叠成靶心);**≥640px 逐字节回到出厂值**,桌面窗口输出不变。640 这个锚点取在「手机宽(360–430)与窄分屏不再主导短边」之上,320 触底保证最小可用窗口仍是一幅完整地形。
474
474
 
475
475
  三处消费同一组函数(`contourLevelsFor` / `contourBumpsFor`,以视口短边为锚):布隆布局 `contourBuildCandidate`、覆盖率校验 `contourCoverageScore`、抽取 `contourExtract`。其中校验器必须按「该视口**实际将画的**层级数」算墨量代理——若仍按 `CONTOUR_LEVELS` 评分,10 级的小屏会被要求 2 倍墨量,重抽循环每次都触顶。
476
476
 
477
- 改造后 375×812 实测:路径数降到 ~54(另有种子随机性),最薄分区墨迹 **3.56%**(8×5 网格,0 空白格),嵌套环从 8~9 层降到 3~4 层。回归锁在 `test/contour-density.test.js`(≥640px 断言出厂值逐值相等;小屏断言按公式递减且能完整成图)。注意 worker 内核里 `src/contour-worker.js` 有一份**手工镜像**的同名函数——构建脚本只抠函数不抠常量,闭包名缺失会在 worker 首帧抛 ReferenceError(见 worker 一节的教训)。
477
+ 改造后 375×812 实测:路径数降到 ~54(另有种子随机性),最薄分区墨迹 **3.56%**(8×5 网格,0 空白格),嵌套环从 8~9 层降到 3~4 层。回归锁在 `test/contour-density.test.js`(≥640px 断言出厂值逐值相等;小屏断言按公式递减且能完整成图)。注意 worker 内核里 `src/contour/contour-worker.js` 有一份**手工镜像**的同名函数——构建脚本只抠函数不抠常量,闭包名缺失会在 worker 首帧抛 ReferenceError(见 worker 一节的教训)。
478
478
 
479
479
  ### 重启动画的首帧曾是空转
480
480
 
@@ -924,7 +924,7 @@ div.pI_x6G_centerCol
924
924
 
925
925
  **为什么第一次"修完"看起来像没生效。** 在浏览器控制台里量了一次真实 DOM,得到的三个值把三种可能一次分清了:`HASNEW true`(我改的规则**已经到页面**,所以不是缓存/没刷新)、`ACCENT #fff500`(变量有值,不是变量问题)、`BG rgb(36, 38, 36)`(最终仍是上游灰底)+祖先链里那个 `DIV.`。**一条"已经生效但什么都没匹配到"的规则,和一条"根本没送到浏览器"的规则,症状完全一样**;不先在页面里量这三个值,只会在"是不是缓存"和"选择器又写错了"之间来回猜——本次就是这样多花了一整轮。
926
926
 
927
- **为什么整套验证都没发现。** `test/shoot.js` 的截图夹具先后把标签直接挂在 `.wSkVaW_header` 和 `.wSkVaW_headerActions` 底下,**两次都是照着当时的选择器搭的**;`test/preset-chip.test.js` 的夹具也一样漏了那层无 class 包裹层。于是选择器在夹具上命中、截图正常、测试全绿,而真实页面一个元素都没匹配到。这是本次最值得记的教训:**夹具一旦是为了"让选择器通过"而搭的,它就从验证退化成了同义反复。**
927
+ **为什么整套验证都没发现。** `test/tools/shoot.js` 的截图夹具先后把标签直接挂在 `.wSkVaW_header` 和 `.wSkVaW_headerActions` 底下,**两次都是照着当时的选择器搭的**;`test/preset-chip.test.js` 的夹具也一样漏了那层无 class 包裹层。于是选择器在夹具上命中、截图正常、测试全绿,而真实页面一个元素都没匹配到。这是本次最值得记的教训:**夹具一旦是为了"让选择器通过"而搭的,它就从验证退化成了同义反复。**
928
928
 
929
929
  **最终修法:不再数层数,改用徽章自身的特征。**
930
930
 
@@ -990,10 +990,37 @@ fO69Vq_addButton / _3nPmjq_addButton (旧 `:not()` 排除的两个)
990
990
  | 输入区 `+` hover(暗色) | 18.31:1(本来正常) | 18.31:1(**未因收敛作用域而丢失**) |
991
991
  | 输入区 `+` 常态墨色 | `#fff500` | `#fff500`(作用域仍命中) |
992
992
 
993
- `test/hover-check.js` 现在同时守这三面(编辑按钮 / 模型页添加按钮及其容器 / 输入区 `+`),`test/selector-guard.test.js` 新增第三条断言:**任何裸的 `[class*='_add']` 都要报错**,把这条钩子钉在输入区作用域里。
993
+ `test/tools/hover-check.js` 现在同时守这三面(编辑按钮 / 模型页添加按钮及其容器 / 输入区 `+`),`test/selector-guard.test.js` 新增第三条断言:**任何裸的 `[class*='_add']` 都要报错**,把这条钩子钉在输入区作用域里。
994
994
 
995
995
  > 亮色模式本来就不受影响(该规则是 `body[data-ds-dark-theme]` 限定),测试也如实只在暗色下报红——这本身就是"作用域写对了"的证据。
996
996
 
997
+ ### 九类:`will-change` 也建立 containing block(审批白条塌成一条线)
998
+
999
+ 审批白条是 `position: fixed; inset: 0` 的全屏遮罩 + 通栏纸带,靠「遮罩只在纸带上下露出」还原参考图。这条设计有一个从未写进铁律的前提:**面板到视口之间,没有祖先构成 containing block**。旧注释其实意识到了这件事,但结论写反了——它说「若将来有祖先带 transform/filter,纸带只是退回文档流,位置降级,不会坏」。
1000
+
1001
+ DSH 0.2.2-rc.1 给 composer seat 加了 `will-change: transform`。按 CSS Will Change 的规定,**会让元素生成 containing block 的属性出现在 `will-change` 里时,元素必须真的生成 containing block**——所以它与真 transform 等价。审批面板替换输入框、正好住在这个 seat 内部,于是 fixed 的参照系从视口变成 seat 的 padding box;fixed 又让面板脱离文档流,seat 唯一在流内的子元素随之消失、高度塌成 0:
1002
+
1003
+ | 量 | 修复前 | 修复后 |
1004
+ | --- | --- | --- |
1005
+ | seat 的计算值 `will-change` | `transform` | `auto` |
1006
+ | 白条(fixed 层)rect | `720×0`(列宽 × 0 高,贴在窗口底边) | `1200×800`(整个视口) |
1007
+ | 卡片 rect | `706..895`,下半截被视口裁掉 | `306..495`,居中 |
1008
+ | 胶囊 rect | 落在视口外,点不到 | `190×41`,`y=430` |
1009
+
1010
+ 「退化而不是坏掉」错在**以为 fixed 会失效退回静态定位**。fixed 不会退回:它照旧按 containing block 布局,只是那个盒子刚被自己清空。于是症状不是「位置偏了一点」,而是「整个对话框只剩一行字」。现场为此多花了 152 秒才点下审批(主题关闭时同样的操作 1–2 秒)。
1011
+
1012
+ 修法的方向也只能反过来:**CSS 无法从 fixed 后代内部逃出 containing block**,补丁必须打在祖先上,而且只在需要时打——
1013
+
1014
+ ```css
1015
+ body:has([data-approval-key]) :is([data-composer-seat], [class$='_composerSeat']) {
1016
+ will-change: auto !important;
1017
+ }
1018
+ ```
1019
+
1020
+ `!important` 不是保险,是必需的:上游那条是 `.<hash>_root[data-phase=active] .<hash>_composerSeat`(0,3,0),本规则(0,2,1)压不过。`:has()` 让上游的合成提示只在「没有审批在屏幕上」的帧里失效,代价压到最小。两个钩子取并集的理由同八类:`[class$=]` 在上游给该元素追加第二个类时静默失效,`[data-composer-seat]` 是上游自己声明的 e2e 锚点。
1021
+
1022
+ 这同时解释了为什么**报告者给的补丁能修好、但会退化**:把 root 改成 `position: relative` + 透明背景,面板回到文档流,功能立刻恢复,但通栏纸带被收进聊天列宽、上下遮罩一并消失——那是「把设计也一起删了」,不是等价修复。修在祖先上,两样都留着。
1023
+
997
1024
  ---
998
1025
 
999
1026
  ## 验证方法论
@@ -1026,7 +1053,7 @@ fO69Vq_addButton / _3nPmjq_addButton (旧 `:not()` 排除的两个)
1026
1053
 
1027
1054
  `settings-buttons.test.js` 读计算样式,所以它用同特异度的 `.HOVERPROBE` 类替代 `:hover`(类与伪类特异度同为 0,1,0,层叠结果不变)。这是合理的层叠等价,但**反向对照暴露了它的边界**:把主题规则里真正的 `:hover` 那一半删掉,该测试**照样全绿**——因为 `.HOVERPROBE` 那一半独自扛住了。
1028
1055
 
1029
- 因此补了 `test/hover-check.js`:通过 DevTools 协议**真的移动鼠标**到按钮上,再截图量字形与填充的对比度。同样的反向对照下,它如实报出 1.05:1 / 2.61:1,与反馈截图逐位吻合。
1056
+ 因此补了 `test/tools/hover-check.js`:通过 DevTools 协议**真的移动鼠标**到按钮上,再截图量字形与填充的对比度。同样的反向对照下,它如实报出 1.05:1 / 2.61:1,与反馈截图逐位吻合。
1030
1057
 
1031
1058
  ### 做「有字 / 无字」差分时要保留一个零宽空格
1032
1059
 
package/docs/features.md CHANGED
@@ -96,7 +96,7 @@
96
96
  - **线** = marching squares 在 21 个等间距高度上取等值线
97
97
  - **连线** = 线段按边 ID 缝合成连续折线,再以二次曲线绘制
98
98
 
99
- **密度随视口自适应**:上面的 21 级 / 22 岛是按桌面窗口调的,按「每面积」施加在窄窗口就会读作一堆乱线。短边低于 640px 时,层级数向 10、岛屿数向 12 随宽度线性递减(320px 触底),线距随之拉开;**短边 ≥640px 逐字节回到出厂密度**,桌面输出不变。覆盖率验证器(accept-or-reroll)按「该视口实际将画的层级数」评分,小窗的无空白区保证不变。见 `contourLevelsFor` / `contourBumpsFor`。
99
+ **密度随视口自适应**:上面的 21 级 / 22 岛是按桌面窗口调的,按「每面积」施加在窄窗口就会读作一堆乱线。短边低于 640px 时,层级数与岛屿数都向 10 随宽度线性递减(320px 触底),线距随之拉开;**短边 ≥640px 逐字节回到出厂密度**,桌面输出不变。覆盖率验证器(accept-or-reroll)按「该视口实际将画的层级数」评分,小窗的无空白区保证不变。见 `contourLevelsFor` / `contourBumpsFor`。
100
100
  算法细节与实测数据见 [engineering-notes.md § 等高线](engineering-notes.md#等高线背景).
101
101
 
102
102
  **图层挂在应用外框内部**,`inset:0; z-index:0`,落在「外框底色之上、所有定位子元素之下」。正文永远在图层之上,前景对比度不受影响。挂载期间会把外框、对话列、右侧栏列(0.1.x 叫详情列)与侧栏的不透明底色置为透明(`:has()` 守卫使功能关闭时全部规则失效)。
Binary file
package/docs/testing.md CHANGED
@@ -91,6 +91,38 @@ _centerCol > _header > _titleRow > _titleCluster > _headerActions
91
91
 
92
92
  ---
93
93
 
94
+ ## 审批白条与祖先的 containing block(issue #41)
95
+
96
+ ```bash
97
+ node test/approval-band-transform.test.js # 真实浏览器:三个座位,同一份白条
98
+ ```
99
+
100
+ **`approval-band-transform.test.js`** 守的是「CSS 写对了、位置却归浏览器管」这一类失效。审批白条的设计是 `position: fixed; inset: 0` 铺满视口、卡片被压成通栏纸带,两者之差就是「上下有遮罩」;但 fixed 只在**祖先不构成 containing block** 时才落到视口。DSH 0.2.2-rc.1 给 composer seat 加了 `will-change: transform`——按 CSS Will Change 的规定,会让元素生成 containing block 的属性写进 `will-change` 就必须真的生成——而审批面板正好替换输入框、住在这个 seat 里。
101
+
102
+ 失效的样子非常安静:fixed 让面板脱离文档流 → seat 的**唯一**在流内子元素消失 → seat 高度塌成 0 → 白条解析成「列宽 × 0 高」的一条线,卡片以这条零高线居中,下半截被视口裁掉,只剩顶部「● 等待审批」。现场反馈是同一台机器上审批从 1–2 秒变成 152 秒。
103
+
104
+ 修法只能打在祖先上(CSS 无法从 fixed 后代内部逃出 containing block),且限定在面板存在时生效:
105
+
106
+ ```css
107
+ body:has([data-approval-key]) :is([data-composer-seat], [class$='_composerSeat']) {
108
+ will-change: auto !important;
109
+ }
110
+ ```
111
+
112
+ 夹具同时挂三个座位,**除座位外完全一样**——少一个,断言就会为错误的理由通过:
113
+
114
+ | 座位 | 钩子 | 用途 | 实测 |
115
+ | --- | --- | --- | --- |
116
+ | `#seatRaw` | 带 `will-change`,两个钩子都不匹配 | 反向对照:修复前的形状 | 白条 `720×0`,卡片下沿越界被裁 |
117
+ | `#seat` | `[class$='_composerSeat']` | 类名后缀钩子 | 白条 `1200×800`,胶囊 `190×41` 在视口内 |
118
+ | `#seatAttr` | `[data-composer-seat]` | 上游自己声明的 e2e 锚点 | 同上 |
119
+
120
+ 四条断言缺一不可:反向对照必须**仍然塌成一条线**(否则夹具不再复现 #41,其余断言就是空过);两个钩子都要把 `will-change` 降成 `auto` 并让白条回到视口大小;两颗胶囊必须落在视口内且可命中(`≥150×24`);遮罩层必须仍高于纸带(否则「上下有遮罩」这个形状本身没了)。
121
+
122
+ > 变异验证:把规则里的 `will-change: auto` 换成 `transform` → 第 2 段断言红(`got transform`),证明这条测试量的确实是那一条声明,不是纸张颜色或别的巧合。
123
+
124
+ ---
125
+
94
126
  ## 样式表投递与幂等
95
127
 
96
128
  ```bash
@@ -126,8 +158,8 @@ npm run test:corners # 源码级:stadium/圆形规则
126
158
  node test/palette-contrast.test.js # 两套配色每个角色的对比度(从真实 CSS 里读值)
127
159
  node test/palette-switch.test.js # 真实浏览器里切换配色,22 项断言
128
160
  node test/settings-buttons.test.js # 强调色底上的按钮文字对比度(32 项,含回归守卫)
129
- node test/hover-check.js # 用 CDP 真的移动鼠标,验证真实 :hover 规则(3 个面,24 项)
130
- node test/verify-shots.js # 解码四张截图统计强调色像素
161
+ node test/tools/hover-check.js # 用 CDP 真的移动鼠标,验证真实 :hover 规则(3 个面,24 项)
162
+ node test/tools/verify-shots.js # 解码四张截图统计强调色像素
131
163
  ```
132
164
 
133
165
  **`palette-contrast.test.js`** 从 `client.js` 的实际样式表里把变量读出来再验算。覆盖 27 项:实心底 + 墨色字达 AA、悬停底同样达标、回合状态四个色标对**两种**可能底色都达 AA、暗色强调色作图标墨色 ≥3、两配色的等高线合成对比度相差 ≤20% 且高于 1.06 感知下限、hero 光晕不比原品牌蓝更响(该 `*_heroGlow` 模块在 0.2 已不存在,规则保留为自愈钩子)、两配色确实不同、强调色写成 6 位十六进制,以及**武陵青的亮度必须落在 45%–56% 区间且留在青碧色轴上**。
@@ -323,7 +355,7 @@ node test/loader-late-prefs.test.js # 设置节晚于 apply() 到达时,动
323
355
 
324
356
  ```bash
325
357
  node test/thunder-edges.test.js # 边沿/生命周期/样式契约
326
- node test/thunder-shot.js # 真实渲染截图 + 像素对比度断言
358
+ node test/tools/thunder-shot.js # 真实渲染截图 + 像素对比度断言
327
359
  node test/thunder-dismiss.test.js # 点击关闭:真实指针事件 + 命中测试 + 监听器核账
328
360
  ```
329
361
 
@@ -356,7 +388,8 @@ node test/contour-perf.test.js # 稳态帧成本(n=80)
356
388
  node test/contour-density.test.js # 视口密度自适应(≥640px 出厂值不变,小屏递减)
357
389
  node test/contour-bounds.test.js # 扫描边界与全扫的逐坐标等价
358
390
  node test/contour-worker.test.js # worker 内核与主线程一致
359
- node test/shoot.js # 输出亮/暗 × 两配色共四张截图供肉眼复核
391
+ node test/contour-worker-budget.test.js # worker 背板预算:超大视口缩 dpr,普通视口逐位不变
392
+ node test/tools/shoot.js # 输出亮/暗 × 两配色共四张截图供肉眼复核
360
393
  ```
361
394
 
362
395
  **`contour-render.test.js`** 覆盖:关闭时不创建节点且**不改动应用底色**;开启时画布挂进应用外框、图层确实上色、不透明底色已让位;正文颜色不变且仍可命中测试(图层在其**之下**);动画开启时像素随时间变化、关闭后**完全静止**、**重新开启后再次变化**;暗色仍上色;拆除后节点归零。
@@ -381,7 +414,7 @@ node test/shoot.js # 输出亮/暗 × 两配色共四张截
381
414
 
382
415
  该脚本对 24 / 60 / 120 fps **逐个**比预算,并在第一个超标处失败——所以真正卡住它的门槛是 **8.3ms(120fps)**,不是 41.7ms:报数时别只看「41.7ms 预算」那一行。
383
416
 
384
- > **提取到的名字必须跟着 `client.js` 走。** 这些「原样切出」的脚本按名字抓函数与常量,切出来少一个就是页面里的 `ReferenceError`,而它只会表现为 `no result` / 一行 `CONTOUR_... is not defined`。新增或重命名内核里的函数与常量时(例如 `contourStepFor`、`CONTOUR_MAX_CELLS`、`CONTOUR_MIN_BUMPSAMPLES`、`CONTOUR_SMOOTH_*`),要同步 `contour-smoothness` / `contour-cusps` / `contour-bounds` / `contour-perf` 的名单、`scripts/build-contour-worker.js` 的 `names`,以及 `src/contour-worker.js` 顶部手工镜像的常量——worker 里少一个常量同样是运行时 `ReferenceError`。
417
+ > **提取到的名字必须跟着 `client.js` 走。** 这些「原样切出」的脚本按名字抓函数与常量,切出来少一个就是页面里的 `ReferenceError`,而它只会表现为 `no result` / 一行 `CONTOUR_... is not defined`。新增或重命名内核里的函数与常量时(例如 `contourStepFor`、`CONTOUR_MAX_CELLS`、`CONTOUR_MIN_BUMPSAMPLES`、`CONTOUR_SMOOTH_*`),要同步 `contour-smoothness` / `contour-cusps` / `contour-bounds` / `contour-perf` 的名单、`scripts/build-contour-worker.js` 的 `names`,以及 `src/contour/contour-worker.js` 顶部手工镜像的常量——worker 里少一个常量同样是运行时 `ReferenceError`。
385
418
 
386
419
  ---
387
420
 
package/index.js CHANGED
@@ -884,6 +884,13 @@ function installAudio(ctx, settingsScope) {
884
884
  const key = `${sessionId}:${data.turn}`;
885
885
  if (hasVisibleText(data.message)) terminals.set(key, true);
886
886
  else if (!terminals.has(key)) terminals.set(key, false);
887
+ /* The map is only reaped by agent/turn-stopping, and a turn that ends any
888
+ other way (agent/error, a destroyed session) never fires it — in a host
889
+ process that runs for days those entries would accumulate forever. Bound
890
+ it FIFO like playedDone below: the entries evicted earliest belong to the
891
+ oldest turns, and by the time 512 newer turns have been recorded, a
892
+ turn-stopping for the evicted one is not a live question any more. */
893
+ if (terminals.size > 512) terminals.delete(terminals.keys().next().value);
887
894
  });
888
895
 
889
896
  /* The two "a human has to do something" triggers, and the ONLY things that
package/lib/audio.js CHANGED
@@ -82,6 +82,20 @@ function pathHash(value) {
82
82
  return h.toString(16);
83
83
  }
84
84
 
85
+ /** Short stable hash of file CONTENT for the cache-file name: the same FNV-1a
86
+ * over the bytes. Content, not just size, has to be in the key — a replaced
87
+ * sound with an identical duration (e.g. a re-rendered bundled wav) is
88
+ * byte-length-identical, and a name-only key would serve the previous file's
89
+ * scaled copy forever. The wavs are a few hundred KB, so this is microseconds. */
90
+ function bytesHash(bytes) {
91
+ let h = 0x811c9dc5;
92
+ for (let i = 0; i < bytes.length; i++) {
93
+ h ^= bytes[i];
94
+ h = Math.imul(h, 0x01000193) >>> 0;
95
+ }
96
+ return h.toString(16);
97
+ }
98
+
85
99
  /**
86
100
  * The audio runtime: preference view, sound resolution, volume cache, playback
87
101
  * and the diagnostic ring the settings page reads.
@@ -105,6 +119,8 @@ class AudioRuntime {
105
119
  /** Bounded diagnostic ring: what the plugin saw and what it did about it. */
106
120
  this.diag = [];
107
121
  this.warnedNoSubprocess = false;
122
+ /** One-shot guard so an unwritable volume cache says so once, not per play. */
123
+ this.warnedCacheDir = false;
108
124
  }
109
125
 
110
126
  /** Record one diagnostic line and echo it to the host console. */
@@ -255,14 +271,23 @@ class AudioRuntime {
255
271
  if (scaled === bytes) return file; // non-rescalable format: original level
256
272
  try {
257
273
  fs.mkdirSync(CACHE_DIR, { recursive: true });
258
- /* The cache key covers the SOURCE DIRECTORY too: basename + size alone
259
- would let a turn-done.wav from the custom dir collide with an unrelated
260
- same-named, same-length file from the desktop, and the second user
261
- would hear the first one's sound. */
262
- const target = path.join(CACHE_DIR, `${path.basename(file, path.extname(file))}-v${volume}-${bytes.length}-${pathHash(file)}.wav`);
274
+ /* The cache key covers the SOURCE DIRECTORY (basename + size alone would
275
+ let a turn-done.wav from the custom dir collide with an unrelated
276
+ same-named, same-length file from the desktop) AND the source content
277
+ (a replaced file with an identical duration is byte-length-identical,
278
+ and only the content distinguishes its scaled copy from the old one). */
279
+ const target = path.join(CACHE_DIR,
280
+ `${path.basename(file, path.extname(file))}-v${volume}-${bytes.length}-${bytesHash(bytes)}-${pathHash(file)}.wav`);
263
281
  if (!fs.existsSync(target)) fs.writeFileSync(target, scaled);
264
282
  return target;
265
283
  } catch (error) {
284
+ /* An unwritable cache dir (shared/stale tmp on POSIX) must not disable
285
+ volume scaling SILENTLY — say so once, then keep degrading to the
286
+ unscaled file like every other failure here. */
287
+ if (!this.warnedCacheDir) {
288
+ this.warnedCacheDir = true;
289
+ this.note('volume-cache-unwritable', `serving sounds unscaled; cache dir issue: ${error && error.message ? error.message : error}`);
290
+ }
266
291
  return file;
267
292
  }
268
293
  }
package/lib/slots.js CHANGED
@@ -63,9 +63,10 @@ const SLOTS = {
63
63
  },
64
64
 
65
65
  /**
66
- * 需要你回应 (reserved) —— human attention required. Descending minor second
66
+ * 需要你回应 —— human attention required. Descending minor second
67
67
  * with a detuned partial beats against the fundamental: unresolved on
68
- * purpose, so it does not read as "done".
68
+ * purpose, so it does not read as "done". Wired to approval/request and
69
+ * user-questions/request by the host half.
69
70
  */
70
71
  attention: {
71
72
  duration: 1.0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-theme-endfield",
3
- "version": "1.2.3",
3
+ "version": "1.2.4",
4
4
  "description": "DeepSeek Harness web theme: the industrial editorial look of the Arknights: Endfield official site — cream paper, ink text, signal-yellow accents, zero radius. Settings persist through the DSH volatile Config + configForms transport (0.2.0-rc.2 and 0.1.7+), with a ctx.settingsScope fallback for older hosts.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -32,13 +32,13 @@
32
32
  "test:contour": "node test/contour-render.test.js",
33
33
  "test:settings": "node test/settings-rows.test.js && node test/settings-durable-hold.test.js && node test/settings-config-forms.test.js && node test/settings-config-fallback.test.js && node test/settings-namespace.test.js && node test/settings-off.test.js && node test/settings-locale.test.js",
34
34
  "test:font": "node test/font-scope.test.js",
35
- "test:thunder": "node test/thunder-edges.test.js && node test/thunder-shot.js && node test/thunder-dismiss.test.js",
35
+ "test:thunder": "node test/thunder-edges.test.js && node test/tools/thunder-shot.js && node test/thunder-dismiss.test.js",
36
36
  "test:palette": "node test/palette-contrast.test.js && node test/palette-switch.test.js",
37
37
  "test:menu": "node test/menu-surface.test.js dark && node test/menu-surface.test.js light",
38
38
  "test:selector": "node test/selector-guard.test.js && node test/preset-chip.test.js",
39
- "test:buttons": "node test/settings-buttons.test.js && node test/hover-check.js",
39
+ "test:buttons": "node test/settings-buttons.test.js && node test/tools/hover-check.js",
40
40
  "test:balance": "node test/balance-window.test.js && node test/balance-capsule.test.js && node test/credit-capsule.test.js",
41
- "test:live": "node test/live-check.js",
41
+ "test:live": "node test/tools/live-check.js",
42
42
  "test:specks": "node test/contour-specks.test.js",
43
43
  "test:watermark": "node test/watermark-stacking.test.js",
44
44
  "test:smoothness": "node test/contour-smoothness.test.js",
@@ -47,10 +47,10 @@
47
47
  "test:coverage": "node test/contour-coverage.test.js",
48
48
  "test:perf": "node test/contour-perf.test.js",
49
49
  "test:loader": "node test/loader-performance.test.js && node test/loader-late-prefs.test.js",
50
- "test": "node check.js && node selftest.js && node test/font-scope.test.js && node test/palette-contrast.test.js && node test/palette-switch.test.js && node test/menu-surface.test.js dark && node test/menu-surface.test.js light && node test/settings-buttons.test.js && node test/hover-check.js && node test/selector-guard.test.js && node test/preset-chip.test.js && node test/balance-window.test.js && node test/balance-capsule.test.js && node test/credit-capsule.test.js && node test/settings-rows.test.js && node test/settings-durable-hold.test.js && node test/settings-config-forms.test.js && node test/settings-config-fallback.test.js && node test/settings-namespace.test.js && node test/settings-off.test.js && node test/settings-locale.test.js && node test/audio-notify.test.js && node test/audio-attention-watch.test.js && node test/host-no-inject.test.js && node scripts/build-sounds.js --check && node test/thunder-edges.test.js && node test/thunder-shot.js && node test/thunder-dismiss.test.js && node test/contour-render.test.js && node test/loader-performance.test.js && node test/loader-late-prefs.test.js && node test/contour-specks.test.js && node test/watermark-stacking.test.js && node test/contour-smoothness.test.js && node test/contour-cusps.test.js && node test/contour-a11y.test.js && node test/contour-coverage.test.js && node test/contour-perf.test.js && node test/contour-density.test.js && node test/contour-bounds.test.js && node scripts/build-host.js --check && node scripts/build-client.js --check && node scripts/build-contour-worker.js --check && node test/worker-eol.test.js && node test/contour-worker.test.js && node test/theme-sheet-refresh.test.js && node test/titlebar-probe.test.js && node test/corner-shape.test.js && node test/glass.test.js && node test/contour-trail.test.js && node test/contour-trail-browser.test.js && node test/scroll-anim.test.js && node test/contour-loop-reentry.test.js",
51
- "test:ci": "node check.js && node selftest.js && node test/font-scope.test.js && node test/palette-contrast.test.js && node test/palette-switch.test.js && node test/menu-surface.test.js dark && node test/menu-surface.test.js light && node test/settings-buttons.test.js && node test/hover-check.js && node test/selector-guard.test.js && node test/preset-chip.test.js && node test/balance-window.test.js && node test/balance-capsule.test.js && node test/credit-capsule.test.js && node test/settings-rows.test.js && node test/settings-durable-hold.test.js && node test/settings-config-forms.test.js && node test/settings-config-fallback.test.js && node test/settings-namespace.test.js && node test/settings-off.test.js && node test/settings-locale.test.js && node test/audio-notify.test.js && node test/audio-attention-watch.test.js && node test/host-no-inject.test.js && node scripts/build-sounds.js --check && node test/thunder-edges.test.js && node test/thunder-shot.js && node test/thunder-dismiss.test.js && node test/contour-render.test.js && node test/loader-performance.test.js && node test/loader-late-prefs.test.js && node test/contour-specks.test.js && node test/watermark-stacking.test.js && node test/contour-smoothness.test.js && node test/contour-cusps.test.js && node test/contour-a11y.test.js && node test/contour-bounds.test.js && node scripts/build-host.js --check && node scripts/build-client.js --check && node scripts/build-contour-worker.js --check && node test/worker-eol.test.js && node test/contour-worker.test.js && node test/theme-sheet-refresh.test.js && node test/titlebar-probe.test.js && node test/corner-shape.test.js && node test/glass.test.js && node test/contour-trail.test.js && node test/contour-trail-browser.test.js && node test/scroll-anim.test.js && node test/contour-loop-reentry.test.js",
52
- "shots": "node test/shoot.js",
53
- "shots:verify": "node test/shoot.js && node test/verify-shots.js",
50
+ "test": "node check.js && node selftest.js && node test/import-probe.test.js && node test/font-scope.test.js && node test/palette-contrast.test.js && node test/palette-switch.test.js && node test/menu-surface.test.js dark && node test/menu-surface.test.js light && node test/settings-buttons.test.js && node test/tools/hover-check.js && node test/selector-guard.test.js && node test/preset-chip.test.js && node test/balance-window.test.js && node test/balance-capsule.test.js && node test/credit-capsule.test.js && node test/settings-rows.test.js && node test/settings-durable-hold.test.js && node test/settings-config-forms.test.js && node test/settings-config-fallback.test.js && node test/settings-namespace.test.js && node test/settings-off.test.js && node test/settings-locale.test.js && node test/audio-notify.test.js && node test/audio-attention-watch.test.js && node test/host-no-inject.test.js && node scripts/build-sounds.js --check && node test/thunder-edges.test.js && node test/tools/thunder-shot.js && node test/thunder-dismiss.test.js && node test/contour-render.test.js && node test/loader-performance.test.js && node test/loader-late-prefs.test.js && node test/contour-specks.test.js && node test/watermark-stacking.test.js && node test/contour-smoothness.test.js && node test/contour-cusps.test.js && node test/contour-a11y.test.js && node test/contour-coverage.test.js && node test/contour-perf.test.js && node test/contour-density.test.js && node test/contour-bounds.test.js && node scripts/build-host.js --check && node scripts/build-client.js --check && node scripts/build-contour-worker.js --check && node test/worker-eol.test.js && node test/contour-worker.test.js && node test/contour-worker-budget.test.js && node test/theme-sheet-refresh.test.js && node test/titlebar-probe.test.js && node test/corner-shape.test.js && node test/approval-band-transform.test.js && node test/glass.test.js && node test/contour-trail.test.js && node test/contour-trail-browser.test.js && node test/scroll-anim.test.js && node test/contour-loop-reentry.test.js",
51
+ "test:ci": "node check.js && node selftest.js && node test/import-probe.test.js && node test/font-scope.test.js && node test/palette-contrast.test.js && node test/palette-switch.test.js && node test/menu-surface.test.js dark && node test/menu-surface.test.js light && node test/settings-buttons.test.js && node test/tools/hover-check.js && node test/selector-guard.test.js && node test/preset-chip.test.js && node test/balance-window.test.js && node test/balance-capsule.test.js && node test/credit-capsule.test.js && node test/settings-rows.test.js && node test/settings-durable-hold.test.js && node test/settings-config-forms.test.js && node test/settings-config-fallback.test.js && node test/settings-namespace.test.js && node test/settings-off.test.js && node test/settings-locale.test.js && node test/audio-notify.test.js && node test/audio-attention-watch.test.js && node test/host-no-inject.test.js && node scripts/build-sounds.js --check && node test/thunder-edges.test.js && node test/tools/thunder-shot.js && node test/thunder-dismiss.test.js && node test/contour-render.test.js && node test/loader-performance.test.js && node test/loader-late-prefs.test.js && node test/contour-specks.test.js && node test/watermark-stacking.test.js && node test/contour-smoothness.test.js && node test/contour-cusps.test.js && node test/contour-a11y.test.js && node test/contour-density.test.js && node test/contour-bounds.test.js && node scripts/build-host.js --check && node scripts/build-client.js --check && node scripts/build-contour-worker.js --check && node test/worker-eol.test.js && node test/contour-worker.test.js && node test/contour-worker-budget.test.js && node test/theme-sheet-refresh.test.js && node test/titlebar-probe.test.js && node test/corner-shape.test.js && node test/approval-band-transform.test.js && node test/glass.test.js && node test/contour-trail.test.js && node test/contour-trail-browser.test.js && node test/scroll-anim.test.js && node test/contour-loop-reentry.test.js",
52
+ "shots": "node test/tools/shoot.js",
53
+ "shots:verify": "node test/tools/shoot.js && node test/tools/verify-shots.js",
54
54
  "test:bounds": "node test/contour-bounds.test.js",
55
55
  "build:host": "node scripts/build-host.js",
56
56
  "check:host": "node scripts/build-host.js --check",