Files
MailUI4Agents/client/electron/BUILD.md
JianFeeeee 6be5a543af test(suite): 判据总入口 run-all —— 全部跑完再算退出码,并接上两条从未跑过的判据
## 为什么

`npm test` 是 `&&` 链:**前面红一条,后面全部不跑**。于是"只红一条"看起来像
"只有一个问题",实际后面那些判据连跑都没跑(`packaging` 那条能发现"界面改了没重打包"的
判据,长期因此隐身)。而且这套规矩下"全绿"可信、"红"不可信。

改成 `test/run-all.mjs`(pi 建议):每条都跑,红的收集起来最后一起报、一起退出。

- **自检 1**:清单里的文件必须存在(名字写错 = 一条判据静默消失);
- **自检 2**:`test/` 下每个 `*.test.mjs` 都必须接进清单 —— **新增判据忘了接线直接红**。
  这条自检当场抓出 `nav-merge.test.mjs`、`build-stamp.test.mjs` 两个"写好了没接线"的文件
  (8 + 4 条判据此前从未跑过,与 `cross-client-theme` 是同一族问题)。
- 变异验证:强制 `theme.test.mjs` 判红 → 后面 5 条判据照跑,汇总如实报"红 1/9"、退出码 1。

`package.json`:`"test": "node test/run-all.mjs && vitest run"`。

## 接线后暴露的两条陈旧判据(代码没错、判据钉的是旧写法),按"钉行为不钉字面"修

1. `setViewMode(target || commTab` —— 代码后来等价改写成
   `target ?? (isComm ? commTab : modes[0])`。改成钉行为"isComm 时落到 commTab",
   并额外要求桌面分支带 `target`(否则日历/联系人点不动)。
2. `MailView.tsx` 里 grep `glass-control` —— 授权多选胶囊已抽成共用组件 `ComposerChip`,
   样式其实是对的(neutral 未选中态就是 `glass-control`)。改成钉
   "MailView 用 ComposerChip" + "ComposerChip 用控件档"(文件会搬、组件不会)。
   两条都做了变异验证(去掉玻璃档 / 去掉 commTab 回退 → 各判红 1 条)。

## 生成产物再补一条判据(pi 提议)

用 postcss 真解析 `background-takeover.generated.css`:断言每条规则的选择器形状恰好是
`html[data-bg='on'] .bg-xxx`,且规则条数与源码扫到的清单条数一致。
只验"能解析"不够 —— 实测 postcss 对当年那份坏产物照样解析出 1 条规则(选择器前粘了
注释尾巴),所以卡形状与条数。变异:把坏头注释塞回生成产物 → 该条判红。

## BUILD.md:deb 结论撤回(不是 fpm)

`TMPDIR=/var/tmp/ebtmp` 在本沙箱建不出来;把 TMPDIR 指到工作区大盘后 deb 正常产出
(`release/agentmail-web_0.1.0_amd64.deb`,约 100MB)。日志关键行是 **`Errno::ENOSPC`**:
fpm 要把 291MB 的 `linux-unpacked` 整份复制进 TMPDIR,而本机 `/tmp` 是 9.8G tmpfs、
被 `/tmp/gocache`(4.5G)等占到 99%。所以「本机打不出 deb」是环境症状、不是工具链缺陷,
deb 不必从 `build.linux.target` 摘掉。排查命令写进 BUILD.md。

## 验证

`npm test` 退出码 0:9 个判据文件全绿(markdown-xss、narrow-layout、nav-merge 8、
theme、background 42、cross-client 8、harmony-logic 14、build-stamp 4、packaging 3)
+ vitest 258/258。鸿蒙侧 `hvigorw assembleHap` 仍 BUILD SUCCESSFUL。
2026-09-14 13:44:00 +08:00

