Files
MailUI4Agents/docs/HARMONY-ALIGN-PLAN.md
JianFeeeee a6b5e52f45 test(harmony): 判据按 pi 复核意见补强 —— 手写色登记表、玻璃叠用形状、按标题取小节、遮罩必须被用、废弃 API 清单从 SDK 生成
pi 逐行读了 `cross-client-theme.test.mjs` 与 `Theme.ets` 后指出五处(第一处是真缺口),
外加一条建议。全部处理,并且**每一处都用变异验证过**。

## 一(真缺口):枚举 11 个"必须是系统资源"的名字,挡不住第 12 个新写死的手写色

`static readonly brandSecondary: string = '#123456'` 这种新增**三条判据都碰不到**:
A(不在名单里)、B(六位、不是半透明)、裸色值那条(只管 `pages/`)。
原理与当初 14 处 Google 色逃出去是同一条 —— **枚举挡实例,类才挡漂移**,
只是这次枚举的单位是**名字**。补 `A2` 条:

- 枚举 `Theme.ets` 里所有 `static readonly X: string = '#……'`,未登记的 → 红;
- 名单里已不存在的名字 → 红(名单不能烂成化石);
- `Theme.ets` 里的「手写色登记表」段必须逐个列出这些名字(理由不能只存在于记忆里);
- 自检:把一个**新的**手写色塞进源码字符串,确认这条抓得到。
  `Theme.ets` 因此新增登记表(17 项,每项一行理由,按品牌 / 业务语义 / 档位胶囊分组)。

变异:Theme.ets 加 `brandSecondary` → 红。

## 二:C 的"玻璃只有一处"**说错了自己断言的东西**

`assert.equal(glassCalls.length, 1)` 断言的是"全仓共一处",**不是**"没有嵌套":
同页两个**并列**玻璃面(没问题)会让它红,真嵌套它没在判。P5 正是悬浮玻璃导航,
到时若把 1 改成 2 就等于不判。改成形状判据:

- 收集每个调用点所作用的**组件块**(按括号配对回溯;注意 ArkUI 修饰符是链式的,
  `X.blur(A).blur(B)` 前面是 `)` 不是 `}` —— 第一版只看一个字符,对这种写法**静默失效**,
  是判据自检抓出来的);
- 判两种叠法:同一组件上叠多次(同一块)、以及套在另一层玻璃的子树里;
- 每一处玻璃都要在 `GLASS_REGISTRY` 里登记(附一句为什么),名单里的位置若已不存在 → 红。

变异:链式叠两层 → 红;通信页多开一处未登记玻璃 → 红;导航条块内嵌玻璃 → 红。

## 三:文档小节用 `indexOf('有意差异')` 会**拿错段落**

别的段落正文里出现这四个字,切片就从那里开始,后面所有断言都在**别的段落**上判。
改成**按标题**定位(正则匹配 `^#{2,4}…有意差异…$`),并加了表头列名断言
(WebUI / 鸿蒙 / 为什么)—— 拿错段落时表头不会是这个形状,于是它自己会红。

顺带修掉一个**我自己的**同类毛病:品牌色那行原来用"关键词 + 80 字符窗口"判,
窗口宽度在赌表格单元格字符数(该行两格之和 > 80)。改成**按行取**那一行再断言。
另外把偏移算术的切片换成**按行**切片(偏移差一个字符就会把最后一行拦腰截断,
现象是"品牌色那行只剩 57 字符"这种看着像文案、其实像切片的怪事)。

变异:文件前面插入含「有意差异」的段落 → 仍绿(按标题定位生效);
再改坏真表里的品牌色行 → 红(确实读的是那一张表)。

## 四:`Theme.overlay` 只判了"存在",没判"被用"

没使用点的令牌是自证。补断言:它必须在 `Theme.ets` **之外**有真实使用点
(实测 `SettingsPage.ets` / `MailDetailPage.ets` 两处自绘弹层在用),
并把 `overlayColor`/`overlayAlpha` 消失的理由写进 §7.12 —— 否则下一个人会当成漏改补回来。

## 五:材质档次是这次替换里唯一"判据绿但可能观感错"的地方

`COMPONENT_THICK` 的依据(对 THIN / BACKGROUND_* / ULTRA_THICK 的取舍)写进 §7.12,
并把"材质档次在导航条上的实际观感"列为**模拟器起来后第一个要看的项**(间距是数字,材质是判断)。

## 六(建议):废弃 API 判据**类化** —— 清单从 SDK 生成

原来是手写"不得再用全局 `promptAction.showToast`"(只挡已踩过的那个)。
现在从 SDK 生成:顶层(花括号深度 0)被标 `@deprecated` 的 `declare function`
—— 实测 68 个名字(含 `animateTo` / `getContext` / `px2vp`),配一份**空**的 allow-list。
深度判定是必要的:`declare namespace fileIo { declare function open() }` 里的 `open`
是命名空间成员,算进来会造一堆假红。只算全局调用(排除 `x.name(`)。

变异:调 `px2vp(10)` → 红;`this.px2vp(10)`(成员调用)→ 绿(假阳性自检)。

## 验证

`hvigorw assembleHap` BUILD SUCCESSFUL;`npm test` 退出码 0
(11 个判据文件全绿 + vitest 258/258)。cross-client-theme 13 → 14 条,harmony-system-api 4 → 5 条。
2026-09-14 14:26:45 +08:00

673 lines
48 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. 无法验证的要**如实标注**例如"编译通过视觉未验"不能写成"已完成"。
---
## 五、交接核对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 ✅(壁纸上传除外 | 主题/壁纸`/me/appearance` | 换账号外观跟随 ✅(缓存键带账号服务端无记录时以本地为准 ✅(§7.16)。**未做**壁纸**上传**入口需要 picker)。**未验**真机渲染 |
| 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 调用"变成"可判据的行为"。
**未验 / 未做**:壁纸在真机上的渲染效果(需要真机或模拟器);
**壁纸上传(选图 → `POST /me/appearance/image`)还没接** —— 需要文件选择器picker
这一期的 API 与命名都已就位,但入口没做,所以**不要**把它当成"已完成"。