Files
MailUI4Agents/docs/MVP-SPEC.md
JianFeeeee 0e754617a4 feat: AgentMail —— 以邮件为统一范式的多智能体协作平台
Go 单二进制网关 + React 前端 + opencode 桥接插件。部署产物是
「一个二进制加一个 .db 文件」:前端经 go:embed 打进二进制,
数据库默认内置 SQLite,systemd 托管。

核心设计
- 三维寻址 name@path.session,按最后一个 . 切分;session 位三态:
  省略=默认会话 / new=强制新建 / 具体别名=必须已存在(否则 404 无法送达)
- 会话别名默认复用 Agent 平台自己的命名机制(opencode 的 slug 与模型生成的
  标题),不在本侧另造一套;人显式定过的别名不被平台同步覆盖
- 对话树不建 tree_nodes 表:parent_mail_id 已完整编码树结构,
  再维护一张表就是第二份真相。用递归 CTE 查,按方向分块加载
- 附件内容存磁盘、按 sha256 内容寻址,数据库只存元数据;天然去重,
  且路径与用户 filename 无关,杜绝 ../ 穿越
- 配额约束的是模型的自主发信,不是 harness 的转发:插件代劳的权限询问与
  最终总结走免配额通道,靠上游消息 id 做幂等键而非计数
- 往返预算下沉到会话(写信时给、对话页里改)+ Agent 全局配额,两层都要过

后端 gateway/
- models/repo/handler/middleware/sse/blob 分层;两方言(SQLite/PostgreSQL)
  共用一份 repo 层 SQL,差异集中在 internal/db
- 多用户认证(bcrypt cost12、登录限速、会话隔离、权限边界)
- 密钥体系:Agent 密钥与用户密钥分表,三种生命周期;登记式密钥让全文
  只从客户端流向服务器一次
- 所有「判断 + 自增」都在同一条 UPDATE 里(配额、预算、one_time 密钥、
  附件挂载),并发下不会刷穿

前端 web/
- 三栏布局、三段式地址补全、权限卡片、密钥面板、配额面板、对话树、附件
- 全站纯 SVG 图标,不使用 emoji
- api/ 即可复用的客户端 SDK:基地址与凭证集中在 api/config.ts

插件 plugins/opencode-mail-bridge/
- 六个工具 + 两类自动转发(permission.ask 钩子接管平台原生权限询问、
  session.idle 时转发本轮总结)
2026-09-02 10:29:26 +08:00

797 lines
30 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.

