|
|
139fa19f90
|
跨端: 转发入口移到头部动作行、右下只留回复球(我多摆了一个球)
承接上一条。用户之前说「还有其他行为都要一一对齐,例如邮件展示页面」,
我当时补了头部的「标记已读 / 对话树」,却把**转发**做成了右下角的第二个球。
照 WebUI 核对(`MailView.tsx:520-544` 与 `:994`):
· 头部动作行是**三个**:标记已读 → 对话树 → **转发**
(`ForwardIcon` + 文字,`text-gray-500` = 导航样式)
· 右下角只有**一个**球:`reply-fab`,点开才是回复框。**转发从来不是球。**
多出来那个球是我自己发明的,两个问题:
① 它没有任何对应物,纯属"看着缺就补一个";
② 转发在 WebUI 是**导航**(灰、与对话树并列),球是**主操作**
(蓝、抢注意力)—— 把导航做成主操作,页面里就有了两个同等重量的动作,
看不出主次。
改动:头部动作行补上「转发」(顺序与样式逐项对齐 WebUI),删掉右下角那个球。
设备已验:头部一行是「对话树 · 转发」(同 y),右下只剩一个蓝色回复球。
`files=33 checks=530 pass=530 fail=0`,`mutants=52 ran=52 skipped=0`,`baseline=7/7✓`。
|
2026-09-21 02:34:44 +08:00 |
|
|
|
6f1b4352cd
|
跨端: 回复/转发改内联底栏 + 入场动画对称化(照鸿蒙文档纠正三处误判)
用户:「点击回复按键与新建邮件部分的动画与 webui 不一致,动画不符合鸿蒙视觉
要求」。两个问题是分开的:结构是覆盖式弹层 vs WebUI 的内联底栏;动画则是我
单方面发明的不对称过渡 + 150ms 低于鸿蒙规范下限。
## 结构:覆盖式弹层 → 底部内联条(回复 / 转发)
WebUI `MailView.tsx:410/1057` 的 `ReplyBar`/`ForwardBar` 是 `border-t` 分出的
**内联底栏**,与正文并列(正文 `flex-1 overflow-y-auto` 保持可见可滚),高度由
内容决定。我们原先是整屏遮罩 + `height('60%')` + `position({x:0,y:0})`。
三条用户可感知的差异:弹层盖住正文(写回复时看不到原文)/固定 60% 高(写一行
也占半屏)/遮罩整屏变暗。结构不用动外层 —— 原版那两处本来就是正文 Stack 的
**兄弟**(同在 `Column` 里 ⇒ 本来竖直排列),错只错在给条加了遮罩/定高/绝对定位。
## 动画
① `paneRiseIn()` / `calendarSlide()` 去 `asymmetric`,改**对称**。
WebUI 是 `animation: rise-in 150ms … both` —— `both` 就是进出同一条关键帧。
我原先让出场只做 `opacity` 且更短(120ms),"出现时浮上来、消失时只淡出",
正是"与 webui 不一致"的来源。当初写不对称的理由(换窗格时两层同时半透明会
"闪")只对**换窗格**成立,对回复框/转发条不成立 —— 我把两种场景混用了。
② `durRise` 150 → **200ms**(用户选定"折中")。WebUI 是 150(web 常规档),
鸿蒙官方「元素淡入/位移进入」建议 **200-300ms**,150 比下限还低 25%。
## 照文档纠正三处误判(本轮的真正收获)
我为了搞清"为什么动画不播",先后编出过三个错误理论,读文档后逐条推翻:
① **不是 "NavDestination 吃掉子组件的 `.transition()`"**。
实测:`ComposeView` 根上的 `.transition()` 一直在播。我之所以连测七八轮都报
"没有中间帧",是因为**拿平均亮度当探针** —— 白底窗格 50% 透明叠在浅色背景上
平均亮度几乎不变。换成**位移**探针后,立刻看到"整栏下移 300vp 且半透明"的
中间帧。教训:**探针对被测变化不敏感时,量的是噪声**。
② **不 `customTransition` 也能做**。`NavDestination` 确实有 `customTransition`
(API 15+),但它是**整页转场**,我们要的只是内容块的一次上浮淡入。
(顺带记一条:`NavDestinationTransition.curve` 的类型是枚举 `Curve`,
不收 `ICurve` —— 试过用 `curves.cubicBezierCurve` 会编译报错。)
③ **`.opacity()` 在 `NavDestination` 上是生效的**。先前判定"不生效"同样是那个
废探针害的;换 `opacity(0)` 二元判定后整页消失,证明它一直生效。
## 连带修一个真 bug(判据抓的)
回复/转发改成内联后**失去了"弹层有固定高度"这层键盘保护** —— 官方默认
`KeyboardAvoidMode.OFFSET`(整页上移)会把贴底的「取消/发送/转发」顶出屏幕。
在 `EntryAbility` 里显式设 `RESIZE`(按剩余高度重排)。坑:`@kit.ArkUI` 与全局
作用域各有一个同名 `KeyboardAvoidMode`,**只有前者有 `RESIZE`**。
## 判据(4 条红全部结算,逐条说明为什么不是放宽)
· `harmony-nav` durRise:从"逐字等于 150"改为**区间 200-300**(钉住用户裁定,
退回 150 与写 800 都红,已变异验证)。
· `harmony-nav` asymmetric:**反转**为"不得 asymmetric"(旧断言把上一版设计锁住,
而 WebUI 本来就是对称的)。
· `harmony-admin` 弹层高度:原断言数的形状只属于废弃的覆盖式弹层 → 改为
**新结构下的等价不变式**(键盘避让必须 RESIZE)。这条判据当年抓的是真 bug,
该 bug 换了形态仍在,所以不能简单删。
· `cross-client-theme`:删掉我中途废弃留下的孤儿令牌 `riseCurveEnum`。
新增 4 条变异条目(全部 `红✓`);`mutants=52 ran=52 skipped=0`。
`files=33 checks=530 pass=530 fail=0`,`baseline=7/7✓`。
设备已验:回复/转发确为内联底栏(正文可见、`border-t` 分隔)。
|
2026-09-21 02:19:30 +08:00 |
|
|
|
5e4a1b616c
|
跨端: B 的交付物(跨端纯逻辑一致性判据)+ 照 skill 回扫修掉两处隐形债
用户:「你为什么不加载鸿蒙开发相关skill?」—— 说得对。那份
`arkts-grammar-standards` 写着 "REQUIRED before writing the first .ets file of a
session",而我这轮一直在写 `.ets`。补加载后照它的规则表**逐条回扫**,
当场抓出两处此前没人管的违规。
══ ① 用户要做的 B:`cross-client-logic.test.mjs`(新,6 条判据)
背景:两套纯逻辑各写一份且已分叉(replyTarget 214/170 行、mailGroups 178/459、
appearance 187/324、calendar 208/446)。当天已**踩到**两处分叉
(`participantAddress` 的 `||`、`ThreadPage` 字段全错)。
做法:**同一张用例表喂给两边,逐条比结果**(`--experimental-strip-types`
直接在 node 里跑两侧源码 —— 两边的 model 层都是纯逻辑、无 SDK 依赖)。
不选"生成一份共享源码":harmony 不能 import 工程外文件,
且两边类型系统不同(ArkTS 禁解构/any/对象字面量要具名类型),
生成器要维护"两边都能过"的子集,是另一个大工程。
★ **首轮运行就报出两处真分叉,都不是我踩到才发现**:
① `formatAddress('dsh', undefined, undefined)`:electron 返回 `"dsh"`,
harmony **抛** `Cannot read properties of undefined`。
—— 又是 `omitempty` 那个坑(**第三次**),这次是判据先报的。
② `monthGrid`:electron **固定 6 行**(`grid-rows-6`),harmony **4~6 行**
⇒ 翻月时网格高度跳动。WebUI 的注释明写要避免这个("行数变化会让整个
网格高度跳动,翻月时页面内容上下弹")。
③ 顺着 ② 又发现:WebUI 邻月格子**填真实日期并置灰、可点**
(`CalendarView.tsx:545-556`),harmony 留**空白格**。
★ 判据自身的两次错,都留了档(判据的 bug 与代码的 bug 一样危险):
· 第一版把 `args[0]` 当单个参数传,字符串被当可迭代对象展开 ⇒
`formatAddress('d','s','h')` —— **判据自己造出假分叉**。
· 第一版 `weekStart` 传 0(周日),而两端实际都是 1(周一)⇒ 又一处假分叉。
差一点就去"修"一个不存在的问题。
· `monthGrid` 的投影第一版按 `inMonth ? [y,m,day] : null`,
把"邻月填不填真日期"这个**真分叉**抹平了 —— 投影只该换表示,不该替我看不看。
★ 三类"不同"要分清(写进文件头):**命名不同**(投影归一,不是分叉)、
**签名不同**(ArkTS 没 Date 重载习惯;语义必须一样)、**行为不同**(是分叉,以 electron 为准)。
══ ② skill 回扫抓出的两处隐形债(编译器只告警、判据也不管)
· **正则字面量**(`arkts-no-regexp-literals`):`MailDetailPage.ets:615` 的
`/^\d+$/`(从 2026-09-19 活到今天)。
· **废弃的全局 `router`**:`api/Logout.ets:76` 的 `router.replaceUrl(...)`。
它是个独立函数(没有 `this`)⇒ 拿不到 `UIContext`,改成由调用方传
(两个调用点都持有 `getUIContext()`,零成本)。
★ 这两条为什么能活这么久:**编译器对它们只告警、不挡构建**,
全仓也**没有判据**管 ⇒ 规则事实上不存在。已补两条判据,都做了变异验证。
══ ③ 顺带修正一条**恒真的同义反复**断言
`harmony-calendar` 里 "today 不在本月:不许标在别的月" 那条:
它是在"邻月格子是空 `DayCell`(`iso` 为空串)"时写的 ⇒ `c.iso === today`
**永远不可能**匹配 ⇒ `count === 0` 恒真,**看起来守着一条规则,其实什么都没守**。
改成真不变量:**"被标为今天的那一格,iso 必须就是 today;至多一格"**,
并反向核对"2026-10-01 确实出现在 9 月网格里"(否则那段是空转)。
实测 WebUI `CalendarView.tsx:547` 是逐格 `isSameDay` ⇒ **它会标**,
所以原来那条"不许标"本身就窄了一半。
══ ④ 登记两处盘点发现(**没有**顺手改,因为需要人决定)
· `harmony-dead-pages`:`InboxPage.ets`(238 行) 不可达(不在页面表、无人导航),
`SessionsPage.ets`(170 行) 唯一引用来自 InboxPage ⇒ 一起不可达。
没删是因为 `HARMONY-ALIGN-PLAN.md:214` 把它当变异测试靶子用过 ——
删掉会永久丢代码,是否只是"早期留存"我判断不了。
· `harmony-permission-history`:WebUI 授权栏显示**待决 + 已决策历史**两段
(拿 inbox 自己分组,`PermissionList.tsx:27/174/182`);鸿蒙调专用端点
`/permission/pending`(SQL `WHERE pr.result IS NULL`)⇒ **只拿得到待决的**。
已在 `cross-client-logic` 的 gaps 里如实登记,判据会盯着"不要再少"。
══ 判据状态
`files=33 ran=33 checks=530 pass=530 fail=0 skip=0 red=0 broken=0 unreported=0`;
`baseline=7/7✓`(底本第 9 次重算,已按规矩先 `git diff --quiet HEAD` 取证 + 记录理由)。
`verdict=red` 残余仍是 5 条静态判据的**设备到期提示**(既有机制)。
══ 环境
模拟器昨天起卡死(hdc 能连、shell 超时、CPU 150%、跑了 34 小时),
导致设备判据各跑 836 秒后失败 —— 看起来像"套件卡死"。用户批准后杀掉重启
(`Emulator -start HATriple -noWindow`,`devecocli` 那套因 x11 起不来),
现在**75~150 秒**跑完整套。
|
2026-09-20 22:35:35 +08:00 |
|
|
|
25e7d8f3bf
|
跨端: 补附件区(两端一直都有这个功能,我上次误判成"死代码")+ 修两个真 bug
══ ① 更正我 2026-09-19 的一个**错误结论**(已写进 docs/DEBTS.json 留档)
那天我审计后写下:`GET /me/mail/inbox` 的回包**既没有 `attachments` 也没有
`has_attachments`** ⇒ `MailList.tsx:261` 的 `mail.attachments?.length ?? 0`
恒为 0、WebUI 那个 📎 是**死代码**。并据此在鸿蒙侧**有意不抄**这个标记。
**这个结论是错的**,错在取证方法:我**只看了一封没有附件的邮件**,
看到 key 不在,就断言服务端从不返回它。事实:
· 服务端 `GetInbox`(`mail.go:586`)**明确调了 `fillAttachments`**,注释还写着
理由:「Agent 靠收件箱列表得知有哪些附件可下载,否则它不知道该调 attachment_id」
· `Mail.Attachments` 的 tag 带 **`omitempty`** ⇒ **没有附件的邮件根本不输出这个 key**
实测 `limit=200`(96 封):带 `attachments` 的 **2 封**,正是真有附件那两封。
★ 教训:**`omitempty` 字段的"缺失"不等于"服务端不返回"**。
判「某字段有没有」必须拿**确实有值的那条**去验,而不是拿一条恰好为空的数据。
这与同一天那个白屏崩溃(`session_workspace` 缺失 → `undefined` → 抛)
是**同一个坑的两面** —— 那天是"缺失 → 客户端崩",今天是"缺失 → 我误判成不返回"。
══ ② 补附件区(鸿蒙原来完全没有)
· `model/Attachment.ts`(新)—— `formatSize` / `attachmentLabel`,纯逻辑无 SDK 依赖,
逐字对齐 WebUI `api/client.ts:390`(三档 + 保留一位小数)。
· `MailApi.downloadAttachment` —— 走 `getBytes`(不是 `get<T>`:后者假定 JSON,
取二进制会炸;壁纸当初踩过)。
· `IcsFile.saveBinaryFile` —— 与既有 `saveIcsText` 同一套流程,只是写 `ArrayBuffer`。
· `MailDetailPage` 正文之后渲染附件清单(回形针 + 文件名 + 大小 + 下载),
位置/形态对齐 WebUI `Attachments.tsx`(无附件时**整块不渲染**)。
· 列表行的 📎 + 数字(`attach_count`,与 `cc_count` 同形状派生)。
══ ③ 顺带撞出并修掉两个**真 bug**
**bug A(差一点就是 94/96 必崩)**:我第一版写 `mail.attachments.length` ——
而 `Attachments` 带 `omitempty`,96 封里只有 2 封有这个 key ⇒ 其余 94 封是
`undefined` ⇒ `.length` 抛。**与当天早些时候那个白屏崩溃是同一个坑,
我刚修过、还在 `Models.ets` 里写了一大段注释,然后加新字段时照踩。**
⇒ 说明"记住别这么写"不管用,要在每个真正读的地方把 `?? []` 写出来。
**bug B(潜在白屏)**:`PermissionTab` 读 `req.session_alias` ——
而服务端 `PermissionRequest` struct **根本没有这个字段**(`models.go:239-255`),
`ListPendingPermissionsFor` 的 SELECT 也没查它,WebUI 的类型里同样没有。
它是我照"授权卡总得显示会话名"的直觉加出来的。⇒ 恒 `undefined`,
一旦有待办就抛。**一直没暴露只因为当前待办数一直是 0**(实测 `{"requests":[]}`)。
⇒ 改成服务端确实有的 `agent_name`,并**删掉那个字段声明**:
让误用变成**编译错**,而不是运行时白屏。
同样是 `body_preview`(`omitempty`,值是 `Body` 的截断)——
空正文 ⇒ 空串 ⇒ 服务端省略 key ⇒ `undefined.length` 抛。那批 96 封恰好都有正文,
所以"看起来没问题"——那正是这个坑的形态。已加 `?? ''`。
══ ④ 新判据:`omitempty` 字段的读法(形状,不是实例)
从 `server/internal/models` **算出**"只以 omitempty 形式出现过"的字段名
(不在判据里手抄名单),再扫鸿蒙侧对它们的裸成员调用。
★ 关键:**不能按字段名一刀切** —— 我第一版就是这么写的,报了 6 处、4 处误报:
`session_alias` 在服务端有**两个**声明(`Mail` 上带 omitempty、`repo.Contact` 上不带),
鸿蒙那 4 处读的全是 `Contact` ⇒ 恒有值、不是 bug。
⇒ 只扫"从未不带 omitempty 出现过"的名字,那 4 处自动排除。
已逐个核实 4 处豁免(每条都写了取证理由,不是"看着像就放过")。
变异验证:把 `?? []` 去掉 → 判据转红,且**正是**报 `MailStore.ets: mail.attach_count = mail.attachments.length`。
══ ⑤ 数据路径已实测(模拟器)
临时把 `INBOX_PAGE_SIZE` 提到 200(因为有附件那两封在下标 51/52,
默认 limit=50 **根本取不到** —— 这也解释了为什么之前一直没发现),
加临时 hilog 后拿到:
AttProbe: mail=531a1629-… attach=1
AttProbe: mail=b68cbbe8-… attach=1
正好是那两封。验完已撤掉探针、`INBOX_PAGE_SIZE` 恢复 50。
══ ⚠️ 本轮**未能**完成设备端视觉验收
模拟器已卡死(`hdc` 能连上但 `shell` 超时;进程 152% CPU、已跑 32 小时),
导致套件里的设备判据各跑 836 秒后失败("要能拉起应用")。
主机可用内存只剩 ~3.7GB。附件区的**渲染**(清单外观、下载落盘)
尚未在设备上看过 —— 待模拟器恢复后补。
|
2026-09-20 21:14:20 +08:00 |
|
|
|
f1db99ef41
|
跨端: 发件箱接上 MailStore(A 的剩余)+ 修 3 处把原始 ISO 印到界面上的时间
══ ① A 的剩余:发件箱走了 store(顺带撞出 store 里的一个真 bug)
`SentTab.load()` 原来把收件箱那 107 行**抄了一遍**(注释里还写着"同收件箱"
—— 抄的时候就知道是重复)。现在改成 3 行调用 `MailStore.loadSent`。
★ 接上之后**立刻炸出一个真 bug**:`MailStore.loadSent` 里写的是
snap.groups = [];
而发件箱的列表**就是按会话分组渲染的**(`ForEach(this.groups, …)`)
⇒ 接上 store 之后发件箱会**一片空白**,而"接口有返回、loaded > 0"
会让症状看起来像"数据没到"。
为什么会写成空数组:它是照收件箱那半抄的,而收件箱的 `groups` 来自
`splitByPermission` **筛完之后**的 `inboxMails`(授权邮件要挑出去单独成栏)。
抄的时候只看到"要赋值",没注意发件箱没有那一步筛选 ⇒ 把 `[]` 抄了过来。
发件箱也**不能**照搬那个筛选:那是为"授权待办栏"服务的,发件箱不显示那栏。
★ 这段经历本身值得记:**死代码不会自己暴露错误**。
`loadSent` 写完到今天我接上它之前,那行 `groups = []` 一直没人执行过。
"写完就搁着"和"接上一个调用点"是两件事 —— 后者才算验过。
══ ② 3 处把原始 ISO 直接印到界面上(看到屏幕才发现的)
发件箱那张卡的时间格显示的是
2026-09-19T02:55:33.10099Z
(23 个字符,把「致 homeagent」那行挤到换行)。
WebUI **三种卡片全部格式化**、一个没漏:`MailList.tsx:151/262`、
`PermissionList.tsx:143/251`,全是 `toLocaleString('zh-CN', {month,day,hour,minute})`
⇒ `MM/DD HH:mm`。鸿蒙的 `compactMailTime` 就是那个实现,收件箱一直在用 ——
所以这不是"要不要格式化"的分歧,是**三处漏调**(收件箱 / 发件箱 / 授权栏)。
══ ③ 补判据(形状而不是实例)
新判据:`Text(<x>.created_at)` 这个**形状**不许出现(`Text` 只负责画,
不做格式化)。为什么枚举形状而不是列举调用点:漏的三处分布在**三个不同 struct**,
按名字枚举一定会再漏第四个。
自检 + 变异验证都做了(把 SentRow 那处改回裸字段 → 判据转红;还原 → 绿)。
设备验证:发件箱正常渲染(会话组 + 条目,非空白),
时间显示 `09/19 10:55` 而非 ISO。
|
2026-09-20 18:43:35 +08:00 |
|
|
|
99a7bbccf7
|
跨端: 联系人栏宽度随视图模式变(卡片 400 / 列表 320,原来写死 320)
WebUI `ContactPanel.tsx:59-62` 原文:
// 卡片要放两行摘要 + 预算条,320px 会挤;列表视图保持紧凑
view === 'card' ? 'lg:w-[400px]' : 'lg:w-[320px]'
而鸿蒙写死 `navBarWidth(320)` —— 两个视图一样宽。
卡片视图比列表视图多一行(`N 封 · 时间`)**再加一条预算胶囊**,
320 装不下;WebUI 早就为此单独放宽了,我们没跟上。
★ 顺带一个很容易漏的点:范围也要一起抬(`navBarWidthRange([280, 400])`)。
只改 `navBarWidth(400)` 而 range 还是 `[280, 360]`,值会被**静默夹回 360** ——
"改了宽度但没变",且没有任何报错。这类"值被另一处覆盖"的坑
与 `.attributeModifier` 单一插槽(后一个挤掉前一个)是同一族。
设备验证(模拟器,密度 2.875,实测可点卡片右边界):
· 列表视图:`ListItem [265,326,1117,…]` → 右边界 1117 ⇒ **320vp**
· 卡片视图:`ListItem [265,326,1347,…]` → 右边界 1347 ⇒ **400vp**
同时标题从「联系人」变「工作列表」(与 WebUI 的 `view === 'card' ? '工作列表' : '联系人'` 一致)。
|
2026-09-20 13:56:52 +08:00 |
|
|
|
6f7592d1c1
|
跨端: 按压反馈内联进基础玻璃卡 —— 18 个站点从 1 个变全部(基础组件自带动画)
用户问过两次:
·「各个组件带响应点击、滑动的动画了吗」
·「基础组件包含动画」
之前按压反馈是**单独一个 modifier**,要靠调用点自己叠:
CompositeModifier.of([GlassCardModifier.of(x), PressFeedbackModifier.of()])
实测全仓 18 个玻璃卡站点里**只有 1 个**记得叠。
★ 这不是"写的人不小心"—— **两个东西要一起用时,就该是一个东西**。
把按压并进基础卡之后,18 个站点**自动全都有**按压反馈,
且**不需要在每个调用点改一个字**。这才是"基础组件包含动画"的形状。
实现:
· `GlassCardModifier` 加 `pressable`(**默认 true**),在自己的
`applyNormalAttribute` 末尾内联调用反馈 —— 不走 CompositeModifier,
那是给"调用点自己有好几个 modifier 要叠"用的,这里是基础组件内部要知道的事。
· `PressFeedbackModifier.attach(instance, color?)` 抽成静态方法,
两边共用同一段 `onTouch` 逻辑(`instance` 本来就是 `CommonAttribute`,
类型一致,不需要包一层)。
· 顺带把原先唯一"叠对了"的那处(`MainPage` 会话组头卡)简化掉 ——
它现在是全场唯一的例外写法,反而容易让人以为"要叠才生效"。
★ 为什么默认 **true** 而不是"想按才开":WebUI 那一侧是**全局**的 ——
`index.css:1169` 对 `button, a, input, textarea, select, [role='button']`
统一给了 `transition`。"可点就有点击反馈"在两端都该是默认。
静态信息卡可以传 false —— 但不会有人漏,因为不可点的卡本来就没有按压语义,
而**漏掉真正可点的卡**才是原来那个问题(18 分之 17 漏)。
设备验证(模拟器):在组头卡上长按,hilog 抓到
PressFB: touch type=0 ← Down
PressFB: touch type=1 ← Up
成对到达。验证用的临时 hilog 已撤(不是留在代码里)。
★ 记一个**没验成**的取证方法,免得下次再花时间:
想靠"按住时截图看底色变了"来取证,试了三轮都不行 ——
`snapshot_display` 的往返延迟(~250ms + 传输)比一次按压的窗口长,
抓到的永远是松开后的画面。**延时不敏感的取证是 hilog**:
回调有没有触发是个**事件事实**,不需要抢时间窗。
|
2026-09-20 13:53:40 +08:00 |
|
|
|
6ef079365a
|
跨端: 修「两个动作球不能重叠」那条判据自身(它的结束边界把几十行外的东西也吃进来了)
上一步扩 Unicode 图标扫描面时,这条判据立刻报:
MailDetailPage.ets:664(Stack 里有 2 个圆角元素且没有 Row 包住)
而 L664 那个 `Stack` 是**新加的返回键**,里面**只有一个子元素**(chevron 图标)。
查清根因(判据自身两个缺陷):
① **结束边界靠猜缩进**:原来写 `\n\s{8}\}` —— "8 空格 + 右括号"。
而那个 Stack 的 `}` 缩进是 10 空格、它下面还有一整段同缩进的兄弟节点
⇒ 非贪婪匹配一路吃到**下一个** 8 空格的 `}`,
把几十行外两行的**小徽标**(权限/未读,`.borderRadius(4)`)也算了进来。
⇒ 改成**按大括号配平**取块。缩进是可变的,配平是语法事实。
② **"圆角"被当成"圆球"**:原判据数的是所有 `.borderRadius(\d+)`。
而 `borderRadius(4)` 是**圆角方**(徽标),根本不是球。
本判据要防的是"两个**球**叠在同一个角",不是"任何带圆角的东西"。
⇒ 只认 `borderRadius(N)` 且 **N ≥ 16** 的(动作球是 24/28 那一档)。
★ 改完做了**变异验证**(判据自己的要求:必须"在变异下咬得住"):
把包着两个球的 `Row({ space: 12 })` 拆掉、让它们直接待在 `Stack` 里 ——
判据**立刻转红**(30→29 pass/1 fail);还原后 30 pass。
这说明它测的还是原来那件事,只是不再误伤。
|
2026-09-20 13:42:51 +08:00 |
|
|
|
6085159694
|
跨端: 5 处 Unicode 符号当图标换成 AmIcon(并把判据扩到"排版符号"这一类)
起因:做邮件详情时顺手把 `Text('‹')` 换成 `AmIcon('chevronLeft')`,
想着"本仓不是有这条判据吗,怎么没抓到" —— 去看了判据,发现它**只扫 emoji 那一段**。
判据(`harmony-arkts.test.mjs:168`)扫的是 U+2600–27BF / U+2B00–2BFF / U+FE0F,
而漏网的 5 处全在这三段**之外**:
MainPage.ets Text('‹') 返回键(会话视图)
MainPage.ets Text('›') 会话别名前的小箭头
SettingsPage.ets Text('›') 「管理」的进入下一级指示
ComposePage.ets Text(' ▾') 账号下拉指示
MailDetailPage.ets Text('‹') 返回键
★ 它们与 emoji 那类**问题不同、但同样是"看着像图标其实不是"**:
· 字形宽窄由**字体**决定,与旁边 20vp 的 `AmIcon` 对不齐;
· 而且**WebUI 用的根本不是字符** —— `icons.tsx:135` 是
`ChevronRightIcon`(SVG)、`AccountSwitcher.tsx:84` 是它**转 90°**。
所以这同时是"两端不一致",不只是"字形不好看"。
修法:5 处一律换 `AmIcon`,并按 WebUI 的**同一形状**给:
· 两个返回键 → `chevronLeft` 20vp、可点区 `HEADER_BACK_HIT`(与 `AppHeader` 同值)
· 会话别名 / 「管理」→ `chevronRight`
· 账号下拉 → `chevronRight` + `rotate(open ? 90 : 0)`(照 `AccountSwitcher` 那句)
★ 判据扩了一类(`GLYPH_ISH`:U+2039/203A/00AB/00BB/25A0–25CF/25B2/25BC/25C0/25B6),
并保持原有那条边界不被破坏:**U+2190–21FF 基本箭头仍不扫** ——
`→`/`←` 在正文里是标点,扫进来会误伤大量正常文案(原注释里已经踩过这个坑)。
这次的符号是"单个字符整体",所以 `Text('a → b')` 这类多字符本来也不匹配。
★ 扩完之后判据**当场又抓到一处我自己没注意的**(`SettingsPage.ets` 的 `›`)——
这正是"把判据从'枚举实例'扩到'枚举类'立刻多抓一个"的现场证据,
也说明原来那三条区间是照"已经出现过的实例"框的,不是照"这类东西的共同形状"框的。
|
2026-09-20 13:33:13 +08:00 |
|
|
|
d9bb4f766b
|
跨端: 三条判据转绿(登记新令牌 + 把一条断言错实现的判据改成断言真不变式)
上一步(写邮件右栏)之后整套跑红 3 条,逐个查清:
══ ① cross-client-theme —— 判据是对的,我漏登记
新加的 `accentEdge` / `accentEdgeDark` 是**手写色**,而这个仓有一条硬规矩:
`Theme.ets` 里每个 `static readonly X: string = '#……'` 都必须在
`SELF_OWNED_COLORS` 名单里 + 在文件内的「手写色登记表」注释里写一行理由。
(这条规矩本身就是为防"新写死一个色悄悄溜过去"而立的 —— 第二处断言
会检查名单里没有化石名,所以名单不能只加不改。)
按规矩补两处:
· 名单里登记,并写清它对齐的是 WebUI 的哪个 token
(`MailList.tsx:280` 的 `border-blue-200`,与 `bg-blue-50` 成对)
· `Theme.ets` 登记表里写一行理由 —— 重点记下**为什么"选中"必须两个 token**:
选中与未读的底色相同(都是 `accentSoft`),只靠底色的话
"选中一封未读邮件"看不出任何变化,那圈边才是信息。
并把表头计数 24 → 26 同步改掉。
══ ② harmony-widescreen —— 判据错了,它断言的是一个 WebUI 明令禁止的实现
原断言:`assert.match(code_, /clip\(this\.isWide\)/, '面板内容要被圆角裁剪')`
—— 它把"圆角"和"裁切"当成**同一件事**。而 WebUI `index.css:968`
在**同一个规则块**里就写明了这条教训:
.app-shell > * {
border-radius: var(--radius-card);
// ★ 这里**不能**写 overflow: hidden(2026-09-14 用户:
// 「通信页面完全无法上下滑动」)。
// 面板自己就是滚动容器,而这条规则的特异性比 Tailwind 的 .overflow-y-auto
// 高 ⇒ 滚动被静默干掉:实测当时**一个可滚动容器都不存在**(scrollerCount=0)。
// 圆角仍然生效(border-radius 不影响滚动)
鸿蒙这边同一个形状:那层是内容列的根、`Navigation` 的 `List` 在它里面,
写了 `.clip(this.isWide)` 就是 `List` 节点还在(高 1750px)但**滚不动**
(实测 fling 后第一封仍在 y=526)——这正是用户当时报的"列表没法滚动"。
⇒ 判据改成断言**真正的不变式**:宽屏「圆角开(14)+ 面来自设计令牌 +
**不写** `.clip(this.isWide)`」。原来那句测的是"某句实现写着没写着",
而且那个实现是错的 —— 判据跟着错误实现一起钉住了 bug。
══ ③ build-stamp —— 判据要求重构建,照做(**没有**去改 BUILD_INFO.json)
产物记的是 `c0ab3f5`,HEAD 已是新提交。这条判据自己的文本写得很清楚:
> **别去改 BUILD_INFO.json 里的 gitRev / srcHash 了事** —— 那是把这条判据废掉。
> 正确修法只有一个:重跑构建。
所以 `npm run build` 重跑(rev 0)。注意 srcHash 本来就没变
(`6d1195a4008ebca7`)—— 因为 TRACKED 只扫 electron 侧(`src`/`index.html`/
`vite.config.ts`/…),我这几步改的全是 harmony;**变的只有 gitRev**。
这正说明这条判据在比对"产物来自哪个提交",而不是"文件内容有没有动"。
★ 三条各自的性质不同,值得分开记:① 是判据对、我漏登记(补);
② 是判据错、钉住了一个反向实现(改判据);③ 是判据对且给了唯一正确修法(照办)。
|
2026-09-20 13:05:59 +08:00 |
|
|
|
1df8245a6d
|
跨端: 写邮件改在宽屏右栏打开(原来盖住全屏、把列表栏也带走)
用户:「写邮件 webui 的宽屏样式不是在右侧打开吗」—— 是,这是**结构性不符**。
WebUI `App.tsx:179` 宽屏下写信只是把 `main` 那一格换掉:
const main = composing ? <ComposePage /> : viewMode === 'account' ? ... : ...
侧栏与列表栏都还在。而鸿蒙无条件 `router.pushUrl('pages/ComposePage')` ——
一个 `@Entry` 全屏页,左侧列表整片消失。
修法照**既有先例** `MailDetailView` / `MailDetailPage`(同一个问题上次已经解过):
· `ComposePage` 拆成 `ComposeView`(真内容)+ `@Entry ComposePage`(只负责窗口避让)
· 新增 `ComposeDestination`(`NavDestination` 壳),与 `MailDetailDestination`
**完全同构** —— 宽屏由 `mode(Auto)` 自动并排在右栏,窄屏自动 push 覆盖全屏。
**两条路径同一套代码,不自己判断宽窄**(这正是 `Navigation` 该干的事)。
· 走本页自己的 `Navigation` 栈(`COMPOSE_ROUTE`)而不是布尔 `@State showCompose`:
路由让"返回"自动正确(系统返回键 / 手势 / 头部按钮弹同一个栈);
布尔状态要自己接三条返回路径 —— 那正是 2026-09-17 那批「返回直接回登录页」的来源。
· `InboxTab` 里那个自己的 `openCompose` 也一起改(它抄了同一个 `pushUrl`)——
收件箱内按筛选账号写信、收件箱外用活跃账号写信,两条入口现在共用
`CommPage.openComposeWith(accountId)`。
★ 两个必须守住的细节(都是上一次踩过的坑,这里重复了一遍):
· 避让留给 **`@Entry` 包装层**,`ComposeView` 里 `embedded` 时取 0 ——
同一个 View 被内嵌复用,而 `MainPage` 已经加过避让,加在里面就是**让两次**。
· 「取消」内嵌时必须弹**自己的**栈(`onBack`),不能 `router.back()` ——
那会退掉整个 `MainPage`(写信只是它的一个右栏状态,不是一个页面)。
`MailDetailView.goBack()` 里是同一条判断。
· 内嵌时参数走 `@Prop` 初值,**不读** `getRouter().getParams()` ——
内嵌没走 router,那里拿到的是**上一次 push 的残留**,
会把上一封信的收件人带进来(比空更坏)。
设备验证(模拟器 3184×2232 宽屏):点悬浮加号 → 写信在**右栏**打开,
侧栏与列表栏都在;点「取消」→ 弹回右栏空态,列表与选中态不受影响。
|
2026-09-20 12:57:55 +08:00 |
|
|
|
5c04b41900
|
跨端: 修一个真崩溃(omitempty)+ 邮件详情补"标记已读/对话树"两个动作
用户:「还有其他行为都要一一对齐,例如邮件展示页面」。做这件事时**撞出一个真崩溃**。
══ ① 崩溃:`Cannot read property trim of undefined`(整页白屏、应用重启)
崩在展开邮件头部的那一刻。崩溃日志
`jscrash-com.jianf.agentmail-...-20260920123121173.log`:
at participantAddress (model/ReplyTarget.ts:87:40)
at fromAddress (pages/MailDetailPage.ets:393:12)
根因是**服务端 `omitempty` + 客户端裸转型**这个组合:
· 服务端 `Mail` 有 12 个字段带 `json:"...,omitempty"`(models.go:140-238)——
Go 对零值**根本不输出这个 key**。实测 `/api/v1/mail/{id}`:
session_workspace ★缺失
body_preview ★缺失
permission_result ★缺失
attachments ★缺失
· `ApiClient` 是 `JSON.parse(rawText) as T`(裸转型、无归一化)——
ArkTS 对"JSON 里没这个 key"**不会**套用 class 的 `= ''` 默认值
(那只在**整个对象**缺失时生效)⇒ 字段变成 `undefined`。
· 于是 `workspace.trim()` 当场抛。
★ 这正是用户要做的 **B**(两套纯逻辑各写一份)的实证分叉:
electron 写的是 `(workspace || '').trim()`,鸿蒙写的是 `workspace.trim()`。
少了那两个 `||`,代价是一个崩溃。已在 `ReplyTarget.ts` 照 electron 逐字对齐。
★ 但**不在那里了事**(同一个坑还有十几个字段,逐个打补丁必然漏):
在**解析边界**加一层归一化 `MailDetail.normalize()`,接在 `mailApi.mailDetail()` 上。
下游从此可以按"字段一定存在"来写(那本来就是类型声明该保证的事)。
不改服务端去掉 omitempty —— 那会动已发布的 API 契约,代价大得多;
而且 electron 一直靠 `?.`/`|| ''` 兜,说明这个契约是既成事实。
★ 为什么以前没暴露:`fromAddress()` 只在**展开头部**时才调用,
而展开头部是个 14px 的薄弱点击区(之前修过一次)。我把动作行加进展开区,
等于把这条路走宽了 —— 一展开就崩。
══ ② 又一处 B 分叉:对话树回包类型整个是错的(接口 200,界面空白)
鸿蒙 `ThreadResponse` 声明的是 `dir / has_more_up / has_more_down / next_up /
next_down` —— **服务端一个都没有**;服务端真正返回的 `root_mail_id /
anchor_depth / has_more / next_offset` 这里**一个都没声明**。
更糟的是 `ThreadApiResponse` 还包了一层 `thread`,而服务端是**平铺**的:
{"anchor_depth":1,"anchor_mail_id":"...","has_more":false,"hidden":0,
"next_offset":60,"nodes":[...],"root_mail_id":"...","total":2}
⇒ `resp.thread.nodes` 永远读不到 ⇒ 点「对话树」什么都不显示。
已按 electron 的 `types/index.ts:179 ThreadPage` 与服务端实测回包对齐。
★ 教训记下来:`JSON.parse as T` 是裸转型,**照自己直觉声明第三方回包类型,
编译器不会查**。改成 electron 那样的 `ThreadPage` 形状后才对。
══ ③ 补两个动作(对齐 WebUI `MailView.tsx:520-544`)
· **标记已读** —— 仅 `status === 'unread'` 时出现;成功后**就地**把 `this.status`
改成 `'read'`(只发请求不改状态的话按钮还挂着,用户会以为没生效再点一次);
失败要 toast(写操作静默失败比报错更坏)。不做乐观更新 ——
"标已读失败了却显示已读"比慢 0.2 秒更糟。
· **对话树** —— 盖在内容之上的弹层(不改路由),与 WebUI 的 `onThread` 同义。
· 转发不重复(右下球已有)。
· 位置、顺序、显隐条件逐项对齐:动作行在**元信息行之前**(WebUI 是 `mb-1.5`
那一行),蓝色表示动作、灰色表示导航,与 WebUI 的 `text-blue-600` /
`text-gray-500` 同一取舍。
设备验证:展开头部不再崩(无新 faultlog);`POST /mail/{id}/read` 已发出且
「未读」徽标与按钮同时消失;对话树弹出并显示真实数据(2 封,dsh → jianf)。
★ 工具坑记一笔:这台模拟器上 `devecocli ui tap` **点了不生效**,
`hdc shell "uitest uiInput click X Y"` 才有效 —— 为此白跑过两轮。
|
2026-09-20 12:48:37 +08:00 |
|
|
|
44e277e0b6
|
跨端: 补「当前选中邮件」高亮 —— 鸿蒙原来完全没有这个概念
用户:「你自己看看跟 webui 相比,观感真的差很多」。逐项比对后找到的**行为缺口**
(不是配色问题,是少了一个状态)。
WebUI 一直有:`MailList.tsx:104/116/229` 把 `currentMail?.mail_id` 传进卡片,
卡片据此上 `active ? 'bg-blue-50 border-blue-200'`。
鸿蒙**一个都没有** —— 点开一封邮件后,左侧列表那一行和旁边几行长得一模一样:
你不知道自己正在读哪一封、读完了该往哪回。
修法:
· `CommPage` 拥有 `@State currentMailId`,`openMail()` 里记下,以 `@Prop` 下发给
`InboxTab`/`SentTab`。**单点写、多点读** —— 两个 tab 各存一份必然会分叉
(收件箱和发件箱都能触发同一个动作)。
· 存 `mail_id` 而非索引/组键:SSE 会让列表重排,索引会错位。
· 三级优先 **选中 > 未读 > 普通**。★ 这里有个必须成对的理由:
选中与未读底色**相同**(都是 `accentSoft`),只靠底色的话
"选中一封未读邮件"看不出任何变化 ⇒ 选中必须额外加那圈边。
这正是 WebUI 两个 token 并存的原因,不是随手加的边框。
· 新增 `Theme.accentEdge/accentEdgeDark/accentEdgeFor()`(= tailwind blue-200
及其深色值)——我第一版只搬了底、忘了边,选中态淡到几乎看不见。
★ 顺带记一个工具坑:`devecocli ui tap` 在这台模拟器上**点了不生效**
(截图前后一样、布局树无变化),而 `hdc shell "uitest uiInput click X Y"`
有效。以后点不动就先换这条,别以为是代码没生效 —— 我为此白跑了两轮。
|
2026-09-20 12:24:17 +08:00 |
|
|
|
d10c641e41
|
跨端: 账号徽标对齐 WebUI(中性灰胶囊 + 80px 截断,原来是被染成品牌蓝)
用户:「你自己看看跟 webui 相比,观感真的差很多」。逐字段对比卡片后找到这一处。
WebUI(`MailList.tsx:303-310`):
className="text-[10px] leading-4 px-1.5 rounded-full
bg-gray-100 text-gray-600 shrink-0 max-w-[80px] truncate"
我们原来:
.fontColor(Theme.accentFor())
.backgroundColor(Theme.accentSoftFor(this.isDarkNow)).borderRadius(4)
(没有 maxWidth)
**三处都不一样**:
· 底色/字色:品牌浅蓝 + 品牌蓝 vs **中性灰**。徽标表达的是"这条来自哪个账号",
是**中性的元信息**,不该与主操作抢注意力 —— WebUI 选 gray-100 正是这个意思。
我们把品牌色用在元信息上,等于把每一行的副标题都标成了"主操作"。
· 形状:圆角方(4) vs 全圆胶囊(rounded-full)
· 截断:原来没有 `maxWidth` —— 账号名一长就把主题挤没了(WebUI 有 `max-w-[80px]`,
而多账号正是这个徽标存在的场景,名字长是常态)。
措辞上保持"用户看到什么"这个层次:这条判的是**渲染结果**,
不是"有没有这个字段"(两端字段集本来就是一致的)。
|
2026-09-20 12:12:08 +08:00 |
|
|
|
a97b83e221
|
跨端: 宽屏右栏空态(splitPlaceholder)—— WebUI 有引导,我们原来一片空白
用户:「你自己看看跟 webui 相比,观感真的差很多」。同 1107vp 视口并排后,
差异确实明显,其中**最扎眼**的一条:宽屏两栏并排时,右栏是**一大片空白**。
WebUI 有引导(`MailView.tsx:121-129`):
信封图标 + 「选择一封邮件查看,或点击左侧「新建」写邮件」
我们什么都没渲染。
★ 第一版放错位置(记下来,这个坑很隐蔽)
我把它写进 `NavDestination` 的 `else` 分支 —— 而 **`NavDestination` 只在
push 之后才挂载**,栈空时它根本不存在 ⇒ 那个 else **一次都不会显示**
(实测:加上去之后右栏仍然全空)。
正确入口是 `splitPlaceholder(ComponentContent)`:系统给"右栏默认页"的专用 API
(`navigation.d.ts`,API 20+,我们是 23),由 `Navigation` 在栈空时自己渲染。
★ 三个 ArkTS 约束(都撞了才过)
① `splitPlaceholder` 要的 `wrapBuilder` 只接受**全局** `@Builder` 函数 ——
写成 struct 成员方法会报 "The wrapBuilder's parameter should be '@Builder' function"。
内容本身是静态的(不需要 `this`),所以全局正好。
② `wrapBuilder` 是**全局声明**(`common.d.ts:27448`),**不在** `@kit.ArkUI` 里 ——
从 kit 引会报 "has no exported member"。
③ `ComponentContent` 要从 `@kit.ArkUI` 引。
文案**逐字对齐 WebUI**(同一句话,不自己改写)—— 与本仓既有纪律一致
(`harmony-logic` 里「空态主句与 WebUI 逐字一致」那条是同源要求)。
设备验证:`选择一封邮件查看,或点击左侧「新建」写邮件` 已在右栏渲染(布局树可见)。
|
2026-09-20 12:07:32 +08:00 |
|
|
|
e988b6f24d
|
跨端: 去掉宽屏内容列与两栏的 .clip(圆角保留、不裁切)+ 更正两处我自己的误判
★ 改了什么
`MainPage.ets` 三处 `.clip(...)` 去掉,**圆角保留**:
· 宽屏内容列的 `.clip(this.isWide)` —— 这处**本来就在**(不是今天加的),
它是内容列的根,`Navigation` 的 `List` 在它里面
· 今天新加的两处(左栏 navBar 根、右栏 NavDestination)
★ 依据是 WebUI 自己写在**同一个规则块**里的教训(`index.css:968`):
.app-shell > * {
border-radius: var(--radius-card);
// ★ 这里**不能**写 overflow: hidden(2026-09-14 用户:
// 「通信页面完全无法上下滑动」)。
// 面板自己就是滚动容器,而这条规则的特异性比 Tailwind 的
// .overflow-y-auto 高 ⇒ 滚动被静默干掉:实测当时**一个可滚动
// 容器都不存在**(scrollerCount=0),内容是直接被裁掉的。
// 圆角仍然生效(border-radius 不影响滚动);角落的方角残影
// 改用 background-clip 处理……比不能滚动好得多。
我照"两栏各自圆角"改的时候,把 `overflow: hidden` 一起搬了过来 ——
而那条警告**就在同一个块里**,正是为了防这一手。
★ 更正我自己两处**错误结论**(都写进注释,免得下次重犯)
① 「你一改了之后列表没法滚动了」——**不成立**。实测该账户只有 6 组
(内容 1434px),而列表容器 1750px,**内容比容器矮,本来就不需要滚动**。
用会溢出的页面验证(「我的」页):fling 之后页面从「背景」滚到「管理」,
**滚动是好的**。我把"内容没溢出"误读成了"滚动坏了"。
② 「列表栏宽一倍」——不成立。两端**绝对宽度相同**(列表 320vp、侧栏 60vp),
百分比差异只是视口宽度不同(1280 vs 1107vp)。我漏了密度换算(2.875)。
★ 同视口并排的结论(1107vp × 2 端,逐项量过)
侧栏 60vp、列表栏 320vp、卡片 300×78vp —— **几何两端一致**。
真正的差异在**内容**,不在尺寸(下一步按这条去做)。
|
2026-09-20 11:58:16 +08:00 |
|
|
|
f604badf86
|
判据: 三条判据锚在"实现位置"上,A 步搬家后假红 —— 改成锚不变量
用户裁定做 A(harmony 补 store 层)之后,`harmony-logic.test.mjs` 红了 4 条。
**一条都不是 bug**:它们读 `pageCode`(`MainPage.ets`)找那些调用,
而调用已经搬进 `common/MailStore.ets`。
这正是本仓反复出现的形状:**判据锚在实现位置上,而不是它声称的东西上**。
一处实现搬家(哪怕搬得更对)就假红 —— 而假红比漏报更坏,
它会让人去改本来对的代码。
★ 修法:引入 `compositionCode`(页面 + store 合起来看),按**归属**拆断言
· **取数/组装**(谁调 `sumUnreadTotals` / `partialLoadNotice` /
`groupMailsBySession`)→ 读 `compositionCode`
· **渲染**(用户看到什么:单封平铺、多封按展开态、组头可点、卡片视图)→ 仍钉 `pageCode`
(那是页面该管的事,不该被搬走)
不变量是"这条纯逻辑真的被用上了"——**用它的人搬去哪一层不该让判据红**。
★ 顺带修掉一条**自己骗自己**的断言(变异测试抓出来的)
「先分家再折叠」原本写成 `splitAt < groupAt` 两个 `indexOf` 比大小,
并断言两个都 `>= 0`。我把顺序反过来做变异 —— **它没红**:
因为反过来之后第二个字面量变了、`indexOf` 返回 -1,红的是"两个都 >=0"
那条,而不是顺序那条。也就是说:**顺序这条判据从来没在判顺序**。
改成直接断言**数据流形状** `groupMailsBySession(split.normal)` ——
它同时表达"折叠的是筛过的那批"与"必须先分家"。再变异(换成 `mergedMails`)
⇒ fail=2,这次真的咬住了。
★ 判据 14 也从一条拆成两条(取数侧 / 渲染侧),名字改成
「折叠逻辑真正接上了(不是"逻辑写好了没人用")—— 取数侧 + 渲染侧都要在」。
harmony-logic 32/32 绿。注册数不变(拆一条加一条,净持平 32)。
|
2026-09-20 11:34:44 +08:00 |
|
|
|
97037c0424
|
跨端: A 步 —— harmony 补 store 层(MailStore),搬走 InboxTab 107 行重复实现
用户裁定:「A+B 都做」「electron 是唯一真实源泉」。本条做 A 的第一步。
★ 量的结果(决定为什么要做)
· harmony 48 文件 / 18,444 行,**无 store 层**,223 个 @State + 47 @Prop 散在各页
· electron 58 文件 / 13,801 行,**9 个 store**(mailStore/sessionStore/contactStore/
accountStore/appearanceSync/uiStore/themeStore/authStore/backgroundStore)
· `MainPage.ets` 3,289 行,其中 4 个 load* 方法合计 250 行在重复
"遍历账号 → 逐账号拉 → 合并 → 派生 cc_count → 分流 → 分组 → 算未读 → 出提示"
· 同一套多账号遍历在 `InboxTab`(107 行) / `SentTab`(40 行) / `ContactsTab` 各写一遍
★ 做了什么
新增 `common/MailStore.ets`(318 行,照 `AppearanceStore` 的单例形状):
· `loadInbox(ctx, accountFilter)` —— 全仓**唯一**的收件箱取数实现
· `loadSent(ctx, accountFilter)` —— 与收件箱共用同一套聚合形状
· `dropSession(sessionId)` —— 归档后就地剔除(与 WebUI `mailStore.dropSession` 同动作)
· `clear()` —— 退出/换账号时清空(否则下一账号会看到上一人的邮件,哪怕只有一帧)
· `MailSnapshot` / `AccountError` / `INBOX_PAGE_SIZE`(从页面搬进来)
它只做"取数 + 组装 + 错误归类"——**纯逻辑仍留在 `model/MailGrouping.ts`**,
这样 `harmony-logic.test.mjs` 不必起设备/网络就能跑(本仓既有纪律)。
`MainPage.InboxTab` 的 `loadData()` 从 107 行变成 3 行调用 + 两个辅助
(`syncAccountFilter` / `applyStoreSnapshot`)。
★ 途中撞到的四个 ArkTS 约束(都已修,记下来免得重犯)
① `INBOX_PAGE_SIZE` 在页面里已有一份 ⇒ 导入与本地声明冲突(`arkts-unique-names`)
② `MailApi.sent()` **不收参数**(与 `inbox(status, limit)` 不同),照着 WebUI 写会错
③ `SentResponse` 定义在 `model/Models.ets`,**不在** `api/MailApi.ets` 里再导出
④ store 持有接口类型 `MailLike[]`,页面字段是 `MailSummary[]` ⇒
`MailSummary implements MailLike` 成立,但**赋值要显式向下转型**
★ 为什么页面还保留 `@State` 镜像而不直接读 store 字段
ArkUI 的 `@State` 只观察**它持有的那个值**;store 是普通类实例,改内部字段
不触发刷新(本仓 `appearance-defaults` 判据钉过同类问题)。所以照既有约定:
store 每次改完自增 `revision`,调用方把快照接进 `@State`。
设备验证:收件箱正常渲染(6 组 · 50 封、25 未读),与改动前逐项一致。
|
2026-09-20 11:28:48 +08:00 |
|
|
|
ed1ae02441
|
跨端: 宽屏两栏各自圆角(原来只有外壳有,内部两半是直角)
用户:「同时邮件展示左侧没有圆角」。
WebUI 宽屏的真结构是**三个独立圆角面板并排**(App.tsx:262):
<div className="app-shell"> ← padding/gap = 10px
<Sidebar/> {list} {main} ← 每个子元素各自 border-radius:14px
</div>
(index.css:968 的 .app-shell > * 统一给 border-radius,第 1070 行在壁纸态再加
overflow:hidden + 阴影)。所以列表栏与详情栏**各有自己的四个圆角**,
中间的缝里透出壁纸。
我们这边是一个 Navigation,由系统 Split 成 navBar + content 两半,
圆角只加在**外壳**上 ⇒ 内部这两半是直角。实测(模拟器 3184×2232):
详情栏白区左缘在 y=145 与 y=2195 都是 x=1152 —— **完全垂直,没有圆角**。
修法:给两栏**各自**补圆角 + 裁切(与 WebUI 的 .app-shell > * 逐条对应),
左栏在 navBar 内容根、右栏在 NavDestination 上。
修后实测白区左缘:y=145→x=1178、y=165→1156、y=185→1152(圆弧正确),
下角同理(y=2190→1165、y=2196→1172)。
★ 顺带给 MailDetailDestination 补 bgActive 参数(圆角要在壁纸开启时才有 ——
与 WebUI 一致:非壁纸态 .app-shell > * 也圆角,但那时底色是实心的,
圆角看不出来;壁纸态才需要圆角把缝让出来)。
|
2026-09-20 11:14:05 +08:00 |
|
|
|
6ca0113fd2
|
跨端: 统一顶栏组件 + 按压反馈(真跑通)+ 滑动判定从"时长门"改"速度门"
用户四条意见,逐条都是真问题,且**两条是我写完没生效**:
① 「所有的顶栏都应该应用玻璃圆框效果」② 「抽象一个统一的顶栏组件出来吧」
③ 「你不觉得发件箱太大了吗」④ 「各个组件带响应点击、滑动等的动画了吗」
⑤ 「还有转场动画呢?」⑥ 「还有其他行为都要一一对齐,例如邮件展示页面」
★ ① ② 统一顶栏 `AppHeader`
改造前**六处各写各的**:`AdminUsersPage`(40×40 返回键+16 号标题)、
`SettingsPage`(20 号标题+40×40「+」+**实心 Theme.surface**)、
`MailDetailPage`(36×36 + 14 号标题,行高只有 14px 被裁过)、
`MainPage.SentTab`/`PermissionTab`(通栏、无圆角)、`CommTabBar`。
尺寸(36/40、14/16/20)、底色(实心白/透明/无)、返回键(有/无)三类都不一致;
且四处用 `Text('‹')` 当返回键 —— **用字符当图标**(本仓明令禁止,字形随字体变、
基线对不齐),而 `ICON_PATHS` 里**早就有** `chevronLeft`。
⇒ 抽成 `AppHeader`(返回键 + 标题 + `@BuilderParam` 右侧动作区),
几何复用底部条常量,材质走 `Theme.navMaterial`。已接入 5 处。
★ `topInsetPx` 由调用方传:状态栏避让是**窗口级**事实(属页面),
组件自己读会变成"每层各加一次"(平板侧栏 83+83 双计就是这么来的)。
★ ③ 发件箱"太大"—— 我的理由错了
我第一版让 `HEADER_HEIGHT = NAV_BAR_HEIGHT`(56),理由是"顶栏与底栏同在一根
竖轴上,高度不同会一眼看出来"。**那个理由不成立**:
底栏是**两行**(图标+文字标签)⇒ 需要 56;顶栏只有**一行标题** ⇒ 56 里一半是空白。
实测 `Row [277,315,1105,476]` = 161px = 56vp,标题那行只占 54px。
"两根轴上的条要一样高"是把**对齐**理解成了**等高**。真正要对齐的是**左右留白与圆角**。
⇒ `HEADER_HEIGHT = 44`(= 可点区下限,返回键装得下)。
★ ④ 按压反馈 —— 两次失败才跑通,两个坑都记进注释
· **坑一**:`.attributeModifier()` **一个组件只能挂一个**,链两个是**后者覆盖前者**。
会话组头卡同时挂了玻璃与按压反馈 ⇒ 只生效一个,**编译器不报错、运行不提示**。
WebUI 是 CSS,类名天然叠加;ArkUI 是单一插槽,"叠加"必须显式做
⇒ 新增 `CompositeModifier`。
· **坑二**:`applyPressedAttribute` **只对自带按压状态机的组件**(`Button`)回调。
实测:挂 `Button` 上 → hilog 有输出;挂只有 `.onClick` 的 `Row`/`Column` 上
→ **一次都不回调**(按下与常态逐像素相同 `239,244,255`)。而全仓 117 处
`onClick` 的主体正是纯 `Row`/`Column` ⇒ 那条路对本仓没用。
改用 `onTouch` + `animateTo`。
· 底色用**系统点击效果色** `ohos_id_color_click_effect`(不是我自己挑的):
第一版用 `Theme.surfaceMuted`,实测只差 `3/3/3`,肉眼看不出 ——
因为"一块面的颜色"与"按下时叠的提示色"在系统色板里是**两个不同语义**。
设备验证:`onTouch type=0`(Down) → `type=1`(Up) 成对到达,像素确有位移。
★ ④ 滑动判定:从「时长门」改成「速度门」(**设计错误,不是调参**)
旧门 `SWIPE_MAX_DURATION_MS = 700`("整个手势超 700ms 就拒")。
而**时长 = 距离 ÷ 速度** —— 它把两个量混成一个 ⇒ 同样的手速下
**滑得越远越容易被拒**,屏幕越大越严重(平板同一手势像素更多)。
设备实测(3184×2232,位移恒 542.6vp):
600px/s→5909ms | 1200→3161ms | 1800→2006ms | 2500→1429ms | 5000→616ms | 15000→123ms
⇒ 旧门意味着**只有 ≥5000px/s 才过**,而那一档重复测试也只有 2/4 成功
—— 用户报的"滑不动/时灵时不灵"就是这个。
改 `SWIPE_MIN_SPEED = 0.25`(vp/ms,取自上表两档中间)后实测:
1200px/s → 0/3(正确拒绝:那是拖动) 2500px/s → **0/4 → 3/3**
5000px/s → **2/4 → 3/3**
两条判据跟着改形态(**语义不变**:都是"够快才翻页"):
· `cross-client-gesture` 断言"算的是 dx/ms"——只断言"有个门"会漏掉这次的错
(旧的时长门**存在**、常量**有名字**、数值**是正数**,三条全过,而真机拒掉正常滑动);
· `harmony-logic` 的行为判据加了两条**回归钉**:
`542.6vp/1429ms`(实测的正常甩动)必须过;
`1200vp/3000ms`(同速度、更远)也必须过 —— 旧门正是在这里误拒。
★ ⑤ 转场动画:查清了,"不做页面级"是**决定**而不是漏做
WebUI `index.css:1230` 写明:页面级淡入会让**已在那儿的框架**也一起暗一下
("观感还是闪"),且用户 2026-09-14 **亲口否掉过**"整屏一起淡",
所以那一档整体删掉,只保留**局部**动效。鸿蒙侧当前是:
· `pushUrl` 推页 → **系统自带的滑动转场**(连拍 8 帧验证:第 3 帧能看到
写信页正在覆盖发件箱,是真实的横向滑入,不是硬切);
· 局部入场 → `paneRiseIn` / `menuIn` / `calendarSlide`(已接 12 处)。
所以"加 pageTransition"反而会与用户当时的否决冲突 —— 这一条**不做**,
理由记在这里,免得下次又当成遗漏。
判据:files=32 ran=32 checks=519 pass=519 fail=0 red=0;baseline 7/7✓。
|
2026-09-20 10:51:28 +08:00 |
|
|
|
c0ab3f57b2
|
跨端: 顶部页签条改悬浮玻璃 + 深色压暗下限(修 8 处深色可读性)+ 手势判据自搭现场
用户:「顶部的收件箱发件箱和授权为什么不做成悬浮玻璃?」
问得对,而且它当时是**整页唯一一块实心白**。
★ 顶部页签条 → 悬浮玻璃
`CommTabBar` 原来写的是 `backgroundColor(Theme.surface)`(系统卡片色=实体)。
上一轮把列表容器、卡片、底部导航条都玻璃化了,**唯独漏了这条** ——
它既不是卡片也不是容器,是"条状 chrome",逐处修时最容易漏的那一类。
⇒ 现在与底部浮动条**同一族**:圆角 + 左右留白 + `Theme.navMaterial`。
★ 几何常量复用底部条的(`TAB_BAR_RADIUS = NAV_BAR_RADIUS`、
`TAB_BAR_SIDE = NAV_BAR_SIDE`),不各写一份 —— 两个数一旦分叉,
"同一条轴线上的两种条"就不齐了。为什么选悬浮而不是 WebUI 的"通栏无底色"
(WebUI 页签条自己**没有**底色,靠父面板的玻璃)也记在那个常量的注释里。
新增判据:页签条必须与导航条同族(不许实体面 / 要引 TAB_BAR_* 常量),
变异验证过(改回 `Theme.surface` 即红)。
★ 深色压暗下限 —— 本轮最重要的真 bug
玻璃让壁纸**真的透进卡片**之后,"浅壁纸 + 深色文字令牌"这个组合会在
**卡片内部**发生。设备实测(深色、aurora、压暗 37):
「角色」ink rgb(183,195,211) on bg rgb(68,95,128) = 2.57:1
「创建时间」2.27:1、「状态」2.68:1、「压暗」2.19:1(共 8 段 <3:1)
而**同一套代码在浅色下全部达标** ⇒ 差别不在"选错了色",在"底没暗下来"。
WebUI 早就撞过并写下了必然值(`index.css:442`):
.dark { --bg-dim-min: 92%; } /* 浅色下是 0% */
background-color: rgb(var(--bg-scrim) / max(var(--bg-dim), var(--bg-dim-min)));
我们只有 `scrimOpacity(bgDim)`、**没有下限**。补上 `DARK_DIM_MIN = 0.92`
(浅色下用户设的值照旧生效,不忽略设置)。
复扫:低对比 8 段 → 通信 0 / 日历 0 / 联系 1 / 我的 1
(剩下两条经手核是扫描器的假阳性:近黑底上把抗锯齿暗像素当成了"墨")。
★ 手势判据"没有自己搭现场"(套件红、单独绿)
`cross-client-gesture` 只保证"在月档"、**不保证在哪一月**,而它算的是
相对最初那一月的 delta ⇒ 前面某个判据(或一次失败的手势)把日历翻到 10 月后,
期望值就差一格。修法:先点「今天」归位到本月(该按钮语义就是"回本月")。
验证:从 9 月起步、从 10 月起步,两种脏状态都通过。
★ 顺带确认手势本体是好的:`--speed 1500` 时 1000px 要走 670ms,
紧贴 `SWIPE_MAX_DURATION_MS=700` 而被丢弃;`--speed 3000` 两个方向都翻。
★ 判据基建:修掉一个**静默失效**的 API 名(沿用上轮的教训修法)
`Surface.ets` 里两处块注释内嵌了 `/* ... */` —— 会把块注释**提前闭合**,
于是后面的代码跑到注释外面(报"Use let instead of var"/"Invalid character")。
本仓已有这条纪律("当 `code()` 因注释里的 `/*` 吞掉 import 时,改注释文本"),
这次又踩了两处(`Surface.ets` 与 `Appearance.ts`)。
判据:files=32 ran=32 checks=519 pass=519 fail=0 red=0;baseline 7/7✓。
|
2026-09-20 09:01:46 +08:00 |
|
|
|
eb4e413c67
|
跨端: 玻璃走基础组件(透明度/磨砂/投影)+ 判据去芜存菁(修 4 个判据自身缺陷)
用户两条:「玻璃不透明/不够磨砂炫酷,要更基础组件化」+「判据去掉无意义的、保留有用的」。
★ 玻璃的透明度与质感(逐层调出来的,不是拍的)
· 透明度:`BlurStyle` 是固定档、没有 saturate 旋钮 ⇒ 观感偏灰。
改用参数化 `backgroundEffect({ radius, saturation })`,字段与 CSS `backdrop-filter`
一一对应,于是可以**照抄 WebUI 的数**:`blur(18px) saturate(1.5)`
(WebUI `.narrow-nav`,那里最"玻璃"的一档)。**saturate 才是玻璃感的主要来源**。
· 质感:只模糊+饱和仍然"像一块平的白方块" —— 玻璃板之所以像板靠的是**边缘**。
WebUI 的完整配方是 `box-shadow: 0 8px 28px rgb(0 0 0 / 0.16)` + 发丝描边。
只有模糊没有投影 ⇒ 一片雾;加上投影 ⇒ 一块板。**这是"不够炫酷"的主因**
(我先前两个方向都调了模糊与饱和,方向就偏了)。
· 投影用**系统预置档** `ShadowStyle.OUTER_FLOATING_SM`("浮起的小面板"),
不是手写 `{radius, color, offsetY}` —— 手写色要么是 `#AARRGGBB`、
要么是 `rgba()`,两者都被判据明令禁止,而那条禁令**是对的**:
手写阴影色在深色下看不见(深底叠深影 = 无变化),WebUI 在 `.dark` 里
要把同一处从 0.16 加重到 0.55 —— 那正是"替系统猜深浅"。
★ 基础组件(用户:「定义一个基础玻璃容器给各个组件引用」)
· `GlassCardModifier` / `PaneModifier`(`AttributeModifier<CommonAttribute>`):
**一处定义、处处引用**。页面里的玻璃从 15 处内联三连收敛成
`.attributeModifier(...of(this.bgActive))`。
★ 为什么不是 `@Styles`:全局 `@Styles` 读不到 `this`(而玻璃要知道 `bgActive`);
成员级 `@Styles` 能读 `this` 但**每个 struct 各一份**,MainPage 有 7 个 struct
⇒ 只是把"每处抄 3 行"换成"每 struct 抄 1 份",没解决问题。
· `Motion.dur()`:接系统「减弱动态效果」(`isAnimationReduceEnabledSync()`,API 23)。
WebUI 有 `prefers-reduced-motion` 硬约束,鸿蒙这边**改造前一次都没调过**。
★ 途中撞出并修掉的**真 bug**(都是我自己的)
· 把 `@Styles glassCard()` 里也替换成 `attributeModifier` ⇒
「我的」页 ProfileSection/AppearanceSection/PushSection **整块不渲染**
(设备实测:Scroll 直接跳到「客户端连接密钥」)。删掉那个死 `@Styles` 即恢复。
· `Theme.shadowColor/hairline` 用了 `rgba()` + `#AARRGGBB` ⇒ 违反本仓两条既有禁令
("鸿蒙侧不写 CSS 颜色函数"、"半透明属于系统材质/职责")。改用系统阴影档。
★ 判据去芜存菁:修了 4 个**判据自身的缺陷**(都是它声称在管、实际漏判/误判)
· 材质来源只扫 `backgroundBlurStyle` —— 玻璃改成 `backgroundEffect` 后**整类漏判**。
实测:往 Modifier 里塞 `backgroundEffect({radius:99,saturation:9.9})` 照样全绿。
⇒ 补上这一类,并加"参数必须引 Theme 令牌"(否则只是换个旋钮写死)。
· 参数抽取用 `[^)]*` —— 对 `this.materialOf()` 与 `{...}` 对象字面量都提前截断,
**误伤合法写法**(本次被它误红两次)。改成配对括号 + 去外层花括号。
· 嵌套判定**没比文件**:拿 A 文件的字符偏移去比 B 文件 ⇒ 判出
"MainPage 内含 SettingsPage 的玻璃"这种物理上不可能的红。
假红比漏报更坏 —— 会让人去改本来对的代码(本次差一点)。
· `harmony-appearance` 数的是**内联字面量**("三元出现 ≥5 次"),
收敛成组件后写法变了、行为没变却变红 ⇒ 改成判不变式
(让位只有一条路径 + 每个窗格都收到开关)。
· 顺带:设备判据找「背景」时只在首屏可见节点里找(而 dumpLayout 只给可见节点),
且我的第一版只朝一个方向滑(越滑越远)⇒ 改成"先回顶、再逐屏向下扫"。
判据:files=32 ran=32 checks=518 pass=518 fail=0 red=0;baseline 7/7✓(第 7 次,逐个取证)。
|
2026-09-20 08:16:10 +08:00 |
|
|
|
5103e0aee3
|
跨端: 抽出基础面/玻璃组件(Motion + Surface)+ 三处硬弹的弹层补过渡
用户两条要求,各自都指向"决策被复制到各处"这个病根:
① 「应该定义一个基础玻璃容器给各个组件引用」
② 「很多页面还是不够精致……封装为基础组件供所有页面使用。基础组件包含动画」
★ 玻璃不透明的真根因(前几轮都没找到,这次是像素证据定的)
实测模拟器 3184×2232、壁纸 aurora + 压暗 37:
Navigation [229,140,3156,2204] ← 255,255,255(整块内容区)
y=2210(Navigation 之外那条缝) ← 226,225,235(壁纸清楚可见)
那条缝是决定性的:**壁纸层本身是好的**,白是因为上面盖了不透明的壳。
链路上一共三层,逐层修:
· `Navigation` 外壳:不设背景 ⇒ 系统默认不透明白,把里面已透明的窗格整片盖住;
· navBar 内容层(列表栏根容器):同样没有背景;
· **卡片**:铺的是 `Theme.surface`(实体系统色)。
修后:缝隙 203,213,228 / 卡片 191,204,220 —— 壁纸透得出来。
★ 新增基础组件(common/)
· `Surface.ets` —— `GlassPane`(正文面)/ `GlassCard`(嵌套面)/ `PageHeader` / `Pressable`
· `Motion.ets` —— `Motion.dur()`:接系统「减弱动态效果」
(`accessibility.isAnimationReduceEnabledSync()`,API 23 正好够用)。
WebUI 侧有 `prefers-reduced-motion` 硬约束(index.css:1266),
鸿蒙这边**改造前一次都没调过** —— 系统里关了动画,我们照样动。这是无障碍义务。
· `Theme.cardMaterial` —— 卡片材质令牌。不是拍脑袋选的:
`COMPONENT_THIN` 实测卡片 252,254,254(吃掉 94% 壁纸,就是用户看到的"白卡");
`BACKGROUND_THIN` → 185,190,201,壁纸透得出来且文字对比度仍够。
★ 判定形状按"玻璃从例外变成默认"重写(判据 C 条)
原来是"允许出现玻璃的位置"白名单,逐处登记。那个形状在"玻璃是少数例外"时成立,
但用户要的是**全部玻璃化** ⇒ 白名单退化成"把每处抄一遍",且挡不住新写的裸枚举。
改成**规则**:材质档次只能来自 `Theme.navMaterial` / `Theme.cardMaterial`
(或 `BlurStyle.NONE`),页面里出现裸 `BlurStyle.XXX` 即红;
辅助方法(`this.materialOf()`)体内也查,否则等于开了后门。两条变异都验证咬得住。
★ 判据 C 条自身的两个 bug(都被本次触发)
· 嵌套判定**没比文件**:`c.at`/`o.at` 是各文件自己的字符下标,直接比大小
于是判出"MainPage 内含 SettingsPage 的玻璃"这种物理上不可能的红。
假红比漏报更坏 —— 会让人去改本来对的代码(本次差一点)。
· 参数抽取用 `[^)]*`:`this.materialOf()` 里含一层 `()`,在内层括号处截断,
把合法写法读成 `this.materialOf(` 判红。改成配对括号抽取。
★ 出现/消失动画:用户点名的三处硬弹
这些挂在 `if (cond)` 上,条件一变整块出现/消失,原来一帧过渡都没有 ——
而代码上"看着像挂了动画"(外层页面有 transition),所以最容易漏。
· `MainPage` 账号选择器下拉 → `menuIn()`(从上往下落的浮层档)
· `AdminUsersPage` 新建用户表单 → `paneRiseIn()`(就地展开档)
★ 过渡必须挂在**调用点的 Column**:`@Builder` 返回 void,链不上修饰符。
第一版写进了 `CreateForm()` 内部,是新判据抓出来的。
· `SettingsPage` 新增账号弹层 → `paneRiseIn()`(与回复/转发弹层同一挂法)
新增判据「出现/消失的那类元素真的挂了过渡」:**枚举所有条件挂载的浮层/展开块**
(不是数数 —— 数数挡不住"新增第四处又漏了"),变异验证过。
★ 顺手修的两处真嵌套玻璃
`SettingsPage` 的账号列表与密钥列表都是"卡里面的一段列表",两层都铺材质 =
WebUI 用 `.bg-white .bg-white { backdrop-filter: none }` 明确禁止的嵌套
(alpha 相乘 0.82×0.82=0.97 把壁纸吃光)。去掉内层材质,玻璃只留一层。
★ 判据放宽一处(不是放水)
`harmony-contacts` 那句写死 `pushUrl`,判的是"tryRestore 有没有跳转",
与用哪个原语无关。修 LoginPage 返回栈时改成 `replaceUrl` 后它误报了;
改成 `(pushUrl|replaceUrl)`,原语该用哪个由 `harmony-nav` 那条专门判。
判据:files=32 ran=32 checks=518 pass=518 fail=0 red=0;
baseline 7/7✓(第 6 次重算,两个文件逐个 `git diff --quiet HEAD` 取证为有意编辑)。
|
2026-09-20 07:37:16 +08:00 |
|
|
|
a7896a7e3a
|
文档: CRITERIA.md §16.1.3 —— 判据锚在"字面相邻"隐含断言"不许再多一层"(假红的来源)
记 pi `7d98245a` §三 报的那条假红(`Motion.dur(...)` 包一层 ⇒ `duration:\s*Theme\.durRise`
失配),连同我自己量到的两半:
· 无障碍层**零判据**:`isAnimationReduceEnabled`/`Motion.dur`/`Motion.reduced`
在 test/ 里 grep 各 0 次;WebUI 侧**有**(animation-audit.test.mjs:112)。
变异(`Motion.dur` 恒 return want)⇒ 失败集合逐条相同 ⇒ **零反应**。
· ★ 绕过面比"都包了"更大:**11 个真动画时长站点里 5 个绕过开关**
(CalendarPage:373,410 / MainPage:2607,2845,2884 裸 `Theme.durX`)
⇒ "收成一个入口"的设计意图**只落了一半**。
通用纪律:**判据锚在"字面相邻"时,它其实断言了"两层之间不许再有一层"** ——
而那个隐含断言几乎从不是作者本意(本意是"这个令牌要被引用")。
二者在实现多一层合法卷绕(包装函数/无障碍开关/单位换算/类型转换)时**必然分叉**,
方向是**假红**。
⇒ 与 §16.1.2 合成一条:**判据的严格度必须与它真正想守的那件事对齐,
而不是与"当时那版实现的写法"对齐。**
|
2026-09-20 05:45:42 +08:00 |
|
|
|
b16d07b625
|
修复: 假红 —— Motion.dur(...) 包一层就让 paneRiseIn 的时长断言失配(pi 报)
pi `7d98245a` §三 报的形状,我**独立复现并量了两棵树**(隔离 worktree,同刻对照):
A 干净 HEAD ⇒ 19 tests / 18 pass / **1 fail**(not ok 16,设备条)
B HEAD + 那份未提交的无障碍改动 ⇒ 17 pass / **2 fail**(多出来的正是 `not ok 8`)
`Theme.paneRiseIn()` 现在写成
.animation({ duration: Motion.dur(Theme.durRise), curve: Theme.easeRise })
而 `harmony-nav.test.mjs:441` 是 `/duration:\s*Theme\.durRise/` —— 要求 `duration:`
与 `Theme.durRise` **紧邻**,中间多一层 `Motion.dur(` 就失配。
语义上 `durRise` **仍被引用**(`Motion.dur(x)=reduced()?0:x`,只在系统开"减少动效"时折成 0)
⇒ **假红**。
★ 放宽的**只是"中间能不能多一层卷绕",判据的区分力一点没动**(3 个方向实测):
/duration:\s*(?:Motion\.dur\()?Theme\.durRise/
① `Motion.dur(Theme.durBase)`(换令牌) ⇒ 仍红 ✓
② 裸 `Theme.durBase`(包一层来蒙混) ⇒ 仍红 ✓
③ 干净 HEAD 形状但换令牌 ⇒ 仍红 ✓
且下面那条 `!/Theme\.durBase/` **逐字仍在**、扫的是**整个函数体**
⇒ 放宽的是**包装**,不是**令牌**。
修后:主树 `18 pass / 1 fail`(只剩 not ok 16 那条设备条);
全套 `red 5 → **4**`(`harmony-nav` 从红名单里出去了,其余 4 条红都是别的会话的)。
★★ 顺带量出一条**我认为更值钱**的:无障碍层**零判据**(pi §三 的另一半,我确认)
· `client/electron/test/` 里 `isAnimationReduceEnabled` / `Motion.dur` / `Motion.reduced`
grep **各 0 次**;对照 WebUI 侧 `animation-audit.test.mjs:112` **有** reduced-motion 判据。
· **pi 的变异我复现了**:把 `Motion.dur` 改成恒 `return want`(整个无障碍开关失效,
源里 6 处受影响)⇒ `harmony-nav` 失败集合与变异前**逐条相同**(`17/2`)⇒ **零反应**。
· ★ 而我还量到一个 pi 没说的:**11 个真·动画时长站点里有 5 个绕过开关** ——
`CalendarPage.ets:373,410`、`MainPage.ets:2607,2845,2884` 都是裸 `Theme.durX`,
只有 6 处走了 `Motion.dur(`。即"收成一个入口"这个设计意图**目前只落了一半**。
⇒ 这一半我**没有**加判据也没改源码:`Motion.ets` **尚未被 git 跟踪**,
对它写判据会让 HEAD 立刻变红(那是别人未完成的在制品,不是我的改动范围)。
已把精确读数报给 pi,由落 `Motion.ets` 的那次提交去补判据。
|
2026-09-20 05:45:03 +08:00 |
|
|
|
1c3335f0c9
|
修复: 第五个落点 —— 判据写对了,却**没有任何人执行它**(check-file-modes.sh 空转)
pi `4fc16f66` 报的是 rc=2 那半个缝(M18)—— 我复核后**已经在 `e99a657` 修好了**,
而它的**父提交正是 pi 报信时读的 `6ee9902`**。又是同一形状的竞态。
但照 pi §四 那句「**每条分支的前提本身也要能被构造**」做审计时,
我发现**第五个落点,而且这一格的判据本身写得完全正确**:
`deploy/check-file-modes.sh` = 源文件权限政策的**唯一**判据。
它甚至专门修过自己那份 `[ -x ]` 恒真的 root 陷阱(`b7dc9e9`),改成直接读权限位 —— 修得对。
**但它从来没有被任何东西执行过。** 全仓 `grep -rn "check-file-modes"` 只有:
· `check-deploy-drift.mjs` 3 处**注释**(谈分工)
· `run-all.mjs:951` **注释**(讲 root 陷阱)
· `summary.py:77` **注释**,且**说法与实现不符**
· `summary.py:431` 一句 `print(...)` 的**文案**
⇒ 唯一一处非注释提及是一句 print 的文案。没有 install.sh / redeploy-* / hook / CI / cron 调它。
**而它当时正红着**:2 个文件是 600 + `jobs/` 缺属主 x 位。
★ 为什么这一类**只能靠判据守**(实测):
`git status` 看不见、`git diff` 看不见、`git ls-files -s` **只记 100644/100755**
(组/其他读位**不进版本库**)⇒ 把受跟踪文件 `chmod 600 ↔ 644` 两次 `git status` 都空。
套件也看不见(`jobs/` 缺 x 位时实测 `diag=none`、`ran=48`,全绿)⇒ **没有第二条通道**。
0600 在"跑的人恰好是属主"时不炸,换身份就是 EACCES,而那串报错**看起来像代码问题**。
修法:
· 接进 `deploy/install.sh`(check-shared-libs 之后),**走 `--check` 累积通道**
(照既有 npm_rc/CHECK_GATE_RC)。⚠️ 我第一版**直接调**,而门禁红时它 `exit 1`、
install.sh 是 `set -e` ⇒ 当场吞掉后面所有诊断:**实测**接线后 `构建 Gateway`
在日志里命中 **0 次**(接线前 2 次)—— 正是 install.sh:141-157 刚修过的同一个毛病,
我自己又造了一遍。改累积后回到 2 次,门禁报 `[FAIL] 权限政策没过(退出码 1)`。
· 修那两个文件:600 → 644 ⇒ 权限政策现在绿(rc=0)。
· 新增判据 `criteria-hygiene` 第 7 条:政策门禁必须被入口脚本在**可执行位置**调用。
★★ 变异验证时**我自己先假绿了一次**(单记):第一版用 `code()` 剥注释,而
`lib/read.mjs` 的 `stripComments()` 只认 `//` 与 `/* */` —— 那是 **JS** 的注释,
**`install.sh` 是 shell,注释是 `#`** ⇒ 对 shell 文件**原样返回** ⇒ 判据读到了
**我写在它上面那段解释里的** `` # `check-file-modes.sh` 红时 `exit 1` ``。
变异①(接线整段删掉)⇒ 第一版 **rc=0(假绿)**;自剥 shell `#` 后 **rc=1** ✓
变异②(只在 echo 里提一句)⇒ rc=1 ✓
变异③(gates 恒空 ∀x∈∅)⇒ rc=1(`只找到 0 个`)✓
(③ 第一次 python 锚点没匹配上、变异没施加,我重测并**先打印"变异已施加"**才算数。)
★ 射程如实标出:只管 `deploy/check-*.sh`(政策门禁族);**不管** `check-deploy-drift.mjs`
这类**按需手动工具**(自带 `--self-check`、文档写明"事后自查")——要求它进入口是**错的**。
它实测同样 0 处调用,但**本判据不覆盖它,也没修它**。判"有没有接线",不判"接得对不对"。
★ 通用纪律(CRITERIA.md §16.1.2):**判据自己也要能"被证明它真的在看代码"。**
若判据读的**语言**与 stripComments 实现的**语言**不一致(shell vs JS、Python vs JS),
那"剥注释"是**假的** ⇒ 判据消费散文,而**变异验证是唯一能戳破它的东西**。
全套 checks=516→**517** pass=512 fail=5 red=5 broken=0 verdict=red(5 条红都是别的会话的);
mutants=48 ran=48 skipped=0 diag=none baseline=7/7✓;5 个自检各 rc=0。
|
2026-09-20 05:22:42 +08:00 |
|
|
|
efb1c1d2ca
|
修复: 第四个落点 —— **变异条目的锚点失效**(hits=0):守具有齿,却不在位
pi `6aa2b17f` 报的第三例(`unlisted`/`ghosts` 退 0)**我复核后已经在 `6ee9902` 修好了**
—— 而它的**父提交正是 pi 报信时读的 `4b841e0`**(差 11 分钟)。又是同一形状的竞态。
但顺着同一条线**审计全部 61 个变异条目的锚点**,发现**还有一个同形状的落点,
而且它在真树上是活的**(不是构造的):
hits=0 SettingsPage.ets 「管理入口不做门禁」(jobs-all.json)
根因:该锚点写的是 **6 空格**,而 `c523c21`(09-17 17:43)把该文件**重排成 4 空格**
⇒ 锚点从此命中 0 次(写进清单时 `bcd4f97`(09-15)它是**对的**)。
为什么没人发现:`summary.py` 把「hits=0 … 过期条目」**只打印**、**不进严重度链**
⇒ `rc=0`、`diag=none` ⇒ 套件 `whyLines: (note || status!==0) ? … : []` 为假 ⇒
**整段丢掉**。端到端实测(真树、干净工作树):套件输出里 grep「过期条目」= **0 次**。
★ 危害是"**一个变异守具被静默关掉**",不是"数字错了":`ran` 少 1、`skipped=1`
是个**中性数字**,读者看不出少了哪一个。而**手工施加那个变异仍能让判据红**
(`harmony-admin.test.mjs` `# fail 1`)⇒ **有齿,只是没挂上**。
修法(沿用 `summary.py` 的**唯一严重度链**):
· 新增 `mutant-anchor-stale` ⇒ **1 档**(清单/数据该改;**不是** 2 档 ——
照 `env-defaults.sh:25` 的反方向:别让"清单没跟上"冒充环境)。
· 修锚点 6→4 空格 ⇒ `ran` 47→48、`skipped` 1→0,守具重新挂上(实测变异能红)。
· `whyLines` 过滤器加 `^\s*hits=`:只说"有锚点过期"不够,**点名的才是可行动的**。
· 两张表都登记(`UPSTREAM_RC` + `DIAG`,`blocksGreen: true`)+ 真跑案例
(迷你仓库里放一个不含锚点的同名文件 ⇒ `hits=0`)+ 读者侧具名案例。
★ 射程如实标出:判据只看 **`hits == 0`**,**不看 `hits == -1`**(文件打不开是另一回事,
且迷你夹具里目标文件本来就不在 ⇒ 算进来会造假红)。**`-1` 那一半无判据守着**(真树 0 条)。
变异验证(4 个方向全抓):
① 新档关闭(回到"只打印")⇒ exitcode-selftest rc=1、2 条红 ✓
② 从 UPSTREAM_RC 删掉新码 ⇒ 反向覆盖点名 ✓
③ blocksGreen true→false ⇒ 双向口径漂移红 ✓
④ 放宽成任何 skipped_detail(含 hits=-1)⇒ rc=1、**5 条红**(夹具假红)
⇒ **`h == 0` 这个射程是承重的** ✓
端到端 A/B(隔离 worktree,同刻对照):
A 锚点已修 ⇒ rc=0、diag=none、ran=48/skipped=0、"锚点已失效" grep **0**
B 锚点退 6 格 ⇒ rc=1、diag=mutant-anchor-stale、ran=47/skipped=1、grep **2**
★ 通用规则(本仓第 4 次同一形状):**"跑了多少个"与"该跑多少个"之间也要有判据。**
被跳过时 `ran` 只少 1、`skipped` 只多 1 —— 都是中性数字,而"少了哪一个"没有通道。
凡"登记一批东西、再逐个挂上"的结构(变异条目、判据、样本表)都要问:
**挂不上的那一个,谁来说?**
自检 5 个各 rc=0;全套 checks=516 pass=511 fail=5 red=5 verdict=red;
mutants=48 ran=48 skipped=0 on_new_criteria=36 diag=none baseline=7/7✓。
CRITERIA.md §16.1.1 记这一笔(含 4 个变异与 A/B 表)。
|
2026-09-20 04:49:50 +08:00 |
|
|
|
c1465e09ab
|
跨端: 平板三处真 bug(返回回登录页 / 避让重复叠加 / 联系人卡被裁 10vp)
用户报的「在主页返回为什么会直接回到登陆页」是**原语用错**:
LoginPage 用 pushUrl 进 MainPage,路由栈成 [LoginPage, MainPage],
返回自然弹回登录页。而 Logout.ets 早就是 replaceUrl 并写了理由
(「退出后不该还能'返回到已登出的页'」)—— 同一个不变式、相反方向,
只改了一半。三处 pushUrl → replaceUrl,设备实测:返回直接退出 app
(前台变 com.huawei.hmos.browser),不再回登录页。
另两处平板(HUAWEI MatePad Pro, 2800x1840, ratio 1.52 ⇒ 宽屏):
· 内容列 `this.isWide ? Theme.surface : (bgActive ? 透明 : surface)`
—— 宽屏分支把 bgActive 丢掉了 ⇒ 平板上壁纸永远被挡。
· `top: this.isWide ? paneGap : statusBar` —— 宽屏分支把状态栏避让丢掉
⇒ 页签字压在系统时钟下。
· 侧栏自己也加了一次 topInset,而父 Row 的 padding 已经含状态栏
⇒ 83+83=166px 空白(用户:「避让有点用力过猛」)。
· 联系人列表项硬写 .height(85)+clip,内容实际要 95vp ⇒ 写信/归档行被裁一半。
判据 harmony-nav 新增「登录/退出必须用同一个原语」并做变异验证
(改回 pushUrl 即红);注册数 18→19,全绿 19/19。
★ 途中发现的记账缺口:设备判据连续失败时走 noteBusySkip 计数,
.tmp/harmony-busy-skips.json 累到 10 后拒绝再当'礼貌跳过'。
清除账本 + 让 app 真在前台后立刻 19/19 —— 说明**不是代码问题**,
是账本把'设备忙'当成了证据。这一点记进 DEBTS。
|
2026-09-19 23:38:35 +08:00 |
|
|
|
110de79401
|
跨端: 三级文字**浅色也一样不够**(12 处 2.85:1)+ 判据不再只验一半
## 一、这是"我自己推的理由"被实测推翻
上一条我把 `textSubtleFor()` 写成**只改深色**,理由是:
> 「浅色下这些令牌本来就是为浅底配的,量它们没有信息量」
**那个理由是错的。** 切到浅色主题复扫,立刻抓到 12 处:
```
"用户名 / 显示名 / 角色 / 状态 / 创建时间 / 最后登录" 2.85:1
"已同步" / "37%" / "3px"(外观区) 2.85:1
"这一天没有日程" 2.85:1
```
`ink rgb(153,153,153)` 压白底 —— 全是 `textSubtle`。
对照 WebUI 浅色(`index.css` `:root`):
- `gray-400` = `107 114 128` = **4.83:1**
- `gray-500` = `90 98 112` = **6.15:1**
⇒ 系统三级色**两个主题下都不够**。它的语义只是"比二级更淡",
**不保证任何可读性下限**;WebUI 那边两套主题都做过适配,这边一档都没有。
修:`textSubtleFor()` 两个主题都返回 `textMuted`
(系统二级色,语义就是"次要文字",且**跟随主题** ——
不必再维护两个手写常量、不必登记)。
## 二、判据从"只验深色"改成"两个主题都验"
原来那条设备判据**在浅色下直接 skip**,skip 的理由正是上面那句错话。
现在两个主题都扫,断言消息里带上主题名(`深色主题下…` / `浅色主题下…`)。
★ 纪律:**色令牌的可读性是两个主题各自的事,不能只验一半。**
"另一半没问题"若没量过,就只是推测。
**变异验证**:把 `textSubtleFor()` 改回"两主题都用 textSubtle"
(此时设备正在浅色主题)⇒ **判红**;还原 ⇒ 绿。
## 三、设备复扫(浅色主题)
```
[通信] 扫 41 段,低对比 0
[日历] 扫 84 段,低对比 0
[联系人] 扫 50 段,低对比 0
[我的] 扫 40 段,低对比 0
```
**215 段文字全部 ≥3:1。**
两个主题合起来:深色 219 段 + 浅色 215 段,全绿。
## 四、顺手改掉的
测试名从「深色下短文本必须都读得动」改成「**当前主题**下…」
—— 名字得跟着它实际判的东西走(否则下一个人会以为它只管深色)。
`run-all.mjs` → `checks=515 pass=515 fail=0 skip=0 red=0 broken=0 unreported=0`。
**当前设备主题**:浅色(本轮为验另一半切过去的;服务端 `theme=light`)。
|
2026-09-19 20:58:59 +08:00 |
|
|
|
1da4e15a7b
|
跨端: 补齐「我的」页深色可读性(4 页 219 段全 ≥3:1)+ 判据自己漏报的那一类
上一条把通信/日历/联系扫干净了,但**「我的」页没扫到**
(扫描脚本按文案点不到它 —— 那是侧栏**底部的头像按钮**,不是导航项)。
补上后立刻又抓出问题,并把判据自身的**漏报形状**一并修了。
## 一、「我的」页两处真 bug
1. **主题分段按钮 `SettingsPage.ets:834`**:
`.fontColor(... ? Theme.surface : Theme.textPrimary)` —— `surface` 是
**会翻转的面色**,压在 `Theme.accent` 蓝底上,深色下变成近黑。
设备读数:`深色 2.93:1 ink rgb(32,34,36) bg rgb(34,96,228)`。
2. **权限档徽标 `MailDetailPage.ets:391`**:同一个错法(第 13 处)。
3. **账号名 `SettingsPage.ets:1179`**:三元里的裸 `Theme.accent`
压在 `accentSoftFor()` 底上 ⇒ `jianf 2.83:1`。
`AdminUsersPage.ets:386` 同形状(角色徽标)。
## 二、判据自己的漏报形状(比 bug 本身更值得记)
静态防线(`C|会翻转的 Resource 不得当前景色`)**在 bug 存在时是绿的**。
原因是它的提取正则:
/\.fontColor\(Theme\.([A-Za-z0-9_]+)\)/ ← 要求令牌是**唯一实参**
于是**三元里的令牌全被漏掉**:
.fontColor(this.appearanceTheme === t ? Theme.surface : Theme.textPrimary)
改成"在 `fontColor(` 之后的整段实参里找所有 `Theme.X`"之后,
它**立刻报出上面第 1、2 两处**(此前一直绿)。
★ **判据漏报的常见形状是它自己的正则太窄,而不是被测代码太隐蔽。**
这条写成注释留在判据里了。
## 三、我自己犯的批量替换错误(已加判据钉住)
把裸 `Theme.accent` 换成 `accentFor()` 时,**误把 6 处 `backgroundColor` 也换了**。
`accentFor()` 深色给浅蓝 `#80AFF9`,而搭档前景是白色 `accentFg`
⇒ 白字压浅蓝 ≈ **1.4:1**,主按钮文字会彻底看不见。
- 6 处已逐处还原(复核:`grep -c "backgroundColor(Theme.accentFor())"` = 0)。
- 新增判据 **`C2`**:`accentFor / dangerFor / approveFor / warnFgFor / textSubtleFor`
**只能用于前景**,`backgroundColor(Theme.XFor(...))` 直接判红。
(`accentSoftFor` 是例外 —— 它本来就是"面"。)
★ 修法不是"下次小心点":批量替换一定会再犯,**一行判据把它变成不可能**。
## 四、设备复扫(修后)
```
[通信] 扫 42 段,低对比 0
[日历] 扫 85 段,低对比 0
[联系人] 扫 51 段,低对比 0
[我的] 扫 41 段,低对比 0 ← 新增
```
**219 段文字全部 ≥3:1**(本轮全部针对深色)。
## 五、底本
`baseline.sha` 第 5 次重算。重算前**专门复核**了"那 6 处误改有没有残留"
(不只看 `git diff` 非空就放行):`backgroundColor(accentFor)` 计数为 0、
`git diff HEAD~1` 里 backgroundColor 只有 calendar 那一处(有意改动)。
`run-all.mjs` → `checks=515 pass=515 fail=0 skip=0 red=0 broken=0 unreported=0`。
**未验**:浅色主题下的对比度没扫(本轮全部针对深色)。
|
2026-09-19 20:43:59 +08:00 |
|
|
|
4ecddfc2b6
|
跨端: 语义色/三级文字在深色下没提亮(设备扫出 12 处)+ 判据取样法修到第三版
## 一、判据基建先修对,否则是在听噪声
深色可读性扫描的**取样法迭代了三版**,每版都因为**假红**才改的
(记在这里,因为"判据自己错"比"界面错"更费时间):
1. **取中心一个像素** ⇒ 落在笔画之间、读到的是底色 ⇒ 一片 ratio=1.00 假红。
2. **取全局最暗/最亮** ⇒ 会采到**两个不同的东西**:细字的抗锯齿中间色。
实例:segmented「日」报 ink `rgb(32,34,35)` / bg `rgb(98,100,101)` 2.69:1 ——
而截图里它要么蓝底白字、要么深底浅字,两种都远高于 3:1。
**反复核对才确认是判据的错,不是界面的错。**
3. **步长采样 + 众数** ⇒ 仍然假红:周表头「一」报 2.03:1,
而逐像素的真实墨是 `rgb(166,167,167)`(**6.6:1**)——
稀疏网格**整个跳过了笔画**。这不是调参能修好的(字越小越糟)。
4. **逐像素 + 众数=底 + 离底最远者=墨**(最终版)。
理由:一个文字框里**面积最大的一定是底**,而"离底色最远"就是笔画最实那部分。
⚠️ 逐像素是**负担得起的**(整屏已解码在内存,一个框最多 9 万像素)——
**不要再为了省开销退回抽样**,前两版都因此假红。
## 二、真 bug 三批(都是"深色下没提亮")
### ① 语义色前景(12 处 → 修完设备复扫 0)
WebUI 的语义色也是**双通道**,`.dark` 段把 `red/green/amber-700` 换成浅色
(`248/118/249 → 164/219/195` 那组)。鸿蒙只有一个深色值:
- 「归档」按钮 `Theme.danger` #B91C1C 压深面 ⇒ **2.47:1**(联系人页 7 处)
- 「今天」/「日」等 ⇒ **2.38–2.77:1**
加 `dangerDark/approveDark/warnFgDark`(逐字对齐 WebUI `.dark`)
+ `dangerFor()/approveFor()/warnFgFor()` 入口,22 处接入。
### ② 日历工具条整块配色抄错(不是"深色档选错")
对照 WebUI `CalendarView.tsx:313-331`:
| 元素 | WebUI | 鸿蒙(改前) |
|---|---|---|
| 「今天」 | `border-gray-300` + `text-gray-700`(**中性**) | `accentSoft` 底 + `accentStrong` 字 |
| 月/周/日 未选中 | `bg-white` + `text-gray-700` | `accentSoft` 底 |
鸿蒙把**次要的中性按钮**画成了**品牌色按钮** —— 视觉重量跟主操作一样,
三个档看起来"都像选中"。已按 WebUI 改成中性。
### ③ 三级文字在深色下 2.82:1(**71 处**,全局最大的一个)
WebUI 的 `.dark` 段注释逐字写着这个坑:
> 「`gray-400/500`(次要文字)→ **提亮**。深底上的浅色 gray-400
> 只有约 2:1 对比度,远低于 WCAG AA 的 4.5:1 —— **看得见但读不动**。」
鸿蒙用的系统三级色 `ohos_id_color_text_tertiary` 深色下是 `rgb(102,103,105)`
压深面 **2.82–3.05:1**,而它被用在 10–11px 的**小字**上
(时间戳、农历、空态、辅助说明)。
**不换掉系统令牌**("系统拥有的维度"纪律照旧),加 `textSubtleFor()`:
深色下改用二级色(实测 `rgb(166,167,167)` ≈ 7.15:1,
且与 WebUI 的 `gray-500` 同档位)。71 处接入。
**设备复扫(修后)**:通信/日历/联系三页 **176 段文字全部 ≥3:1**。
## 三、欠账与底本
- `baseline.sha` 第 4 次重算。仍**先逐个取证**:
`git diff --quiet HEAD -- <f> && echo STALE || echo INTENTIONAL`
⇒ 三个全是 `INTENTIONAL`(我改的),不是变异残留。
把这条取证命令也写进文件,下次照抄。
- `dangerDark/approveDark/warnFgDark` 已登记进 `SELF_OWNED_COLORS`
+ `Theme.ets` 登记表(各带理由与 WebUI 出处)。
`run-all.mjs` → `checks=514 pass=514 fail=0 skip=0 red=0 broken=0 unreported=0`。
**未验**:①「我的」页(底部头像按钮,扫描脚本没找到入口)没扫;
② 浅色下的对比度没扫(本轮全部针对深色)。
|
2026-09-19 20:27:09 +08:00 |
|
|
|
13b557a742
|
跨端: 品牌蓝深色下没提亮(24 处字/图标看不见)+ 补深色可读性设备判据
## 一、真 bug:WebUI 深色下把品牌蓝**提亮**了,这边没有
WebUI 的强调色是**双通道**(`tailwind.config.js` 的 `backgroundColor`
/`textColor` 覆盖 + `index.css` 两段定义):
| 通道 | 用途 | 浅色 | 深色 |
|---|---|---|---|
| `--s-blue-600` | **实心按钮底** | `37 99 235` | `37 99 235`(**同值**)|
| `--c-blue-600` | 内容/交互的**蓝字与图标** | `37 99 235` | **`128 175 249`** |
`index.css:353` 写了理由:主按钮底跟着变「会让主按钮在深色页面上
失去『这是主操作』的视觉重量」;而蓝字必须提亮,否则深底上读不动。
鸿蒙只有一个 `Theme.accent = '#2563EB'` ⇒ 24 处字/图标在深色下
对比度 **2.61:1**(设备实测:管理页返回箭头 `‹`),低于 WCAG 图形下限 3:1。
**取证方式**:在跑着的 WebUI 上用 CDP 读**计算样式**(不是读 CSS 源)——
浅色 `37 99 235` / 深色 `128 175 249`,实测确认。
## 二、修法:加前景专用的深色档 + 唯一入口
- `Theme.accentDark = '#80AFF9'`(= WebUI `.dark --c-blue-600`)
- `Theme.accentFor(dark?)` 作为**前景**唯一入口
- **当背景的 20 处保持 `Theme.accent` 不动**(跟 WebUI 的 `--s-*` 一致)
24 处 `.fontColor/.iconColor(Theme.accent)` → `Theme.accentFor()`。
## 三、顺手修掉「转述一层就会漏」这个结构问题
`accentSoftFor(isDark)` 原本的约定是"页面算好深浅色传进来"。
给 `accentFor` 做准备时一数:**8 处**直接用了 `Theme.accentSoft`(没走入口)
—— 约定**已经漏了**,而漏掉的症状正是上一轮那个"深色下白底卡片刺眼"。
于是把两个 `For()` 的参数都改成**可选**:`AppStorage` 是 ArkTS 全局键值存储,
静态类可以直接读(原来"拿不到 Context"的理由不成立)。
⇒ `Theme.isDarkNow()` 成为唯一判断点,调用方不必再各自转述。
## 四、设备判据:深色可读性**扫一屏**
原来只有悬浮球那一条(单个点)。这个 bug 类一天撞到**两批**(12 处 + 24 处),
逐处写判据追不上 ⇒ 改成把当前页所有小段文字都量一遍对比度。
三个实现要点(第一版全踩了,都写进注释):
- **不能只取中心一个像素**:中心多半落在笔画之间 ⇒ 读到的是底色,
报出一片 ratio=1.00 的假红。改成**框内网格扫描取极值**(最亮=底/最暗=墨)。
- **整屏解码一次**:每点 spawn 一次 ffmpeg 太慢 ⇒ 新增
`readPixels()`(157ms 解整屏,比逐点快三个数量级)。
- **只判"有真实墨迹"的框**(`hi.L - lo.L >= 0.02`),否则跳过而不是判红。
实测:修前 1 处低对比(2.61:1),修后 **40 段文字全部 ≥3:1**。
## 五、判据自身的三个修正
- `accentSoftFor(dark:)` 的签名断言跟着放宽成 `dark?`,并**补上 `accentFor` 的**。
- **孤儿令牌判据从"一跳"改成"走整条链"**:`KEY_IS_DARK` ← `isDarkNow()`
← `accentFor()` ← 24 处页面。只查一跳时它假红 ——
★ **"有没有人用"是可达性问题,不是邻接问题**;加中间层(抽 `For()` 入口)
恰恰是我们鼓励的写法,而旧判据会因此假红。
- 手写色登记表补 `accentDark` 一行理由。
## 六、`baseline.sha` 重算(先核过不是残留)
三个文件哈希对不上。逐个 `git diff --quiet HEAD -- <f>` 取证:
- `AdminUsersPage.ets` / `SettingsPage.ets` —— 本次**有意编辑**;
- `api/AppearanceApi.ets` —— **与 HEAD 逐字节相同** ⇒ 底本取完后被**合法改过**
(提交 `f811c98`),属 `stale` 不是 `residue`。
按该文件自己那条纪律(「重算必须是一次有记录的动作」)在文件里记了理由。
`run-all.mjs` → `checks=514 pass=514 fail=0 skip=0 red=0 broken=0 unreported=0`。
|
2026-09-19 20:05:33 +08:00 |
|
|
|
3123d83979
|
判据: 把「面当字」的防线从死名单改成判矛盾(三次迭代都记下来了)
上一版加的静态防线只 grep `fontColor(Theme.surface)` —— **一个名字**。
那是死名单:以后加了新令牌它不会跟上(本仓已记录过多次这类错法)。
改成从源码推:
**条件 A**:值是 `Resource` 且指向 `sys.color.*` ⇒ **跟随系统主题翻转**
**条件 B**:既当 `backgroundColor` 又当 `fontColor/iconColor` ⇒ **被当成「面」也被当成「字」**
判决 = **A ∩ B**,不需要维护任何名单。
## 三次迭代(每次错得很典型,所以都写进注释了)
- **① 死名单**(只盯 `surface`):新增令牌不会跟上。
- **② 只要 A**("是翻转的 Resource 就不许当前景色"):**太宽** —— 实测立刻
报出 20+ 处"违规",全是 `textMuted` / `textSubtle` / `textPrimary`。
那些**本来就该**当前景色(语义就是文字色,跟随主题翻转是对的)⇒ 假红一片。
- **④ 只要 B**("矛盾就报"):**也宽** —— `accent` / `danger` / `approve` / `warnFg`
两边都用是正常设计(蓝底白字主按钮 + 白底蓝字返回箭头)。
和 `surface` 的**关键区别**:那些是**饱和的字符串常量**(不翻转、语义是"色"),
而 `surface` 是**中性面 + 跟随主题翻转**(当字色 = 把底板当墨水,深色下必然看不见)。
- **最终 = A ∩ B**:恰好命中 `surface` 这一类,且**不随新增令牌失效**。
允许清单现在是**空表**(修完 `surface` 后没有例外),
并写明「已审过、不需要进这张表的」那几个令牌与理由。
## 变异验证(两个都做了,第二个是关键)
1. 把一处 `.accentFg` 改回 `.surface` ⇒ **判红**;还原 ⇒ 绿。
2. **造一个判据从没见过的新令牌**(`surfaceAlt`,同样 `Resource` + `sys.color.*`),
让它同时当背景与前景 ⇒ **判红**(报文里点名 `Theme.surfaceAlt`);还原 ⇒ 绿。
—— 这条比 ① 重要:它证明判据**不是靠记住 `surface` 这个名字**在工作。
## 顺带确认
`pageBg` / `surfaceMuted` / `wallpaperScrim` / `border` / `overlay` 这五个
同类"会翻转的面"**一处都没被当字色用过** —— 这一类现在全清了。
`run-all.mjs` → `checks=513 pass=513 fail=0 skip=0 red=0 broken=0 unreported=0`。
|
2026-09-19 19:39:02 +08:00 |
|
|
|
21132647bc
|
跨端: 12 处「深色下字看不见」的真 bug + 判据基建补上「看像素」这一层
## 一、判据基建:本目录终于能**看像素**了
此前只能靠 `dumpLayout` —— 那是**结构化描述**,报的是"组件声明了什么",
不是"屏幕上画成什么样"。两者会分叉,而观感类结论只能在像素上得出来。
新增 `lib/harmony-device.mjs`:`screenshot()` / `pixelAt()` /
`hexToRgb()` / `closeColor()`(用 ffmpeg 转 1×1 原始 RGB,不引依赖)。
**它当场证明了它的价值**:`cross-client-theme` 新增的设备判据
用真实像素抓到下面这个 bug —— 静态判据全绿时它藏得好好的。
## 二、真 bug:**12 处**把 `Theme.surface` 当前景色用
`Theme.surface` 是 `sys.color.ohos_id_color_list_card_bg` ——
一个**跟随系统主题翻转**的 Resource:浅色近白、**深色近黑**。
- 浅色下当白字用**碰巧对**(白字压蓝底)
- **深色下字变成黑的**,压在品牌蓝 / danger 红 / warn 琥珀上**几乎看不见**
设备现场:写邮件悬浮球是品牌蓝 `#2563EB`,截图里那个铅笔图标**几乎是隐形的**;
读圆心像素得到 `rgb(32,34,36)`。往左偏 50px 读到底色才见 `rgb(36,99,235)`。
`Theme.accentFg`(`#FFFFFF`)的注释原话就是「品牌底上的文字」—— 为这个场景存在,
却**一处都没用**。
修:12 处 `fontColor/iconColor(Theme.surface)` → `Theme.accentFg`
(`MainPage` 10 + `InboxPage` 1 + `SessionsPage` 1)。改完全仓 0 处残留。
另在 `Theme.ets` 给 `surface` / `accentFg` 都补上"能当什么、不能当什么"的注释。
## 三、判据(两条,都做了变异验证)
1. **设备条**(`cross-client-theme`):读悬浮球像素 ——
① 品牌色**真的画成** `#2563EB`(声明 ≠ 渲染);
② 球上图标与底色 **WCAG 对比度 ≥3:1**(压在上面的东西得看得见)。
把 `.accentFg` 改回 `.surface` ⇒ **判红**;还原 ⇒ 绿。
2. **静态防线**(同文件):全局 grep「`fontColor/iconColor(Theme.surface)`」一处不许有。
设备条只能看一处,而这个错法有 12 处 —— 静态防线管住整类。
## 四、判据自身踩的三个坑(都写进注释了)
- **采样点撞上图标**:第一版取球心,读到 `rgb(32,34,36)`,差点当成"品牌色没渲染"。
截图一看球是蓝的,深色那点是**铅笔图标**。⇒ 往中心左偏 30% 球宽。
- **假设错了 FAB 的位置**:按"屏幕右下角"找(`x1 > 屏宽*0.6`),
实测 `[942,1997]`(`x1=942` vs 阈值 1910)⇒ 永远找不到、**静默跳过**。
原因是列表窗格是**左栏**,球在"左栏的右下角"。⇒ 形状只用站得住的那部分(下半部)。
- **设备判据要自己搭现场**:不加自导航时它**永远跳过**(前面的判据把前台留在管理页),
而那看起来像"功能没了"。加自导航后立刻开始工作并抓到 bug。
## 五、欠账
- `harmony-maildetail-missing-three` → **count 0(结算)**:三块都做完了
(转发 `b7c5d8b` / 改名建议 `c2f35d1`+`e79a86a` / 往返预算 `ac62daf`)。
如实记着**未验**的那点:预算条的**点击**没在设备上走通
(模拟器顶部 155px 是系统手势区,折叠头部恰在其中)。
- `static-criteria` 5:`cross-client-theme` **升级了一半**,仍留在名单里 ——
`.ets` 那半只有悬浮球这一处上了设备,其余令牌仍是静态对齐。
- `debt-visibility` 登记 `cross-client-theme` 1 处边界声明(带出处)。
`run-all.mjs` → `checks=513 pass=513 fail=0 skip=0 red=0 broken=0 unreported=0`;
Go 侧 `./internal/repo/...` 通过。
|
2026-09-19 19:29:49 +08:00 |
|
|
|
ac62dafde1
|
跨端: 往返预算编辑上线(详情页缺的第三块)+ 修折叠头部点不中的真 bug
审计发现详情页缺三块功能之三:**往返预算**(WebUI `BudgetEditor`)。
`MailApi.setBudget` 早就有了,缺的是 `getBudget` 与 UI。
## 一、预算条(对齐 WebUI,两态)
显示态是徽标(`已用/上限 来回` / 不限时「预算不限」/ 用尽时标红),
点它进入编辑态:输入框 + 保存 + 重置 + 取消。
★ 「用尽」的判定**走服务端的 `remaining`**,不自己拿 `used >= max` 算:
不限时服务端给 `remaining: -1`,那个式子在那种情形下会算出"已用尽"。
两端都得用服务端的口径。
★ 「保存」与「重置计数」走**同一条 PUT**(服务端支持两者同时给,
它的注释原话:「加到 20 并从头算」是一次很自然的操作,
拆成两个请求只会让前端多一次往返)。
**契约实测**(不是形态检查):
GET → {"max_rounds":0,"used_rounds":1,"remaining":-1,"unlimited":true}
PUT {"max_rounds":7,"reset":false} → {"max_rounds":7,"remaining":6,"unlimited":false}
PUT {"max_rounds":0,"reset":false} → {"remaining":-1,"unlimited":true} ← 0 = 不限
## 二、真 bug:折叠头部的展开区只有 **14px** 高
设备实测 dump:`Row [1277,112][3122,126]` —— 可点区高度 = 文字高度(14px 字号)。
而全屏之后**状态栏也在 y=112 那个区间** ⇒ 点它反复触发**系统手势(下拉通知)**
而不是展开头部。我为此试了七八次,每次都回到桌面。
而收起态头部里藏着**这个会话的全部旋钮**(收件人/时间/抄送/权限档/预算)——
点不中就等于那些都看不到。
修:给那一行 `height(36)`(与左边返回键同高)。实测可点区
**14px → 43px**(`[1277,112][3122,155]`)。
★ 判据 + **变异验证**:去掉 `.height(36)` ⇒ 判红;还原 ⇒ 绿。
## 三、判据
`harmony-logic` 新增「折叠头部展开区高度 ≥32vp」一条(含变异自检)。
`run-all.mjs` → `checks=511 pass=511 fail=0 skip=0 red=0 broken=0 unreported=0`。
**未验**:预算条在设备上的**观感与点击**(模拟器顶部 155px 是系统手势区,
而头部恰在其中 —— 坐标式点击在那里会被系统抢走;真机上头部在状态栏之下)。
预算条的**数据链路**已用 curl 逐条实测(见上),但"点保存按钮后徽标变化"
这一步没在设备上走通。
|
2026-09-19 17:41:09 +08:00 |
|
|
|
e79a86ab79
|
跨端: 改名建议条接到详情页(端到端实测:点「接受」别名真的改了)
上一批做完了接口层,这批接 UI —— 详情页现在真的显示建议条并能操作。
## 做了什么
提示条放在正文之前(与 WebUI 同位置):Agent 给的理由 + **后果说明**
(「改名后需用 name@path.xxx 寻址;旧别名立即失效……」)+ 「接受」/「忽略」。
蓝底(`accentSoft`)而不是警告色:它不是故障,是一个**待决定**;
深色下走 `accentSoftFor`(就是前一批修的那个主题感知浅底)。
`doAcceptRename` 走 `PUT /sessions/{id}/alias`(与"人手改别名"同一端点),
`doDismissRename` 走专用 dismiss 端点 —— 服务端记下被驳回的别名,
否则每次打开会话都要重新点一次「忽略」。
## 途中三处**我自己的**错误(都值得记)
1. **写错了请求体字段名**:我按记忆写成 `session_alias`,而服务端
`updateAliasRequest` 的 tag 是 **`alias`**(`sessions.go:95-97`),
WebUI 也是 `{ alias }`(`client.ts:537`)。而它的 `Decode()` 是
`DisallowUnknownFields()` ⇒ 传错名字**直接 400**(不是"被忽略")。
这是今天第三次同源错误(`AppearancePayload` / `ForwardMailRequest` / 这次)——
**请求体字段名必须去服务端 struct tag 里读,不能凭记忆写**。
2. **判据跟着实现一起错**:我把判据也写成"断言 `session_alias: alias`" ——
等于把这处错误**锁进两处**。判据已改成断言 `this.alias = alias`,
并加了一条反向断言"不许出现 `session_alias: alias`"。
★ 这是"判据引自己的实现当依据"的又一次实证。
3. **UI 写完忘了接数据**:提示条的 UI 做完、编译过,但**没有调用
`getRenameProposal`** ⇒ 页面上永远不显示(服务端明明有建议)。
—— 正是本仓反复出现的"写好了但没人调用":静态判据查得出"代码里有这个方法",
查不出"页面从没调过它"。
⇒ 补上 `loadRenameProposal()`,并**单独一个 try**(建议条是附加信息,
它失败不该把正文也变成错误态)。
## 端到端验证(真数据)
造一封带标记的真邮件投进一个真会话 → 服务端识别(`rename_proposed: "rename-verify"`)
→ 详情页出现建议条(dump 读到理由 + 后果说明 + 两个按钮)
→ 点「接受」→ 库里别名真的变了:
处理四桥冒烟测试邮件 → rename-verify
## 判据
`run-all.mjs` → `checks=510 pass=510 fail=0 skip=0 red=0 broken=0 unreported=0`。
`hvigorw assembleHap` 成功;前端重建 + 重打包。
**未验**:「忽略」按钮的端到端(服务端记下驳回、不再重复弹)——
接口层已有判据钉住路径,但没在设备上真点过。
|
2026-09-19 17:23:23 +08:00 |
|
|
|
c2f35d1023
|
跨端: 会话改名建议的接口层(详情页缺的第二块,服务端三个端点齐)
审计发现详情页缺三块功能之二:**Agent 的改名建议条**(WebUI `MailView.tsx:324`
的 `RenameProposalBar`)。这一批做**接口层**,UI 接线下一批。
## 为什么这件事不只是"少个提示条"
WebUI 那段的注释写得很清楚:
> Agent 干到一半自己改掉,人上一秒记住的地址下一秒就失效。
> 提议 + 人确认,既让 Agent 表达意图,又保证寻址稳定性由人掌握。
也就是**寻址稳定性**的设计 —— 别名是人的寻址入口,Agent 只能**提议**。
## 做了什么
1. `model/SessionRename.ets` —— `RenameProposal`(`alias` / `reason`,
字段名与服务端 json tag **逐字对齐**)
2. `api/SessionApi.ets` —— 三个动作:
· `getRenameProposal(sessionId)` → `{"proposal": {...}}` 或 `{"proposal": null}`
· `acceptRename(id, alias)` → **`PUT /sessions/{id}/alias`**
· `dismissRename(id)` → `POST /sessions/{id}/rename-proposal/dismiss`
3. 判据(`harmony-logic`,+1 条):字段名、响应外壳容忍 `null`、
接受走别名端点、驳回走专用端点、方法名与 WebUI store 同名、+ 变异自检。
★ **接受为什么不另开端点**(服务端注释原话):
「那条路径已经有唯一性校验与 409 处理,复制一遍只会多一个出错的地方」。
所以"接受建议"与"人手改别名"是**同一条路**,而"驳回"是另一条
(它不是改别名,是"别再问了"——服务端记下被驳回的别名,
否则每次打开会话都要重新点一次「忽略」)。
## 端到端验证(真数据,不是形态检查)
造了一封带标记的真邮件投进一个真会话:
POST /mail/send {reply_to: …,
body: "内容正文。<!-- agentmail:rename-session alias=\"rename-verify\" reason=\"验证改名建议链路\" -->"}
→ 200 {"rename_proposed":"rename-verify", …}
然后:
GET /sessions/{id}/rename-proposal
→ {"proposal":{"alias":"rename-verify","reason":"验证改名建议链路"}} ✓ 形状与我的一致
SELECT body FROM mails …
→ "内容正文。" ✓ 标记被剥掉
两件事都验到了:**服务端识别建议**,且**标记从人读的正文里剥离**
(HTML 注释在 Markdown 渲染器里会变成可见文本,所以必须剥,不能指望渲染器吞掉)。
## 判据
`run-all.mjs` → `checks=510 pass=510 fail=0 skip=0 red=0 broken=0 unreported=0`。
`harmony-logic` 30 → 31。`hvigorw assembleHap` 成功;前端重建 + 重打包。
**未做**:UI 接线(详情页的提示条)。接口层已完成并验证,
但"页面上真的显示建议条并能点接受/驳回"要下一批。
|
2026-09-19 17:12:40 +08:00 |
|
|
|
b7c5d8b1e7
|
跨端: 邮件转发上线(详情页缺的入口)+ 修两个真 bug(动作球重叠 / 键盘挡住按钮)
审计发现详情页缺三块功能之一 —— **转发**。服务端 `forward.go` 完整、
`MailApi.forward()` 也早就写好了,缺的只是这一页的入口。
## 一、转发(对齐 WebUI `ForwardBar`)
字段与占位文案逐条对齐:收件人 / 抄送(**可折叠**,默认收起)/ 说明 / 引用原文。
`subject` 留空让服务端自动加 `Fwd: ` 前缀(它处理了 "Fwd: Fwd:" 无限叠加)。
★ 请求体用了**专用类型** `ForwardMailRequest`,不复用 `SendMailRequest`:
服务端 `forwardRequest` 只认五个字段,而它的 `Decode()` 是 `DisallowUnknownFields()`
⇒ 多带一个(`body` / `reply_to` / `attachment_ids`…)就 **400**。
—— 这正是我今天在 `AppearancePayload` 上刚犯过的那个错,**端点一个类型一个请求体**。
★ 顺带发现:`MailApi.forward` 的签名**原本就写错了**(参数类型是 `SendMailRequest`)
—— 一直没被发现,因为**从来没有调用方**。"写好了但没人用"的代码,
连它自己的类型对不对都没人验过。
## 二、真 bug ①:两个动作球**几乎完全重叠**
转发球与回复球**各自**写在 `Stack({alignContent: BottomEnd})` 里、各带一个 margin。
实测 dump 的 bounds(密度 2.875):
转发 [2984,2010][3122,2148]
回复 [2949,1998][3110,2159] ← 重叠区 x ∈ [2984,3110]
屏幕上只看得到一个球,**转发入口等于不存在**。
根因:`Stack.alignContent` 把**每个**子元素都摆到同一个角,margin 只是各自微调。
修法:用 `Row({ space: 12 })` 包住两个球、由 Row 带 margin 到角落。
**设备实测**(修后 dump):`[2776,2021][2914,2159]` 与 `[2949,1998][3110,2159]`,不重叠。
## 三、真 bug ②:键盘一弹,「转发」按钮就被顶出屏幕
转发弹层第一版**没有高度**(只有 `padding(16)`)⇒ 尺寸由内容决定。
而 ArkUI 默认的键盘避让是 `KeyboardAvoidMode.OFFSET`(整体上移)——
上移之后 `TextArea` 与「取消 / 转发」按钮**跑到键盘下面**,点不到。
实测截图:只看得见收件人输入框 + 键盘。
修法两半(缺一不可):
① 弹层给明确高度 `height('60%')`(与回复弹层一致,它一直没出问题);
② 说明框改 `layoutWeight(1)`(不是固定 `height(70)`)—— 键盘顶上来时它自己缩短,
把按钮留在屏内。
**设备实测**(键盘弹出时 dump):`取消 [1196,2054][1426,2158]`、
`转发 [2880,2054][3110,2158]` 都在屏内(屏高 2232),且 `clickable=true`。
## 四、判据(这两条固化了上面两个形状)
`harmony-admin` 新增两条**静态形状**判据(它们抓的是写法,不需要设备):
1. **同一 `Stack` 里的多个圆形按钮必须被 `Row` 包住**(否则重叠);
2. **底部弹层必须有明确高度** + 会撑高的子元素用 `layoutWeight`
(否则键盘一弹按钮就被顶出屏幕)。
★ 为什么用静态判据而不是设备判据:这两个 bug 的**形状**在源码里就看得见
(`Stack` + 各自 margin / 弹层缺 `.height`),而设备判据要摆出"键盘弹出"这个态,
成本高且不稳。静态判据在这里是**更快更准**的那一层。
(设备判据仍保留在别处,验"真的能打开、真的渲染出来"。)
## 五、验证
`run-all.mjs` → `files=32 ran=32 checks=509 pass=509 fail=0 skip=0
red=0 broken=0 unreported=0`(`harmony-admin` 28 → 30)。`hvigorw assembleHap` 成功。
**设备实测**:转发球与回复球分开显示(各自图标可见);
点转发球 → 转发框打开(「转发「…」」+「抄送」折叠开关 + 取消/转发);
键盘弹出后按钮仍在屏内可点。
**未验**:真发一封转发(收件人输入在自动化里不稳 ——
`uitest inputText` 是**追加**而非替换,且 `keyEvent Back` 会退出页面而不是收键盘。
这条留待真人操作窗口,与 `harmony-p4c-boundary-decls` 那笔同性质)。
|
2026-09-19 17:00:44 +08:00 |
|
|
|
1bf687f506
|
判据: 图片上传链的设备判据(用真实素材)+ 静态欠账从 6 减到 5
继续升级到期的静态判据。这一批做 `harmony-imageprep`(上传链),
并顺手把已完成的 `harmony-admin` 移出欠账名单。
## 一、图片上传链:用**真实素材**验压缩决策的输入
`run-all.mjs` 的 `STATIC_ONLY` 里那条登记写着
「上传链的设备侧:`@ohos.multimedia.image` + 相册要设备才能真跑」。
上面 30 条判的都是 `model/ImagePrep.ts` 的纯逻辑(阈值、单调性、边界…),
它们全绿时有一件事从未验过:**那些数字与设备上真实的图片对得上吗**。
新判据用**用户真上传过的那张壁纸**(`GET /me/appearance/image` 取回,
1402×1122 / 152570 字节 —— 不是合成图),断三个跨端事实:
① 设备上读到的像素尺寸 = 决策时用的尺寸;
② 真实体积在客户端上限之内;③ 该尺寸走 `planCompress` 首档**不缩小**
(长边 1402 < 2560)+ `judgePick` 放行。
★ **为什么不合成图**:纯色能压到几 KB、噪声几乎压不动,用它们验阈值
会得到"怎么都对"的假绿。真实照片的行为才是要验的那个。
★ **诚实标注了没做的那一半**(写在判据注释与 `DEBTS.json` 里):
本判据用的是**设备上的 `file`/`ls`** 这一独立来源读素材属性,
**没有**跑 `image.createImagePacker()`。跑它需要一个**用户选图**入口
(`DocumentViewPicker`,要人操作系统选择器),自动化里没有稳定路径;
而编一个"绕过选择器直接调 `packJpeg`"的测试专用入口,会是**只有测试在用的代码**
——那种代码不会被真实场景触到,验它等于验一个不存在的东西。
## 二、静态欠账 6 → 5(还完就划掉)
`harmony-admin` 那条**已升级为设备判据**(上一批做的),
所以它**不该再留在 `STATIC_ONLY` 里** —— 那个名单是给"还欠着的"记账的。
留着会让余额虚高,而这正是这个机制要防的(欠账不显形就等于没有)。
## 三、途中被两条"登记一致性"判据拦了两次(都按它们给的方向修)
1. `debt-visibility`:我在 `harmony-imageprep` 里新增了两处边界声明
("未覆盖/未验"这类词),而余额里没登记 ⇒ 红。
**按它要求的顺序做**:先补 `docs/DEBTS.json`(`harmony-p4c-boundary-decls`
那一笔的 note 里写明"只做了一半"),再把登记次数 4 → 6。
2. `commit-hygiene`:`DEBTS.json` 说 `static-criteria=6`,实测 5 ⇒ 红。
—— 这条正是"可见的那个数字是副本,漂移了必须两边一起改"。
改数字之外还在那一笔里写了**为什么减**(admin 升级并移出名单)。
★ 第三处被拦很有意思:`debt-visibility` 是**按词表数自己**的判据,
我第一次修时在那个文件里写了一句话里含"仍未覆盖",于是它把自己数多了 1 处
(12 → 13)。那一刻是**判据在正确地工作**("多一处即红")——
我写的其实是**引用**另一笔账,不是新的边界声明,所以改成了不带判定词的措辞。
## 四、判据
`run-all.mjs` → `files=32 ran=32 checks=507 pass=507 fail=0 skip=0
red=0 broken=0 unreported=0`。`harmony-imageprep` 30 → 31;
`harmony-admin` 移出静态名单(仍在套件里,28 条)。
`hvigorw assembleHap` 成功;前端重建。
**剩余到期未升级**:`harmony-appearance`(已有 2 条设备判据)、
`harmony-logic`、`cross-client-theme`、`appearance-defaults` —— 逐条来。
|
2026-09-19 16:18:10 +08:00 |
|
|
|
fc295893cb
|
跨端: 深色模式下的品牌浅底不跟随 —— 12 处「选中/未读」底色在深色页上刺眼
## 真 bug(设备实测)
深色主题下,「我的」页**选中**的那张账号卡片仍是接近纯白的浅蓝
(实测像素 `(255,255,255)` 级别的浅底压在 `(32,34,36)` 的深色页上)。
根因:`Theme.accentSoft = '#EFF6FF'` 是**写死的浅色**,而它被当作
「选中态背景」用在 12 处(未读邮件行、选中账号、分段选中、登录页模式切换…)。
**为什么只有 WebUI 没这个问题**:它有 CSS 变量的**反转发**机制 ——
`index.css:113` 的 `--c-blue-50: 239 246 255` 在 `.dark` 段(`:475`)被换成
`28 37 54`(深蓝黑)。ArkTS 的 `static readonly` **一个常量一个值**,
没有那层机制 ⇒ 静态常量必须自己提供两个取值。
## 修法
1. `Theme.accentSoftDark = '#1C2536'`(对齐 WebUI `.dark --c-blue-50` 的 `28 37 54`)
2. `Theme.accentSoftFor(dark)` 作为**唯一入口** —— 不在页面里各自
`isDark ? a : b`:那样每处都会各写一遍,迟早漏一处
(WebUI 那条"由 test/theme.test.mjs 逐档断言"就是为防这个)
3. **深浅色从哪来**:只有 `MainPage` 算得出(它读 `resourceManager` 的
`colorMode`)。所以走 `AppStorage` 单向发布(与徽标、windowInsets 同一套):
MainPage 算 → 写 `agentmail.appearance.isDark` → 各窗格 `@StorageProp` 读。
6 个文件、12 处,全部改用 `accentSoftFor(this.isDarkNow)`。
**设备实测**:深色下「我的」页账号卡片与收件箱未读行都变成深蓝底,
像素 `(32,34,36)` 与页面底一致(不再刺眼)。
## 判据(这条是新加的,形状值得记)
`cross-client-theme` 新增:**品牌浅底必须有深色变体**。断三件事:
1. 深色变体存在,且**取值从 WebUI 的 `.dark` 段反推**(不是随手挑一个深色);
2. 有按主题选值的**入口**(防"页面各自写三元");
3. **用到它的地方真的走那个入口** —— 扫描所有 `backgroundColor(… accentSoft …)`
并排除 `accentSoftFor`,把漏改的位置**逐行报出来**。
第 3 条在我改到一半时**当场列出了剩下 9 处**(`CalendarPage:1172`、
`ComposePage:255`、`LoginPage:327`…)—— 这就是它该有的样子:
不是"断言存在某个常量",而是"断言没有一处漏改"。
★ 写这条判据时踩了自己一次:JS 模板串里嵌了反引号包围的标识符
(`` `accentSoft` ``),直接 SyntaxError。改用字符串拼接。
## 判据
`run-all.mjs` → `checks=506 pass=506 fail=0 skip=0 red=0 broken=0 unreported=0`。
`hvigorw assembleHap` 成功;前端重建。
**未验**:日历/登录页在深色下的观感(只逐处改了底色,没逐页截图)。
|
2026-09-19 16:02:26 +08:00 |
|
|
|
a18014e1e3
|
判据: 管理页设备判据(到期静态判据的第一条升级)+ 修设备判据基建的三个真 bug
`run-all.mjs` 的 `STATIC_ONLY` 里登记着 6 条"只能静态验"的判据,前提是
「本工作区能装、能点设备」。设备现在可用 ⇒ 它们**到期**了(欠账当场变红)。
这一批升级第一条:`harmony-admin`(用户管理页)。
## 一、新设备判据:管理页真的能打开、列表真的渲染
上面 27 条静态判据判的都是逻辑与接线形态。它们全绿时,
"管理页能不能打开、列表能不能渲染"**一句都没验过** —— 而它最容易坏:
路由注册对了但入口没接上、接口回来了但列表没渲染。
判据自己搭现场:拉起应用 → 进「我的」→ 滚到底 → 点「管理」→
断言真的在管理页(出现「新建用户」)且**渲染出用户行**。
## 二、途中撞出三个**判据自身**的 bug(都已修,都写了教训)
1. **`tapText` 从来没成功点过任何东西**(`lib/harmony-device.mjs`)
`findByText` 返回的是**数组**,我当单个节点用了 ⇒ `node.attributes` 恒
`undefined` ⇒ 恒返回 `false`。症状极隐蔽:调用方以为"没找到那个文案",
实际是帮手自己坏了。修:从数组里挑,且**优先挑可点的那个**
(同名文案常常一个可点、一个不可点)。
2. **判据之间互相干扰**(新增 `backToMain`)
每个设备判据都是写操作,会把前台留在它操作完的那一页。`harmony-admin`
把人留在管理页 —— 那是 `pushUrl` 出去的独立 `@Entry` 页,**没有侧栏**
⇒ 后面的 `cross-client-gesture` / `harmony-appearance` 找不到侧栏、
双双 skip(skip 原因写的是"宽屏侧栏找不到",看起来像功能没了)。
`launchOurApp` 解决不了(`aa start` 只切前台,不弹栈)⇒ 新增 `backToMain`。
3. **"找不到元素"要先分清是功能缺失还是判据没摆好现场**
本判据连栽四种形态,每一种都伪装成"功能缺失":
· 锚点文案错(入口是「管理」,我按「用户管理」找 —— 后者只是说明的一部分)
· 元素在滚动下方(dumpLayout 只报可见节点)
· 文字节点不可点(`.onClick` 在包住它的容器上,ArkUI 的常态)
· **滚动步长跨过了它**(诊断打印现场才发现:停在了「系统通知」那一带,
而「管理」只占 ~0.04 屏,一次 0.2 屏的滑动必然越过)
⇒ 最后改成"**先滚到底、再小步回扫**"(只管往前找在不均匀列表上必漏),
并加了 `AGENTMAIL_ADMIN_DEBUG=1` 的诊断入口把现场打出来。
## 三、判据
`run-all.mjs` → `files=32 ran=32 checks=505 pass=505 fail=0 skip=0
red=0 broken=0 unreported=0`(连跑两次稳定)。
`harmony-admin` 27 → 28 条。`hvigorw assembleHap` 成功;前端重建。
**设备实测**:管理页从「我的」页打开,显示「管理 / 用户管理 3 / 新建用户」
与三行用户(jianf 管理员 / gui-lab 用户 / test 用户)。
**仍到期未升级**:`harmony-appearance`(已加 2 条设备判据,但登记里其余部分仍静态)、
`harmony-logic`、`cross-client-theme`、`appearance-defaults`、`harmony-imageprep`
—— 按"每条缺什么设备侧验证"逐条来,不为了消数字而凑。
|
2026-09-19 15:53:34 +08:00 |
|
|
|
ff172ed2c1
|
判据: 修 AmIcon 尺寸检查的假红 —— 它跨过 } 吃到了下一个组件的修饰符
这条判据抓的是真形状(`AmIcon({…}).width(48)` 会让图标贴左上角),
但正则写宽了:`[\s\S]{0,200}?` 允许**跨过 `}`**,于是这种**正确**写法被判红:
Row({ space: 4 }) {
AmIcon({ … })
Text('新建')
}
.height(26) ← 这是 Row 的,不是 AmIcon 的
实测(日历工具栏新加的「+ 新建」按钮):报 `CalendarPage.ets:1223 → .height(`,
而那一行链在 `Row` 上。**是判据误报,不是代码错。**
修法:扫描区间**不允许出现 `{`/`}`**。`AmIcon({…}).width(48)` 里参数花括号已被
`)` 收尾,而跨到下一个组件的路径会被排除。
★ 修完**立刻做了变异验证**(这类放宽最容易顺手把判据改废):
把真实的 `AmIcon({…})` 改成 `AmIcon({…}).width(48)` ⇒ **判红**;
还原 ⇒ 绿。既要"不误报",也要"仍能抓真错",两半都得验。
|
2026-09-19 15:22:34 +08:00 |
|
|
|
81621974b9
|
跨端: 日历「新建」按钮补文字(原先只有一个加号)+ 详情页三处缺失登记
## 一、日历的「新建」按钮(对齐 WebUI 的主动作)
WebUI `CalendarView.tsx:362-369` 那个按钮是工具栏里**唯一的实心按钮**:
`px-2.5 py-1 bg-blue-600 text-white` + `<PlusIcon/>` + **「新建」两个字**。
鸿蒙原先只有一个光秃秃的 `Button('+')` —— 丢掉的不只是文字,是**主动作这层语义**:
用户得猜"这个加号加什么"(加日程?加订阅?加日历?)。
改成蓝底 + 图标 + 「新建」,与 WebUI 同形。**设备实测截图确认**。
★ 与这条对照的是它左边那两条「导入/导出」:鸿蒙**有意**用文字而非图标,
理由已写在代码注释里(WebUI 有 `title` 可悬停,手指没有悬停)。
同一个道理在主动作上更成立 —— 主动作不该是个谜语。
## 二、邮件详情页缺三块功能(审计发现,服务端都已支持)
对照 `MailView.tsx` 逐段核对,鸿蒙 `MailDetailPage.ets` 缺:
| 功能 | WebUI | 服务端 |
|---|---|---|
| `RenameProposalBar` | `MailView.tsx:324` | ✅ `rename_proposal.go` 已在解析 `propose_alias` |
| `ForwardBar`(转发) | `MailView.tsx:378` | ✅ `forward.go`(含 `forwardSubject` 的 Fwd: 叠加处理) |
| `BudgetEditor`(预算) | `MailView.tsx:235` | ✅ `max_rounds` 字段 |
改名建议那条尤其重要(WebUI 注释:「Agent 干到一半自己改掉,人上一秒记住的
地址下一秒就失效」)—— 是否决制而不是 Agent 单方面改,这是**寻址稳定性**的设计。
三条都**没有**在这批里硬做:各含交互 + 接口 + 状态,不是顺手能补的量级。
登记进 `docs/DEBTS.json`(`harmony-maildetail-missing-three`)——
硬塞的结果是每条都半成品,那比缺着更糟(缺着是可见的,半成品是不可见的)。
## 三、审计中确认**已对齐**的部分(不是漏做)
- 通信:工具条三页签分组与顺序、列表行的字段集(发件人/主题/摘要/时间/未读点/
账号徽标/档位徽标)、会话折叠与组头(别名/主题/未读/封数)、空态文案、
发件箱的三处差异(数据源/`to_name`/不筛权限)、归档确认框。
- 日历:工具条分组与顺序、宽屏两栏(左网格 + 右 400vp 常驻)、右栏三态、
月/周/日三档网格与农历小字、非本月淡出、今天高亮、事件圆点、
点某天的行为(宽屏换右栏 / 窄屏滑入)、导入导出的三条结果分支。
- 联系人:卡片/列表两视图、字段集、归档确认框(含破坏性操作的二次确认)。
- 「我的」:八个分段的字段与顺序、壁纸选择器、主题三态、多账号入口。
- 管理页:列与操作、管理员门禁。
- 外壳:侧栏三项 + 底簇、底栏四项、徽标三档色调与位置、玻璃材质、让位。
## 判据
`run-all.mjs` → `checks=504 pass=503 fail=1`(唯一那条是 build-stamp 的
产物过期,重建后 7/7)。`hvigorw assembleHap` 成功。
`docs/DEBTS.json` 新增三条登记(断点差异 / 列表附件数 / 详情页三功能)。
|
2026-09-19 15:19:57 +08:00 |
|
|
|
f655453424
|
跨端: 审计补强 —— 邮件行的「抄送 N」+ 断点差异登记 + 两处"看起来有其实没有"的澄清
继续「全面对齐 WebUI 和鸿蒙」。这一轮做的是**逐页对照审计**(子代理通道被
session daemon 的端口占用堵死,改为自己逐处读源码对照)。
## 一、补上邮件行的「抄送 N」(真缺失)
WebUI `MailList.tsx:350` 行上有「抄送 N」,鸿蒙**完全没有**。而数据一直在:
`cc_list` 服务端确实返回(实测回包字段列表里有),只是鸿蒙的 `MailLike`
接口漏了这个字段 ⇒ 一封抄送给多个人的邮件在列表里看不出任何区别。
修法:接口加 `cc_count`,`MailSummary` 加 `cc_list` + `cc_count`,
收件箱/发件箱两处填充派生值,行上按 WebUI 的位置渲染。
**设备实测**(造了一封带 2 个抄送的真邮件):行上出现「抄送 2」,
位置与 WebUI 一致(主题行下方、灰字)。
★ 接口用**数字**而不是 getter:`MailSummary implements MailLike`,
而 ArkTS 的 interface 里不能声明 getter(编译报 "incorrectly implements interface")。
## 二、有意**不抄**附件标记(发现 WebUI 那段是死代码)
WebUI 行上还有一个 📎 + 数量的标记(`MailList.tsx:351-355`)。但核对服务端
**实测回包**:`GET /me/mail/inbox` 既没有 `attachments` 也没有 `has_attachments`
⇒ `mail.attachments?.length ?? 0` **恒为 0**,那个标记在 WebUI 上**从不出现**。
所以鸿蒙这一轮**有意不抄它** —— 照抄一个不工作的东西,只会多一处
"看起来有、永远不亮"的代码。要做这个功能得先让服务端在列表回包带上附件计数
(一次 JOIN 的事),那是独立的一件事,已登记进 `docs/DEBTS.json`
(`mail-list-attachment-count`)。
★ 这一条与鸿蒙 `MailSummary.has_attachments` 那个字段一起处理掉了:
它还留在那里会误导人(服务端永不返回它 ⇒ 恒 false)。
## 三、两端宽屏断点不同 —— 登记 + 判据(此前**无任何记录**)
审计点名要核实的这条确认成立:
WebUI: `NARROW_QUERY = '(max-width: 1023px)'`
含义 = 「三栏(60 导航 + 320 列表 + ≥520 详情 ≈ 900px,再加余量)放不下就退化单栏」
鸿蒙: `isWide = width >= 768`
含义 = 「要不要显示**侧栏**」(鸿蒙内容区是一个窗格,没有并排三栏)
**含义不同,所以数值不同本身不算错** —— 这与手势阈值同一条口径
(语义各自成立时,数值不必强求一致)。但**用户可见的后果**是:768–1023 宽
(常见竖屏平板、窄窗口)下 WebUI 是单栏+底栏、鸿蒙是侧栏+内容,
同一宽度在两端长得不一样。
处理:① 登记进 `docs/DEBTS.json`(`wide-breakpoint-divergence`,
带三个待定选项);② 加判据 —— 它**不**要求两端取值相同,而是要求
「取值可读 + 含义写清 + 差异被登记」三件事同时成立。
★ 写这条判据时又踩了同一坑:用 `code()` 读注释 ⇒ 永远红。
`criteria-hygiene` 判据的头顶就写着"判代码用 code、判理由用 prose"。
## 四、判据
`run-all.mjs` → `checks=505 pass=505 fail=0 skip=0 red=0 broken=0 unreported=0`
(cross-client-theme 15→16)。`hvigorw assembleHap` 成功。
**未验**:附件标记(有意不做);抄送行在**深色**下的对比度未单独验。
|
2026-09-19 15:13:51 +08:00 |
|
|
|
f811c9887a
|
跨端: 三个真 bug(外观保存 400 / 改档位界面不动 / 内容列溢出屏幕)+ 设备判据
这一轮从「全面对齐 WebUI 和鸿蒙」开始,先做设备层判据升级,结果**判据一上线就连撞三个真 bug**
—— 它们全都是静态判据照不到的形状:**数据对、界面不动**。
## 一、外观保存从来就没成功过(PUT 400)
`payloadFromLocal` 复用了 `AppearanceResponse` 当请求体,而那个类型是 **GET 的响应**:
带着 `has_image` / `image_bytes` / `saved`。服务端的 `Decode()` 是
`DisallowUnknownFields()`(严格,**有意为之**)⇒ **每一次保存都被拒收(400)**。
症状极隐蔽:本地 `@State` 立刻变 ⇒ 肉眼看着像成功了;只有看 hilog 的 HTTP 状态码
才发现 400。修法是加 `AppearancePayload`(**恰好**服务端 `models.Appearance` 的五个字段)。
★ 这是"两端共用同一个类型"的代价:请求与响应本来就不该同形。
★ 服务端严格是**对的** —— 它帮我们抓到了这个错误。修客户端,不是放宽服务端。
## 二、改了档位,页面背景一点不变(两层原因)
**第一层**:`SettingsPage` 存进 store 了,但 `MainPage` 的 `bgPlan` 只在启动时算一次,
之后没人动 ⇒ 发布一个 `AppStorage` revision(计数器,不是布尔 —— 布尔 true→true
不发变化通知),`MainPage` 用 `@StorageProp + @Watch` 接住。
**第二层(更隐蔽)**:接上之后**还是不动**。因为 `bgPlan` 是 `@State BackgroundPlan`,
而 **ArkTS 的 `@State` 观察不到类内部字段**的变化 —— 渲染读的正是
`this.bgPlan.kind` / `.layers`。hilog 一对证据同一次启动相差 100ms:
Appearance: sync: bgKind=preset … hasImg=true ← 数据是对的
Wallpaper: kind=none layers=0 active=false ← 渲染读到的还是旧值
修法:加 `@State bgContentRev: number`,每次算完 plan 就 +1,**并在 Builder 的
条件表达式里消费它**(ArkUI 按"这个 Builder 读了哪些 @State"决定是否重渲染;
只加计数器而渲染不读,等于没加 —— 判据同时断这两半)。
★ 同一个坑本仓出现过(`AppearanceStore` 的注释里写着这句),这次换了地方发作。
## 三、「我的」页右端内容被顶出屏幕(追了很久的 `56.000000` 之谜)
真相有**两层,两层都值得记**:
1. `dumpLayout` 里 `Slider` 节点的 `text='56.000000'` 是**无障碍文本**
—— 屏幕上根本没这串字(截图可证)。**dump 的 text ≠ 看得见的字**。
2. 真正的问题是那个**看得见的** `Text('56%')` 落在 `x=3250`,而屏宽 3184
⇒ **它在屏幕外**。用户只看得到滑杆、看不到数值。
根因:`MainPage` 里"侧栏 + 内容列"是 `Row` 并排,内容列写 `.width('100%')`
—— 在 Row 里 `100%` 是**父容器全宽**,与侧栏的 60vp **相加** ⇒ 必然溢出。
实测内容列 `[229,28][3357,2204]`,右边缘超出屏幕整整 173px(= 60vp)。
修法:改 `.layoutWeight(1)`(吃剩余空间)。修完实测 `[229,28][3156,2204]`,
`Text('56%')` 落在 `[3049,1803]` —— 屏内。
★ 为什么值得一条设备判据:**同一处错误在不同 pane 上表现不同**
(日历页自己算宽度就没露出来),很容易被当成"某一页的样式问题"去调。
## 四、设备判据基建(这一轮加的能力)
- `lib/harmony-device.mjs` 新增 `launchOurApp` / `ourAppInFront` / `tapText` / `swipe`。
`swipe` 里 clamp velocity 并写明那个坑:`uitest` 的 velocity 越界**不报错**,
只回一句 "out of range, the default value will be used",静默换成默认 600。
- 三条新设备判据(`harmony-appearance`):壁纸档位真的切换 / 窗格内容不得超出屏幕。
- 修了一个**元问题**:设备判据在套件里**恒跳过**(要求"现场已经摆好"),
只有手动摆好才通过 ⇒ 那等于没有判据。现在它们**自己搭现场**
(拉起应用 → 导到目标页 → 操作 → 复位)。`cross-client-gesture` 与
`harmony-appearance` 都改成了这样,套件里 `skip=0`。
- 途中撞出的两个判据自身缺陷(都写了注释):
· `root0` 用**切页前**的快照 ⇒ 套件里红、单独跑绿(通过与否取决于跑之前那一屏)
· 侧栏项筛选没排除**品牌标** ⇒ 想点「日历」却点到「通信」
## 验证
`run-all.mjs` → `files=32 ran=32 checks=503 pass=503 fail=0 skip=0
red=0 broken=0 unreported=0`(含设备判据:gesture 9、appearance 27)。
`hvigorw assembleHap` 成功;前端重建 + 重打包(`build-stamp` 7/7、`packaging` 5/5)。
设备实测(HATriple 3184×2232):
· `PUT /me/appearance` 从 **400 → 200**(服务端访问日志),
库里 `jianf` 的记录从空变成 `bg_kind=preset / bg_dim=56 / bg_blur=3`。
· 壁纸真的透出来了:预设档缝隙 `#E0E2E4`、不设档 `#FFFFFF`(像素级对比)。
· 「我的」页 `56%` / `3px` 正常显示在屏内。
**未验**:壁纸在真机上的观感(渐变是否好看、压暗 56% 是否合适);
这一轮只验了"数据通了、界面响应了、内容没被裁掉"。
|
2026-09-19 15:03:40 +08:00 |
|
|
|
4a5318ca28
|
跨端: 手势补设备判据(真滑、标题真变)+ 修三处"设备判据假红"的典型错法
上一提交把滑动翻页做完了,但只验到"代码形态对"。**真装上跑时一次都没触发** ——
这一条补上设备实测,并把途中撞出来的三类错法记进判据注释。
## 一、为什么必须有设备判据
第一次装上跑:**代码全对、手势一次都没触发**。原因是起点 x=2600 落在
**右栏**(日程面板 `Column [2207,112][3184,2204]`)—— 事件根本没进网格列。
只有静态判据的话,结论会是"手势已实现、判据全绿",而用户真去滑时一动不动。
所以判据断的是**界面真的翻了**(滑动前后各 dump 一次,断言月标题变一格),
不是断"日志里有 turned=true" —— 后者只证明判定通过、证明不了有人会动。
## 二、途中撞出来的三类错法(都写进注释了)
1. **`uitest` 的 velocity 越界会被静默替换**
我传 150(想表达"慢一点"),它只回一句
`The swipe velocity out of range, the default value will be used.`
—— 不报错、不改退出码,只是默默换成默认 600。于是"慢滑"变成"更慢的滑",
看起来像手势没生效。**传合法值(200~40000)+ 读回执**才能避免。
新增 `lib/harmony-device.mjs` 的 `swipe()` 帮手(与 `tap()` 同族,
内部 clamp,并在注释里写了这个坑)。
2. **滑动是有副作用且不可撤销的写操作 ⇒ 不能盲目重试**
第一版写"不生效就再发一次(最多 3 次)",结果实测**把日历一次翻了 3 格**
(标题跑到 2026年11月)。重试不是"再试一次",是"再翻一页"。
只有幂等操作才允许盲目重试。改成:**只发一次 + 等足够久**(最多 8 秒轮询)。
3. **坐标不能用"到边界差一点"的比例**
取 `width * 0.68` = 2165,距网格列右边界 2207 只有 42px ⇒ **一次都不触发**;
同一台设备、同一份代码,起点改 2000 立刻生效。
起点贴边时触摸点会被判到相邻的右栏。改成取**列中段**
(`0.62` / `0.13`,两端各留几百 px 余量)。
另:本判据第一版用"滑一次 + 固定等 2500ms + dump",**单独跑通过、接进
run-all 后失败**(设备繁忙时不够)。固定等待是设备判据最常见的假红来源 ——
改成轮询到标题变化。诊断留了 `AGENTMAIL_GESTURE_DEBUG=1` 门控,
失败时能一次看到"前台/坐标/轨迹",不用事后手动复现。
## 三、设备实测结果(HATriple 3184×2232)
· 月档:左滑 9月 → **10月**(`dx=-556.5 dy=0 ms=624 turned=true`);
右滑回 **9月**(`dx=+556.5`)。
· 周档:`2026年9月14–20日` → 左滑 → **`9月21–27日`**(正好 +7 天,
证明步长走的是 `stepDaysOf` 而不是写死 1)。
· 斜滑(dx=400 dy=800):`PanGesture({direction: Horizontal})` 在系统层
就没识别 ⇒ 比鸿蒙侧的 `SWIPE_AXIS_RATIO` 更早拦住(正确行为)。
**仍未验**:56vp / 1.4× / 700ms 这三个数**手感是否合适**,只能真人滑过才知道。
我验的是"判定逻辑 + 接线 + 真能翻页"。
## 四、验证
`run-all.mjs` → `files=32 ran=32 checks=498 pass=498 fail=0 skip=0
red=0 broken=0 unreported=0`。
(含新设备判据:cross-client-gesture 9 条,其中第 9 条是真滑。
`build-stamp` 7/7、`packaging` 5/5 —— 按判据要求重建 + 重打包,没有改记录迁就。)
|
2026-09-19 14:16:30 +08:00 |
|
|
|
28e3da76d4
|
跨端: 左右滑动翻页(P6 第 3 步)+ 还 gesture-semantics 债 + 修跑不起来的判据基建
★ 这一轮从用户一句「滑动手势呢?」开始。查下去发现它不是"顺手加个手势",
而是 `docs/DEBTS.json` 里挂着的一笔债 —— `gesture-semantics` 的原话是:
「P6 第 3 步:鸿蒙侧出现滑动手势代码时**立即建**判据
(此前建 = 只有一端存在的假判据)」
也就是**先有手势、再钉语义**。WebUI 2026-09-14 就有滑动翻页(用户当时
亲口提的),鸿蒙一直没有 ⇒ 之前建判据会是空真(∀x∈∅)。
## 一、手势本体(两端语义逐项对齐,数值各自定)
按 `HARMONY-ALIGN-PLAN.md:118-128` 显式选的 **(b) 口径**:
「手势的物理量本来就不该强求同值……该对齐的是**语义层**」。
· 判定逻辑放**纯逻辑层** `model/Calendar.ts` 的 `judgeSwipe`(可被 node 直跑,
写在 .ets 里就跑不了判据,语义没法被单测钉住)。四道门:位移 / 纵向优先 /
快滑窗口 / 方向。
· 阈值**不引用** WebUI 的 40 / 1.5 / 600(那是把巧合当契约),
各自定为 56vp / 1.4× / 700ms,并在注释里写出取值依据。
· 接线在 `CalendarPage.ets`:`PanGesture({direction: Horizontal})` +
`onActionStart`(记时 —— `GestureEvent` **没有时间戳字段**,我查了 SDK
的 gesture.d.ts 确认)+ `onActionEnd`(读 offsetX/offsetY)。
· ★ 挂在**网格列**上而不是整页:右栏(日程/编辑器)里有输入框与可滚内容,
整页挂会让「在表单里横划一下」变成翻月。
· ★ 翻页复用 `shiftRange()`(与 ‹ › 按钮**同一个来源**)—— WebUI 的注释
专门交代过:各写一套的话,阈值、边界、三档行为迟早分叉。
手势回调里**不准**直接改 year/month(锚点是唯一真相,年月只能由
`shiftRange → syncYearMonthFrom` 派生)。
**有意差异(记录在案,不是漏做)**:WebUI 在周/日档会额外检查「触点是否落在
可横向滚动的区域里」,是则让给滚动条(用户 2026-09-14 报过这个冲突)。
鸿蒙周档是「一行 7 格按 layoutWeight 等分」、**不横滚** ⇒ 该条件不适用。
哪天加了横滚必须同时补上它。
## 二、判据(8 条语义契约 + 行为层)
新增 `cross-client-gesture.test.mjs`:两端都真有手势 / 方向映射逐项相同 /
纵向优先 / 快滑窗口 / 复用同一翻页函数 / 无边界回弹 / 有意差异被记录 / 自检。
**只比语义、不比数值**,并反向断言鸿蒙的阈值常量不得直接取 WebUI 的那三个数。
`harmony-logic.test.mjs` 加行为判据:真跑 `judgeSwipe`,把四道门各自验一遍
(只钉字符串的话,一个 return 写漏了照样全绿)。
## 三、顺手修掉的三处**判据基建**缺陷(不修就没法验证上面这些)
1. `run-all.mjs` 只认 `# pass N`,而 node v24 打的是 `ℹ pass N`
⇒ **21 个文件被记成"没自报条数"**、套件在 HEAD 就恒红(memory 里记过这条,
修法也记过,今天终于落进代码:4 个正则加 `(?:#|ℹ)`)。修完 `unreported` 27 → 0。
代价是暴露出一批此前被"没自报"掩盖的真实问题(下面 4~6 条)。
2. `harmony-nav` 的设备判据 `navItemsOf`:宽屏过滤条件从「左边缘靠左 1/6」
改成「**整个盒子在侧栏轨道内**」。旧条件把**日历网格的格子**
(实测 `[229,511][366,794]`)也当成导航项,一屏数出 11~12 个。
这是设备实测抓出来的 —— 我第一版还以为是"底部簇混进来了",
打印真实数据才发现是隔壁页面的格子。
3. `harmony-logic` 两处:从 `NAV_ITEMS` / `NAV_SIDEBAR_ITEMS` **各自的数组**里取
label。原先把全文件 `label:` 一网打尽 ⇒ 得到 7 个(4+3 混在一起),
任何一边改对了它都会红。
## 四、被判据拦住后的正经修法(每条都按判据自己给的方向改,不改判据迁就代码)
· `cross-client-theme` A2 拦住我:新增的 `navActiveBg`/`navBrandFg`/`badgePlain`
未登记;又拦住我:`sseColorOf` 里四个裸色值。→ 抽成 `Theme.sseConnected` 等
四个令牌(取值对齐 WebUI 的 Tailwind 类)+ 登记 + 在 Theme.ets 的表里写理由。
· 同一条判据的"死令牌"检出:`Theme.durBase` 声明了却从没人读。
**查 WebUI 才发现壁纸淡入是真有的动效**(`index.css:742-752` 的
`.app-backdrop` 从 opacity:0 → 1,180ms)⇒ 补上而不是删令牌
(删掉等于把差异抹平、还说成"清理")。reset 在 `animateTo` **外**,
与 `calPaneIn` 同一条纪律。
· 玻璃登记:`NavItemBuilder` → `SidebarItem`(重构后按最近的 @Builder 命名),
登记同步跟上。
· `harmony-admin` 的退出判据:退出逻辑抽成 `api/Logout.ets` 的 `performLogout()`
(两个入口——「我的」页与侧栏底簇——必须做同一件事,尤其"先注销推送 token"
那一步)。判据相应改成**追到实际执行处**(两半都断:按钮调了 + 函数真清了全部),
并写明"别再退回直接匹配 SETTINGS_PAGE 的写法"(那会随重构假红,
下一个人只会去改判据而不看行为)。
· `harmony-widescreen` 的连接点色值:色值搬进 Theme 后,判据改成断
「令牌定义对了 + 侧栏真的用了它」两半(只断任一半都有假绿形态)。
· `align-refs`:`CalendarView.tsx` 变了,按判据要求**读一遍差异**再更新登记
(差异只有农历小字的灰阶档位 gray-300→gray-400/500,**骨架未变**)。
· `criteria-hygiene`:`harmony-contacts` 自造了一个 `code()`、`harmony-widescreen`
裸用 `readFileSync` ⇒ 都改用 `lib/read.mjs` 的共享入口。
途中撞出一个**判据自己的 bug**:`code()` 的块注释正则
`/\*[\s\S]*?\*\//` 会把注释里出现的 `/*`(如 `/」**` 这种中文夹星号)
当成块注释起点,一路吃到几十行后的 `*/`,把中间的 import 全吞掉 ——
于是 hygiene 判据假红"没 import"。改掉那处写法后正常。
## 五、验证
判据面:`run-all.mjs` → `files=32 ran=32 checks=497 pass=497 fail=0
skip=0 red=0 broken=0 unreported=0`。
其中新/改判据:gesture 8、nav 18、widescreen 7、logic 30、admin 27、
cross-client-theme 15、criteria-hygiene 6。
构建:`hvigorw assembleHap` 成功;前端 `npm run build` + 重新打 AppImage/deb
(`build-stamp` 7/7、`packaging` 5/5)。
**设备实测(HATriple 三折叠 3184×2232,hdc 连 127.0.0.1:5555)**:
· 农历在格子里真的显示(1=二十 / 7=廿六 / 19=**初九** / 11=八月),与 WebUI 一致;
这条同时验证了**服务端农历路由已部署**(之前线上是 404)。
· 日历左右两栏:编辑器出现在**右栏**、左栏月份仍可见(单栏模式下编辑器会整页盖掉它)。
· 侧栏 3 项 + 底部一簇;徽标回到图标右上角。
**仍未验(如实标注)**:滑动翻页的**手感**(阈值 56vp/1.4×/700ms 是否合适)
只能真人滑过才知道;我只验了判定逻辑与接线形态。动画同理 ——
机制已验证(`animateTo` 驱动 + reset 在窗口外),但"看起来顺不顺"未做取样验证。
docs:`DEBTS.json` 销掉 `gesture-semantics`(并记结算说明)、
`HARMONY-ALIGN-PLAN.md` P6 从「✅(滑动翻页除外)」改为 ✅。
|
2026-09-19 14:01:21 +08:00 |
|
|
|
e8b260dd70
|
维护: 底本重算(4 个文件漂移 = **底本过期**,不是变异残留)—— 逐个核实后按本文件的协议记一行"为什么"
pi `df62ad3d` 那封的 §四 我复核后**已经在 `d07494e` 修好了**(父提交正是 pi 报信时的
HEAD `93dabb4`):`if (r.red) reds.push(r.red)` + `DIAG[*].blocksGreen` 让
`summary.py` 的 status 真的进了 `verdict`。同刻 A/B(只切 baseline 一个变量)实测:
残留态红清单 diff **恰好 +1 行**(`(summary.py)baseline-residue ——…`),`red` 10→11;
stale 态 diff **0 行**。⇒ "stale ⇒ 提示(0) / residue ⇒ 红(进 verdict)" 这条口径成立。
这轮顺手核"每个文件的登记/实际读数"时,撞上底本自己的状态:
`baseline=3/7(底本过期…)`。逐文件核过 —— 4 个漂移**全部**是
`git diff --quiet HEAD -- <f>` 为空的**提交态**(各自有明确的提交):
· AdminUsersPage.ets ← 0e5eec6 (09-18 11:47)
· BackgroundPicker.ets、SettingsPage.ets ← 36ef15a (09-19 12:30)
· ApiClient.ets ← bcd7e7f (09-18 12:55)
⇒ 是**底本过期**,不是残留(两者在 `sha256sum -c` 眼里一模一样,
所以"重算"必须是有记录的动作,否则会永久掩盖真残留)。
重算后:`sha256sum -c` 7/7 OK、套件 `diag=none baseline=7/7✓`(原 3/7)。
**并验证重算没有把真残留一起盖掉**:改脏一个在底本里的文件 ⇒ 仍报
`diag=baseline-residue baseline=6/7✗` + 那条红进 verdict(red 10→11)✓。
★ 记一条观察:这次漂移属 `baseline-stale`(rc=0、只提示)那一档 ——
它**不假红**(正常提交不会天天红),但也**不会被自动发现**:
我是靠主动逐文件核查撞上的,不是它自己报的。
"不假红"与"会被发现"在这个闸上仍有取舍,这条记为已知残留。
|
2026-09-19 13:01:39 +08:00 |
|
|
|
d3140c213c
|
补充: 给"条数登记校验"加锚点(自检 5b)—— ★ 而我第一版锚点自己写成了**空真**
`788d7cc` 把条数校验挪进 `else { … }`(红绿都跑)之后,**它没有自检**:
下一个人完全可以再挪回 `else if` 后面,那时**什么都不会红**
(红文件又收不到条数回执),而缺口**只在文件恰好红时隐形** ——
正是它上次潜伏到 `c523c21` 的原因。⇒ 补自检 5b。
做法(锚点落在**实际发生的比较**上,不许落在源码文本上,§16.3):
在 `else { … }` 里每次比较都 `countCheckRan.add(file)`,
5b 从 `records`(谁真的自报了条数)**独立重算**应当被评估的集合,再要求它被覆盖。
**不读 `reds`、不看那条校验自己的输出** —— 否则就是"读数器自作证"。
★★ 而**我第一版 5b 是空真的**(自捉,如实记):
我原来比的是"凡**条数不符**的文件都必须被记录过" ——
**而这条修复本身就把 harmony-admin 的登记数对齐了 ⇒ 那个集合恒空 ⇒ 断言恒真。**
变异测试当场抓到:把 `countCheckRan.add` 挪回"只绿才走",**5b 一声不响** ——
那一刻我才发现它不是"通过",是"**没有对象**"。
改成比 **"所有自报了条数、且没崩的文件"**(红绿都含):红文件必在其中 ⇒ 非空,
且"红文件被漏记"必被抓。另加**反空转**:集合为空而并非全部 broken ⇒ 自检自己报失效。
变异验证:挪回"只绿才走" ⇒ 5b 报出 6 个红文件名
(cross-client-theme / build-stamp / align-refs / harmony-push / criteria-hygiene / harmony-admin);
基线不报 ✓。修后全套(树内):files=31 ran=31 checks=487 pass=476 fail=11 red=10
broken=0 unreported=0。
`CRITERIA.md` 补两条可复用教训:
① **"拿现有数据试一遍"要试到"数据非空"** —— `∀x∈∅` 的判据看起来和真判据一样绿;
② **修好一件事会同时消灭它自己的测试对象** ⇒ 锚点不许建立在"当前的错误状态"上,
要建立在**恒在的集合**上("谁自报了条数",而不是"谁条数不符")。
|
2026-09-19 12:56:19 +08:00 |
|
|
|
788d7ccb20
|
修复: 条数登记校验**只在"绿"的那条路上** —— 文件越红,它的登记数越没人守(假绿方向)
设备在场时跑全套,顺手核了每个文件的『登记条数 vs 实际条数』,发现:
narrow-layout 登记=64 实际=88 exit=0 绿 ⇒ 校验生效(报了)
nav-merge 登记=8 实际=9 exit=0 绿 ⇒ 校验生效(报了)
background 登记=43 实际=44 exit=0 绿 ⇒ 校验生效(报了)
harmony-presets 登记=5 实际=6 exit=0 绿 ⇒ 校验生效(报了)
harmony-admin 登记=22 实际=27 exit=1 ★ 红 ⇒ 校验**走不到**(grep 0 命中)
根因:这条校验原来写在**最后一个 `else`**("退出码 0"那条路)里,而
`r.status !== 0` 会**先在 `else if` 里 reds.push 并跳过它**。
⇒ **文件越红,它的条数登记越没人守** —— 假绿方向:
harmony-admin 那多出来的 5 条判据**不在"被删会红"的保护内**,
而它恰好是红的 ⇒ **只要它一直红,缺口就一直是隐形的**;
等它修绿那天校验才第一次生效,那时多出来的几条可能早被删了。
(同族:`unlisted`/`blind`/`baseline=` 的结论到不了 `verdict`,§16.1。)
★ 而且这个缺口**已经真的吃过一次**:`c523c21`("邮件详情与「我的」页 1:1 对齐")
给 harmony-admin **+5 条判据**(它自己的 commit message 就写着 "+5"),
**却没同步登记数**,而当时该文件是红的 ⇒ 5 条新判据至今裸奔。
修法:把条数校验移进 `else { … }`(红绿都跑;先 push 退出码红,再判条数)。
⚠️ broken(崩了/一条条数都没自报)**不在这里**判 —— `diedWithoutReporting` 已吃掉它,
再叠一条"没找到自报条数"只是噪音:**"判据没答 ≠ 判据答错了"**(§17)。
并做那个"显式、可复核的编辑":登记数 22 → 27(和 c523c21 欠下的那 5 条对齐)。
变异验证:
· 红的 harmony-admin 登记数改 99 ⇒ 报『自报 27 条 < 登记的 99 条』✓
· 改成 27(对齐)⇒ **不报条数**、只剩"退出码 1" ✓
· 修后全套:red=10(harmony-admin 那条从"走不到"变成"报出来"后 +
对齐登记数又收回,净额 0),另 4 个绿文件的条数不符**照旧照报** ✓
`CRITERIA.md` 新增通用规则:**校验写在哪条分支上,决定它保护谁。**
凡"出错时要额外检查 X"的守卫,先问:**这条分支真红的时候,它还跑得到吗?**
|
2026-09-19 12:51:29 +08:00 |
|