178 lines
8.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 桌面安装包构建
生成 Linux / Windows 安装包。日常开发只需 `npm run build`(出 web 产物),
本文只讲**分发用安装包**。
## 命令
```bash
npm run build:linux # AppImage + deb
npm run build:win # NSIS 安装器
```
产物在 `release/`(已在 `.gitignore` 里,不要提交 —— AppImage 约 122MB、deb 约 100MB
> ⚠️ **安装包里嵌的是前端的一份快照**(构建时的 `dist/`)。
> 因此**任何前端改动(组件、样式、主题、背景)之后都必须重打安装包**
> 否则 `release/` 里那份会静默地是旧界面 —— 而它本身不会报错,
> 只有装上去的人会看到与 Web 不一致的样子。
>
> 自查(不需要真正安装):直接查包内 asar 里的 CSS 有没有你刚加的东西:
>
> ```bash
> node -e "const a=require('@electron/asar');const l=a.listPackage('release/linux-unpacked/resources/app.asar');
> const c=l.find(p=>/\\/dist\\/assets\\/index-.*\\.css$/.test(p));
> console.log(c, a.extractFile('release/linux-unpacked/resources/app.asar', c.replace(/^\\//,'')).toString().includes('--bg-scrim'))"
> ```
## 本机环境的两个坑
### 1. `SHASUMS256.txt` 不可达 → 构建会卡 10 分钟后失败
现象:
```
• downloaded label=electron progress=100%
Timeout awaiting 'request' for 600000ms failedTask=build
```
**不是构件下载失败**。实测该网络下:
| URL | 结果 |
|---|---|
| `…/releases/download/v44.2.0/electron-v44.2.0-linux-x64.zip` | HTTP 2060.8s ✅ |
| `…/releases/download/v44.2.0/SHASUMS256.txt` | 直连 20s 超时、走 Clash 代理也失败 ❌ |
`@electron/get` 即使命中本地缓存,也会去取 `SHASUMS256.txt` 校验,于是卡死。
**绕过**(命令行覆盖,不要写进 `package.json`
```bash
npx electron-builder --linux -c.electronDownload.isVerifyChecksum=false
```
**为什么不写进配置**:那会让今后**每一次**构建都跳过完整性校验。一次性绕过
网络限制不该固化成永久弱化的默认值。关校验后,被篡改的镜像可以在 TLS 之外替换
构件而无人察觉 —— 本场景可接受(构件来自官方 GitHub 的 TLS 连接,且本地缓存
已验证可读),但不该成为默认。
### 2. 依赖缓存齐全,可离线完成
`~/.cache/electron-builder/` 下以下条目的 `.state` 均为 `complete` 即可离线:
`appimage-12.0.1`AppImage 运行时)、`fpm@2.1.4`deb`7zip@1.0.0`
`nsis-resources-3.4.1` + `nsis-3.0.4.1`Windows 安装器)。
Electron 二进制在 `~/.cache/electron/`(首次需要网络,`electron-v44.2.0-linux-x64.zip` 约 123MB
## package.json 里与打包有关的元数据
这些字段缺失会让某个 target 直接失败,报错信息不一定指向字段本身:
| 字段 | 位置 | 不填的后果 |
|---|---|---|
| `description` | 根 | deb 描述为空 |
| `author`(含 email | 根 | deb 缺 maintainer 信息 |
| `homepage` | 根 | **deb 直接构建失败**`Please specify project homepage` |
| `desktopName` | 根 | 窗口无法与 `.desktop` 关联(`StartupWMClass` 缺失) |
| `linux.syncDesktopName` | `build.linux` | 同上,配合 `desktopName` 一起用 |
| `linux.synopsis` | `build.linux` | deb 描述只有一行 |
> ⚠️ 当前 `author.email` 是容器占位值 `jianf@noreply.localhost`
> `homepage` 是内网 Gitea 地址。**正式对外分发前必须替换成真实值。**
## 两个坑:白屏与登录(都是静默的)
### 1. 白屏 —— `vite.config.ts` 必须写 `base: './'`
同一份 `dist/` 有两个宿主:网关在 `/` 下伺服它WebElectron 用
`loadFile()`**`file:///…/dist/index.html`** 加载它(桌面)。
Vite 的默认 base 是 `/`,产物里写的是 `src="/assets/index-xxx.js"` ——
`file://` 下它会解析成 `file:///assets/index-xxx.js`(不存在),
**JS 根本没加载**
现场非常不显眼:进程活着、窗口标题是 `AgentMail`、CDP 连得上、
**``#root`` 里一个子节点都没有**。没有报错对话框,控制台里只有一条
不起眼的资源加载失败。
改回绝对路径的后果是桌面端直接不可用,而 `npm run build`、deb 元数据检查、
asar 内容清点**全都是绿的**(它们只看文件在不在,不看文件引用什么)。
所以有一条结构性断言把它钉住(进 `npm test`
```bash
node --test test/packaging.test.mjs
```
它验三件事:`vite.config.ts` 里的 `base` 是相对的;产物里没有绝对资源引用;
**安装包里的 dist 与当前构建一致**(前端改了没重打包时,装上去的人看到的是旧界面,
两边不一致但谁都不报错)。
### 2. 桌面壳不能用账号密码登录(会话 Cookie 存不下来)
账号密码登录靠 `SameSite=Lax` 的会话 Cookie而桌面壳的页面是 `file://`
**不透明源**)—— Chromium 按第三方上下文处理它,**Cookie 不予存储**。
实测现场:`POST /auth/login` 返回 **200**、响应体能读出用户名,
但 `document.cookie` 是空的,紧接着的 `/auth/me` 返回 **401**
界面停在登录页,看起来像「密码错了」,而同样的账号密码用 curl 登录是成功的。
所以桌面壳里**不提供**账号密码表单(一个必然失败的按钮比没有更糟),
改成粘贴**用户密钥**`Authorization: Bearer`
```bash
# 启动时注入(推荐:脚本化部署)
AGENTMAIL_USER_KEY=<用户密钥> ./agentmail-web
```
也可以用主进程环境变量 `AGENTMAIL_GATEWAY_URL` 指向别的网关。
用户密钥在网页版的「账号 → 用户密钥」里创建,`electron/main.cjs` 把它经 preload
注入成 `__AGENTMAIL_TOKEN__``src/api/config.ts` 从此对**所有**请求(含 SSE 与附件下载)
自动带上。
## 两个产物的实质区别(实测)
| | AppImage | deb |
|---|---|---|
| `.desktop` 的 Exec | `AppRun --no-sandbox %U` | `/opt/AgentMail/agentmail-web %U` |
| Chromium 沙箱 | **禁用** —— squashfs 里无法保留 setuid 的 `chrome-sandbox` | **启用** —— postinst 按能力决定:能跑用户命名空间则 `0755`,否则设 `4755`;并安装 AppArmor 配置Ubuntu 24+ 需要) |
| 卸载 | 删文件即可 | postrm 清理 alternatives、AppArmor 配置、desktop/mime 数据库 |
**要给用户装,优先分发 deb**:沙箱可用、依赖声明完整、可被包管理器卸载。
### deb 打不出来时:先看 `/tmp` 有没有空间2026-09-14 实测)
本机曾以为"deb 打不出来是 fpm 的毛病"(报错停在 `fpm process failed 1`,栈顶是
portable ruby 的 `Dir.chdir`,看着像路径权限)。**实际是 ENOSPC** —— fpm 会把
`release/linux-unpacked`(约 291MB**整份复制**到 `TMPDIR` 里再打包,而本机
`/tmp` 是 9.8G 的 tmpfs 且被 `/tmp/gocache` 等占到 99%(只剩 ~100MB
```bash
df -h /tmp # 先看这里Avail 只剩几十 MB 就该警惕
grep -o 'Errno::ENOSPC' <构建日志> # 真正的报错行(栈里那行只是表象)
# 把 TMPDIR 指到大盘上再打工作区所在文件系统147G 可用)
mkdir -p "$PWD/.tmp"
TMPDIR="$PWD/.tmp" npx electron-builder --linux deb -c.electronDownload.isVerifyChecksum=false
# → release/agentmail-web_0.1.0_amd64.deb约 100MB
```
结论:**不要**为了"本机打包能过"把 deb 从 `build.linux.target` 里摘掉,
也不要改 fpm 缓存 —— 那是在改测试迁就环境。deb 是声明的交付物之一。
## 验证产物的方法(别只看文件存在)
```bash
# deb 元数据与依赖
dpkg-deb --info release/*.deb
# deb 内容布局(含 chrome-sandbox 权限、desktop 文件、图标尺寸)
dpkg-deb --contents release/*.deb | grep -E '\.desktop|chrome-sandbox|app\.asar'
# postinst 的沙箱策略
dpkg-deb --control release/*.deb /tmp/ctl && cat /tmp/ctl/postinst
# AppImage不依赖 FUSE 解包并看载荷
./release/AgentMail-0.1.0.AppImage --appimage-extract
ls squashfs-root/resources/app.asar
# 确认 asar 里是真正构建的应用(而不是空壳)
node -e "const a=require('@electron/asar');console.log(a.listPackage('release/linux-unpacked/resources/app.asar').filter(p=>/^\/(dist|electron)\//.test(p)).join('\n'))"
```