Files
MailUI4Agents/docs/HARMONY-ALIGN-PLAN.md
JianFeeeee 4babc96f8b test(criteria): runner 不再手写 --test(由内容推导)+ 自检 4「这条判据能不能红」;规范补两档
pi 提的三条,第一条落地前先按他的要求**贴真实样本实测**,结论与他的猜测不同(记在代码里)。

## 1 runner 内部那个"配错 flag = 绿" —— 实测后换了个形状

pi 的猜测:给自定义 `check()` 的判据传 `--test`,runner 会报"0 个测试"并以 0 退出。
实测(node v22.22.2,两条真实样本):

- `node --test <自定义 check() 判据>`:**退出码照样传出来**(文件 exit 1 → 命令行 exit 1),
  没有被吞;
- 但 `node --test <什么都不做的文件>` 报 `# tests 1 / # pass 1` ——
  **pass 计数不是"检查跑过"的证据**。

所以"解析 pass 计数、0 就判红"这条路两头不讨好:抓不到空判据(它报 1),
还会在 `narrow-layout` 上误报(它的汇总行是"窄屏布局:全部通过",里面没有数字)——
正是 pi 提醒的"别照抄我的正则,先贴样本"。

换成两条**结构证据**:

- **`--test` 不再手写**:由文件内容推导(源码里 `from 'node:test'` 就走 node:test),
  清单里出现手写 `--test` 直接红 —— 配对错误不再靠记性维护;
- **自检 4**:每条判据文件里必须存在"能红"的路径(`test(` / `check(` / `process.exit(1)`),
  外加"跑完必须有输出"。一个都没有 = 它永远不会红,与"全通过"长得一模一样
  (这是"判据自己不会跑"家族的第 6 个宿主,家族表和六种宿主都写进规范了)。

变异:清单手写 `--test` → 红;加一条"什么都不做、退出 0"的判据 → 红;
静默成功(有能红路径但一行不输出)→ 红。

## 2 规范 §3 补一档:变异红了还要看**红在哪**(pi)

"只报红了不算,要能指名红的是哪几条";**红在解析/加载失败上不算红**(先让变异
"语法正确、语义错");变异作用于被剥掉的注释也不算。

## 3 `CRITERIA.md` 的可见性(pi 提的位置问题)

它管两个客户端的判据,却躺在 electron 的测试目录里。已在
`docs/HARMONY-ALIGN-PLAN.md` §四(验收纪律)加指针,并顺手把 pi 点名过的两条口径写死在那儿:
**"未验"只能用于"步骤做过、结果没看",功能不存在必须写"没做"**;
**"机制上确定不同"要判、不许记成"未验"**(深色档预设那次)。

## 验证

`npm test` 退出码 0(12 个判据文件全绿 + vitest 258/258)。
2026-09-14 15:00:26 +08:00

