Commit Graph

5 Commits

Author SHA1 Message Date
71585dc42b 跨端: 预设档补遮罩(WebUI 的 --bg-dim 不分档位)+ 主题变化时重算"我们自己算的值";缓存键差异记入表
pi 读 WebUI 源码后发现两处两边不一致,都处理了。这一提交同时改 harmony 与 electron,故自报家门。

## 1 预设档遮罩:**补上**(选"与 WebUI 一致",因为那服务的是可读性)

pi 的证据:WebUI 的 `applyBackground()` **无条件**写 `--bg-dim`(默认 24),
`.app-backdrop::after` 是 `rgb(var(--bg-scrim) / var(--bg-dim))` —— **遮罩不区分档位**;
它的注释写着目的「背景越花,正文越需要一层遮罩才读得动」。
而鸿蒙当时只在 image 档压(`resolveBackground` 里那行注释还写着"preset 档不用"),
且我们已经让出了页面底 → 正文直接压在原色渐变上,**比 WebUI 更艳更亮、更不好读**。

现在两档用**同一个浓度**(同一个服务端字段),页面在预设分支的渐变之上加一层
系统遮罩色 × 浓度(浅色由系统洗白、深色压黑,不自己写 alpha)。

判据:旧断言"preset 档不压暗"**反过来**(留着理由),另加一条钉"两档同一浓度"+
"两处遮盖层都在"+"WebUI 确实无条件写 --bg-dim"(这条差异有据可查)。
变异:预设档不压 → 红 2 条;页面预设分支的遮盖层被删 → 红。

## 2 多账号缓存键:**鸿蒙是对的,不许退回**(pi 点名)

WebUI 的键是全局常量 `agentmail.background`,后果是切到服务端没有记录的账号时
`saved=false` 分支会把**上一个账号的外观** push 上去(新账号"继承"了外观,还写进了服务端)。
这条差异进 §7.12「有意差异」表,**明确写"鸿蒙是对的"**,
判据防的就是"将来有人为了两边一致把它改回去":取键函数的**正文**里必须拼账号 id
(按块取,不是看调用点出现过 `accountId` 就当数 —— "判结构要配对/解析"那条对我自己也适用),
且不许出现 WebUI 那个全局键。变异:`prefKey` 去掉账号 → 红 2 条。

## 3 "未做" → 按 pi 的三档口径改成**已知不一致**,并把系统侧的接法做掉

pi 指出这不是"没做":色板确实按主题算了,只是没接环境变化事件 ——
现象是运行期切系统深浅色时"系统语义色/材质立刻跟随、我们自己算的色板不重算"的**撕裂**。
他给的规则我记成了通用规则(会咬到 P5/P6):**系统自动跟随的东西不会顺带把
"我们自己计算/缓存的值"一起更新** —— 凡随主题变化的自算值都要挂在**同一个主题变化事件**上。

照做:`applicationContext.on('environment')` → `onConfigurationUpdated` 里
**只在 `colorMode` 真换向时**重算(`applyAppearance()`),页面销毁 `off` 退订。
状态 = **未验**(只有真机/模拟器能验:切一次系统深浅色看预设是否跟着换)。

SDK 锚点与两个编译错都记进 §7.17b:`EnvironmentCallback.onConfigurationUpdated(config: Configuration)`
(`Configuration` 从 `@kit.AbilityKit` 取,**不在** `common` 命名空间下)、
`Configuration.colorMode` 是**枚举 | undefined**(字段写成 `number` 直接编译失败)、
`on('environment')` 返回 **number 型 callbackId**。变异:不订阅 → 红。

## 4 文档

- §7.12 加两行:缓存键(含 WebUI 侧开项)、遮罩浓度默认值那两套(`24/8` vs `12/4`,
  按 `clamp` 的 12/4 对齐,因为服务端缺字段时落地的是它)。
- §四 验收纪律改成**四档口径**:没做 / 未验 / 已知不一致(要写触发条件与现象)/
  机制上确定不同(必须判),并把 pi 那条"自算值必须挂主题事件"的可复用规则写进去。

## 验证

