From dbdb2f5039b27881e974f0321d5ff64b469f65b9 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Mon, 14 Sep 2026 15:20:30 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B7=A8=E7=AB=AF:=20=E4=BF=AE=E6=89=93?= =?UTF-8?q?=E5=8C=85=E5=9D=91=EF=BC=88=E6=9E=84=E5=BB=BA=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E8=A2=AB=E7=AE=A1=E9=81=93=E5=90=9E=E6=8E=89=EF=BC=89+=20dist?= =?UTF-8?q?=20=E6=96=B0=E9=B2=9C=E5=BA=A6=E5=88=A4=E6=8D=AE=EF=BC=9B?= =?UTF-8?q?=E5=88=A4=E6=8D=AE=E4=BB=8E"=E7=AA=97=E5=8F=A3=E5=BC=8F"?= =?UTF-8?q?=E6=94=B9=E6=88=90=E5=90=8C=E8=A1=A8=E8=BE=BE=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 接手 pi 的 WebUI 开项途中踩到三个坑,都已修好并配判据(承接 e94313f)。 ## 坑 1:`npm run build 2>&1 | tail -4 && electron-builder …` 吞掉构建失败 pipeline 的退出码是 `tail` 的 → 构建失败、`&&` 照样往下走、electron-builder **拿旧的 dist 打了新包**。而所有判据都是绿的: - vitest 绿(见坑 2);packaging 绿(它比 dist vs 安装包,两边都是旧的,自然一致)。 新增判据:**dist 必须比 src 新**(build-stamp)。变异:`touch` 一个 src 文件 → 红; 重新 `npm run build` → 绿。错误信息写明"注意别把它的退出码丢在管道里"。 已重构建 + 重打包(deb 与 app.asar 同批,15:19)。 ## 坑 2:测试通过 ≠ 能打包 真正的失败:`AGGREGATE_ID` / `isUsableAccount` 被我当成 `accountStore` 的导出 (它们住在 `lib/accounts.ts`)。vitest 263 条全绿,生产构建直接报 `"AGGREGATE_ID" is not exported by "src/stores/accountStore.ts"` —— 测试运行时对缺的具名导出是宽容的(拿到 undefined)。**生产构建是一道独立的门。** ## 坑 3:判据写成"窗口式",被自己的变异测试抓住两次 `appearance-defaults` 的"取键函数必须把账号拼进去": 1. 第一版从 `export function` 切到 `}` → **参数表里的 accountId 满足了正则**, 把实现退回全局键仍然全绿(假判据!); 2. 第二版只取函数体 → `return accountId ? PREFIX : PREFIX;`(提了一下没用)又骗过去; 3. 第三版要求**同一个表达式里既有常量又有账号的插值/拼接**: 两种退化都红,合法的 `PREFIX + accountId` 写法仍然绿。 三种变异都验过(红/红/绿),还原后基线绿。 ## 文档 §7.12 两行改为「已修」并写明依据(默认值=服务端契约;缓存键差异已消除,旧全局键 只作一次性迁移源);新增 §7.20 记这三个坑与推论(产物是 gitignore 的, "源码修好"≠"用户手上那个包修好")。 ## 验证 `npm test` 退出码 0(13 个判据文件全绿 + vitest 263 passed); `hvigorw assembleHap` BUILD SUCCESSFUL;`npm run build` + electron-builder 均 exit 0。 --- client/electron/src/stores/backgroundStore.ts | 10 ++++- .../test/appearance-defaults.test.mjs | 33 ++++++++++++-- client/electron/test/build-stamp.test.mjs | 43 ++++++++++++++++++- docs/HARMONY-ALIGN-PLAN.md | 36 +++++++++++++++- 4 files changed, 114 insertions(+), 8 deletions(-) diff --git a/client/electron/src/stores/backgroundStore.ts b/client/electron/src/stores/backgroundStore.ts index 81a16ce..60440e5 100644 --- a/client/electron/src/stores/backgroundStore.ts +++ b/client/electron/src/stores/backgroundStore.ts @@ -1,6 +1,14 @@ import { create } from 'zustand'; -import { AGGREGATE_ID, isUsableAccount, useAccountStore } from './accountStore'; +/* + * ⚠️ `AGGREGATE_ID` / `isUsableAccount` 住在 `lib/accounts.ts`(不在 accountStore 里, + * accountStore 只是引用了它们)。我第一版从 `./accountStore` 导入,**vitest 全绿、生产构建直接失败** + * (rollup: "AGGREGATE_ID is not exported by src/stores/accountStore.ts")—— + * 测试运行时对"缺的具名导出"是宽容的(拿到 undefined),只有打包器会报。 + * 教训:**生产构建是一道独立的门**,测试通过不等于能打包(见 §7.20)。 + */ +import { AGGREGATE_ID, isUsableAccount } from '../lib/accounts'; +import { useAccountStore } from './accountStore'; import { DEFAULT_BLUR, DEFAULT_DIM } from '../lib/appearanceDefaults'; /** diff --git a/client/electron/test/appearance-defaults.test.mjs b/client/electron/test/appearance-defaults.test.mjs index 7f13eb7..dad4a59 100644 --- a/client/electron/test/appearance-defaults.test.mjs +++ b/client/electron/test/appearance-defaults.test.mjs @@ -92,15 +92,40 @@ test('★ 缓存键按账号分:两端的键都带账号,且都不许退回 // 键函数:必须把 accountId 拼进去(按块取函数体,不看调用点 —— "配对/解析"那条) const at = store.indexOf('export function storageKey('); assert.ok(at > 0, '要有 storageKey 函数'); + /* + * ⚠️ 只取**函数体**(第一个 `{` 到配对的 `}`),不能把签名一起算进来 —— + * 我第一版就是从 `export function` 开始切、然后在整段里找 `accountId … STORAGE_KEY_PREFIX`, + * 结果**变异测试抓出了这个判据是假的**:把实现改成 `return STORAGE_KEY_PREFIX;`(退回全局键) + * 时它依然绿,因为参数表里的 `accountId` 已经满足了那个正则。 + * 这正是本仓规范第 1 条(判结构要配对/解析,不要靠邻接与窗口)—— 我写规范时没想到 + * 它的适用对象也包括"这个函数自己有没有把账号拼进去"。 + */ + const bodyStart = store.indexOf('{', at); let depth = 0; let end = at; - for (let i = store.indexOf('{', at); i < store.length; i++) { + for (let i = bodyStart; i < store.length; i++) { if (store[i] === '{') depth++; else if (store[i] === '}') { depth--; if (depth === 0) { end = i; break; } } } - const body = store.slice(at, end + 1); - assert.match(body, /STORAGE_KEY_PREFIX[\s\S]*accountId|accountId[\s\S]*STORAGE_KEY_PREFIX/, - `取键函数必须把账号拼进键里(现在:${body.replace(/\s+/g, ' ')})`); + const body = store.slice(bodyStart + 1, end); + /* + * 仍然不能用"两个词离得近"来判:第二次变异(`return accountId ? STORAGE_KEY_PREFIX : STORAGE_KEY_PREFIX;` + * ——提了一下 accountId 但根本没用它)又把松正则骗过去了。 + * 要求**同一个表达式里**既出现常量又插值/拼接账号:模板字面量 `` `a${b}` `` 或 `a + b`。 + * 这样"提到"与"用上"就分开了。 + */ + const exprs = [ + ...[...body.matchAll(/`[^`]*`/g)].map(m => m[0]), + ...[...body.matchAll(/[^;\n{}]*\+[^;\n{}]*/g)].map(m => m[0]) + ]; + const joins = exprs.filter(e => e.includes('STORAGE_KEY_PREFIX') && e.includes('accountId')); + assert.ok(joins.length > 0, + `取键的函数体里必须**真的**把账号拼进键(模板插值或 + 拼接)。现在函数体是:${body.replace(/\s+/g, ' ').trim()}`); + /* + * 行为面由 vitest 兜底(`test/stores/background.test.ts` 直接断言 + * storageKey('acct-a') === 'agentmail.background.acct-a' 且两个账号不相等)—— + * 静态判据只负责"形状",值对不对由真跑一遍的函数说了算。 + */ /* * 不许写死键名:写入的键只能是 `storageKey()`,或由它算出来的变量(迁移时写 `key`)。 * 我第一版写成"只允许字面量 setItem(storageKey(" —— 结果把迁移那次合法写入 diff --git a/client/electron/test/build-stamp.test.mjs b/client/electron/test/build-stamp.test.mjs index 15f7f9e..b567860 100644 --- a/client/electron/test/build-stamp.test.mjs +++ b/client/electron/test/build-stamp.test.mjs @@ -13,7 +13,7 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { execFileSync } from 'node:child_process'; -import { readFileSync, readdirSync } from 'node:fs'; +import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -86,3 +86,44 @@ test('★ 版本库里不得跟踪缓存/构建产物(.tmp、node_modules、di // 那个占位文件必须还在:它是 go:embed 落点存在的唯一理由,删了 Go 侧就编不过 assert.ok(tracked.includes(EMBED_PLACEHOLDER), 'go:embed 的占位文件丢了 —— 没有它 static/ 目录不存在,Go 侧编不过'); }); + +test('★ dist 必须比 src 新(前端改了没重新构建 —— 套件照样全绿,只有这道门会红)', () => { + /* + * 这条是踩出来的(2026-09-14 接手 pi 的 WebUI 开项时): + * 我把 `npm run build` 串在管道里(`npm run build 2>&1 | tail -4 && electron-builder …`), + * **构建失败被管道吞了**(pipeline 的退出码是 tail 的),于是 electron-builder + * 拿旧的 dist 打了一个新包 —— 而所有判据都是绿的: + * - vitest 绿:测试运行时对"缺的具名导出"很宽容(拿到 undefined),只有打包器会报; + * - packaging 绿:它比的是"dist vs 安装包",两边都是旧的,自然一致。 + * 结果就是"代码改了、产物没改",而没有任何一条判据看得见。 + * + * 这里直接比时间戳:dist 的入口必须不早于 src 下最新的文件。 + */ + const distIndex = join(ROOT, 'client/electron/dist/index.html'); + if (!existsSync(distIndex)) { + console.log('(没有 dist —— 先 npm run build 才验得到这条)'); + return; + } + const distAt = statSync(distIndex).mtimeMs; + // 构建过程自己会写 src/background-takeover.generated.css(在 dist 之前),不算"源码改动" + const newest = { at: 0, file: '' }; + const walk = dir => { + for (const e of readdirSync(dir, { withFileTypes: true })) { + const p = join(dir, e.name); + if (e.isDirectory()) walk(p); + else if (e.name !== 'background-takeover.generated.css') { + const at = statSync(p).mtimeMs; + if (at > newest.at) { newest.at = at; newest.file = p; } + } + } + }; + walk(join(ROOT, 'client/electron/src')); + assert.ok(newest.file, 'src 下应当有文件'); + assert.ok( + distAt >= newest.at, + `前端源码比 dist 新(改了没重新构建):\n` + + ` src 最新:${newest.file.replace(ROOT + '/', '')}\n` + + ` dist:${distIndex.replace(ROOT + '/', '')}\n` + + ` 重构建:cd client/electron && npm run build(注意别把它的退出码丢在管道里)` + ); +}); diff --git a/docs/HARMONY-ALIGN-PLAN.md b/docs/HARMONY-ALIGN-PLAN.md index f39537d..8d0006e 100644 --- a/docs/HARMONY-ALIGN-PLAN.md +++ b/docs/HARMONY-ALIGN-PLAN.md @@ -516,9 +516,9 @@ deb 也不必从 targets 里摘。已写进 `client/electron/BUILD.md`(含排 | 动效 | 自定义 transition/时长 | `animateTo` + 系统 `curves` | 动效曲线应跟随系统设置(含"减弱动效") | | 遮罩 | 自声明 `--bg-scrim` + `--bg-dim` 两段式 | 系统 `sys.color.ohos_id_color_mask_regular` | 遮罩要随主题换向(浅色洗白/深色压黑),这件事系统已经做了 | | **品牌色** | `--c-blue-600: 37 99 235` | `Theme.accent = '#2563EB'` | **不允许差异** —— 两个客户端是同一个产品 | -| **本地外观缓存的键** | 全局常量 `agentmail.background` | **按账号**:`appearance.` | **鸿蒙是对的,不许为"对齐"退回全局键** —— 全局键的后果是切到服务端没有记录的账号时,`saved=false` 分支会把**上一个账号的外观** push 上去(新账号"继承"了外观,而且写进了服务端)。pi 2026-09-14 确认这条记在 WebUI 侧为开项(换键,或至少在把继承来的值当"本地的"推给从未有记录的账号前停一下) | +| **本地外观缓存的键** | `agentmail.background.`(**已修**:原来全局) | `appearance.` | **差异已消除**(2026-09-14 dsh 接手 pi 的开项):两端都按账号分键,且都留有「不许退回全局键」的判据。全局键的后果是切到服务端没有记录的账号时 `saved=false` 会把**上一个账号的外观** push 上去(新账号"继承"了外观,还写进了服务端)。WebUI 侧保留旧全局键**仅作一次性迁移源**:接管后立刻删除,且未登录时不迁移 | | **遮盖色的令牌** | `--bg-scrim`(浅色白 / 深色黑,"朝底色淡化") | `Theme.wallpaperScrim` = `sys.color.ohos_id_color_background`(同向);`Theme.overlay` = mask **只用于模态弹层** | **不允许混用** —— mask 两套主题下都是深色(浅色 `#99182431`),拿它当壁纸遮盖会在浅色主题下压暗(与 WebUI 反向)。两个语义两个令牌,理由与实测值见 §7.17b-2 | -| **遮罩浓度的默认值** | 两套:store 的 `dim=24/blur=8` 与 `lib/appearance.ts` 的 `clamp(...,12,4)` | 12 / 4(只有一套) | 服务端**缺字段**时真正落地的是 `clamp` 的默认值,所以按 12/4 对齐;WebUI 那两套值迟早要统一(pi 记在他那边) | +| **遮罩浓度的默认值** | `12 / 4`(**已修**:原来 store 的 `24/8` 与 `clamp` 的 `12/4` 两套并存) | 12 / 4(只有一套) | **不是审美,是服务端契约**(pi 更正了自己上一封):`DefaultAppearance()` 明写 `BgDim: 12, BgBlur: 4` 且注释宣称"与客户端默认值一致" —— WebUI 的 24/8 使那句注释为**假**。现已统一到共享常量 `src/lib/appearanceDefaults.ts`,判据**直接去 Go 源码读**这两个数比对。本来后果很重:服务端"没有记录"时客户端以本地为准推上去,**新账号的初始外观由第一个同步它的客户端决定** | **关于 `overlayColor` / `overlayAlpha` 消失**(pi 要求把删除理由记在这里,否则下一个人会当成漏改补回来): 鸿蒙这边的模态走**系统弹窗**(`bindSheet` / 自绘 `Stack` 只做位置,遮罩本身用系统遮罩色), @@ -773,6 +773,38 @@ pi 指出"邻接不是结构"这条已经在同一个仓库露头**三次**( 判据要点用户真正会点的那一层。另外补了一条本仓刚踩的:**验证要按真实入口跑** (`node --test test/run-all.mjs` 会把 runner 内部的 `process.exit(1)` 吞掉)。 +### 7.20 接手 pi 的两个 WebUI 开项(dsh,2026-09-14):两个实缺陷 + 途中踩到的三个坑 + +pi 明确说他那条链上**没有 shell**,而挂着的两个开项虽然读得出精确修法却没人执行; +他担心的是它们"以 §7.12 已记录的样子漂着,看起来像已处置"。**结论:我接**,两件都做完(`e94313f`), +各配判据 —— 见 §7.12 那两行的「已修」。 + +**坑 1(最值得记):`npm run build 2>&1 | tail -4 && electron-builder …` 会把构建失败吞掉。** +pipeline 的退出码是 `tail` 的 —— 于是构建失败、`&&` 照样往下走、electron-builder +拿**旧的 dist** 打了一个**新的包**。而所有判据都是绿的: + +- vitest 绿:测试运行时对"缺的具名导出"是宽容的(拿到 `undefined`),只有打包器会报; +- packaging 绿:它比的是"dist vs 安装包",两边都是旧的,自然一致。 + +已加判据:**dist 必须比 src 新**(`build-stamp` 里;变异:`touch` 一个 src 文件 → 红), +错误信息里写明"注意别把它的退出码丢在管道里"。 + +**坑 2:测试通过 ≠ 能打包。** 真正的失败是 `AGGREGATE_ID` / `isUsableAccount` +被我当成 `accountStore` 的导出(它们住在 `lib/accounts.ts`)。vitest 263 条全绿, +**生产构建直接报** `is not exported by`。**生产构建是一道独立的门**,前端改动必须过它。 + +**坑 3:我把判据写成了"窗口式"的,被自己的变异测试抓住两次。** +`appearance-defaults` 里"取键函数必须把账号拼进去"那条:第一版从 `export function` 切到 `}` +—— 参数表里的 `accountId` 就满足了正则,于是"把实现退回全局键"**仍然全绿**; +第二版只取函数体,但 `return accountId ? PREFIX : PREFIX;`(提了一下、没用)又骗过去了; +第三版要求**同一个表达式里既有常量又有账号的插值/拼接**,才既抓住两种退化、 +又不误伤合法的 `PREFIX + accountId` 写法。 +**规范第 1 条(判结构要配对/解析、不靠邻接与窗口)的适用对象包括"我自己刚写的判据"。** + +**一条推论**:`dist` / 安装包是**产物**(`.gitignore` 忽略),所以"源码修好了"不等于 +"用户手上那个包修好了"——改前端要 `npm run build` **再** `electron-builder`, +并用刚加的那条判据确认产物不旧。本次已重构建并重打包(deb 与 app.asar 同批)。 + ### 7.19 两条跨端约定(pi 2026-09-14 复核后确认) - **`Theme.` 的成员名不改**(pi 三条理由:判据钉的是"值来自系统 + 品牌色仍手写",