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

48 KiB
Raw Blame History

鸿蒙客户端与 WebUI 的对齐计划

用户2026-09-14「安排对齐」「鸿蒙 ui 应当交给对这一块更熟悉的 dsh 负责」。 这份文档把差距分期写清楚,免得每轮都从"感觉还差什么"重新猜。

负责人dsh2026-09-14 起由 jianf 指定)。移交信: 线索 harmony-ui-alignmentdsh@/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 钉住取值一致性。
  • 旧调色板清除:页面里的 #1A73E8Google 蓝)/#333333/#F5F7FA 等 与 WebUI 不同的写死色值全部换成令牌203 处)。
  • 修掉只剩第一个 tab 高亮的 bugcurrentIndex === 0 写死)。

二·五、做法上的一条硬要求:用系统方案

用户2026-09-14「鸿蒙也同步但是鸿蒙要求用系统方案」。

含义:能交给系统的就交给系统 —— 用 ArkUI 的组件、材质与语义资源, 而不是把 WebUI 那套"手写 rgba + 自定义模糊 + 自制卡片"照搬过来。 系统材质会跟随深色模式、动效曲线与无障碍设置,手写的那套不会。

WebUI 做法 鸿蒙应该用
手写 rgba(...) + backdrop-filter backgroundBlurStyle(BlurStyle.*)(系统材质)
自制圆角导航条 系统 Tabs + TabBarbarBackgroundBlurStyle
自制 Scroll + Column 系统 List / ListItemdivider / 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/decidebody {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 testHEAD 上就是红的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(实例 HarmonyPhonehdc 在 /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.tsgroupMailsBySession()session_id 折叠、组头取最新一封, isFlatGroup() 让单封不成组)。

落地顺序(原样采纳):

  1. 给「联系人」页补列表 / 卡片切换,卡片视图承接预算条 / status / 对方 Agent 收件箱加按会话折叠(组头带未读)。折叠前先看 total —— 别让"只取了 50 封" 被折叠伪装成"只有这么多会话"。
  2. 两件都到位后再删平级「会话」tab。在那之前删 = 丢信息:轮次预算、statusfrom_agent 会无处可看,而"预算跑满"正是需要人介入的信号。

合并后导航正好是 通信 / 日历 / 联系人 三项P4P6 都在这个形态上做。 ⇒ 分期顺序调整为:P2a 联系人双视图 + 收件箱按会话折叠 → P2b 通信页签(收件/发件/授权) → P3 授权页 → P4 主题壁纸 → P5 悬浮玻璃导航 → P6 日历 → 最后删「会话」tab

6.2 遮罩令牌

overlay 拆成 overlayColor + overlayAlphaWebUI 是 --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/componentstest/stores)—— 那条"防漂移判据"从没在默认套件 里跑过。已加进 npm test

6.6 npm test 现在全绿(退出码 0

窄屏布局全通过、主题 30、背景 34、cross-client 8、packaging 3、vitest 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.mjs19 条)—— 断言的是行为 折叠后组头是不是最新一封、单封是不是不成组、同一时刻是否用 mail_id 倒序兜底、 时间解析失败会不会让顺序依赖入参、多账号同名会话会不会被错并、预算剩 1 个来回是哪档。
  • 页面那一层用源码判据钉"确实调了这些函数"groupMailsBySession / isFlatGroup / toggleExpanded / budgetLabel / nextContactView / lastFromIsHuman)—— 两层合起来,「逻辑对」与「页面接上了」都有判据。
  • 变异验证4 处,全部判红):去掉组内排序 → 2 条红;预算阈值 <=1<1 → 1 条红; 分组键去掉账号前缀 → 1 条红;页面不再区分单封组 → 1 条红。

这条判据已接进 npm testnode --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 返回的 totalCountUnread(未读总数),不是总封数 server/internal/handler/me.goMeGetInbox)。鸿蒙底部原来写「共 N 封」, 于是同一屏上会出现「共 7 封」和「未读 7」这种自相矛盾的两行字。