`hvigorw assembleHap` BUILD SUCCESSFUL;`npm test` 退出码 0
(12 个判据文件全绿 + vitest 258/258;harmony-appearance 20 → 23 条)。
2026-09-14 15:04:07 +08:00
22c9be7181 跨端: 预设色板两套(pi:这不是"观感未验"而是机制上确定不同)+ 手写色清册跨文件 + TMPDIR 按会话分家 + 提交归属可判
pi 读完 `model/Wallpaper.ts` 后指出四处,全部处理。这一提交同时改了
`client/harmony/` 与 `client/electron/`(跨端改动),所以 subject 按新约定自报家门。

## 1 预设色板不随主题 —— **类别判错了:不是"未验",是机制上确定不同**

我上一版把"深色档预设"记成"观感未验"。pi 指出:WebUI 的 `.bg-preset-*` 写的是
`rgb(var(--c-blue-100))`,而 `--c-*` 在 `.dark` 里整体换了一套(blue-100 → `30 43 67`),
所以 **WebUI 的预设自动随主题变**;这边只有浅色那套 = 深色主题下"浅色渐变垫在深色系统表面之下",
正是这一整轮在治的病。**它不需要真机就能判**(机制写在代码里)—— 我把可判的东西
记成了"未验",这跟上一轮把"没做"写成"没验"是同一类错。

选 pi 倾向的那条(跟随主题,与 WebUI 一致):

- 色板两套:`LIGHT_*` 取 CSS `:root`、`DARK_*` 取 CSS `.dark`;`paletteFor(dark)` 选一套,
  `layersFor(id, dark)` 按主题出层;
- `isDarkMode(theme, systemColorMode)` 放在纯逻辑里:选了 dark/light 就照办,
  `system` 看系统当时的 `colorMode`(锚到 SDK:`COLOR_MODE_DARK = 0` / `COLOR_MODE_LIGHT = 1`;
  读不到按浅色,与 WebUI 的 `:root` 默认一致);
- 系统深浅从 `resourceManager.getConfigurationSync().colorMode` 读
  (`Context` 基类没有 `config`;`UIAbilityContext.config` 要转型;两个枚举取值一致,都核过 SDK);
- 判据:两套值与 `:root`/`.dark` **逐个相等**;每个预设的深浅两套**必须真的不同**
  (否则"两套"是抄了两遍);网格线色也要换;`isDarkMode` 五种输入。
- **未做**:运行期间改系统深浅色不会自动重算(要重进页面)——系统侧正确做法是订阅
  `applicationContext.on('environment', …)`,记在 §7.17b。

变异:`DARK_BLUE_100` 偏一位 → 红;`paletteFor` 永远返回浅色(= 我原来那个状态)→ 红;
`isDarkMode` 把系统深浅记反 → 红;页面把深浅写死成 false → 红。

## 2 手写色清册**跨文件按类扫**(原 A2 只保护 `Theme.ets`)

`Wallpaper.ts` 也有手写色。若对照是"按名字枚举"的,第 15 个色就会逃掉 ——
与 A2 要防的是同一件事,只是换了文件。现在一份清册按类扫:全 `ets/` 树里每个
`X: string = '#RRGGBB'` 都必须登记(Theme 的品牌/业务语义色,或预设色板 ——
后者常量名必须带 `LIGHT_`/`DARK_` 前缀,值由 CSS 两段比对负责)。反向也判清册过期。

变异:`Theme.ets` 加未登记色 → 红;`Wallpaper.ts` 加未登记色 → 红;
加一个"看着合规"的 `DARK_EXTRA` → 红。

## 3 `TMPDIR` 互踩(pi 提出)

这个 worktree 可能同时有多个 agent 跑构建,而 fpm 会把 291MB 的 `linux-unpacked`
**整份复制**进 `TMPDIR` —— 撞车就是随机的产物损坏。`whoami` 区分不开(大家都是 root),
所以按**会话**分家:`TMPDIR=$PWD/.tmp/${DSH_SESSION_ID:-$(id -un)-$$}`
(进了 `npm run build:linux` 与 BUILD.md 的手敲命令;普通终端退化成"用户+PID")。

## 4 提交归属变成**跑判据就看得出来**(pi 给的形状)

