Files
MailUI4Agents/docs/reviews/fix-report-2026-09-26.md
JianFeeeee 2530229180 docs(审查): 归档本轮四份代码审查报告
`docs/reviews/` 此前一直是**未跟踪**状态 —— 审查报告只在磁盘上,
不进版本库 ⇒ 换机器、换会话、给别人看时全部拿不到,
而它们正是本轮五个修复(hap 出库 / SSE 写锁 / 换身份清数据 /
鸿蒙门禁三态 / HMS 配额)的**来源**。

  push-and-gui-review.md     推送链 + Electron GUI(HMS 配额那两条)
  electron-gui-review.md     换身份不清数据
  harmony-client-review.md   鸿蒙:门禁 fail-open / MailStore 快照共用 / clear() 零调用方
  harmony-pages-review.md    页面层
  harmony-state-review.md    状态层
  fix-report-2026-09-26.md   上述修复的实施记录

其中 `harmony-client-review.md` §三.1 记的那条值得单独留意:
该报告自己声明「ArkTS 语言规范层面零违规,本文所有问题都是**逻辑缺陷**」——
本次提交的三处鸿蒙改动也只动逻辑(门禁条件、logout 清理),
不碰语法层。
2026-09-28 08:46:02 +08:00

12 KiB
Raw Blame History

修复报告 —— 2026-09-26

按「CRITICAL + HIGH + MEDIUM」范围修完两端,29 个文件。 每处修复都带可复现的验证;没有验证过的改动一条都没有。


0. 验证口径(先说清楚什么算「修好了」)

层 工具 结果
ArkTS 编译 devecocli build(SDK 6.1.0(23),API 26) ✅ BUILD SUCCESSFUL
端上安装 + 启动 devecocli run ✅ 装得上、起得来
577 条判据 node test/run-all.mjs 见 §3
单测 npx vitest run ✅ 15 文件 / 266 → 270 通过(新增 4 条)
类型 npx tsc --noEmit ✅ 无错
Go go test ./... ✅ 全套通过(本轮才第一次跑通,见 §5)

★ Go 测试之前跑不起来(GOMODCACHE/GOPATH/GOCACHE 都没设)。 补齐之后(含 GOTOOLCHAIN=local,本机 go1.25.12 与 go.mod 的 1.25.0 匹配) 才发现推送额度那个 bug —— 上一轮我"跑不了 Go 测试"是能力缺口,不是环境缺陷。


1. 两处跨账号数据泄露(CRITICAL)

1.1 GUI:切账号不清 store ⇒ A 的邮件显示在 B 下

AccountSwitcher.tsx:56-62 原来只 setActive + fetchInbox('all')。 而 setActive 会 syncAuth() 翻 api/config 单例的 API_BASE 与 bearer —— 此后 getSent() / getMail() / archiveContact() 全带新账号的凭证, 而 sent、currentSession(+Mails)、contacts 全是旧账号的。

修法:把"清空"收成一个函数 lib/resetAccountData.ts,三条身份路径共用:

入口 调用点
主动切账号 AccountSwitcher.pick
登出 authStore.logout
401 掉登录 authStore.markAnonymous

后两条原来一条都没有 —— 它们不在"切账号"的代码路径上,grep 找不到, 是最容易漏的那种。三个 store 各加 resetAll(),纯界面状态仍归 uiStore.reset, 不混("切账号"不能变成"退出登录")。

行为锁:test/stores/resetAccountData.test.ts(4 条)。 做过变异验证 —— 把 resetAccountData 改回只清 mailStore, 两条断言立刻红(另两条本就该绿,因为它们只查幂等与 API 形状)。 不是"写了测试就绿"的空壳。

1.2 鸿蒙:删账号不清快照 ⇒ 下一个登录的人看到上一个人的邮件

MailStore.clear() 从写下来到今天零调用。同一个根因的另一端。

修法(两处,都收口):

  • api/Logout.ets ③′ —— 放在 disconnectAll() 之后(晚一步清更保险);
  • SettingsPage.removeAccount —— 只在删的是当前账号时清(删别人的账号不该清)。

★ 为什么放 performLogout 而不是各调用点自己清:它已经写明"两个入口必须做同一件事", 复制一份到侧栏就会重新分叉。


2. 推送:额度按「设备数」在扣(HIGH)

hms.go:192 传的是 len(tokens),而华为按 messages:send 的调用次数计。

⇒ 3 个设备收到 1 封邮件就吃掉 3 条额度,实际只发出去 1 条。 多设备自部署用户按 1/设备数 的速度提前耗尽 1000 条/天。

