# 鸿蒙客户端与 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 已完成)** —— 令牌 + 颜色替换 + 编译通过。以下 P2–P6 归 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. 无法验证的要**如实标注**(例如"编译通过、视觉未验"),不能写成"已完成"。 --- ## 五、交接核对(dsh,2026-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-*` 比鸿蒙令牌,防"看起来差不多")。 **③ 导航的差距不止"观感不同",是信息架构不同。** WebUI(2026-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**(不是断言页签个数);点「通信」回到上次的子页签 | | P2b | 发件箱页:`GET /me/mail/sent`,复用列表项 | 能看到已发邮件、点开进详情、空态有说明 | | P3 | 授权页:`GET /permission/pending` + `POST /permission/decide` | 未决口径与 WebUI 一致(无 `permission_result`);点同意/拒绝后状态立刻变;拒绝可填备注且**备注送达模型**;决策请求已过期(`expired`)必须当场说清"这次批准不会恢复原调用" | | P4 | 主题/壁纸(`/me/appearance`) | 换账号外观跟随;服务端无记录时以本地为准,不用默认值覆盖 | | P5 | 悬浮玻璃导航(取代系统 TabBar) | 模糊只由壁纸层负责;列表项每项一张卡;命中区 ≥44vp | | P6 | 日历(`/calendar/events`,含 ics 导入导出) | 手势阈值与 WebUI 一致(水平 ≥40px、≥1.5× 垂直、<600ms) | **视觉/交互怎么验**:本机有 `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-700),WebUI 侧不需要动代码; `overlay` 是唯一没有对应物的(WebUI 弹层不压遮罩)。若 pi 认为 WebUI 侧也该 立同名令牌,我再改。 → **已答(见 5.6):WebUI 侧不立同名令牌;鸿蒙侧的 `overlay` 拆成 `overlayColor` + `overlayAlpha`。** --- ## 六、pi 的回复已落地(dsh,2026-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` 会无处可看,而"预算跑满"正是需要人介入的信号。 合并后导航正好是 **通信 / 日历 / 联系人** 三项,P4–P6 都在这个形态上做。 ⇒ 分期顺序调整为:**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、背景 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:1),hover 还会在白导航上闪出一块近黑。已换成 `.nav-item` + `border-gray-200`。 **未加"Sidebar 里不许有 chrome-*"的判据**:`Sidebar.tsx` 另有一处 `bg-chrome-600 text-chrome-100` 是实心小色块(正常用法),一刀切会误红。 ### 6.8 仍未验的 鸿蒙侧**视觉与点击**仍未验(模拟器在本机沙箱下起不来,见 5.4)—— 本轮的鸿蒙改动只有颜色令牌,判据能覆盖;**到 P2a 一定要有人眼或设备**, 否则"点页签落在哪个 pane"这条判据无法证明。 --- ## 七、P2a 落地(dsh,2026-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:` 各自成组。 ### 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、背景 34、cross-client 8、 harmony-logic 14、packaging 3、vitest 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. 两条已同步的语义照做:**列表项与顶部都是"每项一张卡/气泡"(不是通栏)**、 **玻璃只出现在一层**(不嵌套各自加模糊)—— 收件箱现在的通栏行 + 分隔线要改成卡片/气泡,这项排在系统材质替换之前做, 因为它决定组件结构。