# 邮件驱动·多智能体协作平台 — MVP 技术规格书
> 基于 v2.0 完整设计文档,聚焦最小可用闭环。
> 版本v0.1 | 日期2026-09-01
---
## 一、MVP 范围定义
### 1.1 核心目标
**跑通一个完整的人-Agent协作闭环**
```
人类发任务 → Agent 收到 → Agent 执行 → Agent 遇到需要人决策的事 → 请求权限 → 人类批准 → Agent 继续执行 → Agent 回复结果
```
### 1.2 MVP 包含(✅)与不包含(❌)
| 功能 | MVP | 说明 |
|------|-----|------|
| 邮件收发人↔Agent | ✅ | 核心闭环 |
| 三维寻址name@path.session | ✅ | 核心寻址范式session 位三态:省略=默认会话 / `new`=新建 / 别名=必须已存在(否则无法送达) |
| 会话别名(命名与改名) | ✅ | 别名负责寻址,全局唯一;`new` 为保留字 |
| 别名/标题复用 Agent 平台命名 | ✅ | 平台(如 opencode由模型生成会话摘要标题 + slug`POST /sessions/:id/sync` 回写;撞名自动加 `-2` 后缀 |
| 密钥认证Agent / 用户分离) | ✅ | agent_keys 用于注册/心跳/SSEuser_keys 仅用于 /me/*;三种生命周期 permanent/one_time/timed |
| 内置 SQLite外部库可选 | ✅ | 默认零依赖;`DATABASE_URL` 非空时切 PostgreSQL |
| systemd 一键部署 | ✅ | `deploy/install.sh`,产物为一个二进制 + 一个 .db |
| 附件 | ✅ | 内容寻址存盘sha256 去重),上传与发信两步;下载强制 octet-stream |
| 转发 | ✅ | 引用原文 + 附件随行;只能转发自己参与过的邮件 |
| Agent 发信配额 | ✅ | 只限发信不限收信;剩余次数随响应与心跳回传 |
| WebAPI 等价接入 | ✅ | WebUI 与第三方客户端同一套 API见 docs/API.md |
| 对话树 | ✅ | 沿 parent_mail_id 递归展开,跨会话,按方向分块加载,不建 tree_nodes 表 |
| Agent 提议改会话名 | ✅ | 正文里的 HTML 注释标记,入库时剥除;改名需用户确认 |
| 会话往返预算 | ✅ | 写信时给 / 对话页随时改;与 Agent 全局配额两层都要过 |
| 插件自动转发 | ✅ | 平台原生权限询问 + 本轮最终总结;**不消耗配额** |
| 会话管理(创建/列表/状态) | ✅ | 会话是协作的边界 |
| 权限请求与决策 | ✅ | Agent 需要人批准才能继续 |
| Agent 注册与发现 | ✅ | 最小 Registry |
| 前端收件箱/发件箱列表 | ✅ | 基础 UI |
| 前端新建邮件/回复 | ✅ | 基础 UI |
| 邮件正文 Markdown 渲染 | ✅ | Agent 输出多为 MD |
| Agent 桥接插件 | ✅ | opencode 为第一个接入平台(`plugins/opencode-mail-bridge` |
| 对话树 | ❌ | 后续迭代 |
| 抄送CC | ✅ | 已实现:`cc_list` + 收件箱抄送可见 |
| 转发 | ❌ | 后续迭代 |
| 配额机制 | ❌ | 后续迭代 |
| 会话别名命名/改名 | ✅ | 发信时命名、事后改名、平台命名自动同步 |
| Agent 正文里主动提议改名 | ❌ | 后续迭代 |
| 工作列表/卡片视图 | ❌ | 后续迭代 |
| DeepSeek Harness 插件 | ❌ | 后续迭代 |
| 跨主机 Agent 发现 | ❌ | 后续迭代 |
---
## 二、系统简化架构
```
┌─────────────────────────────────────────────────────┐
│ 前端Web UI
│ 收件箱 / 发件箱 / 新建邮件 / 回复 │
│ Markdown 编辑器 + 渲染器 │
└──────────────────────┬──────────────────────────────┘
│ HTTP REST + SSE
┌─────────────────────────────────────────────────────┐
│ Mail Gateway单体服务
│ │
│ ┌───────────┐ ┌───────────┐ ┌───────────────┐ │
│ │ Mail API │ │ Registry │ │ Session Mgr │ │
│ │ 收发路由 │ │ 注册/发现 │ │ 会话/状态 │ │
│ └───────────┘ └───────────┘ └───────────────┘ │
│ ┌───────────────────────────────────────────────┐ │
│ │ Agent 通信层WebSocket │ │
│ └───────────────────────────────────────────────┘ │
└──────────────────────┬──────────────────────────────┘
│ WebSocket
┌─────────────────────────────────────────────────────┐
│ Agent 侧Pi Agent + 插件) │
│ ┌─────────────────────────────────────────────┐ │
│ │ pi-mail-bridge 插件 │ │
│ │ - 注册 send_mail / read_inbox / req_perm │ │
│ │ - WebSocket 连接 Gateway │ │
│ │ - 新邮件注入 Agent 上下文 │ │
│ └─────────────────────────────────────────────┘ │
│ Pi Agent 本体文件读写、终端、Git、推理
└─────────────────────────────────────────────────────┘
┌─────────────────┐
│ SQLite默认 │ 邮件 + 会话持久化
│ 或外部 PostgreSQL│ DATABASE_URL 指定
└─────────────────┘
```
**简化要点:**
- Gateway 和 Registry 合并为单体服务
- 前端用 SSEServer-Sent Events替代 WebSocket更简单单向推送够用
- 数据库默认内置 SQLite零外部依赖与 go:embed 的前端一起构成「一个二进制 + 一个 .db」
`DATABASE_URL` 非空时切换到外部 PostgreSQL。方言差异收在 `internal/db`repo 层只写一份 SQL
- SSE 推送不依赖数据库通知机制(无需 Redis 或 PG LISTEN/NOTIFY单体进程内 `sse.Manager`
按收件人名分流Agent 通道与人类用户通道共用一套投递
---
## 三、数据模型
以下 DDL 以 PostgreSQL 方言书写。SQLite 侧结构与语义完全一致,仅方言不同
`UUID``TEXT``TIMESTAMPTZ``DATETIME``JSONB``TEXT``VARCHAR(n)``TEXT`
`gateway/internal/db/migrations/init_sqlite.sql`
### 3.1 agents 表
```sql
CREATE TABLE agents (
agent_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
agent_name VARCHAR(64) NOT NULL UNIQUE, -- 全局唯一
secret VARCHAR(128) NOT NULL, -- 注册密钥
host_url VARCHAR(256) NOT NULL, -- 插件通信地址ws://...
workspaces JSONB NOT NULL DEFAULT '[]', -- [{name, path}]
platform VARCHAR(32) NOT NULL DEFAULT 'pi', -- pi / dsh / cline
status VARCHAR(16) NOT NULL DEFAULT 'offline', -- online / offline
max_rounds INT NOT NULL DEFAULT 10, -- 通信配额Phase 2 启用)
used_rounds INT NOT NULL DEFAULT 0,
last_seen TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
```
### 3.2 sessions 表
```sql
CREATE TABLE sessions (
session_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
session_alias VARCHAR(128), -- 会话别名(如 add-health-check全局唯一负责寻址
from_agent VARCHAR(64) NOT NULL, -- 发起者 agent_name或 'human'
subject VARCHAR(512) NOT NULL, -- 会话主题
status VARCHAR(32) NOT NULL DEFAULT 'active', -- active / waiting / completed
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_sessions_alias ON sessions(session_alias);
CREATE INDEX idx_sessions_status ON sessions(status);
-- 别名负责三维寻址 name@path.<alias>必须唯一未命名会话NULL不受约束
CREATE UNIQUE INDEX idx_sessions_alias_uniq
ON sessions(session_alias) WHERE session_alias IS NOT NULL;
```
### 3.3 mails 表
```sql
CREATE TABLE mails (
mail_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
session_id UUID NOT NULL REFERENCES sessions(session_id),
parent_mail_id UUID REFERENCES mails(mail_id), -- 回复链(线性,非树)
from_name VARCHAR(64) NOT NULL, -- 发件人 agent_name或 'human'
from_workspace VARCHAR(128), -- 发件人工作区
to_name VARCHAR(64) NOT NULL, -- 收件人 agent_name或 'human'
to_workspace VARCHAR(128), -- 收件人工作区
subject VARCHAR(512) NOT NULL,
body TEXT NOT NULL, -- Markdown
mail_type VARCHAR(32) NOT NULL DEFAULT 'normal', -- normal / permission_request
permission_options JSONB, -- 权限请求的选项 ["同意","拒绝"]
permission_result VARCHAR(32), -- approved / rejected / null
status VARCHAR(16) NOT NULL DEFAULT 'unread', -- unread / read / archived
created_at TIMESTAMPTZ DEFAULT NOW(),
-- 防循环Phase 2 启用)
hop_limit INT DEFAULT 5
);
CREATE INDEX idx_mails_session ON mails(session_id);
CREATE INDEX idx_mails_to ON mails(to_name, status);
CREATE INDEX idx_mails_parent ON mails(parent_mail_id);
```
### 3.4 设计决策说明
| 决策 | 理由 |
|------|------|
| `parent_mail_id` 而非 `tree_node_id` | MVP 只有线性回复链不需要树结构。Phase 2 引入 CC/转发后再加 tree 节点表 |
| `from_name` 用字符串而非 UUID | MVP 阶段 agent_name 全局唯一简化查询。Phase 2 可加 agent_id 外键 |
| `permission_options` 存 JSONB | 允许每个权限请求自定义选项,前端动态渲染按钮 |
| 不单独建 `conversations` 表 | session 已经承载会话概念,不重复建设 |
---
## 四、API 设计
### 4.1 基础信息
- **Base URL**: `http://gateway:8080/api/v1`
- **认证**: Agent 侧用 `X-Agent-Name` + `X-Agent-Secret` header前端用 session cookie
- **内容类型**: `application/json`
### 4.2 Agent 注册与心跳
```
POST /agent/register
Body: { name, secret, workspaces: [{name, path}], platform }
Response: { agent_id, status: "registered" }
POST /agent/heartbeat
Header: X-Agent-Name, X-Agent-Secret
Response: { status: "ok", pending_mails: 3 }
```
心跳响应中 `pending_mails` 告诉插件有多少未读邮件需要拉取。
### 4.3 邮件收发
```
POST /mail/send
Header: X-Agent-Name, X-Agent-Secret
Body: {
to: "builder@ModelRouter", -- name@workspace 格式
subject: "请为 ModelRouter 增加健康检查接口",
body: "## 需求\n\n...",
session_alias: "add-health-check", -- 可选,新建会话时用
reply_to: "uuid" -- 可选,回复某封邮件
}
Response: { mail_id, session_id }
GET /mail/inbox
Header: X-Agent-Name, X-Agent-Secret
Query: ?status=unread&limit=10
Response: {
mails: [
{
mail_id, session_id, session_alias,
from_name, from_workspace, subject,
body_preview, mail_type, status, created_at
}
],
total: 5
}
GET /mail/:mail_id
Header: X-Agent-Name, X-Agent-Secret
Response: { ... 完整邮件字段 ... }
POST /mail/:mail_id/read
Header: X-Agent-Name, X-Agent-Secret
Response: { status: "read" }
```
### 4.4 权限请求
```
POST /permission/request
Header: X-Agent-Name, X-Agent-Secret
Body: {
to: "human", -- 固定为 human
question: "是否允许合并 PR #42",
options: ["同意合并", "拒绝,需要修改"],
context: "PR 改动了 3 个文件CI 全部通过...",
session_id: "uuid" -- 可选,关联现有会话
}
Response: { mail_id, session_id, permission_mail_id }
POST /permission/decide
Body: {
mail_id: "uuid",
decision: "同意合并", -- 必须是 request 中 options 之一
note: "可以合并,但请补充测试" -- 可选备注
}
Response: { status: "decided" }
```
### 4.5 会话与 Agent 列表
```
GET /sessions
Query: ?status=active&limit=20
Response: {
sessions: [
{
session_id, session_alias, subject,
from_agent, status, created_at, updated_at,
mail_count: 5
}
]
}
GET /sessions/:session_id
Response: {
session: { ... },
mails: [ ... 按时间排序的邮件列表 ... ]
}
GET /agents
Query: ?status=online
Response: {
agents: [
{ agent_name, workspaces: [...], platform, status }
]
}
```
### 4.6 SSE 推送(替代 WebSocket
```
GET /events/stream
Header: X-Agent-Name (Agent 侧) 或 Cookie (前端)
Query: ?agent_name=builder (Agent 侧)
或无参数 (前端,推送所有相关事件)
Event: new_mail
Data: { mail_id, session_id, from_name, subject, mail_type }
Event: permission_decision
Data: { mail_id, decision, note }
Event: session_update
Data: { session_id, status, updated_at }
Event: agent_online
Data: { agent_name, status }
```
**为什么选 SSE 而不是 WebSocket**
- 前端只需要接收推送,不需要双向通信
- SSE 基于 HTTP穿透代理/防火墙更容易
- 浏览器原生支持 EventSource无需第三方库
- Agent 侧用 WebSocket需要双向前端用 SSE只需单向
---
## 五、Pi Agent 桥接插件设计
### 5.1 插件能力(基于 Pi 真实 API
Pi Agent 的插件机制通过 `pi install` 安装,插件可以:
- 注册自定义 tool工具
- 在 session 生命周期钩子中执行逻辑
- 通过 HTTP 与外部服务通信
### 5.2 插件架构
```
pi-mail-bridge/
├── index.js # 插件入口 + 钩子拦截器注册
├── tools/
│ ├── send_mail.js # 发送邮件工具Agent 主动调用)
│ └── read_inbox.js # 读取收件箱工具Agent 收到通知后调用)
├── hooks/
│ ├── request_permission.js # 拦截 request_permission格式化权限邮件
│ ├── ask_user.js # 拦截 ask_user格式化提问邮件
│ └── final_message.js # 拦截最终输出,格式化总结邮件
├── transport/
│ ├── sse.js # SSE 客户端(接收推送)
│ └── http.js # HTTP 客户端(发送请求)
├── config.js # 配置管理
└── package.json
```
### 5.3 核心区分:工具 vs 钩子拦截器
**重要设计原则:**
| 类型 | 内容 | 谁来触发 |
|------|------|----------|
| Agent 可调用的工具 | `send_mail``read_inbox` | Agent 主动调用 |
| 自动触发器(钩子) | 权限请求、提问、最终总结 | 平台钩子自动拦截 |
**邮箱不直接注入 Agent**:新邮件到达时,插件只向 Agent 注入一条**通知**来自谁、主题是什么Agent 看到通知后自行调用 `read_inbox` 工具拉取邮件正文。邮件全文从不直接塞进 Agent 上下文。
**三种自动转邮件触发器**(钩子拦截,非 Agent 调用工具):
| 触发场景 | Agent 行为 | 插件钩子动作 | 邮件特征 |
|----------|-----------|--------------|----------|
| Agent 请求权限 | 调用 `request_permission()` | 冻结 Agent格式化 `[权限请求]` 邮件发至人类 | `mail_type=permission_request`,正文自动渲染 ✅❌ 按钮 |
| Agent 提问人类 | 调用 `ask_user(question)` | 冻结 Agent格式化 `[需回复]` 邮件发至人类 | subject 含 `[需回复]`,正文=问题 |
| Agent 最后一条消息 | 完成所有任务/配额用尽 | 拦截输出,格式化 `[最终总结]` 邮件发至人类 | subject 含 `[最终总结]`Agent 进入休眠 |
### 5.4 工具注册Agent 可调用)
```javascript
// tools/send_mail.js
module.exports = {
name: "send_mail",
description: "发送邮件给指定 Agent 或人类",
parameters: {
type: "object",
properties: {
to: {
type: "string",
description: "收件人,格式 name@workspace如 builder@ModelRouter"
},
subject: {
type: "string",
description: "邮件主题"
},
body: {
type: "string",
description: "邮件正文,支持 Markdown 格式"
},
session_alias: {
type: "string",
description: "会话别名(新建会话时使用)"
}
},
required: ["to", "subject", "body"]
},
async execute(params, ctx) {
const { to, subject, body, session_alias } = params;
const [toName, toWorkspace] = to.split("@");
const result = await ctx.http.post("/api/v1/mail/send", {
to_name: toName,
to_workspace: toWorkspace || null,
subject,
body,
session_alias: session_alias || null,
reply_to: ctx.currentMailId || null // 如果是在回复某封邮件
});
return `✅ 邮件已发送。Mail ID: ${result.mail_id},会话: ${result.session_id}`;
}
};
```
```javascript
// tools/read_inbox.js
module.exports = {
name: "read_inbox",
description: "查阅收件箱中的邮件。收到新邮件通知后调用此工具查看完整内容。",
parameters: {
type: "object",
properties: {
filter: {
type: "string",
enum: ["unread", "all"],
description: "过滤条件,默认 unread"
},
limit: {
type: "number",
description: "返回数量,默认 5"
}
}
},
async execute(params, ctx) {
const { filter = "unread", limit = 5 } = params;
const result = await ctx.http.get("/api/v1/mail/inbox", {
params: { status: filter, limit }
});
if (result.mails.length === 0) {
return "📭 收件箱为空。";
}
return result.mails.map(m =>
`📧 [${m.status}] ${m.from_name}: ${m.subject}\n` +
` 会话: #${m.session_alias || '未命名'} | ${m.created_at}\n` +
` 预览: ${m.body_preview?.substring(0, 100)}...`
).join("\n\n");
}
};
```
### 5.5 钩子拦截器(自动触发器,非 Agent 工具)
三种场景下,插件通过平台钩子**自动拦截** Agent 行为格式化为邮件发出。Agent 无需(也不应)手动调用 send_mail 来做这些事。
```javascript
// hooks/request_permission.js
module.exports = {
name: "request_permission",
description: "拦截 Agent 的 request_permission 调用,自动格式化为权限请求邮件",
// 由平台钩子触发,非 Agent 调用
async intercept(ctx, question, options = ["同意", "拒绝"], context = "") {
// 1. 调 Gateway 创建权限请求邮件
const result = await ctx.http.post("/api/v1/permission/request", {
question,
options,
context,
session_id: ctx.sessionId
});
// 2. 冻结 Agent标记等待状态
ctx.setWaitingForPermission(result.permission_mail_id);
// 3. 返回挂起状态给 AgentAgent 不再继续执行)
return { suspended: true, wait_type: "permission" };
}
};
// hooks/ask_user.js
module.exports = {
name: "ask_user",
description: "拦截 Agent 的 ask_user 调用,自动格式化为提问邮件",
async intercept(ctx, question) {
// 1. 调 Gateway 发送普通邮件(主题自动加 [需回复]
const result = await ctx.http.post("/api/v1/mail/send", {
to: "human",
subject: `[需回复] ${question.substring(0, 100)}`,
body: question,
session_id: ctx.sessionId
});
// 2. 冻结 Agent标记等待状态
ctx.setWaitingForReply(result.mail_id);
return { suspended: true, wait_type: "reply" };
}
};
// hooks/final_message.js
module.exports = {
name: "final_message",
description: "拦截 Agent 最后一条消息,格式化为最终总结邮件",
async intercept(ctx, message) {
// 1. 调 Gateway 发送最终总结邮件(主题自动加 [最终总结]
const result = await ctx.http.post("/api/v1/mail/send", {
to: "human",
subject: `[最终总结] ${ctx.sessionAlias || '任务完成'}`,
body: message,
session_id: ctx.sessionId
});
// 2. Agent 进入休眠
ctx.enterSleepMode();
return { suspended: true, wait_type: "sleep" };
}
};
```
### 5.6 插件生命周期
```javascript
// index.js
const { registerTools, startSSE, registerAgent } = require('./transport');
const config = require('./config');
module.exports = {
async onSessionStart(ctx) {
// 1. 向 Gateway 注册
await registerAgent({
name: config.agentName,
secret: config.agentSecret,
workspaces: config.workspaces,
platform: 'pi'
});
// 2. 启动 SSE 监听新邮件
startSSE(config.gatewayUrl, config.agentName, (event) => {
if (event.type === 'new_mail') {
// 注入系统消息到 Agent 上下文
ctx.addSystemMessage(
`📬 新邮件(${event.data.mail_type}\n` +
`来自: ${event.data.from_name}\n` +
`主题: ${event.data.subject}\n` +
`会话: #${event.data.session_alias || '未命名'}\n` +
`使用 read_inbox 工具查看详情。`
);
}
if (event.type === 'permission_decision') {
// 恢复 Agent 执行
ctx.resumeFromPermission(event.data.mail_id, event.data.decision);
}
});
// 3. 定期心跳每30秒
setInterval(async () => {
await ctx.http.post('/api/v1/agent/heartbeat');
}, 30000);
}
};
```
### 5.7 权限等待的恢复机制
Agent 被钩子拦截并冻结后的恢复流程:
```
1. Agent 调用 request_permission → 发邮件给 human → 标记等待
2. 插件监听 SSE收到 permission_decision 事件
3. 插件调用 ctx.resumeFromPermission(mailId, decision)
4. Pi Agent 恢复执行decision 作为 tool_result 返回给 Agent
5. Agent 根据 decision 继续后续操作
```
**关键点:** Agent 进程不阻塞,只是标记为"等待权限"状态。SSE 收到决策后,通过 Pi Agent 的上下文注入机制恢复执行。
---
## 六、前端设计MVP 简化版)
### 6.1 布局
MVP 阶段用**两栏布局**(去掉中间的工作列表/对话树):
```
┌────────────────────┬────────────────────────────────────┐
│ 左侧300px │ 右侧(剩余宽度) │
├────────────────────┼────────────────────────────────────┤
│ 📬 收件箱 (3) │ 当前查看的邮件正文 │
│ 📤 发件箱 │ Markdown 渲染) │
│ 📝 草稿箱 │ │
│ ✏️ 新建 │ ──── 分隔线 ──── │
│ │ │
│ ─── 邮件列表 ─── │ 回复编辑器 │
│ │ 📧 邮件A [未读] │ [ Markdown 编辑器 ] │
│ │ 📧 邮件B [权限] │ [发送] [取消] │
│ │ 📧 邮件C │ │
│ │ ... │ │
└────────────────────┴────────────────────────────────────┘
```
### 6.2 权限请求邮件的特殊渲染
当邮件类型为 `permission_request` 时,正文下方自动渲染按钮组:
```
┌──────────────────────────────────────┐
│ 📧 权限请求 │
│ 来自: @builder.ModelRouter │
│ 主题: 是否允许合并 PR #42
│ │
│ ## 背景 │
│ PR 改动了 3 个文件CI 全部通过... │
│ │
│ ┌──────────┐ ┌──────────┐ │
│ │ ✅ 同意合并 │ │ ❌ 拒绝 │ │
│ └──────────┘ └──────────┘ │
│ [可选备注输入框] │
└──────────────────────────────────────┘
```
### 6.3 技术栈
- **框架**: React 18 + TypeScript
- **样式**: TailwindCSS
- **Markdown**: react-markdown + remark-gfm支持表格、代码高亮
- **编辑器**: @uiw/react-md-editor轻量 Markdown 编辑器)
- **状态管理**: Zustand简单够用
- **实时推送**: EventSourceSSE 原生 API
- **构建**: Vite
### 6.4 核心页面/组件
```
src/
├── App.tsx
├── api/
│ ├── client.ts # HTTP 客户端
│ └── sse.ts # SSE 连接管理
├── stores/
│ ├── mailStore.ts # 邮件状态
│ └── sessionStore.ts # 会话状态
├── components/
│ ├── Sidebar/
│ │ ├── Sidebar.tsx # 左侧图标栏
│ │ ├── MailList.tsx # 邮件列表
│ │ └── MailItem.tsx # 单封邮件条目
│ ├── MailView/
│ │ ├── MailView.tsx # 右侧邮件正文
│ │ ├── MailBody.tsx # Markdown 渲染
│ │ ├── PermissionCard.tsx # 权限请求卡片
│ │ └── ReplyEditor.tsx # 回复编辑器
│ └── Compose/
│ └── ComposeModal.tsx # 新建邮件弹窗
└── types/
└── index.ts # TypeScript 类型定义
```
---
## 七、完整 MVP 工作流(端到端)
### 场景:人类要求 Builder Agent 增加健康检查接口
```
步骤 1: 人类在前端新建邮件
- 收件人: @builder.ModelRouter
- 主题: 为 ModelRouter 增加健康检查接口
- 正文: ## 需求描述\n\n请为 ModelRouter 的 HTTP 服务增加 /health 端点...
- 会话别名: add-health-check
→ 前端 POST /api/v1/mail/send
→ Gateway 创建 session + mail通过 SSE 推送 new_mail 给 Builder
步骤 2: Builder Agent 收到通知
- Pi Agent 收到系统消息: "📬 新邮件...使用 read_inbox 查看"
- Agent 调用 read_inbox 工具
- Gateway 返回邮件详情
→ Agent 读取正文,理解需求
步骤 3: Builder Agent 执行任务
- Agent 调用终端: git checkout -b feature/add-health-check
- Agent 调用文件读写: 编写 health check 代码
- Agent 调用终端: 运行测试
- Agent 调用终端: git commit, git push
步骤 4: Builder Agent 请求权限(如需要)
- Agent 调用 request_permission: "是否允许合并 PR #42"
- Gateway 发送权限请求邮件给人类
→ 前端收到 SSE 推送,显示权限请求卡片
步骤 5: 人类决策
- 人类点击 "✅ 同意合并"
- 前端 POST /api/v1/permission/decide
→ Gateway 发送 permission_decision SSE 给 Builder
步骤 6: Builder Agent 恢复执行
- 插件收到 permission_decision
- Agent 恢复执行,得到 "同意合并" 的结果
- Agent 执行 gh pr merge
- Agent 调用 send_mail: "PR 已合并,健康检查接口已就绪"
→ 人类收到完成通知
```
---
## 八、MVP 实施计划
### Week 1后端核心
| 任务 | 产出 |
|------|------|
| schema 建表(两方言各一份) | `internal/db/migrations/` |
| Gateway HTTP APImail CRUD + session | 可用的 REST API |
| Agent 注册/心跳 API | Agent 可注册并保持在线 |
| SSE 推送机制 | /events/stream 端点 |
| 权限请求/决策 API | 完整的权限闭环 |
### Week 2Pi 插件 + 前端骨架
| 任务 | 产出 |
|------|------|
| pi-mail-bridge 插件框架 | pi install 可用 |
| send_mail / read_inbox 工具 | Agent 可收发邮件 |
| request_permission 工具 | Agent 可请求权限 |
| 前端两栏布局 + 路由 | 可访问的 Web UI |
| 邮件列表 + 正文渲染 | 可查看邮件 |
| 新建邮件 + 回复编辑器 | 可发送邮件 |
### Week 3闭环联调
| 任务 | 产出 |
|------|------|
| 人→Agent→人 完整闭环测试 | 端到端可演示 |
| 权限请求闭环测试 | Agent 等待→人批准→Agent 恢复 |
| SSE 实时推送验证 | 新邮件到达时 UI 自动更新 |
| 错误处理与边界情况 | 网络断开、Agent 离线等 |
| 基础部署脚本 | `deploy/install.sh` + systemd 单元(数据库为内置 SQLite无需容器编排 |
---
## 九、关键设计决策记录
| 决策 | 选择 | 理由 |
|------|------|------|
| Gateway + Registry 合并 | ✅ 合并为单体 | MVP 阶段单机部署,减少运维复杂度 |
| 前端推送用 SSE | ✅ SSE | 单向推送够用,比 WebSocket 简单 |
| 会话内邮件链用线性回复 | ✅ parent_mail_id | 不需要树结构,线性链够用 |
| Agent 等待权限不阻塞进程 | ✅ 非阻塞 | Agent 进程不能挂起,用状态标记 + SSE 恢复 |
| 第一个插件选 Pi Agent | ✅ Pi | 团队更熟悉API 更直接 |
| 不用 Redis | ✅ PG LISTEN/NOTIFY | MVP 减少依赖PG 原生支持发布订阅 |
| 前端不用对话树 | ✅ 两栏布局 | Phase 1 聚焦核心闭环 |
---
## 十、后续迭代路线
### 下一批MVP 已完成的部分不再列出)
- 对话树数据模型 + 前端渲染
- 转发功能(抄送已完成)
- 工作列表卡片视图(中间栏)
- Agent 在邮件正文里主动提议改会话别名(平台命名自动同步已完成)
- DeepSeek Harness 插件
### Phase 3Phase 2 后 2 周)
- 配额机制
- 跨主机 Agent 发现Gateway + Registry 拆分)
- Agent 间通信签名验证
- Hop-limit 防循环
### Phase 4持续
- 监控面板
- 审计日志查询
- 智能路由推荐
- Agent 自动会话别名建议
---
文档版本v0.1 (MVP)
基于:完整设计文档 v2.0
日期2026-09-01