而且它扣在投递之前:accessToken 失败 / HTTP 失败 / 非成功码, 一条都没发出去,额度已经扣了,失败只 log.Printf ⇒ 一次抖动静默烧配额。

修法:reserveDaily(1),并挪到 accessToken 之后。 为什么不是"发送成功后再扣":那会超发(并发下多 goroutine 都能过检查)。 前置预留 + 放在"确认能发"之后 = 宁可少算也不多发。

测试:新增 TestHMSDailyLimitCountsMessagesNotTokens(3 设备 = 1 条)。 原有的 TestHMSDailyLimitStopsSending 每次只传 1 个 token, 所以 len(tokens) 恰好等于 1 —— 它一直是绿的,却没钉住单位。 这正是"测试通过 ≠ 行为正确"的教科书案例。


3. 顺手挖出来的三个真问题(都不在原报告里)

3.1 小字对比度 2.85:1 —— 而且上一轮的修法是错的 ★

设备判据抓到浅色小字(时间戳、「N 封」,66 处)= rgb(153,153,153) 压白底 = 2.85:1(< 3:1)。

2026-09-19 的修法是让 textSubtleFor() 返回 textMuted,理由写着 「二级色语义对得上,且跟随主题」。

复扫证明那个前提不成立:本机 HarmonyOS 6.1.1 上 ohos_id_color_text_secondary 实测也是 rgb(153,153,153), 与三级色同值 ⇒ 换过去一点没变。

⇒ 这是「系统语义 ≠ 我们的可读性要求」的第三次复发 (前两次:surface 当前景色、accent 深色没调亮)。 系统色只保证"比一级淡"这层层次关系,不保证 WCAG 下限。

修法:自己拥有这一档 textSubtleLight #6B7280 / textSubtleDark #8A92A1 (= WebUI gray-400 的两套主题值,实测 4.83:1 / 5.51:1), 按纪律登记进 SELF_OWNED_COLORS + Theme.ets 的手写色登记表; 顺手移除 textSubtle 这个原始令牌(它就是那个坑的来源, 留着只会让人以为"三级色可以直接用"); 8 处直接用 Theme.textSubtle 的地方(绕过访问器)全部改走 textSubtleFor()。

验证:真机 21/21 通过(改动前 4 处红)。

3.2 dumpLayout 在本机模拟器上必超时(判据自己跑不成)

uitest dumpLayout 裸调用 → Wait for subscribe uitest.broadcast.command.reply timeout (非 0 退出、stdout 空)⇒ 日历手势那类判据直接报错。 加 -p <path> → 正常。

修法:harmony-device.mjs 的 dumpLayout 显式给路径 (顺带 rm 掉设备临时文件;路径带 pid+时间戳,避免并发判据互相读到对方中间态)。 修完 cross-client-gesture 9/9 通过(修前设备半边红)。

3.3 harmony-imageprep 的"素材不在就跳过"守卫形同虚设

hdc shell 把 stderr 并进 stdout,于是文件不存在时 stdout 是 ls: … No such file or directory —— 里面仍然含有文件名, listed.includes('real-wallpaper.jpg') 判成"在" ⇒ 该跳过的继续跑 ⇒ 撞在 /bin/file 上报错(而不是跳过)。

修法:判存在看退出码 + 行首形状,不看"输出里有没有那个名字"。


4. 其余修复

GUI

严重度 位置 问题 修法
HIGH MailView.tsx:756 审批失败只 console.error ⇒ "点了没反应"、Agent 一直阻塞 加 submitError 并渲染(审批/提问两种形态共用一条)
HIGH MailView.tsx:390 转发双发窗口 + 关窗依赖可能失败的刷新 同步 inFlight ref 闸门;刷新改 allSettled
HIGH CalendarView.tsx:267 object URL 同步 revoke ⇒ Firefox/WebKit 静默 0 字节 推迟 setTimeout(…,0) + <a> 挂上 document 再摘
MED CalendarView.tsx:101 翻月无代次守卫 ⇒ 标题与内容错配 loadGen(照 ThreadView.tsx:62-84 的既有形状)
MED BackgroundPicker.tsx:120 内联 backgroundImage 覆盖预设类 ⇒ 6 个格子显示同一张照片 去掉内联样式
MED appearanceSync.ts:40 lastUploadedImage 不分账号 ⇒ B 永不上传却报"已同步" 改 Map<accountId, string>
MED 3 处 setTimeout 无卸载清理、连点留多个定时器 句柄存 ref + 卸载清

鸿蒙

