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

7.9 KiB
Raw Blame History

多账号架构方案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 层                                              │
│  • 顶栏账号选择器(全部邮箱 / 单个账号下拉)         │
│  • 收件箱:单账号(现状) / 聚合(合并+账号徽标)    │
│  • 写信页:默认当前选中账号,顶栏可切换              │
└──────────────────────────────────────────────────┘

三、账号数据结构

{
  "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/meBearer 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.tsAPI_BASE/bearerToken 仍是 模块级状态,含义变成"当前选中账号的认证",切换时由 accountStore 同步 API_BASE 因此改成 let,且 api 层不得在模块作用域缓存它)。 单账号视图的代码路径逐字节不变。
  • 只有两处走显式认证fetchWithAuth):聚合收件箱、以及将来的多账号 SSE。 借用单例来回切会让并发请求串号A 的请求带上 B 的令牌)。
  • 只合并同一网关的账号。跨网关的邮件混进列表后,点开会去问当前账号的 服务器(要么 404要么 mail_id 撞上就打开了别人的信)—— 所以如实排除并在 列表上方说明,而不是静默少一份。
  • 部分失败必须可见:某个账号取不到时列表上方给出账号名与原因, 这正是"聚合"最容易骗人的失败方式(看起来一切正常,只是少了一整份邮件)。
  • 加密存储:本机 safeStorage 依赖系统 keyring本环境无故未启用 现状是有据可查的 0600 明文文件,界面上如实写明存放位置。