Mai 3.0 · Ledger Engine
适用:测试环境,分支 feat/mai-engine-m1(PR #438)。本文只收录有行为变化的协议面;未提及的帧、字段、REST 接口一律不变。所有 JSON 形状取自 ws_adapter.py / gateway.py 当前实现,非设计稿。
Overview
| # | 协议面 | 性质 | 不改会怎样 |
|---|---|---|---|
| 1 | 忙时消息:message_rejected → 排队 | 新帧 + 语义变化 | 用户第二条消息看起来"消失",或被误报错 |
| 2 | ask_user_question 中断 | 4 处字段/状态变化 | 问题卡丢失、UUID 校验拒掉合法 ID、选项渲染崩 |
| 3 | 工具名:1 改名 + 3 新增 + 3 删除 | 枚举变化 | 新工具卡渲染失败,删除工具永远转圈 |
| 4 | 工具结果:bash/mai-upload、spawn_subagent | 结构位置变化 | 上传结果解析不到,子代理进度永不收尾 |
| 5 | generation_bundle 恢复模型列表 | 字段内容变化 | 前端继续用硬编码模型表,与服务端脱节 |
Change 1 · Message Queue
旧行为:会话正在处理时再发 chat_message,收到 message_rejected(reason:"processing"),消息被丢弃,用户只能重发。
新行为:忙时的 chat_message 进入服务端持久化队列;当前轮结束后自动取出,作为下一条用户输入处理。时序两帧:
入队时立即下发
{
"type": "message_queued",
"session_id": "session_x",
"data": {
"queue_id": "queue_abc123", // 配对键,保存到对应的用户气泡上
"position": 1, // 入队时的位置;不会持续推送位置更新
"timestamp": "2026-07-21T18:30:00Z"
}
}
原轮结束、该消息开始处理时下发
{
"type": "message_received",
"session_id": "session_x",
"data": {
"message_id": "queue_abc123", // 恒等于之前的 queue_id,用它把气泡从"排队中"切到"处理中"
"nano_id": "",
"checkpoint_id": "seq:18", // 可能缺席;不透明字符串,见变更 2
"timestamp": "2026-07-21T18:30:08Z"
}
}
| 字段 | 类型 | 说明 |
|---|---|---|
| status | "idle" | "running" | "pending_question" | 状态主判据。pending_question 是新增态,见变更 2 |
| queued_count | int | 仍在等待的排队消息数(已取出的不计) |
| is_processing | bool | 等价于 status=="running" |
| can_send_message | bool | 兼容字段:running 时为 false,但排队是允许的——不得据此禁用输入框 |
| current_task_id / processing_start_time | string? / iso? | 仅 running 时有值 |
message_queued:保留用户气泡,标"排队中"。不报错、不自动重发。queue_id === message_received.data.message_id 配对,把气泡切到"处理中"。message_rejected 没有删除,但触发面收窄到一条:忙碌中收到 regenerate。普通消息不会再收到它;处理分支保留即可。Change 2 · ask_user_question
| # | 旧 | 新 | 前端动作 |
|---|---|---|---|
| 2.1 | 挂起等答案时查状态可能返回 idle | 明确返回 status:"pending_question" | 此状态下保留问题卡并显示"等你选择",不当作轮次已结束 |
| 2.2 | checkpoint_id 通常是 UUID | 不透明字符串:可能是 entry:123、seq:18 或其他 | 删除 UUID 格式校验。收到什么存什么,回答时原样带回 |
| 2.3 | options 元素有时是裸字符串 ["横版","竖版"] | 统一为对象(见下) | 显示与回传都用 label;渲染老账本时字符串兜底当 label |
| 2.4 | 中断帧难与轮次关联 | metadata 新增可选 turn_id(中断帧与工具 call/result 均可能带) | 有则用于轮次归属;配对键仍是 tool_call_id / checkpoint_id,不变 |
interrupt_question 帧(当前实现的完整形状)
{
"type": "interrupt_question",
"session_id": "session_x",
"data": {
"questions": [{
"question": "选哪个画幅?",
"header": "画幅", // ≤12 字符,回答 dict 的 key
"options": [
{ "label": "横版", "description": "适合桌面展示" },
{ "label": "竖版", "description": "",
"media": { "fileId": "Ab12Cd.png" } } // 可选预览;注意驼峰 fileId
]
}],
"checkpoint_id": "entry:123",
"metadata": {
"questions": [...], // 与 data.questions 同
"thread_id": "session_x",
"interrupt_id": "entry:123",
"turn_id": "turn_00044fdc-..." // 可选
}
}
}
option 对象约束:label 必填(既是显示文本也是回传值);description 可为空串;media.fileId 可选。每题至少 2 个选项。
| 帧 | data 关键字段 | 时机 |
|---|---|---|
| interrupt_response | answers, cancelled, checkpoint_id, auto_cancelled, cancel_reason | 回答提交后回显 |
| interrupt_cancelled | reason(如 user_sent_new_message), auto_cancelled, checkpoint_id | 问题被取消(含用户直接发新消息导致的自动取消) |
| interrupt_resumed | checkpoint_id, cancelled | 轮次从中断点继续 |
Change 3 · Tool Names
| 变化 | 旧 | 新 | 前端动作 |
|---|---|---|---|
| 改名 | file_shell | bash | 新卡显示"终端";老账本里的 file_shell 仍映射到终端卡 |
| 新增 | — | memory | 加显示名/图标;参数结果先走通用 JSON 卡 |
| 新增 | — | session_search | 同上 |
| 新增 | — | feishu | 同上;action 枚举见下 |
| 删除 | generate_image | 经 skill_toolcall_curl 调下游 generate-* | 新会话不再出现此工具名;老账本仍要能渲染 |
| 删除 | create_videos | 同上 | 同上 |
| 删除 | in_context_update_preference | 无替代(偏好体系已裁) | 删除依赖其结果显示"偏好保存成功"的逻辑 |
工具名不得做成封闭枚举。未知 tool_name 必须落到通用工具卡(名称 + 参数/结果 JSON),否则每次服务端新增工具前端都会崩一次。
arguments.action 枚举:read / create / edit / extract_media / append_records / update_records / send_card / request_auth,另有可选 hint(面向用户的操作短标题,适合直接当卡片标题渲染)。
授权相关的两条协议事实:业务动作未授权时返回 AUTH_REQUIRED(不含任何 URL);request_auth 触发网关内部把授权卡直接推到用户飞书,授权链接不进模型消息、不进工具结果——前端不需要渲染任何授权链接,失败态只有 AUTH_CARD_FAILED(details.reason,如 FEISHU_USER_NOT_FOUND)。
Change 4 · Tool Results
旧 · data.content
{
"exit_code": 0,
"stdout": "Ab12Cd.png\n",
"batch_result": {
"type": "batch_result",
"successful": [...]
}
}
新 · data.content
{
"type": "batch_result",
"model": "sandbox_upload",
"successful": [...],
"meta": {
"exit_code": 0,
"stdout": "Ab12Cd.png\n"
}
}
content.type === "batch_result" 直接按批量结果渲染;exit_code/stdout 挪进了 meta。successful / failed / total 等 BatchResult 通用字段本身没变。document 是新增资产类型,无预览时显示文件名 + view_url。旧实现可能在已产出文件的情况下仍返回 total=0、successful=[]、status=running。现在终态可信:
{
"type": "batch_result",
"tool": "spawn_subagent",
"title": "Apollo 17 资料检索",
"execution_time": 42.318,
"total": 3,
"successful": [{ "file_id": "Ab12Cd.jpg", "title": "地球照片" }],
"subagent": { "status": "success", "artifacts": [...] }
}
前端以 subagent.status 与 successful[] 收尾进度卡,不再出现永远转圈。
Change 5 · generation_bundle
请求/响应信封与 3.0 之前完全一致(事件名、四个字段名都没变),变的是内容:preferences 恒为 {}(会话级偏好已裁撤,不复活);scope 从偏好提示改装模型目录,内容与 tools-backend /imagine/models 的 data.models 逐字段一致。
{
"type": "generation_bundle",
"data": {
"session_id": "session_x",
"preferences": {}, // 恒空
"scope": {
"models": [{
"model": "seedream",
"tool": "generate-seedream",
"description": "Generate or edit images with Seedream 5 Pro",
"media_type": "image", // image | video | audio
"modes": ["create", "edit"],
"default_mode": "create",
"aspect_ratios": ["auto", "16:9", "1:1", ...]
// 视频类另有 duration:{default,min?,max?};音频类另有 input_media_types/file_count
}]
},
"generation_version": 1784624703 // 上游版本号,无则为目录刷新时间戳;60s 缓存内稳定
}
}
scope: {}、generation_version: 0,连接不报错——按空目录展示即可。GET /api/v2/generation-models、GET /api/generation/providers。旧 provider / 默认模型 / 比例偏好的整套设置 UI 一并下线;update_preferences / update_scope 的兼容 ACK 不代表任何设置被保存。Checklist
message_queued:保留用户气泡,标"排队中",不报错不自动重发。queue_id === message_received.data.message_id 配对气泡,切"处理中"。status(三态)与 queued_count;can_send_message=false 不禁用输入框。status === "pending_question" 时保留问题卡,不当作轮次结束。checkpoint_id / 消息 ID 取消 UUID 校验,按不透明字符串原样保存与回传。option.label(含预览读 media.fileId);老账本字符串选项兜底当 label。turn_id。bash / memory / session_search / feishu 的显示名与图标;未知工具名一律落通用卡。file_shell 映射到终端卡。generate_image / create_videos 的新调用;老账本仍可渲染。in_context_update_preference 依赖。bash/mai-upload 结果按 content.type === "batch_result" 顶层识别,exit_code/stdout 读 meta。document 类型;audio/document 不进画布;子代理进度以 subagent.status + successful[] 收尾。generation_bundle.data.scope.models;删除硬编码表与已下线 REST 调用。