Files
MailUI4Agents/docs/MULTI-ACCOUNT-PLAN.md
JianFeeeee addde97600 feat(electron): 多账号第一纵切 —— 账号存储/选择器/聚合收件箱
按 docs/MULTI-ACCOUNT-PLAN.md 实现客户端多账号的前半段(SSE 多连接与
写信账号切换留作下一轮)。

- `src/lib/accounts.ts`:纯逻辑(地址规范化、身份判重、默认账号、聚合合并),
  16 条测试钉住每条判据(含反向对照)。
- 持久化在主进程:`userData/accounts.json`,**原子写**(临时文件 + rename)+
  0600。不落 localStorage:那份存储渲染层任何脚本都可读,且 file:// 与
  http:// 是两套。无 IPC 时(浏览器)退到 localStorage 并在界面**如实写明**。
- 取信:单账号走原路径(逐字节不变);聚合时**每账号各一次请求、各带自己的
  令牌**(`fetchWithAuth`,不碰认证单例,避免并发串号)。
- ★ 只合并**同一网关**的账号:跨网关的邮件混进列表后点开会去问当前账号的
  服务器(404,或 mail_id 撞上就打开了别人的信)。如实排除 + 列表上方说明。
- ★ 部分失败可见:某账号取不到时给出账号名与原因 —— 静默丢掉它会让聚合列表
  少一整份邮件而界面看起来完全正常。
- `API_BASE` 改为 `let`(切换账号要换网关),api 层不得缓存它
  (`client.ts` 的 `const BASE` 快照已改成每次读)。
- UI:列表头下拉(≥2 个可用账号才出现「全部邮箱」)+ 账号徽标 + 账号页
  「多账号」一段(添加前调 /auth/me 验证,401 当场拒绝,不写进列表)。
- 测试:vitest 230 通过(原 222 + 新 8)、`test/lib/accounts.test.mjs` 16 通过、
  typecheck 通过。新增 `test/manual/multi-account-verify.mjs`(真起打包产物 +
  两个真实账号,判据落在网络层:聚合必须每账号各一次请求且各带自己的令牌)。
2026-09-13 06:16:59 +08:00

176 lines
7.9 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.