829 lines
60 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 鸿蒙客户端与 WebUI 的对齐计划
> 用户2026-09-14「安排对齐」「**鸿蒙 ui 应当交给对这一块更熟悉的 dsh 负责**」。
> 这份文档把**差距**与**分期**写清楚,免得每轮都从"感觉还差什么"重新猜。
>
> **负责人dsh**2026-09-14 起由 jianf 指定)。移交信:
> 线索 `harmony-ui-alignment``dsh@/home/program/agentmail`
> 信里交代了状态、判据纪律与踩过的坑(判据要点"用户真正会点的那一层"、
> 不许新写死颜色、不要给单个面单独做深色、模糊只由壁纸层负责、
> 列表项每项一张卡、ArkTS 编译坑、视觉不可验要如实标注)。
>
> 我pi保留 WebUI/Electron 侧:需要两边一起加令牌之类的配合,回信给 pi。
## 一、现在的差距(有据可查)
| 能力 | WebUI | 鸿蒙 | 差距性质 |
|---|---|---|---|
| 收件箱 | ✅ | ✅ | — |
| 会话 | ✅(通信页签) | ✅(独立 tab | 交互不同 |
| 联系人 | ✅ | ✅ | — |
| **发件箱** | ✅(通信页签) | ❌ | 缺页面(数据现成) |
| **授权(权限决策)** | ✅(通信页签 + 详情内决策) | ❌ | 缺页面API 现成) |
| **日历** | ✅ | ❌ | 缺页面(最大一块) |
| **管理(用户管理)** | ✅(我的页底部,管理员可见) | ❌ | 缺页面 + 权限判定 |
| **写信** | ✅ 共用 Composer | ✅ 独立实现 | 组件未统一 |
| 底部/侧边导航 | ✅ 悬浮玻璃条 | ⚠️ 系统 TabBar | 观感不同 |
| 主题 / 壁纸(账号级) | ✅ 服务端同步 | ❌ | 未接 |
| 设计令牌 | ✅ `:root` | ✅ `Theme.ets` | **已对齐**(判据钉住) |
## 二、已经做完的(本轮之前)
- **设计令牌共用**`common/Theme.ets` 与 WebUI 的 `:root` 一一对应
(品牌蓝、圆角 14/8、导航玻璃 0.72、语义色、字号),并由
`client/electron/test/cross-client-theme.test.mjs` 钉住取值一致性。
- **旧调色板清除**:页面里的 `#1A73E8`Google 蓝)/`#333333`/`#F5F7FA`
与 WebUI 不同的写死色值全部换成令牌203 处)。
- **修掉只剩第一个 tab 高亮的 bug**`currentIndex === 0` 写死)。
## 二·五、做法上的一条硬要求:**用系统方案**
用户2026-09-14「鸿蒙也同步但是**鸿蒙要求用系统方案**」。
含义:能交给系统的就交给系统 —— 用 ArkUI 的组件、材质与语义资源,
而不是把 WebUI 那套"手写 rgba + 自定义模糊 + 自制卡片"照搬过来。
系统材质会跟随深色模式、动效曲线与无障碍设置,手写的那套不会。
| WebUI 做法 | 鸿蒙应该用 |
|---|---|
| 手写 `rgba(...)` + `backdrop-filter` | `backgroundBlurStyle(BlurStyle.*)`(系统材质) |
| 自制圆角导航条 | 系统 `Tabs` + `TabBar``barBackgroundBlurStyle` |
| 自制 `Scroll` + `Column` | 系统 `List` / `ListItem``divider` / `swipeAction` |
| `.glass-card` 手写边框阴影 | `.borderRadius()` + `.backgroundBlurStyle()` + `.shadow()` |
| 自己切 CSS 变量做深色 | 系统语义色 `$r('sys.color.*')` 自动跟随 |
| 自定义 transition | `animateTo` / `transition` / `geometryTransition` |
**影响既有判据**`cross-client-theme.test.mjs` 目前钉的是三个**取值**
(品牌蓝 / 圆角 14 / 导航玻璃 0.72)。改用系统资源后它应当变红 ——
那是预期的,届时要把它从"取值相同"改成"**意图相同**"(品牌蓝仍须一致;
材质与圆角允许各自跟随系统)。改判据需 WebUI 侧pi一起改。
## 三、分期(按"能独立验收"切)
**P1 设计语言pi 已完成)** —— 令牌 + 颜色替换 + 编译通过。以下 P2P6 归 dsh。
**P2 发件箱**:复用现有列表组件,接口 `/mail/sent`。验收:能看到已发邮件、
点开进详情;空态有说明。**为什么先做它**:与收件箱同构,风险最低。
**P3 授权页**导航加一项「授权」or 通信页内页签,见 5.2③),待决列表走
`GET /permission/pending`,决策走 `POST /permission/decide`
body `{mail_id, decision, note}`);详情内也支持决策。
**(接口名已于 2026-09-14 由 dsh 核对修正,原写的 `/permission/{id}/decide` 不存在。)**
验收:待决列表与 WebUI 同口径(未决 = 无 `permission_result`
点同意/拒绝后状态立刻变;拒绝时可填备注(备注必须随决策送达模型 ——
WebUI 侧踩过这个坑,见 `gateway/handler/permission.go` 的 Note 传递)。
**P4 主题/壁纸同步**:接 `GET/PUT /me/appearance` + 图片走带认证的 fetch。
验收:换账号后外观跟随;服务端"无记录"时以本地为准(不要用默认值覆盖 —— 同 WebUI
**P5 玻璃悬浮导航**:自定义底栏(圆角 + 半透明 + `backgroundBlurStyle`
取代系统 TabBar。**注意**:视觉验收需要设备或签名 HAP
本机 `hdc list targets` 为空 ⇒ 只能保证编译与结构,观感要人眼确认。
**P6 日历**:网格 + 事件读写 + 左右滑动翻页(复用 WebUI 的手势阈值:
水平 ≥40px、≥1.5× 垂直、<600ms)。这一块最大单独排
## 四、验收纪律(照 WebUI 那套)
1. 每个页面都要有**点它**的判据WebUI 侧就是因为只验结构没验点击
漏掉了"侧栏点了不翻页")。
2. 判据不许只看截图要量几何/对比度/命中区
3. 无法验证的要**如实标注**例如"编译通过视觉未验"不能写成"已完成")。
两条口径这轮各踩过一次pi 复核时点名
- **"未验"只能用于"步骤做过结果没看"**功能不存在必须写"**没做**"
"没做"写成"没验"会让人以为只剩观感风险实际那里什么都没有
- **"机制上确定不同"要判不许记成"未验"**深色档预设那次就是)。
4. **判据怎么写** `client/electron/test/CRITERIA.md` —— 那份规范管**两个客户端**的判据
判结构与行为不判字面与邻接清单与 allow-list 的形状变异要红在预期位置
"判据自己不会跑"那个家族的六种宿主)。**写在 electron 的测试目录下只是因为
`run-all.mjs` 在那儿强制它存在**鸿蒙侧改判据前先读它
---
## 五、交接核对dsh2026-09-14 收到 pi 的移交信后)
核对方式跑构建跑判据读代码不靠转述
### 5.1 移交信里属实的两条
- `hvigorw assembleHap --no-daemon` **BUILD SUCCESSFUL**改前改后各跑一次)。
- `node --test client/electron/test/cross-client-theme.test.mjs` 6 条全绿
### 5.2 三处纠正(照原计划直接做会踩空)
** P3 的接口名写错了。** 计划里写决策走 `POST /permission/{id}/decide`」,
实际是 `POST /api/v1/permission/decide`body `{mail_id, decision, note}`
`server/internal/handler/permission.go:301`)。而且待决列表**不要**从收件箱里
`permission_*`WebUI 用的是 `GET /api/v1/permission/pending`
`client/electron/src/api/client.ts:623`鸿蒙照这个走才同口径
理由不只是"更省事"`/me/mail/inbox` 默认只取 50 而一个会话能连着产生
十几封权限邮件从收件箱筛会把别的信挤出视野WebUI `mailGroups.ts`
开头就是为这件事写的)。** pending 接口 WebUI 同源。**
**②"不许写死颜色"的判据原来只挡得住列出来的那 8 个旧色值。**
判据全绿的同时`pages/` 里还留着 **14 **另一套写死的色 —— Google/Material
调色板`#E8F0FE``#E8F5E9``#FFF3E0``#D93025``#777777`×2
`#555555``#444444``#cccccc``#aaaaaa`遮罩 `#80000000`×2
透明 `#00000000`×2枚举挡不住漂移只有""能挡已改成
- `pages/` **一个裸色值都不许有**`#RRGGBB` / `#AARRGGBB` 都算颜色只能来自
`common/Theme.ets`页面清单也改成**扫目录**旧版硬编码 7 个文件名
接下来要加的页面会自动逃出判据)。
- 变异测试确认能判红 `InboxPage.ets` 塞一个 `#E8F0FE` 判据红撤回 绿
- 顺带把权限档位徽标的配色收进令牌`Theme.permBg/permFg` WebUI
`PermissionChip.tsx` **同一映射**plan= / workspace=绿 / full=琥珀)。
原先是 `full ? 绿 : 橙`Material WebUI 反着来
- 判据从 6 条加到 7 新增"两边权限档位配色一致"直接拿 WebUI `:root`
`--c-blue-*/green-*/amber-*` 比鸿蒙令牌"看起来差不多")。
** 导航的差距不止"观感不同"是信息架构不同。** WebUI2026-09-14 用户
导航项就只剩通信日历联系人」)已是**通信收件箱/发件箱/授权内部页签
+ 未读红/待决策橙徽标/ 日历 / 联系人**底部另有"我的含管理+ 主题 + 退出"
新建是通信页内**悬浮圆形加号**鸿蒙还是 **收件箱 / 会话 / 联系人** 三个平级 tab
没有发件授权日历会话 WebUI **没有对应入口**WebUI 是按会话把
收件箱分组没有独立会话页)。
P2/P3 不是"再加两个页面"得先对齐信息架构否则加完还是两套导航
### 5.3 一条与鸿蒙无关、但该让 pi 知道的
WebUI `npm test` **HEAD 上就是红的**`test/background.test.mjs`
- `浅色与深色两套令牌都定义了(自动反色)` —— 期望 `--nav-bg` 有浅/深两套
但深色那套已在 `faacd3c`"导航栏还是黑色"里被有意去掉
现在 `index.css` 只剩 `--nav-bg: 255 255 255 / 0.72`
- `导航在壁纸模式下仍参与模糊` —— 期望 `html[data-bg='on'] .nav-rail` 里有
`backdrop-filter: blur(`而该规则现在**是空的**"模糊由壁纸层负责这里不叠"
`index.css:892`)。
两条判据编码的都是**已被有意回滚的设计**判据没跟着改红成了常态
判据也就不再是判据 —— 这条纪律问题比色值本身更值得先修
### 5.4 修订后的分期与"点它"判据
| | 内容 | 判据必须点到用户会点的那一层 |
|---|---|---|
| P1.5 | 裸色值清零 + 判据按类挡 + 权限档位配色统一 | 判据 7/7 绿 + 变异能判红 + 构建成功 |
| P2a | 信息架构:「通信一项内部页签 收件箱/发件箱/授权未读红待决策橙徽标 | **点页签 → 断言落到哪个 pane**不是断言页签个数)—— 页签状态机判据已落地 §7.15 |
| P2b | 发件箱页`GET /me/mail/sent`复用列表项 | 判据接口路径行上主角是收件人空态有说明主句与 WebUI 逐字一致 |
| P3 主体完成 | 授权页`GET /permission/pending` + `POST /permission/decide` | 未决口径与 WebUI 一致 `permission_result`)✅;拒绝可填备注且备注送出 ✅;`expired` 当场说清"这次批准不会恢复原调用" ✅。**未验**真机上点同意/拒绝后状态是否"立刻变"判据只钉到"决策后重新拉列表"这一层 |
| P4 ✅(P4c 上传除外 | 主题/壁纸`/me/appearance` | 换账号外观跟随 ✅(缓存键带账号服务端无记录时以本地为准 ✅(§7.16**预设 6 档都能画出来** ✅、图片壁纸渲染 ✅(§7.17 —— 这一版补的第一版只有数据没有画面)。**未做**P4c 上传入口。**未验**真机观感配色/对比 |
| P5 未做 | 悬浮玻璃导航取代系统 TabBar | 模糊只由壁纸层负责列表项每项一张卡命中区 44vp现状底栏仍是系统 `Tabs`只有自绘的 tabBar builder 带了 `backgroundBlurStyle`(§7.10 |
| P6 未做 | 日历`/calendar/events` ics 导入导出 | 手势阈值与 WebUI 一致水平 40px、≥1.5× 垂直、<600ms)。**有意排序**入口与内容一起上不留空页签(§7.15 |
**视觉/交互怎么验**本机有 `harmony-emu`实例 `HarmonyPhone`hdc
`/opt/harmonyos/ohos-sdk/linux/toolchains`能起来就能截图 + 真点
"编译通过视觉未验"变成"点过有截图"。
**实测2026-09-14**当前文件沙箱workspace-write下起不来 ——
`harmony-emu start` 卡在等一个永远不来的 hdc脚本要写 `/run/harmony-emulator.pid`
直启 `Emulator -start HarmonyPhone -noWindow` 则报
`Error opening logfile /root/.Huawei/Emulator/deployed/HarmonyPhone/Log/qemu.log: Permission denied`
实例目录在 `/root` 沙箱不许写)。**要跑真机/模拟器判据得先放宽文件权限
或换有设备的机器**在那之前鸿蒙侧的判据只能到"构建通过 + 结构/令牌一致"
观感与点击必须如实标注未验 —— 不许写成已完成
**状态口径**:✅ = 有能判红的判据 + 构建成功;⚠ = 主体完成但有未验项;
= 未做。**视觉与交互一律未验**无设备/模拟器见每节末尾的"未验"。
### 5.5 待定(需要 pi 或用户一句话)
1. **会话这个平级入口留不留** WebUI 没有它收件箱按会话分组)。
我倾向跟 WebUI 一致 —— 收件箱按会话折叠去掉平级会话tab
但这是删入口改之前要一句话确认
**已答(见 5.6):不是删功能,是两个 tab 合成一个页面的两种视图;先补视图与折叠,再删 tab。**
2. 新增令牌 `accentStrong / warnBg / warnFg / overlay` 取的都是 WebUI **已有**
tailwind blue-700 / amber-50 / amber-700WebUI 侧不需要动代码
`overlay` 是唯一没有对应物的WebUI 弹层不压遮罩)。 pi 认为 WebUI 侧也该
立同名令牌我再改
**已答(见 5.6WebUI 侧不立同名令牌;鸿蒙侧的 `overlay` 拆成 `overlayColor` + `overlayAlpha`。**
---
## 六、pi 的回复已落地dsh2026-09-14 下午)
### 6.1 「会话」入口的结论pi 作为 WebUI 侧负责人)
不是删功能是两个 tab 合成**一个页面的两种视图****联系人页就是会话列表**
`ContactPanel.tsx` 有列表 / 卡片两个视图 —— `ContactRow` 现在的 ContactsTab
`WorkCard`工作列表」≈ 现在的 SessionsTab主题最新一封摘要权限档位徽标
往返预算 = 现在的 `used_rounds/max_rounds`**收件箱里会话是组织方式**
`mailGroups.ts` `groupMailsBySession()` `session_id` 折叠组头取最新一封
`isFlatGroup()` 让单封不成组)。
落地顺序原样采纳
1. ****联系人页补列表 / 卡片切换卡片视图承接预算条 / `status` / 对方 Agent
收件箱加**按会话折叠**组头带未读)。折叠前先看 `total` —— 别让"只取了 50 "
被折叠伪装成"只有这么多会话"。
2. **两件都到位后**再删平级会话tab在那之前删 = 丢信息:轮次预算、`status`
`from_agent` 会无处可看"预算跑满"正是需要人介入的信号
合并后导航正好是 **通信 / 日历 / 联系人** 三项P4P6 都在这个形态上做
分期顺序调整为**P2a 联系人双视图 + 收件箱按会话折叠 P2b 通信页签收件/发件/授权
P3 授权页 P4 主题壁纸 P5 悬浮玻璃导航 P6 日历 最后删会话tab**。
### 6.2 遮罩令牌
`overlay` 拆成 `overlayColor` + `overlayAlpha`WebUI `--bg-scrim` + `--bg-dim`
透明度的两段式色要能随主题换向焊死成一个 `#AARRGGBB` 等于把枚举写回代码)。
ArkUI 只认单值故用 `Theme.overlay()` 组装。**WebUI 侧不立同名令牌**立了没人用
判据只能验"它存在"那是自证)。判据第 8 条钉住这个形态
### 6.3 那两条红判据的 patch已落地且验证过能判红
`test/background.test.mjs` 18 组三条按 pi patch 重写标签改回它真正断言的东西
两条方向相反的断言取代原两条`.dark` 不许单独给导航换色 / 模糊只由壁纸层负责)。
变异验证
- 往某个 `.dark { }` 里塞一行 `--nav-bg: 15 23 42 / 0.72;` **红**
- `html[data-bg='on'] .nav-rail` 里塞 `backdrop-filter: blur(18px);` **红**
- 撤回 绿背景判据 32 **34 通过**
### 6.4 顺手发现并修掉的真 bug构建只在日志里警告的那种
`src/background-takeover.generated.css` **头注释提前闭合**生成器在注释里写了
src「/」加两颗星加「/」「.tsx」,其中那对星号 + 斜杠 CSS 注释就地结束
剩下的尾巴变成 CSS 正文并与第一条规则的选择器连在一起 非法选择器
**`.bg-amber-100` 那条接管规则被浏览器整条丢掉**壁纸模式下它不再变半透明)。
构建只给一条 `[WARNING] Unexpected "14" [css-syntax-error]`不报错不影响构建
改在生成器 `scripts/gen-background-takeover.mjs`注释里只描述不写 glob 字面量
重新生成压缩输出现在以 `html[data-bg=on] .bg-amber-100{` 起头14 条规则全在无告警
另加两条判据"头注释没提前闭合" + 自检)。
### 6.5 一条判据从来没被跑过
`cross-client-theme.test.mjs` **不在 `npm test` 链里**`npm test` 只跑
markdown-xss / narrow-layout / theme / background / packaging + vitest
vitest 只收 `test/components` `test/stores`)—— 那条"防漂移判据"从没在默认套件
里跑过已加进 `npm test`
### 6.6 `npm test` 现在全绿(退出码 0
窄屏布局全通过主题 30背景 34cross-client 8packaging 3vitest 258 用例 / 15 文件全过
其中 **packaging 第 3 条此前是红的**但它**不是判据过期是安装包真的落后于 dist**
`release/linux-unpacked/resources/app.asar` 里装的还是旧前端)。根因有点讽刺
`npm test` `&&` 链一直在 background 那条就中断**packaging 从来没跑到过**。
`npm run build` + `npx electron-builder --linux -c.electronDownload.isVerifyChecksum=false` 重打包
**deb 目标在本机打不出来**`fpm` portable ruby `Dir.chdir` 处退出
AppImage `linux-unpacked` 正常 —— 交付前要确认这个环境问题不影响目标平台
### 6.7 WebUI 侧同源残留pi 指出的第三处,已修)
`Sidebar.tsx` 底部的**主题切换 / 退出登录**按钮原为
`text-chrome-400 hover:text-white hover:bg-chrome-800`外层分隔线 `border-chrome-700`
—— 那是"框架本来就该深"的假设导航改白玻璃后 `chrome-400` 在白底上约 2.6:1
图标要 3:1hover 还会在白导航上闪出一块近黑已换成 `.nav-item` + `border-gray-200`
**未加"Sidebar 里不许有 chrome-*"的判据**`Sidebar.tsx` 另有一处
`bg-chrome-600 text-chrome-100` 是实心小色块正常用法一刀切会误红
### 6.8 仍未验的
鸿蒙侧**视觉与点击**仍未验模拟器在本机沙箱下起不来 5.4)——
本轮的鸿蒙改动只有颜色令牌判据能覆盖** P2a 一定要有人眼或设备**
否则"点页签落在哪个 pane"这条判据无法证明
---
## 七、P2a 落地dsh2026-09-14 稍晚)
pi 给的顺序**先补视图与折叠再删 tab**。本轮做完前一半tab 保留
### 7.1 做法:把"用户真正会点的那一层"的内核抽出来,让判据**执行**它
没有设备"点一下"在鸿蒙上暂时无法自动验应对不是编个能过的新判据
而是把会点的那一层的内核抽成纯逻辑文件
`client/harmony/entry/src/main/ets/model/MailGrouping.ts` UI 依赖
判据用 node `--experimental-strip-types` **直接跑同一份代码**
- `client/electron/test/harmony-logic.test.mjs`**19 **)—— 断言的是**行为**
折叠后组头是不是最新一封单封是不是不成组同一时刻是否用 `mail_id` 倒序兜底
时间解析失败会不会让顺序依赖入参多账号同名会话会不会被错并预算剩 1 个来回是哪档
- 页面那一层用源码判据钉"确实调了这些函数"`groupMailsBySession` / `isFlatGroup` /
`toggleExpanded` / `budgetLabel` / `nextContactView` / `lastFromIsHuman`)——
两层合起来,「逻辑对页面接上了都有判据
- **变异验证4 全部判红**去掉组内排序 2 条红预算阈值 `<=1` `<1` 1 条红
分组键去掉账号前缀 1 条红页面不再区分单封组 1 条红
这条判据已接进 `npm test``node --experimental-strip-types --no-warnings --test`)。
### 7.2 收件箱:按会话折叠
- 组头取组内**最新一封**的别名与主题 WebUI `mailGroups.ts` 同口径
带未读数徽标与N 」;点组头展开/收起
- **单封不成组平铺** WebUI `isFlatGroup` 同结论给孤立的一封信套组头
只是多一次点击而收件箱里大多数人类来信就是孤立的一封
- 多账号是鸿蒙特有分组键带账号前缀同一 `session_id` 出现在两个账号是两件事
`session_id` 缺失时按 `mail:<id>` 各自成组
### 7.3 顺带修掉一个"看起来是总数、其实是未读数"的显示
`/me/mail/inbox` 返回的 `total` **`CountUnread`未读总数**不是总封数
`server/internal/handler/me.go` `MeGetInbox`)。鸿蒙底部原来写 N 」,
于是同一屏上会出现 7 未读 7这种自相矛盾的两行字
改法未读数**改用服务端 total**权威原来数这一页会少报
N 改成已加载 N 」;并且**这一页取满=50时如实提示
已加载 50 本页上限 50可能还有更多)」** —— 客户端手上根本没有可信的总封数
那就不能把 50 封说成全部这正是 pi 提醒的"别让只取 50 封伪装成只有这么多会话")。
WebUI 侧完全不读这个字段`mailStore` 里没有 `total`所以这条只影响鸿蒙
### 7.4 联系人页:补上卡片视图(为撤 tab 做准备)
- 右上角切换列表 / 卡片标题随视图变(「联系人」/「工作列表」, WebUI 同词
切换规则在 `nextContactView()`判据直接执行它
- 卡片对应 WebUI `WorkCard`Agent + 工作目录 + 未读徽标
会话别名空则「(未命名会话)」)、**主题当主角**、最新摘要 + /Agent 标记
`lastFromIsHuman`)、「N · 时间」、权限档位徽标、**往返预算条**
档位与 WebUI `BudgetChip` 同一判据 0 / 1 / 其余中性上限 0=不限则不显示)。
- 平级会话tab **暂时保留** —— 预算`status``from_agent` 现在卡片视图里都能看了
但按 pi 的顺序 tab 排在 P2b 之后作为独立一步撤早了会丢信息)。
### 7.5 本轮验证与如实标注
- `hvigorw assembleHap` **BUILD SUCCESSFUL**`.ts` 纯逻辑模块能被 `.ets` 引用
实测可行 —— 这是"判据能执行同一份代码"的前提)。
- `npm test` **退出码 0**窄屏布局全通过主题 30背景 34cross-client 8
harmony-logic 14packaging 3vitest 258/258
- **视觉与点击仍未验**无设备 / 模拟器起不来折叠展开的手感卡片间距
组头命中区是否够大这些**没有**任何自动判据能代替人眼 —— 交付时按"结构/逻辑已验证
观感未验"下一轮P2b 通信页签收件箱/发件箱/授权 + 徽标)。
### 7.6 模拟器为什么仍然起不来:权限门是"无人可批准"
7.5 "无设备"这次查到根上了DevEco CLI 的说明`deveco-cli` skill确认本机
****一个按需启动的模拟器实例 `HarmonyPhone`KVM`harmony-emu start` 即可
起来后 `devecocli ui layout / click / screenshot` 能做**真正的点击级验证**)。
`harmony-emu start` 要把 PID/日志写到 `/run` `/root/.Huawei`工作区之外
workspace-write 沙箱下被拒按规矩用 `sandbox_permissions` 升级重试一次得到的是
无法执行 bash权限询问无法送达该任务链上没有人类用户
也就是说**鸿蒙的点击级验证在这条链上不是"还没做"而是"当前做不到"** ——
需要人类在命令行里跑一次 `harmony-emu start`之后 `devecocli ui` 就能自动点
在那之前鸿蒙侧任何界面改动的验收口径只能是
逻辑有可执行判据 + 页面接线有判据 + 观感未验」,**不能**"已完成"。
给上游的可执行请求在有人的环境里执行
`harmony-emu start && cd client/harmony && hvigorw assembleHap && hdc install entry/build/default/outputs/default/entry-default-unsigned.hap`
即可让 P2a 之后所有阶段的"点击级判据"落地。)
### 7.7 顺手把"判据套件"本身修可信(同一族问题的总账)
这轮撞出来的问题里有一类是**判据自己不会跑**比判据写错更隐蔽输出看起来一切正常
1. `cross-client-theme.test.mjs` 从来不在 `npm test` 链里pi 已认领
2. 有人新加的 4 条玻璃判据写在 `process.exit()` **之后** —— 一条都不执行不计通过也不计失败
3. `nav-merge.test.mjs``build-stamp.test.mjs` 写好了**也没接线** 8 / 4 条判据从未跑过
4. `npm test` `&&` **前面红一条后面全部不跑**`packaging` 那条因此长期隐身)。
改法`test/run-all.mjs`pi 建议的"全跑完再算退出码"
- 每条判据都跑红的收集起来最后一起报一起退出
- **自检 1**清单里的文件必须存在写错名字 = 一条判据静默消失);
- **自检 2**`test/` 下每个 `*.test.mjs` 都必须在清单里 —— **新增判据忘了接线直接红**
这条自检当场就抓出上面第 3 条那两个文件
- 变异验证 `theme.test.mjs` 强制红 后面 5 条判据照跑汇总如实报" 1/9"、退出码 1
接线后又立刻暴露出两条**陈旧判据**代码没错判据钉的是旧写法已按"钉行为不钉字面"修好
- `setViewMode(target || commTab` 代码后来等价改写成
`target ?? (isComm ? commTab : modes[0])`改成钉"isComm 时落到 commTab"这个行为
- `MailView.tsx` grep `glass-control` 授权多选胶囊抽成了共用组件 `ComposerChip`
样式其实是对的neutral 未选中态就是 `glass-control`改成钉
"MailView ComposerChip" + "ComposerChip 用控件档"。两条都做了变异验证改坏必红)。
pi 的提议还给生成产物补了一条判据 **postcss 真解析** `background-takeover.generated.css`
断言**每条规则的选择器形状**都恰好是 `html[data-bg='on'] .bg-xxx` **规则条数与清单一致** ——
"生成器写坏产物""只有浏览器能发现"变成"跑判据就红"。
注意只验"能解析"不够 —— 实测 postcss 对当年那份坏产物照样解析出 1 条规则
只是选择器前面粘上了注释的尾巴所以卡的是形状与条数。)
套件现状`npm test` = `node test/run-all.mjs && vitest run`9 个判据文件全绿
markdown-xss / narrow-layout / nav-merge / theme / background 42 / cross-client 8 /
harmony-logic 14 / build-stamp 4 / packaging 3+ vitest 258/258
### 7.8 deb 那条结论要撤回:不是 fpm是 `/tmp` 满了
pi 问的 deb 复测有结果了`TMPDIR=/var/tmp/ebtmp` 在本沙箱里**建不出来**
工作区外不可写但把 TMPDIR 指到工作区大盘后 **deb 打出来了**
`release/agentmail-web_0.1.0_amd64.deb` 100MB)。
真因不是权限也不再是 fpm日志里的关键行是 **`Errno::ENOSPC`** —— fpm 会把
`release/linux-unpacked`291MB整份复制进 `TMPDIR`而本机 `/tmp` 9.8G tmpfs
`/tmp/gocache`4.5G等占到 99%仅剩 ~100MB 可用)。
所以本机打不出 deb这个结论**撤回**它是环境症状不是工具链缺陷
deb 也不必从 targets 里摘已写进 `client/electron/BUILD.md`含排查命令)。
顺带给所有 agent 的提醒本机 `/tmp` 99% `hvigor` 之类的构建会把它进一步挤爆
大产物构建请把 `TMPDIR` 指到工作区所在盘
### 7.9 P2a 收尾撤掉平级「会话」tab + 权限档位的"强制力"(鸿蒙侧)
- ** tab**底部只剩 **收件箱 / 联系人** 两个平级页签依据"信息没丢"
会话列表独有的字段现在落在两处 —— 收件箱按会话折叠组头就是会话
联系人页的卡片视图是会话的进度视角其中 `status`(active/archived) `from_agent`
**参考实现也不显示**判据里有一条"卡片字段集与 WebUI `WorkCard` 一致"钉住这件事
两边字段集**完全相等**多一个少一个都红)—— 哪天 WebUI 补上了这条会红提醒跟着补
- **权限档位 + 强制力**卡片上的徽标改成"**档位 + 强制力标记**"点它弹出说明
WebUI 把说明放在 `title`悬停提示**手指没有悬停** —— 所以鸿蒙拆两步
标记形状当场可辨`●` 平台强制 / `◉` 覆盖不完整 / `○` 仅提示点一下用 toast 说完整那句话
三条纪律落进判据
1. 档位标签强制力标签与 WebUI `MODE_LABEL` / `ENFORCEMENT_LABEL` **逐字一致**
2. **说明文案从 WebUI 源码里抽出字符串逐字比对**9 种组合全覆盖)——
两个客户端对同一个任务不能给两种保证
3. 认不出的强制力归一到 `advisory`保守方向绝不当成"平台拦得住"
且空/未知必须说"仅提示"。
收件箱每封邮件里**没有** `permission_enforcement`那是会话级字段
所以那里只写中文档位 —— 画个强制力标记等于编一个"平台做到了什么"。
**两个"判据自己不可信"的坑,本轮各修一次(都是变异测试逼出来的)**
- 断言一律读**剥掉注释的源码** `showToast` 注释掉正则照样匹配 —— 注释里有某个调用
证明不了那个调用存在`cross-client-theme` 里遮罩那段早有同款教训)。
- "在回调里"不能靠正则窗口`onClick` 体掏空或把 toast 挪到相邻的 `onHover`窗口式正则会**放过**。
改成**括号配对**取那个 `onClick` `{...}` 只在里面找两次变异现在都判红
### 7.10 下一段:改用"系统方案"jianf 追加要求)
用户原话:「鸿蒙也同步但是**鸿蒙要求用系统方案**」(pi 转达 §· 的对应表)。
我这边**同意这个理解**并且补两条可执行的做法
1. **能给系统的全给系统**`backgroundBlurStyle(BlurStyle.*)` 取代手写模糊
系统 `Tabs`/`TabBar``barBackgroundBlurStyle`取代自制导航条
`List`/`ListItem``divider`/`swipeAction`取代自制列表
`.borderRadius()`/`.shadow()` 取代手写卡片`animateTo`/`curves` 取代自定义动画
2. **语义色优先 `$r('sys.color.*')`**而且**名字是可以离线校验的**
SDK 里带着系统资源名表
`sdk/default/openharmony/toolchains/id_defined.json`本机 API 26 版本7826 )。
实测该表里正好有这批语义色
`ohos_id_color_list_card_bg`**列表每项一张卡**的底色)、`ohos_id_color_list_separator`
`ohos_id_color_background` / `_sub_background`
`ohos_id_color_text_primary` / `_text_secondary` / `_text_tertiary`
`ohos_id_color_emphasize`强调色)、`ohos_id_color_warning``ohos_id_color_alert`
`ohos_id_color_mask_light/regular/thick`遮罩)、
`ohos_id_blur_style_component_*_color`
本轮先做成**判据**源码里出现的每个 `$r('sys.color.*')` / `$r('sys.float.*')` 名字
都要在这张表里查得到 —— 因为**没有设备**名字写错在运行前根本发现不了
这条判据把它变成构建期就红做替换之前先把这条判据立起来再逐处替换
3. **判据怎么跟着改(已与 pi 对齐口径)**`cross-client-theme.test.mjs` 现在钉的三个**取值**
品牌蓝 `#2563EB` / 圆角 14 / 导航玻璃 0.72要改成"**意图相同**"——
品牌蓝仍须一致圆角与材质允许各自跟随系统鸿蒙侧由 `BlurStyle` + 系统圆角决定)。
**改之前先跟 pi 说一声,两侧一起改**他明确要求避免各改一半)。
本轮新增的那 6 个预算条取值gray/red/orange 三档同属这批一起改
4. 两条已同步的语义照做**列表项与顶部都是"每项一张卡/气泡"不是通栏**、
**玻璃只出现在一层**不嵌套各自加模糊)——
收件箱现在的通栏行 + 分隔线要改成卡片/气泡这项排在系统材质替换之前做
因为它决定组件结构
### 7.11 「用系统方案」的第一块地基:系统资源名**离线可校验**(判据先于替换)
没有设备`$r('sys.color.写错了')` 编译期不报只有真机运行到那一行才炸 ——
"用系统方案"会变成一块谁都验不了的区域所以先把校验立起来
`client/electron/test/harmony-system-api.test.mjs`4 已接进 `run-all`
- 源码里每个 `$r('sys.<type>.<name>')` 都必须在 SDK 名表
`sdk/default/openharmony/toolchains/id_defined.json` **存在且类型相符**
- 每个 `BlurStyle.<MEMBER>` 都必须在 SDK `declare enum BlurStyle`
- 替换计划里要用的那批系统色**先核过再写代码**`ohos_id_color_list_card_bg`
`_list_separator``_background`/`_sub_background``_text_primary/secondary/tertiary`
`_emphasize``_warning``_alert``_mask_regular`
- 名表里没有 `brand` / `confirm` / `success` —— 这条**边界也钉住**
权限三档预算三档这些**业务语义色没有系统对应物**继续用自定义令牌
不许"为了系统化" plan 档画成 warning
两条纪律写进判据本身SDK 名表找不到时**判红并说明**会静默跳过的判据等于没有
以及变异验证拼错色名 `list_cad_bg` "查无此名"写错 `BlurStyle.COMPONENT_不存在` 报可用取值
名字解析精确到 `,`/`=` 分隔符 —— 早先的宽松写法把文档里的 `T``R` 也算成了枚举成员)。
### 7.12 「有意差异」表(跨端判据从"取值相同"改"意图相同"的依据)
pi 的要求单列一张表这些维度**允许两边不同**因为它们各自跟随自己的平台
判据`cross-client-theme.test.mjs`只钉"来源正确 + 差异被记录"不再钉取值相等 ——
**品牌色不在表内**它是跨客户端身份必须逐字一致`#2563EB`单独一条判据钉住)。
| 维度 | WebUI | 鸿蒙 | 为什么允许不同 |
|---|---|---|---|
| 圆角 | 自声明 `--radius-card: 0.875rem` | 系统 `sys.float.ohos_id_corner_radius_card/button` | 系统圆角会随设备/主题/无障碍设置变跟着系统才是"系统方案" |
| 材质玻璃 | 自声明 `--nav-bg: 255 255 255 / 0.72` + `backdrop-filter` | 系统 `backgroundBlurStyle(BlurStyle.COMPONENT_THICK)` | 系统材质自带深浅两套颜色与模糊半径手写 alpha 跟不了深色 |
| 动效 | 自定义 transition/时长 | `animateTo` + 系统 `curves` | 动效曲线应跟随系统设置"减弱动效" |
| 遮罩 | 自声明 `--bg-scrim` + `--bg-dim` 两段式 | 系统 `sys.color.ohos_id_color_mask_regular` | 遮罩要随主题换向浅色洗白/深色压黑这件事系统已经做了 |
| **品牌色** | `--c-blue-600: 37 99 235` | `Theme.accent = '#2563EB'` | **不允许差异** —— 两个客户端是同一个产品 |
**关于 `overlayColor` / `overlayAlpha` 消失**pi 要求把删除理由记在这里否则下一个人会当成漏改补回来
鸿蒙这边的模态走**系统弹窗**`bindSheet` / 自绘 `Stack` 只做位置遮罩本身用系统遮罩色
所以"遮罩色 + 遮罩透明度"这两个自定值**整类**都不需要了 —— 系统遮罩色自带随主题换向
浅色洗白 / 深色压黑)。保留的是**一个** `Theme.overlay`= `sys.color.ohos_id_color_mask_regular`
它是自绘弹层唯一还需要引用的那一个令牌判据钉两件事值来自系统** `Theme.ets` 之外确有使用点**
只有声明没有使用 = 死令牌pi 点过这条)。
**材质为什么是 `COMPONENT_THICK`**pi 指出判据只能钉"来自系统枚举"钉不了"选对没选对"
所以理由要写下来免得后人以为随便挑的底部导航栏是**内容之上的一层**要挡住滚动内容
又不至于把内容糊没 —— `COMPONENT_*` 系列是"组件材质"作用于一个组件表面
其中 THIN 在浅色壁纸上几乎看不出分层导航条会像没浮起来BACKGROUND_* 系列是
**整窗背景**用的会把下方内容整体重绘这里是叠一层而不是换背景用它会与页面底色打架
ULTRA_THICK 会把导航条底下的内容糊成一块所以取 COMPONENT_THICK
**这条只有真机能判**见下面模拟器起来后第一个要看的项」。
### 7.13 系统方案替换(第一批)+ 跨端判据从"取值"改"意图"
**改了什么**鸿蒙侧
- `Theme.ets` 的表面/文字/分隔/遮罩/圆角**来源换成系统**
`pageBg→ohos_id_color_background``surface→ohos_id_color_list_card_bg`
`surfaceMuted→ohos_id_color_sub_background``border→ohos_id_color_list_separator`
三级文字 `→text_primary/secondary/tertiary`
`overlay→ohos_id_color_mask_regular``radiusCard/Control→sys.float.ohos_id_corner_radius_card/button`
- **删掉手写玻璃**`navBgLight='#B8FFFFFF'` / `navBgDark='#B80F172A'` 两个常量去掉
换成 `navMaterial: BlurStyle = BlurStyle.COMPONENT_THICK`导航条上
`.backgroundBlurStyle(Theme.navMaterial)` —— 深浅两套颜色与模糊半径由系统按主题给
顺带把**遮罩**的那套" + 透明度"两段式也删了`overlayColor`/`overlayAlpha`/`overlay()`
当初拆两段就是为了"遮罩要能随主题换向"而这件事系统已经做了
- **每项一张卡/气泡**收件箱行会话组头联系人列表行都改成卡片
圆角 + 卡片底色 + 行间距联系人列表那条贯通分隔线删掉
- 保留自己写的只有两类**品牌色**`accent = #2563EB`跨客户端身份
**业务语义色**权限三档预算三档 —— 系统只有 warning/alert 两个情绪色凑不出三档)。
**判据怎么改的** pi 对齐后两侧一起改他要的 A/B/C 形状 + 品牌色防线
| 判据 | 旧口径 | 新口径 |
|---|---|---|
| 品牌蓝 | 取值相同 | **不变**唯一必须逐字一致的东西另加"不得退化成 `$r('sys.color.*')`"的防线 |
| 圆角 | 两边都是 14px | 意图相同WebUI 自声明令牌 + 鸿蒙来自 `sys.float.*` + 差异记录在 §7.12 |
| 导航玻璃 | 两边都是 0.72 | 意图相同WebUI 自声明 alpha + 鸿蒙用 `BlurStyle` + 差异记录 |
| 遮罩 | 鸿蒙照 WebUI " + 透明度" | 鸿蒙用系统遮罩色换向由系统做WebUI 仍两段式 |
| 裸色值 | 只扫 `#RRGGBB(AA)` | **四种形态一起扫**`#RRGGBB(AA)` / `rgba(` / `0xRRGGBBAA`pi 指出的洞 |
| 新增 A | —— | 系统拥有的维度**唯一来源** `$r('sys.*')`且不得退回 string/number |
| 新增 B | —— | 旧机制不得回来`navBg*`8 位半透明色`rgb(`/`rgba(`写死的 14/8 |
| 新增 C | —— | 玻璃位置必须调 `backgroundBlurStyle`**只许一层**嵌套各加模糊是 pi 点名要避免的 |
**变异验证**判据必须能红红在哪也要对
| 变异 | 结果 |
|---|---|
| 品牌蓝 `$r('sys.color.ohos_id_color_emphasize')` | 3 含品牌色防线 |
| 手写玻璃 `navBgLight='#B8FFFFFF'` 回来 | 2 |
| 导航改用写死的半透明色不调 `backgroundBlurStyle` | 2 |
| 卡片上再开一层模糊嵌套玻璃 | 1 `玻璃应只出现在一处,实际 2 处` |
**未验**这次替换的**观感**仍然没验 —— 卡片间距是否合适系统材质在导航条上的实际效果
深色模式下的观感都只有真机才看得到模拟器需人在命令行启动)。已验证的是
`hvigorw assembleHap` BUILD SUCCESSFUL13 条跨端判据 + 4 条系统资源名判据全绿
四种变异都能判红`sys.*` 名字全部对着 SDK 名表核过判据持续盯着)。
### 7.14 顺手扫掉 23 处废弃 API全局 `promptAction.showToast`
SDK 里写着`@ohos.promptAction.d.ts` 的全局 `showToast` `@deprecated since 18`
替代品是 `UIContext.getPromptAction()`仓库里原有 **23 **这种调用历史遗留6 个页面)——
既然这一轮在按"用系统方案"整理鸿蒙侧就一次扫干净
`promptAction.showToast(...)` `this.getUIContext().getPromptAction().showToast(...)`
并清掉不再需要的 import
**判据**不得再用全局写法负向断言自检里同时验"认得出旧写法""不误伤新写法")。
这条防的不是这次而是**新增页面照抄旧代码**这条最常见的回退路径 —— 它编译照样通过
同一批整理里还没做的全局 `animateTo` 也要走 `getUIContext().animateTo(...)`
放到动效那一期一起改 —— 那一期本来就是"自定义 transition 系统 `curves`"。)
### 7.15 P2b + P3 主体:把「收件箱」这一项改成「通信」(内部三栏 + 徽标)
用户2026-09-14):「收件发件授权改为一个导航项通过内部导航区分然后新建作为他们
内部的一个悬浮的圆形加号」。所以这一期不是"再加两个页面"而是**对齐信息架构**
- 底部第一项从 **收件箱** 变成 **通信**`CommPage`内部三栏 **收件箱 / 发件箱 / 授权**
- 页签带**徽标**收件箱红未读)、授权橙**待决策**)、发件箱无>99 写 `99+`
- 选中态用**下划线**(不是浮动白胶囊)—— WebUI 侧用户原话是「通信页面的二级页面与其他位置极其割裂」;
- **悬浮圆形加号**挂到通信页这一层(三个栏都要能新建),`⚙` 也搬到这一层
(否则切到发件箱/授权就够不到设置);
- **收件箱不再混权限邮件**:按 `splitByPermission` 分家,未读也按筛过之后算 ——
WebUI 侧实测过"一个会话的 17 封权限邮件挤掉另外两个会话"。
**P3 主体(授权栏)**`GET /permission/pending`(不从收件箱筛,理由见 §5.2①)、
`POST /permission/decide`。两件如实说的事:**拒绝可填备注且备注必须送出**
**请求过期**时当场说明「审批不会让那次调用继续」—— 否则人会以为 Agent 接着跑了。
**顺带清掉一处死代码**:收件箱里那个「写邮件」`bindSheet``composeVisible`
从来没被置为 true**谁也打不开**),随新建入口改成悬浮加号一并删除。
**日历为什么还没有入口**它的内容P6网格 + 事件读写 + 左右滑动翻页)还没做。
先放一个点进去空着的入口,比暂时没有入口更糟 —— 所以等内容一起上。
这是**有意排序**,不是漏做(记在这里以免被当成遗漏)。
**判据**`harmony-logic.test.mjs`28 条):
| 判据 | 说明 |
|---|---|
| 页签键与顺序 | 从 WebUI 的 `uiStore.ts` 抽出 `CommTab` 联合类型、从 `CommTabs.tsx` 抽出 `TABS` 标签,比**键集合与顺序**(两边不一致时"恢复上次页签"会悄悄退回) |
| 页签状态机 | 键↔下标往返;认不出的键/越界下标/非整数 → 回收件箱(不落在空白 pane 上) |
| 徽标 | 收件箱看未读、授权看**待决策**不是权限邮件总数、发件箱无0 不显示;>99 写 `99+`;红/橙与 WebUI 的 `bg-red-600`/`bg-orange-700` 对应 |
| 分家 | 权限邮件不进收件箱;**决策过的不再算待决策**(空串与 null 都算未决策);收件箱未读不含授权栏 |
| 空态 | 主句与 WebUI 的「暂无邮件」逐字一致;三栏各有一句"为什么是空的",且三句互不相同 |
| 接线 | 底部第一项是「通信」;三个 pane 都真的渲染;悬浮加号是圆形且在通信页这一层;收件箱"先分家再折叠";接口路径/决策体三个字段;拒绝要传 `noteText`;过期分支必须说清后果 |
**变异验证**:授权徽标改成看未读 → 红;权限邮件不分出去 → 红;类型名拼错
(权限邮件全混进收件箱)→ 红;决策过的仍算待决策 → 红;收件箱不再分家 → 红 2 条;
发件箱空态去掉说明 → 红。
**判据自己抓到的真 bug**`commTabFromIndex` 原来只判范围,`1.5` 会走到
`COMM_TABS[1.5]``undefined`(表现是"点哪都不亮")。范围检查挡不住非整数,
已加 `Number.isInteger`
**未验****模拟器起来后第一个要看的项**§7.12 里那个材质档次 —— 间距是数字,材质是判断):底部/内部页签在真机上的观感、悬浮加号的位置、徽标与文字的排版 ——
仍然只有真机或模拟器需人在命令行启动能看。已验证构建成功、28 条判据全绿、
六种变异都能判红。
### 7.16 P4外观主题 + 壁纸)跟着**账号**走
服务端 2026-09-13 就已经是权威(`server/internal/handler/appearance.go`,账号级),
WebUI 也接好了;鸿蒙这边此前**完全没有接** —— 主题与壁纸在鸿蒙上是"不存在的东西"。
这一期做的事:
- `api/AppearanceApi.ets``GET/PUT /me/appearance``POST/me/appearance/image`
`GET /me/appearance/image`(图片**带认证取回本体**,不用 `?token=`)。
- `model/Appearance.ts`纯逻辑判据直接跑归一化、PUT 报文、**合并决策**、
模糊值→系统材质映射、主题→系统色彩模式、遮罩浓度、状态文案。
- `common/AppearanceStore.ets`:落地副作用 —— 主题交给系统(`setColorMode`
壁纸取回 `PixelMap`,缓存**按账号**分键。
- 入口两处:`MainPage`(进主界面就应用,否则"改完主题一进主界面就变回去")与
`SettingsPage` 的「外观」段(三档主题 + 同步状态)。
**两条最贵的规则**WebUI 侧都踩过,判据盯着):
1. **服务端"没有记录"时必须以本地为准**`saved === false`)—— 服务端这时回的是一份
*默认值*,拿它覆盖本地 = 把用户已有的主题/壁纸抹掉WebUI 原话:
"每个老用户升级后第一次登录都会发现被重置")。正确动作是把本地那份推上去。
2. **降级必须可见**`local-only` → 界面显示「仅本机」)—— 否则用户以为换设备也能带走。
**判据**`harmony-appearance.test.mjs`11 条,已接进 `run-all.mjs`
归一化(脏值/越界/小数/NaN`image` 档无图 → 退回 `none`PUT 报文字段名(蛇形);
★服务端无记录 → 以本地为准且**一个字段都不能被顶掉**;服务端有记录 → 以服务端为准但
**不擦掉**本地那张服务端还没有的图;离线状态可见;★模糊值→系统材质档次(并与 SDK 的
`BlurStyle` 成员逐一比对);主题→色彩模式(数值与 SDK 的 `ConfigurationConstant.ColorMode`
逐一比对);★路径必须是**相对基地址**的;缓存键**带账号**;两处入口都真的应用。
**变异验证**:服务端无记录时拿默认值覆盖本地 → 红;不管"image 档但服务端没图" → 红;
材质档次写成自造名字 → 红;路径多写一层 `/api/v1` → 红;缓存键不带账号 → 红;
深浅色彩模式数值写反 → 红。
**判据抓到的真 bug两处**
- `snapshotFromResponse` 在"字段缺失"时返回 `bgDim = 0`,而 WebUI 语义是退回 12 ——
因为 ArkTS 的反序列化把缺失字段留成**类里写的默认值**"字段不在"与"字段是 0"分不开。
已把 `AppearanceResponse` 的默认值对齐 WebUI 的 `clamp(..., dflt)` 语义。
- 主题落地时我按"0=浅色、1=深色"写了 `setColorMode` —— **正好反了**
SDK 里 `COLOR_MODE_DARK = 0``COLOR_MODE_LIGHT = 1`):选深色会切成浅色。
发现方式是判据去 SDK 的枚举文件里读数比对,而不是凭印象。映射也因此搬进了纯逻辑
`colorModeValue`),从"某处有个 setColorMode 调用"变成"可判据的行为"。
**⚠️ 更正一处我说得比证据强的地方**:上一版这里写的是"壁纸在真机上的**渲染**效果未验"
听着像"已经画出来了、只是没在真机上看过"。实际情况是:**P4 第一版没有任何东西去画它** ——
`AppearanceStore` 取回了 `PixelMap`、算好了快照,但没有组件把它渲染出来,
也就是说那一版里"壁纸"只有数据没有画面。取回像素这件事是真的(提交信息没写错),
但"渲染未验"这个说法把"没做"说成了"没验"。这条更正记在这里,免得后来人以为是回归。
### 7.17 P4b预设档的画法pi 指出的**信息对等**缺口)
pi 的原话「WebUI 的背景有**预设渐变**,服务端存的是 preset 名 + 参数,
鸿蒙拿到 preset 名画得出来吗?如果只支持 `image``none`,那'换账号后外观跟随'
对预设档就是**不成立**的 —— 用户设了预设,在鸿蒙看到的是没有背景。
这是一个信息对等缺口,不是入口缺口,而且它比上传入口更容易被忽略。」
他说对了,而且当时比这更糟(见上面那条更正)。现在:
- `model/Wallpaper.ts`纯逻辑判据直接跑预设清单id / 中文标签 / 归一化)、
色板、六个预设各由哪些层叠出来、`resolveBackground()` 决定画什么;
- 页面用**系统原语**画:`radialGradient` / `linearGradient`
**网格档**CSS 的 `repeating-linear-gradient`)系统没有对应原语 → 用系统 `Canvas` 画线
(线色/间隔照抄 CSSgray-200 / 0.55 / 28理由写在模块里
- 图片档:`Image(pixelMap)` + 系统遮罩色按服务端浓度压暗;
- `image` 档但图没取回来 → **什么都不画**(画一块空白会被当成"壁纸坏了")。
**判据**`harmony-appearance` 11 → 17 条):预设 id/顺序/标签与 WebUI `PRESETS` 逐字一致;
**每个预设色值与 CSS 调色板变量逐个对照**(这类"看起来差不多"的色值最容易悄悄分叉);
色板反向检查(登记了没用的 → 红);透明必须用关键字而不是 8 位色值;
三档的 resolve 行为;页面真的画了(三种原语 + 图片 + 压暗);
以及**模糊归属的互斥形式**(见下)。
**判据抓到的真 bug**:我把 `--c-blue-200`191 219 254 = `#BFDBFE`)写成了 `#BFDCFE`
(两位字母顺序反了)—— 这正是"照 CSS 读出来比"才拦得住的一类错。
另外判据自己也有两处切片毛病(用 `indexOf('build() {')` 两头夹会跨到别的成员上),已改按行截。
### 7.17a 本节代码的提交归属(免得后来人查不到)
本节的代码(`model/Wallpaper.ts``MainPage` 的壁纸层与页面底让出、判据)**落在这几个提交里**
- `c9717da``1717863` —— 这两个的提交信息写的是 **WebUI** 的事("联系人项玻璃卡"、
"手势与横向滚动分家"):并发写入者与我共用同一个 git 身份,它 `git add -A`
把我尚未提交的工作区一起提交了。**代码是好的,归属是错的。**
- `962df62` —— 提交信息里是本节完整的"改了什么/为什么/怎么验",但 diff 只有判据文件那一处。
所以:`git log -1 -- client/harmony/entry/src/main/ets/model/Wallpaper.ts` 会指向一个
WebUI 提交,顺着本节就能找回来。**给并发写入者的提醒**`git add -A` 会把别人的半成品
一起提交(提交信息与他人工作不符,且一旦对方还要改就只能再补一次提交)。按路径 add 更稳。
### 7.18 pi 撤回的那条口径:模糊归属改成**互斥形式**
pi 撤回了他原来那句"模糊只由壁纸层负责",并说明了它的来源:那是 **WebUI 的架构结论**
——它的壁纸图层自带 `filter: blur()`,浮在它上面的面再 `backdrop-filter` 就是把同一张
糊过的底**糊第二遍**(更脏、更掉帧),所以那条规则在 WebUI 侧是空的;
而同一条 CSS 里它的**底部导航 `.narrow-nav` 是有 `backdrop-filter` 的**
因为那一条背后是**会滚动的内容**,模糊在那里有遮蔽意义。
所以正确的形式是两条性质,而不是"归谁"
1. **同一张底只许被模糊一次**
2. **模糊应出现在"背后是可变内容"的层**
套到鸿蒙:壁纸是整幅图、栏不吃壁纸,用户的模糊偏好被映射成**材质档位**
于是栏上的系统材质就是唯一一次模糊,壁纸层不再糊 —— 满足"只一次",也更符合"用系统方案"。
判据按互斥形式写:**壁纸层不许出现任何模糊/材质****导航条必须有系统材质**
将来 P5 真做悬浮玻璃条(浮在滚动内容上)时,按"背后是可变内容"这条放行第二处,
并在 `GLASS_REGISTRY` 里登记 + 说明它背后确实是滚动内容(不是又一层壁纸)。
**语义转换要记清**pi 要求写进文档,否则以后有人拿"`bg_blur=8px` 与档位对不上"当 bug 报):
WebUI 的"壁纸模糊度px"在鸿蒙变成了"**材质档次**"——**不是同一个物理量**
前者是给 CSS 图层用的半径后者是系统材质的档位Thin/Regular/Thick
映射在 `model/Appearance.ts``blurStyleFor()`,判据与 SDK 的 `BlurStyle` 成员比对。
### 7.19a 判据规范:`client/electron/test/CRITERIA.md`
pi 指出"邻接不是结构"这条已经在同一个仓库露头**三次**(① 窗口式正则被一行注释挤爆;
② 括号配对修掉它;③ 链式修饰符让"看前一个字符"静默失效),共同形式是
**判结构要配对/解析,看邻接或固定宽度都会被合法写法绕过** —— 比记具体招式有用,
所以升格成仓库级的判据规范(`client/electron/test/CRITERIA.md`),并在 `run-all.mjs` 里加了
**自检 3**:规范文件必须在、且必须点到那几条关键规则(规则只活在脑子里等于没有)。
规范里现在有七条:配对/解析而非邻接(含 WebUI 侧 `background.test.mjs` 的存量,
**那是 pi 的地盘,记着不动**)、生成清单 + 结构化 allow-list、变异验证含"变异没生效"
这个反向陷阱)、剥注释读代码 vs 读原文读理由、按行取片段、判据要接线 + 扫描范围要有下限、
判据要点用户真正会点的那一层。另外补了一条本仓刚踩的:**验证要按真实入口跑**
`node --test test/run-all.mjs` 会把 runner 内部的 `process.exit(1)` 吞掉)。
### 7.19 两条跨端约定pi 2026-09-14 复核后确认)
- **`Theme.` 的成员名不改**pi 三条理由:判据钉的是"值来自系统 + 品牌色仍手写"
改名零收益;名字一致本身就是这个跨端词表的价值,`Theme.surface` ↔ WebUI `surface`
是同一件事149 处搬运的风险与收益不成比例)。**边界**:将来某处真需要"系统里更具体的面"
(例如 `ohos_id_color_dialog_bg`**那时新增一个成员**,而不是把已有名字改一遍。
- **可选数值字段的"缺省值"本身就是契约的一部分**WebUI 用 `dflt`、鸿蒙用类字段默认值 ——
两处默认值不一致就会**静默分叉**P4 里真踩过:`bg_dim` 缺失时一边给 12、一边给 0
跨端对齐可选数值字段时,先对齐默认值,再对齐取值。
- **页签键/顺序**`CommTab`/`TABS``WorkCard` 字段集是同一类跨端耦合 —— pi 改 `uiStore.ts`
`CommTabs.tsx` 前会先看鸿蒙这条判据,两边同时改;**不让我单方面红**。
**未做P4c**:壁纸**上传**入口(选图 → `POST /me/appearance/image`。pi 定为 P4c算 P4 范围,
不阻塞别的阶段),照 WebUI 踩过的三条做:**先压缩再上传**(手机直出照片 48MB
**失败必须给原因**(别静默失败)、**上传成功后仍以服务端为权威**`saved` 那套规则对图片同样适用)。
**未验**:预设渐变与图片壁纸在真机上的实际观感(配色是否好看、对比够不够)。
### 7.17b 预设色板**两套**:这不是"观感未验"是机制上确定不同pi 指出)
我上一版把"深色档预设"记成"观感未验"pi 读完 `Wallpaper.ts` 后指出这是**判错的类别**
那边的预设写的是 `rgb(var(--c-blue-100))`,而 `--c-*``.dark` 里**整体换了一套**
blue-100 → `30 43 67`)—— WebUI 的预设**自动随主题变**;这边若只有浅色那套,
深色主题下就是"浅色渐变垫在深色系统表面之下",正是这一整轮在治的那个病。
**它不需要真机就能判:机制写在代码里。**
处理(选 pi 倾向的那条:跟随主题,与 WebUI 一致):
- 色板做成**两套**`LIGHT_*` 取 CSS `:root``DARK_*` 取 CSS `.dark`
`paletteFor(dark)` 选一套,`layersFor(id, dark)` 按主题出层;
- 深浅色的判定放在纯逻辑 `isDarkMode(theme, systemColorMode)`:用户选了 dark/light 就照办,
`system` 时看系统当时反馈的 `colorMode`(数值锚到 SDK`COLOR_MODE_DARK = 0`
`COLOR_MODE_LIGHT = 1`;读不到/未设置按**浅色**,与 WebUI 的 `:root` 默认一致);
- 系统当时的深浅从 `resourceManager.getConfigurationSync().colorMode`
`Context` 基类没有 `config``UIAbilityContext.config` 要转型;两个枚举取值一致,都核过 SDK
- 判据:两套值与 `index.css``:root` / `.dark` 段**逐个相等**;每个预设的深浅两套
**必须真的不同**(否则"两套"是抄了两遍);网格线色也要跟着换;`isDarkMode` 五种输入。
**已知边界(未做)**:用户在应用**运行期间**改系统深浅色,这边不会自动重算
(要重进页面/重进应用)。系统侧的正确做法是订阅 `applicationContext.on('environment', …)`
的配置变化回调 —— 记在这里,没做。
### 7.17c 手写色清册升级成**跨文件按类扫**pi 建议)
原来的 A2 只保护 `Theme.ets`;而 `Wallpaper.ts` 也有手写色14 个预设色)。
那边靠"从 CSS 读出来逐个对照"抓到了 `#BFDCFE`,手法是对的 ——
但若对照是**按名字枚举**的,第 15 个色就会逃掉:这正是 A2 要防的同一件事,只是换了个文件。
现在并成**一份清册、按类扫**:全 `ets/` 树里每个 `X: string = '#RRGGBB'` 都必须在清册里 ——
要么是 `common/Theme.ets` 的品牌/业务语义色,要么是 `model/Wallpaper.ts` 的预设色板
(后者的值由 CSS 两段比对负责,且常量名必须带 `LIGHT_`/`DARK_` 前缀,判据才知道跟哪一段比)。
反向也判:清册里登记了却不存在 → 清册过期。
变异:`Theme.ets` 加一个未登记色 → 红;`Wallpaper.ts` 加一个未登记色 → 红;
加一个"看起来合规"的 `DARK_EXTRA` → 红(不在清册里就是不在)。
### 7.17d 并发写入的两个坑:`TMPDIR` 互踩、提交归属pi 提出)
- **`TMPDIR` 互踩**:这个 worktree 可能同时有多个 agent 跑构建,而 fpm 会把
`release/linux-unpacked`(约 291MB**整份复制**进 `TMPDIR` —— 两边落到同一个临时目录
就是随机的产物损坏/构建失败,比"归属错"难查得多。`whoami` **区分不开**(这里大家都是 root
所以按**会话**分家:`TMPDIR=$PWD/.tmp/${DSH_SESSION_ID:-$(id -un)-$$}`
(已进 `npm run build:linux` 与 BUILD.md 的手敲命令)。
- **提交归属可判**:同时改 `client/harmony/``client/electron/` 的提交**必须在 subject 里
自报家门**`跨端:`)—— 那种提交是合法的("跨端判据要两侧一起改"),只是要标出来;
不标的多半是被 `git add -A` 卷进去的。判据在 `test/commit-hygiene.test.mjs`
基线 = 该判据文件自己的引入提交(**历史不改**,规则管从今往后),
配分类逻辑的合成自检(否则"解析没跑起来"时会全绿)。