Commit Graph

12 Commits

Author SHA1 Message Date
71a3c35610 按 pi 复核改五处:判据的"值/行为"原则、真正的构建门、typecheck 进门、Go 判据零匹配、迁移归属
pi 用反例与变异逐条点了五处,全部**先跑变异再改**(结论都写在原地)。

## 1 判据③:形状正则已删(它永远差一个反例)

实测 pi 的反例 —— `const k = PREFIX + accountId; return PREFIX;`(拼了但没返回)——
对第三版判据**仍然全绿**:第三版锚的是"**函数体里存在**这样的表达式",不是"**返回的**表达式"。
三版的骗法各一个(签名里的参数 / 提了一下没用 / 拼了没返回),都在判"源码里有没有那个形状",
而缺陷是"算出来的值对不对" ⇒ **权威交给行为判据**(`test/stores/background.test.ts` 直接断言
`storageKey('a') !== storageKey('b')`、`=== 'agentmail.background.acct-a'`、不退回全局键),
正则那条删掉并把反例写在原地(否则下一个人会好心加回来)。
保留"键必须来自 storageKey()"那条:那是**来源**约束,不是值对不对,正则在这里合适。
鸿蒙侧只能静态判(`.ets` 本机没有运行时),已把这条限制写进判据说明。
推广进规范:CRITERIA.md §6.7 + run-all 自检关键词(10 → 11)。

## 2 真正的门:`scripts/release-linux.sh`

- 实测 pi 提的变异:**`touch dist/index.html` 时 stamp 判据是绿的** —— 它抓不到"构建失败但碰过 dist"。
  stamp 是**探测器**(抓"src 改了产物没跟上"),门是**喂退出码**,两者不互替(§6.7.1)。
- `build:linux` 里**没有管道**(`&&` 链,退出码本来就传),但那次的哑巴失败是我在命令行手打
  `npm run build 2>&1 | tail -4 && …` 造成的;同时发现它跑的是**裸 `vite build`,跳过 `gen:bg`**。
- 于是把配方收成 `scripts/release-linux.sh`:`set -euo pipefail` + 走 `npm run build` + 再打包。
- 判据是**行为**的:注入失败的构建(`AGENTMAIL_BUILD_CMD='exit 7'`)→ 断言退出码非 0
  **且打包那步没跑**(标记文件不存在)。变异:脚本改成 `|| true` → 红 ✓。

## 3 typecheck 进 `npm test` 链

`npm run typecheck` 原本就有,但没人跑。先修掉它唯一的报错(我自己留下的未使用 import),现在干净;
`test` = run-all + vitest + typecheck。它恰好检查**没被任何测试 import 的文件**(vitest 只解析被测到的图)——
也就是那次 `is not exported by` 的形状。

## 4 Go 源码判据:零匹配 / 读不懂 都要红

改成三分:切不出函数体 → 红;字段在但值不是字面量 → 报「**判据读不懂**」(变异:`BgDim: defaultDim` → 红 ✓);
字段不在 → 红。静默放行是这类判据最危险的失败方式。
**措辞更正**:这条核对的是"与**这份服务端源码**的契约",不是"在跑的服务端二进制是 12/4"
(与"dist 是产物"同构);文档同步改。

## 5 迁移归属:定向做不到,就把"不可恢复"降级成"可恢复"

查实:`accountStore` 的 `activeId` 是**派生视图状态、不落盘**(`persist(accounts)` 只存数组),
所以**本机没有"上次活跃账号"标记可定向** —— 旧值的作者事后无法还原,定向迁移在原理上做不到。
升级前用 B、升级后先登录 A ⇒ A 接管 B 的外观,**一次性错档**,触发条件就这一条。
两件能做的都做了:**写了回读校验**(写不进去就不删旧键,避免净损失;变异:改成先删后写 → 2 条红 ✓)、
**删前另存** `agentmail.background.legacy.bak`(只写不读 ⇒ 不引入新的继承源,判据钉"只写不读")。
§7.12 把本地这半与显形方式写进同一行。

## 验证

`npm test` 退出码 0:14 个判据文件全绿 + vitest **265** 通过(+2 迁移行为测试)+ typecheck 干净;
`hvigorw assembleHap` BUILD SUCCESSFUL;安装包经新脚本重打(dist 与包同批)。
2026-09-14 15:41:52 +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
5ce711fbcd test(suite): 自检 3(判据不得埋在 process.exit 之后)+ TMPDIR 固化 + API 同名不同义表
## 自检 3(pi 提议)