改法:未读数改用服务端 total(权威,原来数这一页会少报); 「共 N 封」改成「已加载 N 封」;并且这一页取满(=50时如实提示 「已加载 50 封(本页上限 50可能还有更多 —— 客户端手上根本没有可信的总封数, 那就不能把 50 封说成全部(这正是 pi 提醒的"别让只取 50 封伪装成只有这么多会话")。 WebUI 侧完全不读这个字段(mailStore 里没有 total),所以这条只影响鸿蒙。

7.4 联系人页:补上卡片视图(为撤 tab 做准备)

  • 右上角切换列表 / 卡片,标题随视图变(「联系人」/「工作列表」,与 WebUI 同词); 切换规则在 nextContactView(),判据直接执行它。
  • 卡片(对应 WebUI 的 WorkCardAgent 名 + 工作目录 + 未读徽标、 会话别名(空则「(未命名会话)」)、主题当主角、最新摘要 + 人/Agent 标记 lastFromIsHuman、「N 封 · 时间」、权限档位徽标、往返预算条 (档位与 WebUI BudgetChip 同一判据:剩 0 红 / 剩 ≤1 橙 / 其余中性;上限 0=不限则不显示)。
  • 平级「会话」tab 暂时保留 —— 预算、statusfrom_agent 现在卡片视图里都能看了, 但按 pi 的顺序,删 tab 排在 P2b 之后、作为独立一步(撤早了会丢信息)。

7.5 本轮验证与如实标注

  • hvigorw assembleHap BUILD SUCCESSFUL.ts 纯逻辑模块能被 .ets 引用, 实测可行 —— 这是"判据能执行同一份代码"的前提)。
  • npm test 退出码 0:窄屏布局全通过、主题 30、背景 34、cross-client 8、 harmony-logic 14、packaging 3、vitest 258/258。
  • 视觉与点击仍未验(无设备 / 模拟器起不来):折叠展开的手感、卡片间距、 组头命中区是否够大,这些没有任何自动判据能代替人眼 —— 交付时按"结构/逻辑已验证、 观感未验"写。下一轮P2b 通信页签(收件箱/发件箱/授权 + 徽标)。

7.6 模拟器为什么仍然起不来:权限门是"无人可批准"

7.5 的"无设备"这次查到根上了。DevEco CLI 的说明(deveco-cli skill确认本机 一个按需启动的模拟器实例 HarmonyPhoneKVMharmony-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.mjsbuild-stamp.test.mjs 写好了也没接线(各 8 / 4 条判据从未跑过);
  4. npm test&& 链:前面红一条,后面全部不跑packaging 那条因此长期隐身)。

改法(test/run-all.mjspi 建议的"全跑完再算退出码"

  • 每条判据都跑,红的收集起来最后一起报、一起退出;
  • 自检 1:清单里的文件必须存在(写错名字 = 一条判据静默消失);
  • 自检 2test/ 下每个 *.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 run9 个判据文件全绿 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-unpacked291MB整份复制进 TMPDIR,而本机 /tmp 是 9.8G 的 tmpfs、 被 /tmp/gocache4.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/TabBarbarBackgroundBlurStyle)取代自制导航条; List/ListItemdivider/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_separatorohos_id_color_background / _sub_backgroundohos_id_color_text_primary / _text_secondary / _text_tertiaryohos_id_color_emphasize(强调色)、ohos_id_color_warningohos_id_color_alertohos_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.mjs4 条,已接进 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_不存在 → 报可用取值; 名字解析精确到 ,/= 分隔符 —— 早先的宽松写法把文档里的 TR 也算成了枚举成员)。

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_THICKpi 指出:判据只能钉"来自系统枚举",钉不了"选对没选对" 所以理由要写下来,免得后人以为随便挑的):底部导航栏是内容之上的一层,要挡住滚动内容 又不至于把内容糊没 —— COMPONENT_* 系列是"组件材质"(作用于一个组件表面), 其中 THIN 在浅色壁纸上几乎看不出分层导航条会像没浮起来BACKGROUND_* 系列是 整窗背景用的(会把下方内容整体重绘,这里是叠一层而不是换背景,用它会与页面底色打架); ULTRA_THICK 会把导航条底下的内容糊成一块。所以取 COMPONENT_THICK。 这条只有真机能判,见下面「模拟器起来后第一个要看的项」。

