From f5c4f56682df06b9f3cc493245e64dea94ae35c8 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Mon, 14 Sep 2026 17:13:19 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B7=A8=E7=AB=AF:=20feat(=E9=A2=84=E8=AE=BE):?= =?UTF-8?q?=20=E9=9D=99=E9=BB=98=E5=85=9C=E5=BA=95=E7=95=99=E4=B8=80?= =?UTF-8?q?=E6=9D=A1=E5=8F=AF=E8=A7=82=E6=B5=8B=E7=97=95=E8=BF=B9=EF=BC=9B?= =?UTF-8?q?"=E5=8F=AA=E5=86=99=E4=B8=8D=E8=AF=BB"=E5=9C=A8=E9=B8=BF?= =?UTF-8?q?=E8=92=99=E4=BE=A7=E4=B9=9F=E8=A1=A5=E4=B8=8A=E5=88=A4=E6=8D=AE?= =?UTF-8?q?=EF=BC=88=E7=85=A7=20LEGACY=5FBACKUP=5FKEY=20=E7=85=A7=E6=90=AC?= =?UTF-8?q?=EF=BC=89=EF=BC=9B=C2=A710=20=E5=87=BA=E5=A4=84=E8=90=BD?= =?UTF-8?q?=E5=9C=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pi 2026-09-14 的三小条。 1. **§3 静默兜底要留可观测痕迹**:`normalizePreset` 把认不出的 id 换成 aurora 这件事, 原先在真实环境里**不留任何痕迹** —— §10 的登记只防"被误报成 bug",防不住 "没人知道它正在发生"。现在 `BackgroundPlan.presetSubstitutedFrom` 带出**原来那个 id** (换过非空、没换过空串),页面据此打一行日志 ⇒ 后果从"可能发生"变成"**可数**"。 **痕迹记在返回值里而不是在这一层直接打日志**,理由写进代码:这一层是**纯逻辑** (无 `@ohos` 依赖 ⇒ 判据能用 node strip-types 直接跑它);为打一行日志引入 `@ohos.hilog`, 等于把"能真跑的行为判据"换成"只能读源码的形态判据"——不划算的交易。 **判据两条方向都钉**:替换必须留痕(且带出原 id);**没替换时必须为空** (痕迹退化成噪声就等于没有)。变异与正反例都在(5 条全绿)。 2. **§2 §10 的"谁批准"落成出处**:那一格原写"产品决定" —— 按 pi 的话这是**事后追认**。 现在写的是 **「无人类批准:这是实现时的默认行为(随 `model/Wallpaper.ts` 引入, `git log --diff-filter=A` 可查出处,2e42aac),本行是补登记」**,并把**意图**与**批准**分开写 (不拿意图冒充批准)。另两行也补了出处(`Appearance.ts:39,52,88,104,133` / `AppearanceStore.ets:142-195` / `backgroundStore.ts:200`)。 3. **§1 "可扫的字段清册"不必先造 —— 同形状已有一边是判据**:他说得对。 WebUI 的 `LEGACY_BACKUP_KEY` 早就钉着"只写不读",鸿蒙的 `bgBlur` 只有文档 ⇒ 差的是**同一个形状只有一边有判据**。已照搬:`harmony-appearance.test.mjs` 新增 **"消费侧出现次数必须为 0"**;豁免**按文件登记 + 写理由**(域模型 / 状态同步 / 线上 DTO 三处是搬运与传输,不是消费)——与 `mail_status_readers_test.go` 的豁免同一形状, 登记表本身就是清册,不必另造一张。 变异验证:让 MainPage 读一次 `bgBlur` → 判据红,红的信息写着"**停下:那时必须先补 px ↔ 材质档位的映射判据**,而不是把登记值从 0 改成 1"。 SUITE 计数同步:harmony-presets 4→5、harmony-appearance 24→25。 --- client/electron/test/CRITERIA.md | 4 +- .../electron/test/harmony-appearance.test.mjs | 52 +++++++++++++++++++ client/electron/test/harmony-presets.test.mjs | 26 ++++++++++ client/electron/test/run-all.mjs | 4 +- .../entry/src/main/ets/model/Wallpaper.ts | 19 +++++++ 5 files changed, 101 insertions(+), 4 deletions(-) diff --git a/client/electron/test/CRITERIA.md b/client/electron/test/CRITERIA.md index b4a22ac..b50012b 100644 --- a/client/electron/test/CRITERIA.md +++ b/client/electron/test/CRITERIA.md @@ -350,8 +350,8 @@ pi 2026-09-14 提的形状:**不是无条件 fail-closed** —— 缺字段可 它的注释写明"省略或为空 = **不受限**",因为 17 个存量清单都没有这个字段, 若把空声明当最小权限,它们会全部静默失去 IO 注入与记忆读写。 -| **未知的预设 id**(`layersFor`/`normalizePreset`) | **静默替换为 `aurora`** | **宽(静默)** | 产品决定:背景不该因未知 id 变成空白。**这是"静默给别人的档",不是"给一个安全的空值"** —— 用户不会看到错误,但会看到**别人的档** | -| 只写不读的字段:鸿蒙 `bgBlur` | **写进去、存下来、同步它,但没有任何消费点** | 无(当前无效果) | 核实于 2026-09-14:`Appearance.ts` clamp 存入、`AppearanceStore` 同步,**无页面读它**(材质档位是固定枚举)。⇒ 钉"px ↔ 档位映射表"会是**假判据**(钉一张不存在的表);将来开始消费时**必须**补映射判据 | +| **未知的预设 id**(`layersFor`/`normalizePreset`) | **静默替换为 `aurora`** | **宽(静默)** | **无人类批准**:这是实现时的默认行为(`client/harmony/entry/src/main/ets/model/Wallpaper.ts` 随该文件一起引入,`git log --diff-filter=A` 可查出处),**本行是补登记**(pi 2026-09-14:写成"产品决定"就是事后追认)。意图(写在这里,不冒充批准):背景不该因未知 id 变成空白。**这是"静默给别人的档",不是"给一个安全的空值"** —— 用户不会看到错误,但会看到**别人的档**;替换发生时会留下痕迹(`BackgroundPlan.presetSubstitutedFrom`,页面据此打日志,判据见 `harmony-presets.test.mjs`) | +| 只写不读的字段:鸿蒙 `bgBlur` | **写进去、存下来、同步它,但没有任何消费点** | 无(当前无效果) | 出处:`model/Appearance.ts:39,52,88,104,133`(clamp + 合并)+`common/AppearanceStore.ets:142-195`(同步);**消费侧 0 处**。⇒ 钉"px ↔ 档位映射表"会是**假判据**(钉一张不存在的表);将来开始消费时**必须**补映射判据。**该侧现在也有判据**(`harmony-appearance.test.mjs`:消费侧出现次数必须为 0),与 WebUI `LEGACY_BACKUP_KEY` 那条同形 | | 只写不读的字段:WebUI `LEGACY_BACKUP_KEY` | 删旧值前**另存一份**,没有任何代码读它 | 无(有意为之) | `backgroundStore.ts:200` 的注释写明目的(否则它会变成新的继承源)。**登记在此是为了审计能一次找全** | | 邮件的 `permission_mode` | **不写、不改档**(不是写默认档) | 窄(fail-closed) | 本仓 2026-09-14:缺字段被 `\|\| 'workspace'` 兜成窄档,等于"一个 bug 以正常形态活着";守卫见 `plugins/*/lib/permission-mode.js` 的 `modeForStateWrite` | | HomeAgent `plugin.json` 的 `sdk` | **内核不读**(字段只对人有效) | 无(不是语义,是文档) | 内核 `internal/plugin/manifest.go` 的结构体里没有该字段;`registry.go` 的 `loadOne` 只用 `NameZh/NameEn` | diff --git a/client/electron/test/harmony-appearance.test.mjs b/client/electron/test/harmony-appearance.test.mjs index a0c1b42..fa78515 100644 --- a/client/electron/test/harmony-appearance.test.mjs +++ b/client/electron/test/harmony-appearance.test.mjs @@ -755,3 +755,55 @@ test('★ 遮盖色方向:`mask_*` 两套主题下都是**深色**(模态遮 assert.match(settings, /Theme\.overlay/, '自绘弹层的遮罩仍然用 mask(那里的语义是压暗背后)'); assert.ok(!/wallpaperScrim/.test(settings), '弹层不该用壁纸遮盖色(两个语义别混)'); }); + +/** + * ★ `bgBlur` 是"只写不读"的字段:**消费侧出现次数必须为 0**(pi 2026-09-14 指出同一形状只有一边有判据)。 + * + * WebUI 的 `LEGACY_BACKUP_KEY` 早就钉着"只写不读,否则它会变成新的继承源"; + * 而鸿蒙侧 `bgBlur`(`Appearance.ts` clamp 存入、`AppearanceStore` 同步)**只有文档**。 + * 同一个形状只有一边有判据 ⇒ 这一边补上,不必等一张新的字段清册: + * **登记表本身就是清册**(与 `server/internal/repo/mail_status_readers_test.go` 的 + * "文件 + 出现次数"同一个模板,只是这里的登记值是 0)。 + * + * 消费侧一旦出现(有人按 px 选档位/设模糊半径)本条就红 —— 那是**必须停下来**的时刻: + * 那时要补的是"px ↔ 材质档位"的**映射判据**(见 CRITERIA.md §10 与计划文档 §7.12), + * 而不是把次数从 0 改成 1 了事。 + */ +test('★ bgBlur 只写不读:消费侧出现次数必须为 0(要消费就得先补映射判据)', () => { + const files = []; + const walk = dir => { + for (const e of readdirSync(dir, { withFileTypes: true })) { + const p = join(dir, e.name); + if (e.isDirectory()) { walk(p); continue; } + if (!/\.(ets|ts)$/.test(e.name)) continue; + files.push(p); + } + }; + walk(HARMONY_ETS); + + /* + * 豁免**按文件登记 + 写理由**(不是"凡是这几个目录都放行"):这三处是**搬运/传输**, + * 不是消费 —— 判据要挡的是"有人拿这个值去决定画什么"。 + * 与 `server/internal/repo/mail_status_readers_test.go` 的豁免同一个形状。 + */ + const plumbing = new Map([ + ['model/Appearance.ts', '域模型:clamp(0,40) 后存下 bgBlur(搬运)'], + ['common/AppearanceStore.ets', '状态同步:与快照互转(搬运)'], + ['api/AppearanceApi.ets:17', '线上 DTO 声明 bg_blur(传输格式,不是消费)'] + ]); + + const consumers = []; + for (const f of files) { + const src = prose(f); + const rel = f.slice(HARMONY_ETS.length + 1); + src.split('\n').forEach((line, i) => { + if (!/\bbgBlur\b|\bbg_blur\b/.test(line)) return; + const where = rel + ':' + (i + 1); + if (plumbing.has(rel) || plumbing.has(where)) return; + consumers.push(`${where} ${line.trim()}`); + }); + } + assert.deepEqual(consumers, [], + `有人在消费 bgBlur 了 —— 停下:那时**必须**先补"px ↔ 材质档位"的映射判据` + + `(CRITERIA.md §10 / 计划文档 §7.12),而不是把登记值从 0 改成 1:\n ${consumers.join('\n ')}`); +}); diff --git a/client/electron/test/harmony-presets.test.mjs b/client/electron/test/harmony-presets.test.mjs index 41cb2e3..dd8270e 100644 --- a/client/electron/test/harmony-presets.test.mjs +++ b/client/electron/test/harmony-presets.test.mjs @@ -105,3 +105,29 @@ test('深色档必须与浅色档不同("深色没换色" → 红)', () => { `${id} 的深色档与浅色档完全一样 ⇒ 深色模式下这一档没有换色`); } }); + +/* + * ★ 静默兜底必须留下**可观测的痕迹**(pi 2026-09-14)。 + * + * §10 登记了"未知 id 静默换成 aurora"这条缺省语义,但**登记只防"被误报成 bug"**, + * 防不住"没人知道它正在发生" —— 真实环境里它不留任何痕迹。所以返回值里带 + * `presetSubstitutedFrom`:换过就是原来那个 id,没换过是空串,页面据此打一行日志。 + * + * 这条判据同时钉住两件事:① 替换**必须**留痕;② 没替换时**不许**留痕 + * (否则痕迹会退化成噪声,等于没有)。 + */ +test('★ 静默兜底留痕:换过要带出原 id,没换过必须是空串', () => { + const unknown = 'ocean-v2-这个档还不存在'; + const bad = W.resolveBackground('preset', unknown, 0.2, false, false); + assert.equal(bad.presetId, 'aurora', `自检:未知 id 应当被换成默认档(实际 ${bad.presetId})`); + assert.equal(bad.presetSubstitutedFrom, unknown, + '认不出的 id 被静默替换了,却没留下痕迹 ⇒ 版本偏移时无法确认它在发生(只能猜)'); + + for (const id of IDS) { + for (const dark of THEMES) { + const plan = W.resolveBackground('preset', id, 0.2, false, dark); + assert.equal(plan.presetSubstitutedFrom, '', + `${id}(${themeName(dark)})是已知档,不该留下"替换过"的痕迹(痕迹变成噪声就等于没有)`); + } + } +}); diff --git a/client/electron/test/run-all.mjs b/client/electron/test/run-all.mjs index 0de7067..14402af 100644 --- a/client/electron/test/run-all.mjs +++ b/client/electron/test/run-all.mjs @@ -66,11 +66,11 @@ const SUITE = [ ['test/cross-client-theme.test.mjs', [], 15], // 预设的**行为**判据:每一档都真的画得出来(能真跑,不需要设备 ⇒ 不进 static 欠账)。 // 与 appearance-defaults 那条「清单 id/顺序相等」配对:值判据管清单,行为判据管渲染器。 - ['test/harmony-presets.test.mjs', ['--experimental-strip-types', '--no-warnings'], 4], + ['test/harmony-presets.test.mjs', ['--experimental-strip-types', '--no-warnings'], 5], ['test/harmony-logic.test.mjs', ['--experimental-strip-types', '--no-warnings'], 28], ['test/harmony-system-api.test.mjs', [], 5], // P4 外观同步:跑 model/Appearance.ts(纯逻辑),所以也要 strip-types - ['test/harmony-appearance.test.mjs', ['--experimental-strip-types', '--no-warnings'], 24], + ['test/harmony-appearance.test.mjs', ['--experimental-strip-types', '--no-warnings'], 25], // P5 悬浮玻璃导航:点击配对 / index 决定挂载 / 命中区 ≥44vp / 悬浮与让位 ['test/harmony-nav.test.mjs', ['--experimental-strip-types', '--no-warnings'], 6], // 外观契约:默认值去 Go 源码里读(服务端 DefaultAppearance 是权威)+ 缓存键按账号 diff --git a/client/harmony/entry/src/main/ets/model/Wallpaper.ts b/client/harmony/entry/src/main/ets/model/Wallpaper.ts index d4af7f3..52b3d4b 100644 --- a/client/harmony/entry/src/main/ets/model/Wallpaper.ts +++ b/client/harmony/entry/src/main/ets/model/Wallpaper.ts @@ -220,6 +220,23 @@ export class BackgroundPlan { /** 用的是深色那套色板吗(跟着主题走,不是"未验") */ dark: boolean = false; presetId: string = ''; + /** + * **静默兜底的痕迹**(pi 2026-09-14 裁定):服务端/WebUI 先加了新档、而这台设备是旧构建时, + * `normalizePreset` 会把认不出的 id **静默换成 aurora** —— 用户以为选的是新档。 + * 这条缺省语义已登记在 `CRITERIA.md` §10,但登记只防"被误报成 bug", + * **防不住"没人知道它正在发生"**(真实环境里不会留下任何痕迹)。 + * + * 所以把"发生过替换"记在这里:换过就是**原来那个 id**(非空),没换过就是空串。 + * 页面拿到非空值时**打一行日志**(`hilog`/`console.info` 皆可)—— + * 于是这个后果从"可能发生"变成"**可数**":出问题时能立刻确认是版本偏移,而不是猜。 + * + * ⚠️ 为什么痕迹记在**返回值**里、而不在这一层直接打日志:这一层是**纯逻辑** + * (无 `@ohos` 依赖 ⇒ 判据能用 node `--experimental-strip-types` 直接跑它, + * 见 `harmony-presets.test.mjs`)。为了打一行日志而引入 `@ohos.hilog`, + * 会把"能真跑的行为判据"换成"只能读源码的形态判据" —— 那是不划算的交易。 + * 记事实在这一层(可判),打日志在页面层(可观测)。 + */ + presetSubstitutedFrom: string = ''; layers: PresetLayer[] = []; /** * 遮盖浓度(0~1):**两档都用**(预设档也压),用系统遮罩色刷一层。 @@ -251,6 +268,8 @@ export function resolveBackground(bgKind: string, presetId: string, scrim: numbe if (bgKind === 'preset') { plan.kind = 'preset'; plan.presetId = normalizePreset(presetId); + // 留痕:认不出的 id 被静默替换时,把**原来那个 id**带出去(页面据此打一行日志) + plan.presetSubstitutedFrom = plan.presetId === presetId ? '' : presetId; plan.layers = layersFor(plan.presetId, dark); // 预设档**也**套遮罩:与 WebUI 的 `--bg-dim` 一致(它不区分档位),服务的是可读性 plan.scrim = scrim;