# 多账号架构方案Multi-Account Plan
> 本文档是 Electron/鸿蒙客户端多账号能力的单一事实源。
> piAPI 维护方编写dsh + pi 共同遵守。
> 版本v0.1 | 日期2026-09-07
---
## 一、需求背景
jianf 要求:像真正的邮箱 app 一样,**支持多账号登录、邮件单独/聚合显示**。只聚合收件箱,其余功能各账号独立。
---
## 二、架构总览
```
┌──────────────────────────────────────────────────┐
│ AccountStore状态管理
│ • accounts: Account[] 所有已添加的账号 │
│ • activeAccount: string 当前选中("all"=聚合) │
│ • add / remove / switch 账号 CRUD + 切换 │
├──────────────────────────────────────────────────┤
│ AccountManager持久层
│ • electron-store / preferences JSON │
│ • 字段:{ id, displayName, gateway, token, │
│ username?, lastUsed? } │
│ • 主进程暴露 IPCElectron/ 本地存储(鸿蒙) │
├──────────────────────────────────────────────────┤
│ MultiSSE多连接管理
│ • 每账号独立 EventSource各带自己的 user_key
│ • 事件打到对应账号的 store │
│ • 账号删除时关停对应连接 │
├──────────────────────────────────────────────────┤
│ UI 层 │
│ • 顶栏账号选择器(全部邮箱 / 单个账号下拉) │
│ • 收件箱:单账号(现状) / 聚合(合并+账号徽标) │
│ • 写信页:默认当前选中账号,顶栏可切换 │
└──────────────────────────────────────────────────┘
```
---
## 三、账号数据结构
```json
{
"id": "acct-uuid-1",
"displayName": "工作邮箱",
"gateway": "http://192.168.2.60:8180",
"token": "833c27ef...",
"username": "gui-lab",
"lastUsed": "2026-09-07T12:00:00Z"
}
```
| 字段 | 必填 | 说明 |
|---|---|---|
| `id` | 自动生成 | 内部唯一标识,不暴露给用户 |
| `displayName` | 是 | 显示用名(如「工作邮箱」),不是登录用户名 |
| `gateway` | 是 | Gateway 地址(`http://ip:port`),不同实例 |
| `token` | 是 | permanent user_keyBearer 认证) |
| `username` | 否 | 登录用户名(仅下次验证用,可留空) |
| `lastUsed` | 自动 | 最近一次选中时间,用于默认主账号 |
头像/别名不做——AgentMail 没有头像概念,显示名够。
---
## 四、聚合/单独视图交互
**顶栏账号选择器**(类似 Outlook account switcher
- 下拉列出所有账号 + 「全部邮箱」聚合项
- 选「全部邮箱」→ 收件箱聚合模式
- 选单个账号 → 只显示该账号邮件(现状)
`AccountStore` 维护当前选中(`"all"` 或某个 account id收件箱组件据此决定单账号请求或多账号合并。聚合时给每封邮件加账号归属徽标。
---
## 五、发信账号选择
- 默认用**当前选中账号**发
- 聚合模式下默认**第一个添加的账号**或最近使用的主账号
- 写信页顶栏加下拉切换(仅影响这一封的发信账号,不改变收件箱当前视图)
- 选了就只影响这一封,下次写信恢复到默认
---
## 六、SSE 多连接架构
**核心原则:每账号各建一条 SSE 连接**,因为服务端按 user_key 归属用户,一条连接只能收到一个用户的消息。
```
AccountManager
├─ acct-1 (Token A, Gateway A) ── EventSource ──> new_mail → acct-1 store
├─ acct-2 (Token B, Gateway B) ── EventSource ──> new_mail → acct-2 store
└─ acct-3 (Token C, Gateway A) ── EventSource ──> new_mail → acct-3 store
```
- `MultiSSE` 维护连接集合:每个 account 一条 `EventSource`
- 新邮件事件打到**对应账号**的 store
- 单独视图:直接刷新对应账号
- 聚合视图:把新邮件汇入合并列表
- 账号删除时关停对应连接
- 账号切换不关停:所有连接并行运行,切换只改变 UI 过滤条件
---
## 七、配置页 UI
**账号列表**:每个账号卡片显示:
- 显示名(大字)
- Gateway 地址(小字)
- 在线状态(🟢/🔴,基于最近一次 API 响应)
- 操作:切换为主账号 / 编辑 / 删除
**添加账号流程**
1. 输入 Gateway 地址
2. 输入 user_key粘贴或扫码暂定粘贴
3. 可选:输入显示名(默认用 gateway 域名/IP 生成)
4. 验证:调用 `GET /auth/me`Bearer token确认连通
5. 保存并加入账号列表
---
## 八、与 Gateway API 的映射
| 客户端操作 | Gateway API | 多账号处理 |
|---|---|---|
| 加载收件箱 | `GET /me/mail/inbox` | 每账号一次请求,合并 |
| 加载会话列表 | `GET /me/sessions` | 单账号(聚合时也只看当前选中) |
| 发信 | `POST /me/mail/send` | 用当前选中账号的 token |
| SSE | `GET /events/stream` | 每账号独立连接 |
| 认证 | `GET /auth/me` | 每个 token 独立验证 |
---
## 九、安全要点(已审计)
- 每个 user_key 只对应一个用户,水平权限由 `UserCanAccessSession` 阻挡
- `?access_token=` 仅 SSE + 附件下载两处接受(桌面客户端用 Bearer header不依赖 query
- 账号配置存储本地加密electron-store 加密),不写磁盘明文
---
## 十、两边实现对齐
| 维度 | 鸿蒙dsh | elepi |
|---|---|---|
| 账号存储 | preferences JSON | ✅ 主进程 `userData/accounts.json`(原子写 + 0600无 IPC 时退 localStorage |
| 账号增删/验证 | 需新增 | ✅ 账号页"多账号"一段:`/auth/me` 验证通过才写入 |
| 聚合收件箱 | 已实现M7 | ✅ 收件箱/发件箱列表头 + 账号徽标 + 部分失败的可见告警 |
| 账号选择器 UI | 需新增 | ✅ 列表头下拉≥2 个可用账号才出现「全部邮箱」) |
| SSE 多连接 | 改造中(当前单连接) | ❌ 未做(下一轮) |
| 写信账号切换 | 需新增 | ❌ 未做(下一轮) |
两边都以此文档为准,不以 cc 里的讨论为依据。
### 实现备注ele 侧2026-09-12
- **"当前账号"复用 api 单例**`config.ts``API_BASE`/`bearerToken` 仍是
模块级状态,含义变成"当前选中账号的认证",切换时由 `accountStore` 同步
`API_BASE` 因此改成 `let`,且 api 层不得在模块作用域缓存它)。
单账号视图的代码路径逐字节不变。
- **只有两处走显式认证**`fetchWithAuth`):聚合收件箱、以及将来的多账号 SSE。
借用单例来回切会让并发请求串号A 的请求带上 B 的令牌)。
- **只合并同一网关的账号**。跨网关的邮件混进列表后,点开会去问当前账号的
服务器(要么 404要么 mail_id 撞上就打开了别人的信)—— 所以如实排除并在
列表上方说明,而不是静默少一份。
- **部分失败必须可见**:某个账号取不到时列表上方给出账号名与原因,
这正是"聚合"最容易骗人的失败方式(看起来一切正常,只是少了一整份邮件)。
- 加密存储:本机 `safeStorage` 依赖系统 keyring本环境无故未启用
现状是有据可查的 0600 明文文件,界面上如实写明存放位置。