自检 1/2 管"文件没接线",管不到"检查写在 `process.exit()` 之后"—— 而那正是实际发生的
第 4 例(4 条玻璃判据被并发写入落到文件末尾)。成因是**结构性**的(并发写入总是往文件末尾
追加),所以它一定会再发生,而它下一次仍然不报错。静态扫一遍即可:`process.exit(` 之后
若再出现 `check(`,直接判红并指出文件。变异验证:往 `theme.test.mjs` 尾部追加一条 check → 判红。

## TMPDIR 固化(pi 建议,采纳)

"记得加 TMPDIR"这种约定活不过两次踩坑(hvigor、fpm 各一次)。所以不再靠口径:
- `npm run build:linux` 自带 `mkdir -p .tmp && TMPDIR=${TMPDIR:-$PWD/.tmp}`;
- `BUILD.md` 的 deb 一节写明这条前置与原因(`/tmp` 是 tmpfs、占内存、常年近满)。

## `docs/API.md`:`total` 不是总封数 + 同名不同义表

`GET /me/mail/inbox` 的 `total` 是**未读总数**(`repo.CountUnread`),不是本页/全部邮件数 ——
鸿蒙端曾因此写出「共 7 封」和「未读 7」两行自相矛盾的字。在人类接口开头加了醒目提示,
并新增一张表:`total`(未读数)/ `status`(邮件=unread|read,会话=active|archived)/
`status` 与 `is_read` 同义不同名。

## 验证

`npm test` 退出码 0:9 个判据文件全绿 + vitest 258/258(安装包已按判据要求重打,
AppImage 与 deb 均为最新)。
2026-09-14 13:53:34 +08:00
f6feb7c0df test(webui): 「滑动硬截断」诊断脚本(自带浏览器,不依赖共享实例)
用户:「webui 大片界面存在滑动硬截断」。

## 现状:这一轮我**没能复现**

量法是"内容溢出但没有可滚动祖先"⇒ 候选 0 个;换成用户视角四问
(有没有可滚动容器 / 能不能滚到底 / 末尾有没有被切 / 页面自己溢不溢出)后:

    390×700  通信  日历  联系人  我的 
    1400×700 通信  日历  联系人  我的 
    (造了 12 个会话、正文都很长;验完已归档)

即按这四种量法都测不到硬截断。**长正文详情页**那一块我的探针超时没跑完 ⇒
**未判定**,不能算"没问题"。

## 这次的交付物

`client/electron/test/manual/scroll-cut-verify.mjs`:把上面这套量法固化下来,
并且**自带浏览器**(不连共享 9222)—— 共享实例被历次被中断的运行留下僵尸上下文后,
`connectOverCDP` 会一直超时(我这个会话里已经遇到三次),诊断脚本必须在
"共享实例状态未知"时也能跑。

## 我需要的(否则只能继续猜)

请指一下**具体页面 + 具体手势/位置**:哪一屏、滚轮还是触摸拖动、硬截断出现在
列表末尾还是详情正文?也可以直接看截图。我上一轮"窄屏显示不全"也是同样情况 ——
我量不出来、但你说有,那多半是我量的维度不对。
2026-09-14 13:43:11 +08:00
3729afd96f feat(harmony): P2a —— 收件箱按会话折叠 + 联系人页卡片视图(判据直接跑同一份逻辑)
按 pi 的结论落地 P2a 的前一半:**先补视图与折叠,再删平级「会话」tab**(tab 本轮保留)。

## 判据怎么"点用户真正会点的那一层"

鸿蒙侧没有设备(`hdc list targets` 为空、模拟器在本机沙箱下起不来),"点一下"暂时
无法自动验。应对不是编个能过的新判据,而是把会点的那一层的内核抽成纯逻辑:
`entry/src/main/ets/model/MailGrouping.ts`(无 UI 依赖),判据用 node 的
`--experimental-strip-types` **执行同一份代码**(`test/harmony-logic.test.mjs`,14 条),
断言的是行为而不是"源码里出现过某个字符串":

- 折叠后组头是不是**最新一封**、组内是否时间倒序、组间排序、同一时刻用 `mail_id` 倒序兜底;
- 时间解析失败**不能让顺序依赖入参**(WebUI 侧踩过的 NaN 比较坑);
- 多账号合并下同名 `session_id` 不能被错并成一组;`session_id` 缺失时各自成组;
- 预算档位与 WebUI `BudgetChip` 完全一致(剩 0 用尽 / ≤1 将尽 / 上限 0 不显示);
- 视图切换与卡片上"最新一封是人还是 Agent"的判据。