新的 `test/commit-hygiene.test.mjs`:扫最近 40 条提交,**同时改两侧目录**的提交
必须在 subject 里自报家门(`跨端:`)。两条防腐:基线 = 该判据文件自己的引入提交
(**历史不改**,规则管从今往后);分类逻辑拿合成输入自检
(未标注的混合提交必须判红、标注过的不许红)——否则"解析没跑起来"时它会全绿。
变异:`COMMIT_HYGIENE_BASELINE` 指到老提交 → 历史里那两个被卷进去的提交立刻判红。

## 验证

`hvigorw assembleHap` BUILD SUCCESSFUL;`npm test` 退出码 0
(12 个判据文件全绿 + vitest 258/258;`commit-hygiene` 在本提交落地后基线生效)。
2026-09-14 14:57:18 +08:00
1717863c87 fix(webui): 手势与横向滚动分家 + 纵向滚动边缘淡出
用户给了两条具体信息(这比我自己猜五轮都管用):
「横向滚动条会与切换视图的手势冲突,我觉得周视图需要卡严条件,
  同时我说的其他硬截断是对应内容项上下滑动会直接被切断」。

## ① 手势卡严(周视图)

冲突是真的:周视图窄屏下必须横向滚(`overflow-auto` + `min-w-[36rem]`),
而我又给整页加了左右滑动翻页 ⇒ 同一次横滑既想滚又想翻,两边都不好用。

规则定死:**手势从可横向滚动的区域里起手,就归滚动条,完全不参与翻页判断**
(触点沿祖先链查找 `overflow-x: auto/scroll` 且真的能滚的容器)。
想翻页就从别处滑(例如上方标题栏)。

## ② 纵向滚动边缘淡出

滚动条本身是"一刀切":卡片滚到边缘被硬生生截断 —— 这就是用户说的"直接被切断"。
给纵向滚动容器(`.overflow-y-auto`)加 12px 上下渐隐遮罩,切得有交代。

只作用在**纵向**容器:横向滚动有自己的滚动条,加纵向遮罩会跟它打架。

## 过程记录(值得记)

这两条改动**第一次没有生效**,因为部署失败了:`/tmp` 是 tmpfs 且已 100% 满,
而部署脚本把构建产物写到硬编码的 `/tmp/agentmail-gateway-build-*` ⇒
`no space left on device`。我差点把"旧构建上的测量结果"当成"改动无效"。
清理后(清掉我自己的探针脚本/截图/旧构建,约 950MB)部署成功。
⚠️ `/tmp` 现在仍占 91%(`gocache` 4.5G 等不全是我的),**下次部署可能还会撞上**;
脚本改成尊重 `TMPDIR` 才是根治(未做)。
2026-09-14 14:35:43 +08:00
c9717daa13 fix(webui): 联系人项改为"每项一张玻璃卡"(补齐上一轮承诺)
上一轮我说过"联系人/会话列表还是老结构,只有 MailList 改了",这条把它补上:

- 联系人项:`rounded-lg border` → `.glass-card`(圆角 14px + 白色玻璃 + 细边框),
  与邮件行、顶部气泡、日历格子同一套语言;
- 会话分组行与邮件行上一轮已经是 `.glass-card`,所以三类列表现在一致了。

实测(自有浏览器,壁纸开,1400×900):联系人页的卡片 `border-radius: 14px`、
底色 `rgba(255,255,255,0.78)`。

另外这一轮我又试了两个"滑动硬截断"的假设,**都不成立**:
- 窄屏日历(月/周/日三种刻度):月与日刻度不溢出;周刻度溢出 208–278px 但
  **有横向滚动条**(`overflow-auto`),不是硬截断;
- 页面级横向溢出:390/320 下都是 0。

所以这条仍然需要用户指路。**而且教训很明确**:上一条"列表与正文之间的大空隙",
我三轮猜都没猜中,用户一张截图我当场就定位了。截图比任何自测量法都快。
2026-09-14 14:31:16 +08:00
2a6ad93ec0 feat(harmony): P4 —— 外观(主题 + 壁纸)跟着账号走
服务端 2026-09-13 起就是外观的权威(账号级 `/api/v1/me/appearance`),WebUI 接好了,
**鸿蒙这边此前完全没接**。这一期补上,并把"谁覆盖谁"的规则做成可判据的纯逻辑。

