pi 的交付清单里缺的两块(`docs/GUI-PLAN-HARMONY.md` 原先把管理后台划在首版之外, 用户明确要求「功能做全再给我」之后收进来)。 标 `跨端:` 是因为判据落在 `client/electron/test/`(鸿蒙的判据目录一向量在那里), 代码本体全在 `client/harmony/`。 ## 管理页(用户管理) - `pages/AdminUsersPage.ets`:新建 / 编辑(显示名·角色·白名单)/ 启停 / 重置密码。 排布照 `AdminUsersPage.tsx`,包括「受限」徽标口径(普通用户且白名单非空才显示)、 最后登录缺席与空串都显示「从未登录」、管理员对白名单两项忽略。 - 入口在设置页底部,**仅管理员可见**(`role === 'admin'` 严格相等,与 `App.tsx` 同口径)。 读不到身份时**不**显示也不报错(乐观放行会让每个普通用户看到点进去 403 的入口)。 - `api/AdminApi.ets` + `model/AdminUsers.ts`(纯逻辑,零 import ⇒ 判据能真跑)。 - 启停**只发 status 一个字段** —— 服务端是部分更新,多发字段会把显示名与白名单一起改掉。 - `model/Models.ets` 补管理端 DTO;`main_pages.json` 注册路由。 ## P4c 壁纸上传 - `model/ImagePrep.ts`:阈值与两档策略(2560/0.85 → 1280/0.78,入口 20MB,压后上限 3.5MiB)。 **一处有意不对齐 WebUI** 并写明理由:WebUI 卡 data-URL 长度(含 base64 膨胀), 鸿蒙内存直传 ArrayBuffer,卡的是字节数。 - `common/BackgroundPicker.ets`:不设 / 预设 / 自定义图片 + 浓度与模糊滑杆。 上传链:picker → 判可不可以 → 逐档按 desiredSize 解码压缩 → 上传 → **请页面以服务端为准重新同步**。 失败**必带原因**(服务端 415/413 文案原样透出)。用户取消选图**不算失败**。 - `ApiClient.uploadBytes`:MultiFormData.data 收 ArrayBuffer(核了 SDK,since 11;本工程 23) ⇒ 内存直传,不需要 base64 也不需要临时文件。 ## 两处真 bug(变异测试逼出来的,不是"新写坏的") 1. **压缩循环的第二档此前是死代码**:循环里的 break 与循环外那句 shouldRetryWithActual 互相抵消 —— 把循环里那处改成 `if (true)`(永远只压一档)整套判据照样全绿。 收成一处判定(overLimit),循环外只读结论,并加结构性判据(该函数在这条链上只许调用一次)。 2. **壁纸的模糊档一直是「只写不读」**(计划文档 §7.12 登记过):滑杆能拖、值能存、 blurStyleFor 也写了,就是**没有调用点**,壁纸一点没糊。 本次补上的调用点分两层:壁纸层 `.blur(px)` = **图片内容模糊** (与 WebUI 的 `filter: blur(var(--bg-blur))` 同一个量、同一个数,所以不需要映射表); 而那张**材质档**映射表 `blurStyleFor` 也终于有了调用点(`navMaterialFor` 内部复用它)。 `docs/HARMONY-ALIGN-PLAN.md` 的 §7.12 两行(材质 / 壁纸模糊度)已一并改准、不再互相矛盾。 ## pi 复核后**改回来的**(这一笔里我自己犯的两处,都由 pi 抓出) 1. **导航条材质一度绑定到 `bg_blur`,`bg_blur=0` 时整个消失。** 我把 `NavBar` 从固定档改成 `blurStyleFor(bgPlan.blurPx)`,而滑杆 `min: 0` 可达、 `blurStyleFor(0) === 'NONE'` ⇒ 用户把壁纸调清晰时**导航条一点材质都没有**。 而且它与本笔自己的论证**相反**:刚论证完"图片内容模糊"与"面板材质"是两个物理量, 转头把面板材质接到壁纸模糊这个输入上。 现在**分层**:`blurStyleFor` 是通用映射(**允许** NONE —— "0 px 不模糊"是它的正确语义); `navMaterialFor` 是**导航条专用、有下限**的入口(0 px ⇒ 最薄档)。 判据钉**可达性**(滑杆 0..40 每个整数 + 界外值都不许 NONE,且三档都要出现 —— 否则"恒定最薄档"会让滑杆成为死控件)。 2. **`Theme.navMaterial` 被我弄成了死令牌**,而看着它的判据**照样绿** (那条只断言"声明存在且不是 NONE" —— 守的是声明,坏的是活的调用路径)。 现在导航条真的用它;并把同文件里**只覆盖 `Theme.overlay` 一个令牌**的死令牌规则 **铺到 Theme 的全部 35 个令牌**(量**外部引用数**:只被 Theme 内部方法读、 而那个方法自己有外部调用点 ⇒ 不算死 —— `chipSpentBg` 是这种;`navMaterial` 当时 唯一的消费者是一张可整体删掉的局部表,所以必须被抓)。 ## pi 复核后**补上的**(这一笔漏掉的接线,都是我造成的) - **`test/run-all.mjs` 的 SUITE 没接两个新判据文件** ⇒ HEAD 上 `npm test` **一条判据都不跑、直接 exit 1**(套件自检 2 就是为这件事写的)。已接入, 并把两条登记进 `STATIC_ONLY`(`.ets` 要设备 ⇒ 静态欠账)。 - **`debt-visibility` 是我自伤**:那两个新文件里有 5 处"边界声明"但一次都没登记。 我当时报"2 条失败是改动前就红" —— **只对一半**:这条在父提交上是**绿的**。 我那次 `git stash push -u -- client/harmony` 的对照是**无效对照** (`-- client/harmony` 把 `client/electron/test/` 整个排除在外,新判据文件根本没被 stash), 所以两次跑都红、看着像"既有"。已按 pi 的建议改用 `git worktree` 到父提交做对照。 现在两处都登记进 `docs/DEBTS.json`(含 `static-criteria` 5→7,Go 侧同一份登记同步改)。 ## 一并修正的旧判据(都是"太宽/太窄/钉错东西",不是放宽标准) - 「模糊归属」:原文「壁纸层不许有**任何**模糊调用」把**图片内容模糊**与**面板材质** 混为一谈(WebUI 侧核实:`.app-backdrop` 的 filter 与它之上那层的 backdrop-filter 是两个不同的量)⇒ 改成按两种模糊分别钉。 - 「bgBlur 只写不读,消费侧必须为 0」:值不再成立,**形状保留**(逐文件登记 + 计数 + 理由), 标题与断言里的假话一并改掉。 - isDarkMode 那条 `/dark\s*\)/` 断的是**参数顺序**(加一个入参就误红)⇒ 改成"dark 在实参里"。 - 三条钉 `backgroundBlurStyle` **整条字面表达式**的断言 ⇒ 改成钉语义 ("用系统材质 + 材质有下限"),不再匹配那一行的字符。 ## 判据 新增 `harmony-admin.test.mjs`(22 条)、`harmony-imageprep.test.mjs`(29 条); `harmony-presets.test.mjs` 加 1 条(模糊档搬运与归一,含 `-0` 那个洞: `Math.round(-0.4)` 是 `-0` 而 `-0 < 0` 为 false ⇒ 改成判 `!(r > 0)`)。 **`node test/run-all.mjs`:22 个判据文件全部跑起来**,红的只有 1 个: `build-stamp`(`dist` 是 `a5fc86b` 上构建的,`gitRev` 对不上当前 HEAD)。 这条**不是我的代码造成的**(可证:`a5fc86b..HEAD` 之间,`srcHash` 覆盖的那批文件 ——`client/electron/src` 等——**一个都没动过**,所以 `srcHash` 没变,差的是 `gitRev`), 但也**不是"改动前就红"**:任何推进 HEAD 的提交都会让它变红,正确修法是重构建。 ## 未验(如实标注) - **本机无设备/无模拟器 ⇒ 全部观感未验**:管理页排版与卡片观感、滑杆手感、 模糊在真机上的实际档位观感、系统材质在自绘悬浮条上的实际效果。代码齐 ≠ 真机验过。 - 预设档**没有**上模糊(壁纸在预设档下是一叠自绘矩形,系统材质对它不生效)—— 这是我**主动收的范围**,不是漏,真机看一眼再决定要不要补。 - **Go 侧的 `debt_registry_test.go` 我没能跑**(沙箱里没有 Go 模块缓存,`go test` 起不来), 只做了 `gofmt` 校验;那处改动是一行 `Count: 5 → 7`。
14 KiB
AgentMail 鸿蒙客户端(ArkUI)构筑计划
邮件驱动·多智能体协作平台 — HarmonyOS 客户端 构筑者:dsh | 版本:v0.1 | 日期:2026-09-07 上游接口契约:docs/API.md · 平台规划:docs/PLAN.md
0. 定位与分工
- jianf(发起人)→ 收件方 pi 负责 ele 客户端(Win/Linux),并作为长期维护者给我方提供 API 解读与支持;
- dsh(我)→ 负责 鸿蒙客户端(ArkUI),本计划即鸿蒙侧的构筑蓝图。
- 平台后端是 同一个 Gateway(Go + SQLite,HTTP REST + SSE),鸿蒙客户端调用其人类接口
(
/api/v1/me/*等),与 WebUI 完全等价,没有私有通道。 - 交付形态:一套 Stage 模型 ArkTS 工程,以 HAP 形式运行于 HarmonyOS 手机/平板/2in1, 覆盖 WebUI 的「邮件收发 + 会话 + 联系人 + 权限决策」核心功能面。
本机工具链(已勘察确认):
| 组件 | 版本/位置 |
|---|---|
| DevEco CLI | 26.0.0 Beta2,/opt/huawei/command-line-tools(devecocli 1.3.0 已包装在 /usr/local/bin) |
| HarmonyOS SDK | 6.1.0(API 23,Version 6.1.0.105 Release),/opt/huawei/harmonyos/ohos-sdk/linux |
| 包管理 | ohpm(command-line-tools/ohpm) |
| 模拟器 | HarmonyPhone(phone / HarmonyOS 6.1.1(24),KVM 加速,按需 harmony-emu start) |
| 真机 | 可选:hdc 无线调试(动态 IP,签名 hap 走 devecocli signature generate,材料在 ~/.ohos/config) |
鸿蒙生态约束(真机安装必须签名):模拟器可装 unsigned hap;真机一律要签名。
签名材料已就绪,devecocli signature generate --force 可随时重新生成。
1. 目标功能面(与 WebUI 对齐的裁剪集)
首版(M1-M3)做「个人邮件客户端」核心闭环,管理员面(用户管理/Agent 密钥/默认预算)不在鸿蒙侧首版 范围内 —— 那是 Web 管理台的职责,鸿蒙端聚焦日常使用。
| 模块 | 功能 | 对应 API |
|---|---|---|
| 登录/凭证 | 账号密码登录;用户密钥登录(跨设备场景,Bearer) | POST /auth/login、GET /auth/me、POST /auth/logout、POST /me/keys |
| 收件箱/发件箱 | 未读/全部列表、未读标记、发件箱 | GET /me/mail/inbox?s=unread|all、GET /me/mail/sent |
| 会话列表 | 我参与的会话(卡片/列表视图)、未读数、预算徽标 | GET /me/sessions、GET /contacts、GET /contacts/suggest |
| 邮件详情 | Markdown 正文渲染、附件清单、抄送显示、对话树 | GET /mail/{id}、GET /mail/{id}/thread |
| 写信 | 收件人三段式补全、抄送、正文、往返预算、附件上传、会话别名(.new 时) |
POST /me/mail/send、POST /me/attachments |
| 回复/转发 | 回复落回原会话、转发开新线索(附件随行) | POST /me/mail/{id}/forward |
| 权限决策 | 待决请求列表、同意/拒绝/备注 | GET /permission/pending、POST /permission/decide |
| 实时推送 | 新邮件、权限决策、会话更新(SSE) | GET /events/stream?access_token= |
| 会话管理 | 改名、预算调整、权限档位调整(plan / workspace / full)、改名提议展示/接受/驳回、归档 | PUT /sessions/{id}/alias、GET|PUT /sessions/{id}/budget、PUT /sessions/{id}/permission、rename-proposal 组、POST /contacts/archive |
| 权限档位 | 会话头部展示当前档位与强制力(permission_mode / permission_enforcement),可随时调整 |
见会话管理行;档位三值 plan / workspace / full |
| 附件 | 下载(保存到本地/分享) | GET /me/attachments/{id} |
明确不做(首版):管理员后台、Agent 侧接口、日历(/calendar/*,Web 端新功能,待 api 解读后二版评估)。
2. 技术方案
2.1 工程形态
- Stage 模型 +
@ohos/http(默认网络库,无需三方依赖),entry单模块起步; 若 UI 面膨胀再拆common/feature多模块,首版不预先拆。 - 语言:纯 ArkTS(严格模式,
arkts-*lint 全开),不用 JS/TS 混写。 - 状态管理:首版用组件内
@State/@Link/@Provide即可;跨页共享(登录态、token、未读数) 提炼为单例 Service 层(@ohos.data.preferences持久化凭证),不引第三方状态库。
2.2 API 层(核心设计)
对齐 WebUI 的 src/api/ 思路,鸿蒙侧实现一份同构的客户端:
entry/src/main/ets/
├── ability/ EntryAbility.ets
├── pages/ 登录 / 主界面 / 会话 / 邮件详情 / 写信 / 权限
├── api/
│ ├── ApiClient.ets # 统一 request():base + headers + 错误归一化(400/401/403/404/409/429)
│ ├── AuthApi.ets # login / logout / me / keys
│ ├── MailApi.ets # inbox / sent / detail / thread / send / forward / read
│ ├── SessionApi.ets # sessions / alias / budget / rename-proposal
│ ├── ContactApi.ets # contacts / suggest / archive
│ ├── PermissionApi.ets # pending / decide
│ ├── AttachmentApi.ets # upload / download
│ └── SseClient.ets # SSE 流式解析 + 指数退避重连
├── model/ # 领域模型(Mail/Session/Contact/…,与 API.md 字段一一对应)
├── store/ # 单例:SessionStore / MailStore / UnreadStore(跨页共享)
├── components/ # MailListItem / SessionCard / AddressInput / PermissionCard / MarkdownView …
└── common/ # 常量(BASE_URL、事件名)、工具(相对时间、rune 截断、Markdown 渲染)
要点:
- 基地址与凭证集中在
common/config:apiBase可运行时配置(设置页填写网关地址), 与 WebUI 的window.__AGENTMAIL_API_BASE__同思路 —— 一个构建产物可指向任意后端。 - 认证双通道:Cookie(账号密码登录,
@ohos.http手动管理 Cookie)与Authorization: Bearer <user_key>(密钥登录)。SSE 与附件下载这两个浏览器侧只能走 query 令牌的端点, 鸿蒙侧可以带请求头,优先 Bearer,避免令牌进 URL 日志。 - 错误归一化:统一把服务端
{"error": "中文可操作描述"}与 HTTP 状态码翻译成ApiError { code, message };401 统一触发「回登录页」;429/403 文案原样上屏。
2.3 SSE 实时(关键难点)
@ohos.http 的流式响应(on('dataReceive'))用于解析 SSE 帧:
- 手工按
\n\n分帧,解析event:/data:行;事件表与 API.md 一致:new_mail/permission_decision/session_update/session_archived/agent_online。 (pi 梳理补充:new_mail与session_update事件还带session_alias/permission_mode/permission_enforcement字段 —— 鸿蒙端模型层按此定义, 会话卡片同时显示档位徽标。) - 断线重连:指数退避 1s→15s 上限(对齐 Web 端
api/sse.ts),AbortController优雅停止。 new_mail到达 → 通知 MailStore 刷新收件箱与未读数;permission_decision→ 刷新权限卡片。- 生命周期:App 前台订阅、后台/退到
onBackground暂停,回到前台重连(省电 + 避免无谓重连风暴)。
2.4 Markdown 渲染
鸿蒙没有现成 react-markdown。选择(按优先级):
- 首版:轻量自研解析子集 —— 标题/粗体/斜体/行内代码/代码块/列表/引用/链接/分割线,
用
Span/RichText逐段渲染;raw HTML 一律不渲染、链接协议白名单(http/https), 守住 Web 端 Markdown XSS 回归测试立下的同一条纪律(见 PLAN.md 6.4)。 - 若正文复杂度超预期,评估
@ohos/webview加载本地渲染页(需白名单校验后再注入,引入 XSS 面,慎用)。
2.5 附件
- 上传:
@ohos.request/httpmultipart 到POST /me/attachments(字段名 file), 返回attachment_id后随send的attachment_ids使用 —— 两步流程与 Web 端一致。 - 下载:
GET /me/attachments/{id}→ 写入应用沙箱文件目录,用@ohos.file.picker/@ohos.share交出去(保存到相册/分享)。注意服务端强制 octet-stream + attachment, 按响应头Content-Disposition解析文件名(API.md 七、已暴露该响应头)。
2.6 UI 布局(ArkUI)
| 界面 | 结构 |
|---|---|
| 登录页 | 服务器地址 + 账号/密码 或 用户密钥;「保存地址」入 preferences |
| 主界面(手机) | 底部 Tab:收件 / 会话 / 联系人 / 权限 / 我的;顶栏未读徽标 |
| 主界面(平板/2in1) | 两栏/三栏自适应:左列表 + 右详情;@ohos.mediaquery 断点切换 |
| 会话列表 | 列表/卡片视图切换;卡片含主题、最新一封发件人+摘要、预算徽标(剩 1 橙 / 用尽红 / 不限不显示) |
| 邮件详情 | Markdown 正文 + 附件行 + 抄送行 + 权限卡片 + 底部「回复 / 转发」+ 对话树入口 |
| 对话树 | 缩进 + 连接线(不引图形库),分块加载:dir=around 首屏 + up/down 增量 |
| 写信页 | 收件人补全(三段式 suggest)+ 抄送 + 主题 + 正文 + 预算档位 + 附件 + 会话别名(仅 .new) |
| 设置 | 网关地址、退出登录、密钥管理(/me/keys 创建/吊销,展示 token_hint) |
窄屏三维地址补全:/contacts/suggest 无参 → name;带 name → path;带 name+path → 别名+new,
逐级展开,方向键选择(对齐 Web 端 AddressInput 交互)。
3. ArkTS 工程注意点(本机实操约束)
在写第一行 .ets 前先加载 arkts-grammar-standards 技能,按规范落地:
- 严格模式:禁
any、禁未声明类型、undefined处理显式化(??/ optional chain 白名单内)。 - 组件用
struct+@Component,状态用@State/@Prop/@Provide;列表用LazyForEach(邮件量可能上百,ForEach全量渲染会卡)。 - 网络模型用
interface描述(与 API.md 字段对齐),服务端多余字段不枚举。 - Sendable / Actor 需求确认:首版无跨线程共享大对象,先不进;若 SSE 解析放 worker 再评估。
@ohos.http需在module.json5声明ohos.permission.INTERNET(示例项目已带,照抄)。- 构建:
devecocli build/hvigorw assembleHap产出entry-default-signed.hap; 模拟器hdc install(unsigned 亦可),真机走签名 hap +hdc tconn。
4. 里程碑与验收
M0 脚手架(0.5 天)
devecocli create建 Stage 工程(包名原为com.agentmail.harmony,2026-09-14 已改为com.jianf.agentmail:AGC 实测拒绝含保留字harmony的包名,用户定新名;AGC 应用 APP ID6917616450599975320。包名必须与 AGC 完全一致,否则 Push Kit 推不到设备),API 23;- 空壳 App 在 HarmonyPhone 模拟器跑通(
harmony-emu start→ build → install → 截图); - 签名配置写入
build-profile.json5(复用~/.ohos/config材料)。
验收:启动图标进桌面,devecocli ui screenshot 可见主页面。
M1 API 层 + 登录(1-2 天)
ApiClient(Base + 错误归一化 + 双认证通道)+AuthApi;- 登录页(账号密码 / 用户密钥)+ token/凭证持久化 + 401 全局回落登录。
验收:连本机 Gateway(http://localhost:8180)用真实账号登录成功,
GET /auth/me 返回当前用户;错密码显示服务端中文文案。
M2 邮件闭环(3-5 天)
- 收件箱/发件箱列表(未读态、分页)、邮件详情(Markdown 渲染 + 附件行 + 抄送)、
回复/转发、写信页(三段式补全 + 预算档位 + 附件上传两步流 +
.new会话别名)。
验收:人 → Agent → 人 端到端:给 pi/opencode 发一封带抄送与附件的任务邮件, Agent 回信后在本端看到详情并可继续回复;新信未读计数正确。
M3 实时 + 会话 + 权限(2-3 天)
SseClient(解析 + 退避重连 + 生命周期);未读徽标与列表随事件刷新;- 会话列表(列表/卡片)、改名提议条(接受/驳回)、预算调整、权限档位调整与档位徽标;
- 权限待决列表 + 决策卡片。
验收:另一浏览器标签给本账号发信,鸿蒙端 ≤3s 内未读 +1(SSE 路径); Agent 权限请求出现在待决列表,决策后 Agent 侧收到(对照 Web 端行为一致)。
M4 对话树 + 附件落盘 + 打磨(2-3 天)
- 对话树分块加载(
around/up/down+ 滚动位置补偿); - 附件下载到沙箱 → picker/share 导出;平板/2in1 自适应布局、深色模式(如有余力)。
验收:251 封长链线索在鸿蒙端分页取完、depth 连续;真机(若可用)安装签名 hap 全流程跑通。
M5 联调 + 文档(1-2 天)
- 与 ele 端(pi)在同一 Gateway 上双端并行联调:双端同账号互斥/一致性问题暴露;
- 对照
PLUGIN-CONTRACT.md的验收清单做客户端视角补测; - 更新本计划为「已落地」,同步 README 接入说明。
预计总工时:约 2-3 周(含联调与返工缓冲)。
5. 风险与依赖
| 风险 | 对策 |
|---|---|
@ohos.http 流式 SSE 稳定性(长连接、断流) |
退避重连 + 生命周期暂停;降级方案:轮询 /me/mail/inbox(SSE 仅作加速) |
| ArkTS 严格模式对三方库不友好 | 尽量零三方依赖;网络/存储/文件全走系统 API |
| Markdown 渲染面 | 首版渲染子集 + 协议白名单;复杂稿需时评估 webview 方案并单独过 XSS 评审 |
| 真机签名链路 | 材料已就绪;devecocli signature generate --force 兜底;无真机时模拟器验收为主 |
| 服务端接口变更 | 依赖方(pi)的 API 解读;所有模型层集中在 model/,变更只动一处 |
| 日历等 Web 新功能 | 首版明确排除,M5 后按 API 解读评估二版 |
需要 jianf / pi 提供的支持:
- 一个稳定的测试 Gateway 地址(开发联调用,含可登录账号);
- 接口变化时按
docs/API.md格式及时更新 —— 鸿蒙端模型层严格对齐该文档。
6. 与 ele 端(pi)的协同点
- 两端共用同一份 API.md 契约,建议把「API 解读」沉淀为
docs/API-INTERPRETATION.md(字段语义、边界行为、踩坑),供双端引用,避免各自重复踩; - 后端新增字段时(如日历),双端各自按需跟进;鸿蒙端涉及协议层的问题由 pi 提供解读支持(按分工)。