# 交互协议与扩展快速指南
> 面向维护者的速记:5 分钟内了解 UI 交互协议、Schema 以及如何新增工具/窗口。所有示例均基于当前统一事件通道 `ui:request` + 命令 `answer_interaction`。
## 1. 协议总览
- 事件通道:Tauri 后端向前端发送 `ui:request`,负载为 `InteractionPayload`(见下)。
- 回传命令:前端完成交互后调用 `answer_interaction`,负载映射为 `UiResponse`。
- 请求唯一性:后端 `UiBus` 为每次交互生成递增的 `id`,前后端按 `id` 对应。
- 队列模型:前端按窗口 label 维护 FIFO 队列;空队列自动隐藏窗口。窗口由 `ui_hints.window` 或 `ToolRegistry` 决定,layout 用于几何持久化与默认尺寸选择。
### 1.1 InteractionPayload(事件载荷)
```json
{
"id": "123",
"kind": "ask-user-question", // 工具标识,与 UiTool::KIND 对齐
"request": { ... }, // 工具专属 UI 参数,透传为 JSON
"uiHints": {
"window": "main", // 选填,窗口模板 label,缺省取 ToolRegistry.window_kind(kind)
"layout": "ask-compact" // 选填,布局 profile,用于窗口状态持久化 & 默认尺寸
}
}
```
字段说明:
- `id`:字符串,自增生成,确保前后端一一对应。
- `kind`:工具类别;前端用 `dialogRegistry` 解码为具体组件;后端由 `UiTool::KIND` 统一声明。
- `request`:对应工具的 UI 数据,保持结构化 JSON;后端使用 `serde_json::Value` 透传。
- `uiHints`:
- `window`:窗口模板/label,覆盖默认窗口选择。
- `layout`:布局 profile,用于 window_state v2 的窗口尺寸/位置存储;前端也可据此应用特定排版。
### 1.2 UiResponse(命令载荷)
`answer_interaction` 将前端结果回传给后端 `UiBridge`:
- `status`:`ok | cancelled | timeout | error`(见 `UiResponseStatus`)。
- `value`:`Option<String>`,通常为序列化后的业务结果(ask: 答案;confirm: {confirmed})。
- `detailMessage`:可选,携带取消原因、错误详情或额外备注。
后端会根据 `status` 将结果映射回 MCP 工具的返回对象或结构化错误(超时/取消/错误)。
## 2. Schema 速查(后端定义)
- `AskUserQuestionParams`: `questions: Question[]`,每题至少 2 个选项(无上限),支持 `multiSelect`;UI 会为每题自动添加一个 `Other` 文本输入选项,因此 **调用方禁止在 `options` 中手动再添加任何“其他/Other”选项**。必填 `topic: string`,用于在弹窗顶栏显示本次交互的主题。返回 `{ answers: string[], extraInfo?: string }`,`answers` 按题目顺序输出字符串,格式为 `question → label`(多选用 `, ` 分隔多个 label;用户自填“其他/Other”时直接输出用户填写文本,不添加 `Other -` 前缀),取消时返回 `{ status: "cancelled", cancelReason?: string }`。
- `QuestionOption.label` 必须是用户可直接理解和选择的**具体决策文本**(如 `Production`、`Use SQLite`、`只做必要修复`),**禁止**使用 “选项A”“选项B”“Option A”“Choice 1” 等占位符,**禁止**使用任何形式的 “Other/其他 - 我将自行补充” 之类写法;在 `multiSelect: true` 时,每个选项必须是可独立勾选的**原子决策**,不得出现“包含关系”选项(例如“添加A特性”与“添加A和B特性”同时出现),如需“只A/只B/A+B”这类互斥组合,应设置 `multiSelect: false` 并将组合显式写成不同选项。
- `ConfirmActionParams`: 标题/正文/按钮文案/危险标记/默认聚焦,必填 `topic: string` 用于在顶栏显示本次交互主题;返回 `{ confirmed: bool }`。
- `InteractionUiHints`: `{ window?: string, layout?: string }`,缺省 window = `ToolRegistry.window_kind(kind)`,缺省 layout = `kind`(前端桥接层中兜底)。
## 3. 快速扩展手册
### 3.1 新增 MCP 工具(后端)
1) 定义参数与 UI 负载结构体,派生 `Deserialize`/`Serialize`/`JsonSchema`(参数侧)。
2) 为工具声明零尺寸类型并实现 `UiTool`(`KIND`、`WINDOW_KIND`)。
3) 在 `ToolRegistryBuilder` 中注册新工具(如在 `builtin_tool_registry` 内追加 `register::<NewTool>()`)。
4) 在服务实现(如 `AskUserService`)中添加 `#[tool]` 方法:校验参数 → 调用 `ui_bus.dispatch(new_kind, ui_hints, |id| build_request(id, params))` → 解析 `UiResponse` 为 MCP 返回值/错误。
### 3.2 新增窗口模板(后端)
1) 在 `WindowManager` 的模板表中新增 `WindowTemplate`:设置 `label`、`decorations`、`transparent`、`default_size` 等。
2) 让新工具的 `WINDOW_KIND` 指向该模板(或在 `ui_hints.window` 中显式指定)。
3) 如需专用布局/尺寸持久化,约定 `ui_hints.layout` 值;前端会将 `layout` 透传给 window-state v2,存储路径 `~/.popup-mcp/window-state.json`。
### 3.3 前端注册对话框
1) 在 `src/dialogs/registry.ts` 注册:`{ kind, decode, layout, component }`,`decode` 负责把 `request` 反序列化为组件 Props。
2) 组件:在 `src/components` 下实现 Vue 组件,Prop 对齐 `decode` 输出;统一放入 `DialogHost` 的动态渲染插槽。
3) 桥接:`useAppDialogBridge` 已统一处理事件 → registry → 队列入队;若需要自定义窗口 label,确保后端 `ui_hints.window` 与模板一致。
## 4. 回归检查清单(手动/自动皆可)
> 可按本清单撰写 e2e;当前仓库未内置测试运行命令,可在外部环境执行。
- ask-user-question:
- 单题单选、多选各一条;提交后返回答案与 `extraInfo`。
- 用户在取消弹层填写原因,后端收到 `status=cancelled` 且 `cancelReason` 保留。
- confirm-action:
- 默认按钮聚焦与自定义文案生效;`danger=true` 呈现危险样式;返回 `confirmed` 布尔值。
- 窗口/布局:
- 不同 `layout` 会写入/读取独立的 window-state v2 记录。
- 队列:
- 同一窗口连续三次请求按 FIFO 逐个展示,队列清空后窗口自动隐藏。
## 5. 术语速记
- `kind`:工具标识,前后端一致,由 `UiTool::KIND` 定义。
- `window`:窗口模板 label,默认为 registry 中该工具绑定的模板。
- `layout`:布局 profile,用于窗口尺寸/位置持久化,也可让前端选择不同排版。
- `dialogRegistry`:前端解码/渲染注册表,新增对话框的唯一入口。