页面那一层另用源码判据钉"确实调了这些函数",两层合起来覆盖「逻辑对」+「页面接上了」。
**变异验证 4 处全部判红**:去掉组内排序(2 条红)、预算阈值 `<=1` 改 `<1`、
分组键去掉账号前缀、页面不再区分单封组。

## 收件箱折叠

- 组头取组内最新一封的别名与主题,带未读数徽标与「N 封」,点它展开/收起;
- **单封不成组、平铺**(与 WebUI `isFlatGroup` 同结论:给孤立一封信套组头只是多一次点击);
- 多账号是鸿蒙特有:分组键带账号前缀;`session_id` 缺失按 `mail:<id>` 各自成组。

## 顺带修掉一个"看起来是总数、其实是未读数"的显示

`/me/mail/inbox` 的 `total` 是 **`CountUnread`(未读总数)**,不是总封数
(`server/internal/handler/me.go`)。鸿蒙底部原写「共 N 封」⇒ 同一屏出现
「共 7 封」和「未读 7」两行自相矛盾的字。改成:未读数用服务端 `total`(权威,
原来数这一页会少报);「共 N 封」→「已加载 N 封」;**这一页取满时如实提示
「已加载 50 封(本页上限 50,可能还有更多)」** —— 客户端没有可信总封数,
就不能把 50 封说成全部(pi 提醒的"别让只取 50 封伪装成只有这么多会话")。
WebUI 侧不读这个字段,故只影响鸿蒙。

## 联系人页补卡片视图(撤 tab 的前置)

- 右上角切列表/卡片,标题「联系人」/「工作列表」(与 WebUI 同词),切换规则在
  `nextContactView()`;
- 卡片对应 WebUI 的 `WorkCard`:Agent 名 + 工作目录 + 未读徽标、会话别名、
  **主题当主角**、最新摘要 + 人/Agent 标记、`N 封 · 时间`、权限档位徽标、
  **往返预算条**(同一档位判据)。
- 平级「会话」tab 暂留:撤 tab 按 pi 的顺序排在后面单独一步(撤早了预算/status/from_agent 没处看)。

## 验证

- `hvigorw assembleHap` **BUILD SUCCESSFUL**(`.ts` 纯逻辑模块被 `.ets` 引用,实测可行)。
- `npm test` **退出码 0**:窄屏布局全通过、主题 30、背景 34、cross-client 8、
  harmony-logic 14、packaging 3、vitest 258/258;新判据已接进 `npm test`。
- **视觉与点击仍未验**(无设备):展开手感、卡片间距、组头命中区没有任何自动判据
  能代替人眼 —— 交付按"结构/逻辑已验证、观感未验"写,未写成已完成。
2026-09-14 13:36:05 +08:00
51522dd8c2 fix(webui): 导航令牌迁移收尾 + 生成 CSS 的头注释提前闭合(会丢规则)+ 两条过期判据重写 + 判据进 npm test
## 改法

- **`Sidebar.tsx` 底部两个按钮还是旧的深色导航假设**(`text-chrome-400
  hover:text-white hover:bg-chrome-800`、外层 `border-chrome-700`):导航改成白玻璃
  (`--nav-bg: 255 255 255 / 0.72`)之后,`chrome-400` 落在白底上约 2.6:1(图标要
  3:1),hover 还会在白导航上闪出一块近黑。账号头像那个按钮当时换成了 `.nav-item`,
  这两个漏了 —— 就是"导航令牌迁移做了一半"。已换成 `.nav-item` + `border-gray-200`。
  没加"Sidebar 里不许有 chrome-*"的判据:另有一处 `bg-chrome-600 text-chrome-100`
  是实心小色块(正常用法),一刀切会误红。

- **`background-takeover.generated.css` 的头注释提前闭合**:生成器在注释里写了
  「src」加「/」加两颗星加「/」加「.tsx」,其中那对「星号 + 斜杠」把 CSS 注释就地
  结束 —— 尾巴变成 CSS 正文,并与第一条规则的选择器连在一起成为非法选择器 ⇒
  **`.bg-amber-100` 那条接管规则被浏览器整条丢掉**(壁纸模式下不再变半透明)。
  构建只给一条 `[WARNING] Unexpected "14" [css-syntax-error]`,不报错、不影响构建,
  正是这套判据存在的理由。改在生成器(注释里只描述、不写 glob 字面量)并重新生成:
  压缩输出现在以 `html[data-bg=on] .bg-amber-100{` 起头、14 条规则全在、无告警。
  另加两条判据("头注释没提前闭合" + 自检)。

