Files
MailUI4Agents/docs/MVP-SPEC.md
JianFeeeee 07e6b789b2 feat: 配额下沉到会话 + 窄屏覆盖式布局 + 工作列表卡片视图
## 配额重构:废除 Agent 终身额度

原实现在 agents 上放一个 max_rounds/used_rounds 计数器,used_rounds 单调递增、
永不重置 —— 跑满就要管理员手工重置才能再干活。那是把一次性资源模型套在长期
在线的服务上,且并行任务互相抢额度。

改为:
- 唯一被强制的预算是【会话】的往返预算(sessions.max_rounds/used_rounds),
  写信时给、对话页里随时改 —— 配额的语义是「这件事值得多少个来回」,
  那是任务的属性而不是 Agent 的属性
- agents.default_rounds 只作为「派给这个 Agent 的新任务」的默认值(默认 20)
- agents.used_rounds 降级为纯统计
- 新建会话速率限制(1h/20 条)堵住用 .new 开一串新会话绕过预算;
  人类不受限(agentLimiterKey 返回空串即不计量)

## 窄屏适配(用户反馈「窄屏基本不可用」)

原先只有三栏并排:60(导航)+320(列表)+详情,375px 屏上详情被挤到 0。

第一版做成「一次只显示一栏」,用户纠正应当是新页面覆盖老页面并带动画,
于是重做为覆盖式:
- NarrowStack:底层列表始终挂载,详情绝对定位盖在上面。两个好处 ——
  列表滚动位置与选中态天然保留;退出动画有东西可播(直接卸载再渲染另一个
  组件的话,没有任何一帧能让旧页面往右滑出去)
- 因此必须区分「逻辑上是否打开」与「是否还在 DOM 里」:关闭时先播 200ms
  滑出,动画结束才卸载
- 入场用双层 requestAnimationFrame:必须让浏览器至少绘制一帧「在右侧之外」
  的状态,否则挂载与 translate-x-0 在同一帧内完成,transition 不触发
- 窄屏专属控件用 useIsNarrow() 条件渲染而非 md:hidden —— 后者只是视觉隐藏,
  宽屏用户按 Tab 会聚焦到看不见的返回按钮
- 底部导航 + 抽屉侧栏 + env(safe-area-inset-bottom)

## 工作列表卡片视图(Phase 7.1 最后一项)

中间栏可切列表/卡片。列表答「跟谁在聊」,卡片答「在聊什么、进展如何」:
主题 + 最新一封的发件人与摘要 + 往返预算徽标。

- 两种视图共用同一份数据与同一套动作;归档确认框也共用 —— 归档是破坏性操作,
  换个视图就换套确认 UI 只会让人对「自己点了什么」更没底
- 预算徽标在「不限」时不显示(对每张卡片都成立的「0/0」是纯噪声)
- 数据一次取回,不让卡片为每条会话再打一次库

## 修掉的缺陷

- GET /me/sessions 一直 500:ListSessionsFor 的 SELECT 加了预算两列却没加进
  Scan,列数不匹配。联系人栏一条数据都拉不到,而错误只是「Failed to list sessions」
- GET /sessions/{id} 忘了填充附件:前端会话视图走的是这个端点,于是 Agent
  回信里的附件在 UI 上完全不存在(另一个端点填了但没人调用)
- 插件曾完全没在加载:为了可测在 index.js 里 export 了辅助函数与一个 Map,
  而 opencode 把入口模块的每一个导出都当成插件工厂逐个检查,多导出一个 Map
  就 "Plugin export is not a function",插件静默失效、邮件全投不进去。
  逻辑挪到 lib/relay-dedup.js,并加断言钉住「入口只有 default 导出」
- 同一件事发两封邮件:模型带附件主动回信后,session.idle 又把它最后那段话
  自动转了一遍(生产实测 311 与 342 字节各一封)。explicitSends 记录本轮
  主动发信,自动转发据此让位;relay_key 幂等管不了这个 —— 那个键保证的是
  「同一条消息不转两次」
- SQLite 时间戳只有秒精度:同秒插入的多封邮件排序不确定(实测同秒插 5 封,
  顺序由随机 UUID 决定)。「会话里最早那封」(决定联系人身份)与「最后那封」
  (决定最新进展)都会取错。NOW() 升到微秒 + mails 的 INSERT 显式传它
  (改 schema 默认值只对新库生效,SQLite 没有 ALTER COLUMN)+ 所有
  ORDER BY created_at 补 mail_id 兜底
- fillAttachments 从逐封查询改成一次 IN(...):原来是 N+1,200 封的会话打开
  要打 200 次库
- repo 层 5 处 rows.Next() 循环补 rows.Err():没有它,读到一半连接断掉会
  静默返回部分结果,UI 上表现为「邮件凭空少了几封」
- go:embed 占位页改名 placeholder.html:叫 index.html 会被 Vite 产物覆盖并
  提交进去,而它引用的 assets/ 是被忽略的 —— 新克隆打开是白屏

## 回复/转发栏

- 两处都加抄送(可折叠);原邮件带抄送时多一个「回复全部」,回填用
  cc_list[].raw 而非重拼 name@path(后者会丢掉会话段)
- 会话视图每张卡片加转发入口:转发之前只存在于单封邮件视图,而人多数时间
  待在会话视图里,等于功能在 UI 上找不到
- ReplyBar 的错误从 console.error 改为显示出来:预算耗尽、地址不存在、
  速率限制都走这条路,之前点发送毫无反应

## 测试

- repo: 列顺序(三个 SQL 分支)、卡片字段、previewRunes 边界、时间戳亚秒精度、
  批量附件查询、速率限制(80 goroutine 断言恰好 20 条通过)
- web: 窄屏布局 16 条结构性断言(覆盖而非分栏、延迟卸载、双层 rAF、
  条件渲染而非 md:hidden)
- 插件: 自动转发去重 17 条(含「入口只有 default 导出」不变量)
- install.sh 把插件测试也纳入部署前门禁
2026-09-02 14:16:46 +08:00

31 KiB
Raw Blame History

邮件驱动·多智能体协作平台 — 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由模型生成会话摘要标题 + slugPOST /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 1 小时 20 条;堵住用 .new 绕过预算,人类不受限
窄屏适配 <768px 改为页面覆盖 + 滑入动画;底部导航 + 抽屉侧栅
插件自动转发 平台原生权限询问 + 本轮最终总结;不消耗配额
会话管理(创建/列表/状态) 会话是协作的边界
权限请求与决策 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/dbrepo 层只写一份 SQL
  • SSE 推送不依赖数据库通知机制(无需 Redis 或 PG LISTEN/NOTIFY单体进程内 sse.Manager 按收件人名分流Agent 通道与人类用户通道共用一套投递

三、数据模型

以下 DDL 以 PostgreSQL 方言书写。SQLite 侧结构与语义完全一致,仅方言不同 UUIDTEXTTIMESTAMPTZDATETIMEJSONBTEXTVARCHAR(n)TEXTgateway/internal/db/migrations/init_sqlite.sql

3.1 agents 表

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 表

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 表

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_mailread_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 可调用)

// 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}`;
  }
};
// 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 来做这些事。

// 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 插件生命周期

// 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