Files
MailUI4Agents/docs/HARMONY-ALIGN-PLAN.md
JianFeeeee 3729afd96f feat(harmony): P2a —— 收件箱按会话折叠 + 联系人页卡片视图(判据直接跑同一份逻辑)
按 pi 的结论落地 P2a 的前一半:**先补视图与折叠,再删平级「会话」tab**(tab 本轮保留)。

## 判据怎么"点用户真正会点的那一层"

鸿蒙侧没有设备(`hdc list targets` 为空、模拟器在本机沙箱下起不来),"点一下"暂时
无法自动验。应对不是编个能过的新判据,而是把会点的那一层的内核抽成纯逻辑:
`entry/src/main/ets/model/MailGrouping.ts`(无 UI 依赖),判据用 node 的
`--experimental-strip-types` **执行同一份代码**(`test/harmony-logic.test.mjs`,14 条),
断言的是行为而不是"源码里出现过某个字符串":

- 折叠后组头是不是**最新一封**、组内是否时间倒序、组间排序、同一时刻用 `mail_id` 倒序兜底;
- 时间解析失败**不能让顺序依赖入参**(WebUI 侧踩过的 NaN 比较坑);
- 多账号合并下同名 `session_id` 不能被错并成一组;`session_id` 缺失时各自成组;
- 预算档位与 WebUI `BudgetChip` 完全一致(剩 0 用尽 / ≤1 将尽 / 上限 0 不显示);
- 视图切换与卡片上"最新一封是人还是 Agent"的判据。

页面那一层另用源码判据钉"确实调了这些函数",两层合起来覆盖「逻辑对」+「页面接上了」。
**变异验证 4 处全部判红**:去掉组内排序(2 条红)、预算阈值 `<=1` 改 `<1`、
分组键去掉账号前缀、页面不再区分单封组。

## 收件箱折叠

- 组头取组内最新一封的别名与主题,带未读数徽标与「N 封」,点它展开/收起;
- **单封不成组、平铺**(与 WebUI `isFlatGroup` 同结论:给孤立一封信套组头只是多一次点击);
- 多账号是鸿蒙特有:分组键带账号前缀;`session_id` 缺失按 `mail:<id>` 各自成组。

## 顺带修掉一个"看起来是总数、其实是未读数"的显示

`/me/mail/inbox` 的 `total` 是 **`CountUnread`(未读总数)**,不是总封数
(`server/internal/handler/me.go`)。鸿蒙底部原写「共 N 封」⇒ 同一屏出现
「共 7 封」和「未读 7」两行自相矛盾的字。改成:未读数用服务端 `total`(权威,
原来数这一页会少报);「共 N 封」→「已加载 N 封」;**这一页取满时如实提示
「已加载 50 封(本页上限 50,可能还有更多)」** —— 客户端没有可信总封数,
就不能把 50 封说成全部(pi 提醒的"别让只取 50 封伪装成只有这么多会话")。
WebUI 侧不读这个字段,故只影响鸿蒙。

## 联系人页补卡片视图(撤 tab 的前置)

- 右上角切列表/卡片,标题「联系人」/「工作列表」(与 WebUI 同词),切换规则在
  `nextContactView()`;
- 卡片对应 WebUI 的 `WorkCard`:Agent 名 + 工作目录 + 未读徽标、会话别名、
  **主题当主角**、最新摘要 + 人/Agent 标记、`N 封 · 时间`、权限档位徽标、
  **往返预算条**(同一档位判据)。
- 平级「会话」tab 暂留:撤 tab 按 pi 的顺序排在后面单独一步(撤早了预算/status/from_agent 没处看)。

## 验证

- `hvigorw assembleHap` **BUILD SUCCESSFUL**(`.ts` 纯逻辑模块被 `.ets` 引用,实测可行)。
- `npm test` **退出码 0**:窄屏布局全通过、主题 30、背景 34、cross-client 8、
  harmony-logic 14、packaging 3、vitest 258/258;新判据已接进 `npm test`。
- **视觉与点击仍未验**(无设备):展开手感、卡片间距、组头命中区没有任何自动判据
  能代替人眼 —— 交付按"结构/逻辑已验证、观感未验"写,未写成已完成。
2026-09-14 13:36:05 +08:00

317 lines
21 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` 写死)。
## 三、分期(按"能独立验收"切)
**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`(14 条)—— 断言的是**行为**:
折叠后组头是不是最新一封、单封是不是不成组、同一时刻是否用 `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、背景 34、cross-client 8、
harmony-logic 14、packaging 3、vitest 258/258。
- **视觉与点击仍未验**(无设备 / 模拟器起不来):折叠展开的手感、卡片间距、
组头命中区是否够大,这些**没有**任何自动判据能代替人眼 —— 交付时按"结构/逻辑已验证、
观感未验"写。下一轮:P2b 通信页签(收件箱/发件箱/授权 + 徽标)。