- **`background.test.mjs` 第 18 组三条重写**:原先断言「浅色/深色两套 `--nav-bg` 都
  定义」与「壁纸模式下 `.nav-rail` 里有 `backdrop-filter`」,两条编码的都是**已被
  有意撤掉的设计**(`faacd3c` 撤深色导航、`9aa702b` 撤导航自叠模糊),于是 `npm test`
  在 HEAD 上恒红。判据红成常态就不再是判据 —— 该改的是判据本身,而不是把缺陷写回代码。
  改成方向相反的两条:`.dark` 不许单独给导航换色 / 模糊只由壁纸层负责。

- **`cross-client-theme.test.mjs` 从来不在 `npm test` 链里**(vitest 只收
  `test/components` 与 `test/stores`)—— 那条"防漂移判据"从没在默认套件里跑过。
  已加进 `npm test`。

## 验证

- 变异测试:往 `.dark { }` 塞一行 `--nav-bg: 15 23 42 / 0.72;` → 红;往
  `html[data-bg='on'] .nav-rail` 塞 `backdrop-filter: blur(18px);` → 红;撤回 → 绿。
- `npm test` 退出码 0:窄屏布局全通过、主题 30、背景 34、cross-client 8、packaging 3、
  vitest 258/258;`npm run typecheck` 通过。
- packaging 第 3 条此前是红的,但**不是判据过期**:安装包真的落后于 dist,而 `npm test`
  的 `&&` 链一直在 background 那条就中断,`packaging` 从没跑到过。已 `npm run build`
  + `npx electron-builder --linux -c.electronDownload.isVerifyChecksum=false` 重打包。
  ⚠️ deb 目标在本机打不出来(fpm 的 portable ruby 在 `Dir.chdir` 处退出),
  AppImage 与 `linux-unpacked` 正常。
2026-09-14 13:29:57 +08:00
ababe4ae56 fix(webui): 壁纸被不透明层盖住 —— 玻璃不再层层相乘 + 浅色表面类全部接管
用户报的:「webui 目前在壁纸底上叠了太多不透明层,导致壁纸效果很差,几乎看不出来」,
追问后补充:「不只是玻璃,而是很多界面是不透明的」。两句都成立,是两个叠加的原因。

## ① 玻璃层层相乘

每层面板都吃同一个不透明度 a,两层就是 1-(1-a)²。a=0.82 时两层 0.97、三层 0.995
—— 壁纸在数学上被吃掉,而且每层各做一次 16px 模糊,图案被糊成灰块。
改法:玻璃只出现一次(外层 0.62 + 18px 模糊;嵌套层只留 0.3 色调且不再模糊;
第三层透明)。同时把遮罩 24%→12%、照片自身模糊 8→4px(那张被洗白的锅之一)。

## ② 14 个浅色表面类根本没被接管("很多界面是不透明的"就是这条)

旧规则只接管 white / gray-50 / slate-100,而源码里在用的是
gray-100(24 处)、blue-50(17)、red-50(13)、blue-100(8)、gray-200(8)、green-50(8)…
—— 全是实心的,正好盖住壁纸。

清单改为**从源码用法生成**(`scripts/gen-background-takeover.mjs` → 生成的 CSS,
构建前自动跑),因为手写清单必然烂;判据保证"源码里出现的浅色表面类必须都被接管",
并明确排除页面底(gray-50/slate-100 必须保持**全透明**,不能被改成半透明)。
按钮/徽标(*-600/700、chrome-600/700)**故意保持不透明**:小控件可读性优先
(index.css 里原有注释记着实测 4.46:1 的教训)。

## 判据

- `test/background.test.mjs` 23 条(新增 8 条):玻璃算式(两层 ≤0.85)、外层 ≤0.70、
  嵌套规则存在、**判据自检**(旧值 0.82 必须算得出 >0.95)、表面类覆盖、页面底不得进清单、
  清单非空跑。犯过一次错:第一版把判据追加在 `process.exit()` **之后** ⇒ 根本没执行,
  从"测试数没变"才发现。