## 改了什么

- `api/AppearanceApi.ets`:`GET/PUT /me/appearance`、`POST /me/appearance/image`、
  `GET /me/appearance/image`(图片带认证取回本体:不用 `?token=`,也不让 Image 直连 http)。
- `api/ApiClient.ets`:新增 `getBytes()`(按 ARRAY_BUFFER 收)—— 复用 `request<T>` 会当场炸,
  因为它假定响应是 JSON(`JSON.parse`)。
- `model/Appearance.ts`(纯逻辑,判据直接执行):归一化 / PUT 报文 / **合并决策** /
  模糊值→系统材质档次 / 主题→系统色彩模式 / 遮罩浓度 / 状态文案。
- `common/AppearanceStore.ets`:落地副作用 —— 主题交给**系统**(`setColorMode`,不自己维护
  深色色值)、壁纸取回 `PixelMap`、缓存**按账号**分键(`appearance.<accountId>`)。
- 入口两处:`MainPage`(进主界面就应用 —— 只在设置页生效的话"一进主界面就变回去",
  WebUI 侧踩过)与 `SettingsPage` 新增「外观」段(三档主题 + 同步状态「已同步 / 仅本机」)。

## 为什么这么写(两条最贵的规则)

1. **服务端"没有记录"时以本地为准**(`saved === false`):服务端这时回的是一份*默认值*,
   拿它覆盖本地 = 把用户已有的主题/壁纸抹掉(WebUI 原话:每个老用户升级后第一次登录
   都会发现被重置)。正确动作是把本地那份推上去。
2. **降级必须可见**(`local-only` → 显示「仅本机」):否则用户以为换设备也能带走。

## 判据(新增 11 条,已接进 run-all;套件 10 → 11 个判据文件)

归一化(脏值/越界/小数/NaN 退回默认);`image` 档无图 → 退回 `none`;PUT 报文蛇形字段名;
★服务端无记录 → 以本地为准且**一个字段都不能被默认值顶掉**;服务端有记录 → 以服务端为准但
**不擦掉**本地那张服务端还没有的图;离线状态可见且三种状态文案互不相同;
★模糊值→系统材质档次(与 SDK 的 `BlurStyle` 成员**逐一比对**);
主题→色彩模式(数值与 SDK 的 `ConfigurationConstant.ColorMode` **逐一比对**);
★路径必须**相对基地址**(WebUI 那条"整套同步从来没生效过而单测全绿"的坑);
缓存键**带账号**;两处入口都真的应用。

变异验证(6 种,均判红):默认值覆盖本地 / 不管"image 档但服务端无图" / 材质档次自造名字 /
路径多写 `/api/v1` / 缓存键不带账号 / 深浅色彩模式数值写反。

## 判据抓到的两个真 bug

- `snapshotFromResponse` 在字段缺失时给 `bgDim = 0`,而 WebUI 语义是退回 12 ——
  ArkTS 反序列化把缺失字段留成**类里写的默认值**,"字段不在"与"字段是 0"分不开。
  已把默认值对齐 WebUI 的 `clamp(..., dflt)` 语义(并让 `saved` 默认 false = 安全的那一侧)。
- 主题落地按"0=浅色、1=深色"写的 `setColorMode` —— **正好反了**
  (SDK:`COLOR_MODE_DARK = 0`、`COLOR_MODE_LIGHT = 1`),选深色会切成浅色。
  靠判据去 SDK 枚举文件读数比对发现;映射已搬进纯逻辑 `colorModeValue`,
  从"某处有个 setColorMode 调用"变成"可判据的行为"。

## 验证 / 未验

`hvigorw assembleHap` BUILD SUCCESSFUL;`npm test` 退出码 0(11 个判据文件全绿 + vitest 258/258)。
套件自检又抓到一次"判据写好没接进套件"(新文件第一版漏了 run-all),已修。

**未做**:壁纸**上传**入口(选图 → `POST /me/appearance/image`)—— 需要 picker,API 与命名已就位。
**未验**:壁纸在真机上的渲染 —— 需真机或模拟器。
2026-09-14 14:19:26 +08:00