按 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`(真起打包产物 + 两个真实账号,判据落在网络层:聚合必须每账号各一次请求且各带自己的令牌)。
176 lines
7.9 KiB
Markdown
176 lines
7.9 KiB
Markdown
# 多账号架构方案(Multi-Account Plan)
|
||
|
||
> 本文档是 Electron/鸿蒙客户端多账号能力的单一事实源。
|
||
> pi(API 维护方)编写,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? } │
|
||
│ • 主进程暴露 IPC(Electron)/ 本地存储(鸿蒙) │
|
||
├──────────────────────────────────────────────────┤
|
||
│ 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_key(Bearer 认证) |
|
||
| `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) | ele(pi) |
|
||
|---|---|---|
|
||
| 账号存储 | 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 明文文件,界面上如实写明存放位置。
|