- `test/manual/wallpaper-layers-verify.mjs`(真实渲染,自带无头 Chromium):
  用**纯红壁纸读绿通道**测有效不透明度(纯色下 backdrop-filter 不影响读数)。
  面板 p95:**修复前 82% → 修复后 62%**;反向对照(改回旧值)能分辨 ≥10 个百分点;
  深色照片面上面板仍是浅底;关掉壁纸的同坐标对照更实。5/5。
  过程中作废了两个指标:只看绿通道会把深色元素误判成"盖死";"面积占比"类阈值
  (Δ≥8/≥40)在 0.97 时仍会蹭过门槛,饱和而无分辨力 —— 同坐标比值才可信。

## 交付

已部署(网关内嵌 WebUI 重建):线上 CSS 已含 `--bg-glass-inner`、嵌套规则与 14 个接管类。
2026-09-14 08:09:22 +08:00
8b2206ed53 fix(electron): Phase 3 验收抓到的两个静默缺陷 —— 白屏与登录
Phase 3(写信 + 附件 + 权限面板)的验收脚本第一次跑就把这两件事翻出来了,
两个都**表现正常**:进程活着、窗口标题对、接口能通,只有结果不对。

## 1. 打包后的应用是白屏(vite 的 base 缺省值)

`vite.config.ts` 没设 `base`,Vite 按默认的 `/` 生成 `src="/assets/index-xxx.js"`。
同一份 dist 有两个宿主:网关在 `/` 下伺服它(Web 正常),Electron 用 `loadFile()`
从 **file:///…/dist/index.html** 加载它 —— 绝对路径在那儿解析成
`file:///assets/index-xxx.js`(不存在),**JS 根本没加载**。

现场:`#root` 里一个子节点都没有。没有报错对话框,控制台里只有一条不起眼的
资源加载失败。而当时所有既有检查都是绿的:`npm run build` 成功、deb 元数据检查、
asar 内容清点(**它们只看文件在不在,不看文件引用什么**)。

修法:`base: './'` —— 两边都对(Web 在 /index.html 里 `./assets/x.js` → `/assets/x.js`;
Electron 在 dist/index.html 里 → `dist/assets/x.js`)。

## 2. 桌面壳用账号密码登录是断的,而且静默失败

账号密码登录靠 `SameSite=Lax` 的会话 Cookie,而桌面壳的页面是 `file://`
(**不透明源**)—— Chromium 按第三方上下文处理它,Cookie **不予存储**。

实测现场:`POST /auth/login` 返 **200**、响应体能读出用户名,但 `document.cookie`
是空的,紧接着的 `/auth/me` 返 **401**;界面停在登录页,看起来像「密码错了」,
而同样的账号密码用 curl 登录是成功的。所以这不是凭据问题。

修法:桌面壳里**不再给账号密码表**(一个必然失败的按钮比没有更糟),改成粘贴
**用户密钥**(`Authorization: Bearer`,桌面端本来就该这么用):
- preload 显式声明 `__AGENTMAIL_SHELL__ = 'desktop'`(宿主契约,而不是让渲染层
  sniff 协议;顺带让 jsdom 里可测 —— 那里的 `location.protocol` 不可重写)
- 新增 `authStore.loginWithKey`:成功后才留下令牌,失败**还原**(否则之后每个请求
  都会带上这个坏 key 并 401,而人看到的是「重输一次也不行」)
- 顺手修了 label 与 input 没有关联(`htmlFor`/`id`)—— 无障碍缺陷,也让测试能按标签查

## 验收

- 结构性守卫进 `npm test`(`test/packaging.test.mjs`,不需要浏览器):base 必须是
  相对路径、产物里不能有绝对资源引用、**安装包里的 dist 与当前构建一致**
  (前端改了没重打包时,装上去的人看到的是旧界面,两边不一致却谁都不报错)。
  判据自检过:把 base 改回 `/` 或把产物改回绝对路径,各自都能让对应那条变红。
- 组件测试 6 条(两种壳的形态、密钥成功/失败、空密钥不可提交)。
- `test/manual/desktop-phase3-verify.mjs`:真起打包好的应用(xvfb + CDP),
  一条贯穿的链 —— 用桌面 UI 写信带附件 → 外部核验信与附件真到了网关 →
  这封信触发 zcode 的真实授权请求 → 在桌面**授权面板**里点同意 →
  外部核验 **Agent 真的执行了**(标记文件出现)。第二次跑 14/14 全绿。