7.13 系统方案替换(第一批)+ 跨端判据从"取值"改"意图"

改了什么(鸿蒙侧):

  • Theme.ets 的表面/文字/分隔/遮罩/圆角来源换成系统 pageBg→ohos_id_color_backgroundsurface→ohos_id_color_list_card_bgsurfaceMuted→ohos_id_color_sub_backgroundborder→ohos_id_color_list_separator、 三级文字 →text_primary/secondary/tertiaryoverlay→ohos_id_color_mask_regularradiusCard/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( / 0xRRGGBBAApi 指出的洞)
新增 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 SUCCESSFUL、13 条跨端判据 + 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 接着跑了。

顺带清掉一处死代码:收件箱里那个「写邮件」bindSheetcomposeVisible 从来没被置为 true谁也打不开),随新建入口改成悬浮加号一并删除。

日历为什么还没有入口它的内容P6网格 + 事件读写 + 左右滑动翻页)还没做。 先放一个点进去空着的入口,比暂时没有入口更糟 —— 所以等内容一起上。 这是有意排序,不是漏做(记在这里以免被当成遗漏)。

判据harmony-logic.test.mjs28 条):

判据 说明
页签键与顺序 从 WebUI 的 uiStore.ts 抽出 CommTab 联合类型、从 CommTabs.tsx 抽出 TABS 标签,比键集合与顺序(两边不一致时"恢复上次页签"会悄悄退回)
页签状态机 键↔下标往返;认不出的键/越界下标/非整数 → 回收件箱(不落在空白 pane 上)
徽标 收件箱看未读、授权看待决策不是权限邮件总数、发件箱无0 不显示;>99 写 99+;红/橙与 WebUI 的 bg-red-600/bg-orange-700 对应
分家 权限邮件不进收件箱;决策过的不再算待决策(空串与 null 都算未决策);收件箱未读不含授权栏
空态 主句与 WebUI 的「暂无邮件」逐字一致;三栏各有一句"为什么是空的",且三句互不相同
接线 底部第一项是「通信」;三个 pane 都真的渲染;悬浮加号是圆形且在通信页这一层;收件箱"先分家再折叠";接口路径/决策体三个字段;拒绝要传 noteText;过期分支必须说清后果

变异验证:授权徽标改成看未读 → 红;权限邮件不分出去 → 红;类型名拼错 (权限邮件全混进收件箱)→ 红;决策过的仍算待决策 → 红;收件箱不再分家 → 红 2 条; 发件箱空态去掉说明 → 红。

判据自己抓到的真 bugcommTabFromIndex 原来只判范围,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.etsGET/PUT /me/appearancePOST/me/appearance/imageGET /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.mjs11 条,已接进 run-all.mjs 归一化(脏值/越界/小数/NaNimage 档无图 → 退回 nonePUT 报文字段名(蛇形); ★服务端无记录 → 以本地为准且一个字段都不能被顶掉;服务端有记录 → 以服务端为准但 不擦掉本地那张服务端还没有的图;离线状态可见;★模糊值→系统材质档次(并与 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 = 0COLOR_MODE_LIGHT = 1):选深色会切成浅色。 发现方式是判据去 SDK 的枚举文件里读数比对,而不是凭印象。映射也因此搬进了纯逻辑 colorModeValue),从"某处有个 setColorMode 调用"变成"可判据的行为"。

未验 / 未做:壁纸在真机上的渲染效果(需要真机或模拟器); 壁纸上传(选图 → POST /me/appearance/image)还没接 —— 需要文件选择器picker 这一期的 API 与命名都已就位,但入口没做,所以不要把它当成"已完成"。