严重度 位置 问题 修法
CRITICAL AdminUsersPage.ets:319 roleKnown && !isAdmin ⇒ 身份未知时把管理台整个渲染出来 三态化:!roleKnown 停在大门外
HIGH MailDetailPage.ets:1939 'permission' 恒不成立(服务端发 permission_request)⇒ 权限面板上回车同时开回复框 改正字面量
HIGH MainPage.ets:2759 轮播换字的 setTimeout 句柄丢弃 存 ref + stopTopbarRotation 一并清
HIGH MailStore.ets:499/628 cc_list.length 无 ?? [](收件箱+发件箱两处) 补兜底
MED Theme.ets 8 处绕过 textSubtleFor() 全部改走访问器

系统返回键:试了,失败,已撤回 ⚠️

这一项我在原报告里评的是 HIGH,动手后发现自己的修法把应用关掉了。

原问题(仍未修):MainPage 的 PopIntent 监听者只接 Esc, 系统返回键/手势绕开它走 Navigation 自己的 pop ⇒ ① KEY_COMM_STACK_DEPTH 停在旧值;② KEY_OPEN_MAIL_ID 留着上一封 (回列表按回车会开"回复");③ closeDetail() 是死代码。

我做的:EntryAbility.onBackPressed() → PopIntent.request()。 为此把 PopIntent 的回调从 () => void 改成 () => boolean (调用方需要区分"退了"和"没什么可退")。

实测结果:应用在前台按一次系统返回键 ⇒ UIAbility 被销毁 (hilog HandleAppDied,aa dump -a 里 EntryAbility 消失), harmony-admin 设备判据挂住不返回。 对照实验:把这三处 stash 掉重编重装,同一条判据 31/31 通过。

⇒ UIAbability.onBackPressed() 在这条链上没被调用到 (Navigation 自己先消费了返回事件)。

为什么撤回而不是继续查:要验的是"框架内部返回事件的分发顺序"这类 框架行为,本机模拟器这条又不能代表真机。与其赌一个会关掉应用的 半成品,不如整块撤回、留成一条带复现步骤的账。

试过并被否掉的三种接法(都写进代码注释了):

  1. Navigation.onPop —— 不存在(编译报 Property 'onPop' does not exist);
  2. pushPath 第三个参数 —— 不接受(只接受 1–2 个);
  3. NavPathInfo.onPop —— 存在,但只在 pop() 带 result 时触发, 系统返回键不走 pop(result) ⇒ 照样漏。

已登记:docs/DEBTS.json 的 harmony-system-back-key(28 笔), 含建议的下一步(挂 NavDestination 的 onWillDisappear, 它是每页自己的出栈时机,不依赖谁分发返回事件 —— 需真机确认)。


5. 三个诚实的缺口

5.1 3 条红判据没修,且与本轮无关

  • build-stamp —— 产物构建于 d9e71a4,HEAD 是别的;
  • commit-hygiene —— 两个 .hap 仍被 git 跟踪(安全问题,见下);
  • criteria-hygiene —— 探针路径在本地历史里找不到。

三条都与本轮改动无关(改动前后完全一致)。

5.2 安全问题仍然开着 ★

AgentMail-v1.2.1-pushdiag.hap 与 AgentMail-v1.2.1-pushlog2.hap 仍被 git 跟踪,内含 AGC 信封密钥/校验和/api_key。 已确认尚未推送(git cat-file -e origin/main:<path> 两个都失败), 引入于 f51c9c8。下次 push 前必须 git rm --cached <两个文件>, 并且建议轮换密钥。

5.3 我改不动的两条

  • HMS click_action 缺失(原报告 HIGH 2):载荷形态要改,但验证需要真机产出的 token —— hms.go:50 自己写着"我无法在本机验证成功路径"。改完无法证明, 不动。模拟器不是真机(拿不到 AGC 真实 token)。
  • 锁屏文案里的邮件主题(原报告 MEDIUM 3):先更正我上一轮的说法 —— push.go:36-38 的注释写的是「正文不进通知」,那是准确的 (hms.go 发的是 title=主题 + body=发件人,邮件正文确实没进去)。 我上轮说它"在骗人"是说错了。主题上锁屏是个设计取舍(且做得一致), 不是文档 bug,故降级为"可选"。

5.4 我修砸了又撤回的一条

系统返回键(见 §4 末)。原报告评 HIGH,我动手后实测把应用关掉了, 已整块撤回并登记成 harmony-system-back-key 这笔账。

为什么把它单独写一节:本轮 20 处修复里,这一处是唯一我自己的改动导致 回归的地方。留着"修好了"的记录比留下一个已知缺口更危险 —— 所以撤回、把复现步骤和被否掉的接法全部写进代码注释与 DEBTS。


6. 变更清单

29 文件,+603 / −77。