- 客户端全量 222/222;网关换新产物后 Web 依旧正常(相对路径在 `/` 下同样成立,
  实测渲染出收件箱、无控制台错误),并真发一封邮件确认回信到达。

## 判据自己的错(记一笔)

第一次跑时「附件真的挂在信上」报红,而库里那 41 字节的附件**明明挂在信上** ——
我把端点写成了 `/me/mail/{id}`(不存在,404),正确是 `/mail/{id}`。
判据用错端点时以「附件是空的」现形,看起来像功能 bug。

另:`pkill -f 'agentmail-web'` 会把**自己这条命令**也杀掉(命令行里含同样的字符串),
表现是「脚本没有任何输出、退出码 143」。改用端口定位(`ss -tlnp | grep :9223`)。
2026-09-12 20:25:00 +08:00
84c1d749cd feat(webui): 自定义背景 + 外观现代化;修正实心按钮白字在深色下的对比度
# 自定义背景(新功能)

三选一:不设 / 预设渐变 / 自定义图片,另加压暗与模糊两条滑杆。

**预设的色值全部复用现有调色板变量**,因此自动随主题变化 —— 那一组
(50–300)在深色下本来就是暗的(见 .dark 与 theme.test.mjs 第 19 条),
于是浅色得到柔和 pastel、深色得到低沉暗调,不需要维护两套渐变,也不会
出现「深色模式下原样落下浅色渐变」这类绕过主题变量的错误。

图片路径的关键取舍:
- **先压缩再存**。手机直出照片 4–8MB,而 localStorage 配额约 5MB,直接写会抛
  异常,用户看到的是「选了图片没反应」。等比缩到最长边 2560px、转 JPEG;
  仍超限则再缩一档;再不行就**明确拒绝并说明原因**(不是静默失败)。
- 失败一律返回 `{ok:false, reason}` 并渲染成 `role="alert"`。

# 背景层为什么不放进主题 store

主题(light/dark/system)是必须全局一致的语义;背景是纯装饰偏好,取值空间
与主题毫无关系。混在一起会让「跟随系统」的实现被背景字段淹没。

# 背景层实现在 CSS,不改 27 个组件

按 Tailwind 生成的实际类名统一接管:背景开启时让出不透明的页面底
(body / bg-gray-50 / bg-slate-100 → 透明),并把卡片(bg-white)与框架
(bg-chrome-800/900)变成半透明 + 背景模糊。

逐个组件加 class 必然漏 —— 漏掉的那块就是一张不透明卡片浮在背景上。
这段 CSS **刻意放在所有 @layer 之外**:它要覆盖的正是 utilities 生成的
`.bg-white`,写进 @layer components 会被 utilities 压过去(静默失效),
而分层 CSS 恒输给未分层 CSS,这是唯一稳定可靠的位置。

**chrome-600/700 刻意保持不透明**:它们不是大面板,而是导航项与 15px 的
计数徽标。真实渲染量得半透明会把徽标上的数字压到 4.46:1,低于 AA 4.5 ——
小控件的可读性优先于装饰效果(已用脚本量出,见下)。

# 「跟随系统」的可见性

三态本来就已实现(system 为默认值 + matchMedia 监听)。这次做的是让它可被
发现与信任:选择器改成分段控件(role=radiogroup + aria-checked),说明文案
写清「跟随系统会随系统的深色开关自动切换」,并保留单选按钮入口的
「当前跟随系统:深色/浅色」提示。

# 外观现代化

- **圆角整体调大一档**(默认 0.25→0.5rem)。原值是几年前的紧凑风格,
  在宽屏桌面应用上偏硬。只改比例尺,200 处圆角一次性刷新,不产生
  「新组件大圆角、旧组件小圆角」的断层。
- 语义化圆角令牌:`rounded-card` / `rounded-control`(数值档位答的是「多大」,
  这两个名字答的是「用在哪」)。
- 自定义滚动条(桌面应用里常驻可见,系统默认样式偏旧)。
- 键盘焦点环(`:focus-visible`,仅键盘导航时出现;可访问性硬要求)。
- 交互元素统一过渡;并尊重 `prefers-reduced-motion`。

# 顺带修正两处真实问题(都由真实渲染量出,不是估算)

