`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 清理), 不碰语法层。
12 KiB
修复报告 —— 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 自己先消费了返回事件)。
为什么撤回而不是继续查:要验的是"框架内部返回事件的分发顺序"这类 框架行为,本机模拟器这条又不能代表真机。与其赌一个会关掉应用的 半成品,不如整块撤回、留成一条带复现步骤的账。
试过并被否掉的三种接法(都写进代码注释了):
Navigation.onPop—— 不存在(编译报Property 'onPop' does not exist);pushPath第三个参数 —— 不接受(只接受 1–2 个);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。