`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 清理), 不碰语法层。
237 lines
12 KiB
Markdown
237 lines
12 KiB
Markdown
# 修复报告 —— 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。
|