1. **实心按钮白字在深色下 4.46:1,低于 AA**。
   深色 `--c-on-accent` 是「近白」244 246 250(为了不刺眼),而结构检查第 23
   条只拿**浅色**的纯白 255 去算 → 4.83 通过。**测试存在盲区**:
   同一个实心底,白字换暗一点点就越过 AA 线。导航未读徽标「12」正是这个组合。

   两处都修:把第 23 条改成**两种模式的 on-accent 都算**(闭合盲区),
   并把深色 on-accent 抬到 250 250 252(4.65:1,仍非纯白,保留原初衷)。

2. **theme.test.mjs 切颜色块的方式很脆**:它用 `indexOf('.dark')` 切片,于是在
   :root 的注释里写一句带点的选择器写法就会把浅色块提前截断(我加注释时
   真的踩到了,第 8 条假失败)。更危险的是反向情形:块被截短后变量集合变小,
   「覆盖齐全」这类断言可能**真空通过**。改为所有块切分都基于**剥注释后**的文本。

# 测试

- 新增 `test/background.test.mjs`(15 条结构检查):遮罩两主题各一份、
  背景层必须负 z-index(0 会盖住界面)、背景开启时必须让出页面底、
  玻璃化只在 data-bg=on 下、悬停态一并接管、预设复用调色板变量、
  图片上限与失败原因存在、尊重 reduced-motion 等。
- 新增 `test/stores/background.test.ts`(16 条):脏数据归一化(未知预设、
  kind=image 却无图、越界数值)、CSS 变量写入与清理成对(残留 --bg-image 会
  让「关掉背景」后仍显示旧图)、localStorage 抛异常不打断操作。
- 新增 `test/manual/background-verify.mjs`:连真实 Chromium 验收**渲染结果**
  (背景层是否真的可见、玻璃化的计算样式、正文在背景之上是否仍达 WCAG AA、
  自动模式在**不刷新**页面时跟随系统切换、显式选择不被系统覆盖)。
  它拦住了上面两个真问题,也拦住了我自己两次写错的判据。

# 验证

- typecheck 干净
- 主题 30/30、背景 15/15、vitest 216/216(新增 16)
- 真实渲染验收 23/23(AGENTMAIL_DIST 注入本地构建 + 活 Gateway,未部署即验收)
- 截图对照:浅色/深色 + 极光背景,面板玻璃化与层次均符合预期
2026-09-12 08:02:30 +08:00
dacf6c0e1f feat(client): 打通 Electron 安装包打包,并留下构建文档
# 背景

Electron 桌面安装包一直没打出来过(`release/` 为空,只有 web bundle)。
这次把它跑通,并验证了产物本身而不只是"文件存在"。

# 改动

**package.json 补齐 electron-builder 需要的元数据**(缺哪个就会让某个 target
直接失败,而报错不一定指向字段本身):

| 字段 | 位置 | 不填的后果 |
|---|---|---|
| `description` | 根 | deb 描述为空 |
| `author`(含 email) | 根 | deb 缺 maintainer |
| `homepage` | 根 | **deb 直接失败**:`Please specify project homepage` |
| `desktopName` | 根 | 窗口无法与 .desktop 关联(缺 `StartupWMClass`) |
| `linux.syncDesktopName` | `build.linux` | 同上 |
| `linux.synopsis` | `build.linux` | deb 描述只有一行 |

**新增 `client/electron/BUILD.md`**:记录构建命令、本机两个坑(见下)、
元数据清单、两个产物的实质区别、以及验证产物的方法。

# 两个产物(已在 release/,被 .gitignore 排除)

- `AgentMail-0.1.0.AppImage` 122MB,有效 x86-64 ELF、可执行位已设
- `agentmail-web_0.1.0_amd64.deb` 100MB,Maintainer/Homepage/Depends/两行 Description 齐全

**实测的实质区别(不是猜测,来自解包对照)**:

| | AppImage | deb |
|---|---|---|
| `.desktop` Exec | `AppRun --no-sandbox %U` | `/opt/AgentMail/agentmail-web %U` |
| Chromium 沙箱 | **禁用**(squashfs 无法保留 setuid 的 chrome-sandbox) | **启用**(postinst 按能力 `0755` 或 `4755`,并装 AppArmor 配置) |
| 卸载 | 删文件 | postrm 清理 alternatives / AppArmor / desktop-mime 库 |

结论写进文档:**对外分发优先 deb**。

# 验证(不只查存在性)

- AppImage:`--appimage-extract` 解包成功;`resources/app.asar` 8.5MB;
  asar 清单里 `/dist/index.html`、`/dist/assets/{index,CalendarView}.js`、
  `/dist/assets/{agentmail.svg,favicon.ico,apple-touch-icon.png}`、
  `/electron/{main,preload}.cjs` 齐全;`index.html` 里 DOCTYPE 仍是大写
  (即格式化器修复也进了包)
- deb:`dpkg-deb --info/--contents` 核对元数据与布局;7 档图标尺寸齐全;
  读 postinst 确认沙箱策略与 AppArmor 安装

# 环境坑(已写进 BUILD.md)

**本网络下 `SHASUMS256.txt` 不可达**(直连 20s 超时、走代理也失败),
而 electron 构件本身 0.8s 就拿到(HTTP 206)。`@electron/get` 即使命中缓存
也会取校验文件 → 构建卡 10 分钟后失败。用命令行覆盖绕过:

    npx electron-builder --linux -c.electronDownload.isVerifyChecksum=false

**刻意不写进 package.json** —— 那会让今后每次构建都跳过完整性校验。一次性绕过
网络限制不该固化成永久弱化的默认值。代价已写进文档(关校验后可被篡改镜像在
TLS 之外替换构件)。

# 待用户确认的占位值

- `author.email` 用了仓库自身的 git 身份 `jianf@noreply.localhost` —— 容器占位邮箱,
  **不是真实联系地址**。项目里没有可用的真实邮箱,我没有编造一个。
- `homepage` 用 git remote 的唯一真实地址(内网 Gitea `192.168.2.106:3000`)。

两者对外分发前都应替换。
2026-09-12 01:17:49 +08:00
a404cbad54 feat(branding): 确定项目图标,并接入 Web / Electron / HarmonyOS
# 唯一源

`client/electron/src/icons/agentmail.svg` 是图标唯一源(24×24 视图框,`currentColor`
跟随文字色)。此前各端用的都是占位物:Electron 的窗口/托盘指向一个**不存在**的
`src/icons/tray-icon.png`(`nativeImage` 拿到空图,托盘不可见),
HarmonyOS 的 `startIcon/foreground/background` 是 1×1 PNG,
Web 端根本没有 favicon。

# 为什么带生成脚本

PNG/ICO 是二进制的,换一次配色要重出十几个尺寸,手工做必然出现
「Web 是旧的、Harmony 是新的」这种不一致,而且没人能复核。
`generate.py` 只认上面那一份源,所有变体都由它推导(本机无 rsvg/ImageMagick,
用 cairosvg + Pillow)。改图标只需改源文件再跑一次脚本。

# 各端产物

- **Web**:`public/assets/{agentmail.svg,favicon.ico,apple-touch-icon.png}` + `index.html` 引用。
  放 `assets/` 下而非根目录,是因为 Gateway 只把 `/assets/*` 与 `/` 交给静态处理器
  (`server/cmd/server/main.go`),放根下会 404。已实测本机与 LAN 均 200。
- **Electron**:应用图标 `icon.png`(512) / `icon.ico`(16–256) / 各尺寸 PNG /
  托盘 `tray-icon.png`(32),`package.json` 里 `win.icon` 与 `linux.icon` 指过去。
  托盘用品牌色字形而非白色 —— 浅色面板下白色会消失。
- **HarmonyOS**:`startIcon.png`(512) 用完整应用图标(启动页底色浅 `#FFF` /
  深 `#000`,白底蓝图标两套都立得住);分层图标的 `background` 是品牌色整块、
  `foreground` 是白色字形并留 12% 安全区,避免被系统圆角裁掉。
- **应用内**:新增 `BrandMarkIcon`(fill 型,与现有描边图标集不同族),
  替换登录页与初始化页品牌位的占位 `MailboxIcon`;后者已无引用,一并删除。

图标色 `#2563eb` 与门户 Dashy 主题主色一致。

# 验证

- 前端 typecheck 与 196 项测试全绿;`npm run build` 产物含三个图标文件
- Gateway 重新部署后 `/assets/{agentmail.svg,favicon.ico,apple-touch-icon.png}`
  在本机与 `192.168.2.60:8180` 都返回 200,Content-Type 正确
- 所有 PNG/ICO 用 Pillow 复核尺寸与 alpha 边界(合成失败会表现为全透明,
  已用 getbbox 排除)
2026-09-11 15:49:41 +08:00
f9d757b5e5 chore: directory migration - gateway→server, web→client/electron 2026-09-08 19:16:35 +08:00