Files
NotesAgentic/docs/contracts/第二阶段接口契约-开发版.md
T

1486 lines
44 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.
# 第二阶段接口契约与开发规划
> 文档状态:接口冻结草案
>
> 更新日期:2026-09-02
>
> 依据:`../architecture/第二阶段团队分工表.md`、`../architecture/AI笔记软件技术栈说明-团队版-v2.3.md`、`后端接口契约-开发版.md`
本文统一第二阶段新增能力的 HTTP、SSE、前端 Service、桌面 Host 和内部模块接口。文中标记为“计划新增”的路径尚未实现,不能据此判断当前服务已经支持;实现完成后以 FastAPI `/openapi.json`、TypeScript Wire DTO 和自动化测试共同作为最终依据。
人员分工和任务优先级不在本文重复维护,以第二阶段分工表为准。
---
## 1. 规划原则
1. 第一阶段已经发布的路径和字段保持兼容,第二阶段优先增加可选字段、子资源和新事件。
2. 前端只消费项目 Contract,不解析 Plugin Manifest、MCP 原始消息或模型厂商响应。
3. Agent、Benchmark 和 Trace 共用同一运行事件,不建立测试专用的旁路执行协议。
4. 长任务统一建模为 Job,创建接口快速返回,进度通过查询或 SSE 获取。
5. 文件、附件、插件包和 Dataset 均使用受控 ID;HTTP 请求不接受任意本地绝对路径。
6. Secret 只经专用写入接口提交,普通读取接口只返回是否配置和引用 ID。
7. Mermaid、函数图像和导出器共享静态渲染 Contract,不从 Vue 组件 DOM 抓取结果。
8. 所有第三方协议在 Adapter 边界转换,错误进入前端前映射为项目错误码。
## 2. 状态说明
| 标记 | 含义 |
| --- | --- |
| 已实现 | 接口已经落地并由当前 OpenAPI 与自动化测试覆盖 |
| 扩展 | 路径已存在,第二阶段增加字段、事件或行为 |
| 计划新增 | 第二阶段需要新增实现 |
| 内部 Contract | 不直接暴露 HTTP,由两个模块共同遵守 |
| Host Contract | 由 Tauri/Rust Host 提供,Web 开发模式可使用 Mock Adapter |
## 3. 第二阶段接口总览
| 域 | 方法 | 路径或 Contract | 状态 | 用途 |
| --- | --- | --- | --- | --- |
| Transcription | POST | `/api/media/transcriptions` | 扩展 | 创建真实音频转写任务 |
| Transcription | GET | `/api/media/transcriptions` | 计划新增 | 分页获取转写任务 |
| Transcription | GET | `/api/media/transcriptions/{job_id}` | 扩展 | 获取分段转写和任务状态 |
| Transcription | GET | `/api/media/transcriptions/{job_id}/events` | 计划新增 | 订阅模型加载、分离和转写进度 |
| Transcription | POST | `/api/media/transcriptions/{job_id}/cancel` | 计划新增 | 取消音频任务 |
| Transcription | POST | `/api/media/transcriptions/{job_id}/notes` | 计划新增 | 将 Transcript 写入 Knowledge Core |
| Agent Trace | GET | `/api/agent/runs/{run_id}/events` | 已实现 | 支持游标恢复并增加模型与权限事件 |
| Agent Trace | GET | `/api/agent/runs/{run_id}/trace` | 已实现 | 分页读取可回放 Trace 快照 |
| Plugin Host | GET | `/api/plugins/{plugin_id}/host` | 已实现 | 获取 MCP Host 健康状态 |
| Plugin Host | POST | `/api/plugins/{plugin_id}/host/restart` | 已实现 | 重启异常 Host 并重新发现 Tool |
| Plugin Command | GET | `/api/plugin-contributions/commands` | 已实现 | 获取前端可展示的 Command |
| Plugin Command | POST | `/api/plugin-contributions/commands/{command_id}/execute` | 已实现 | 受控执行 Command |
| Plugin Settings | GET | `/api/plugins/{plugin_id}/settings` | 已实现 | 获取 Schema 与非敏感配置 |
| Plugin Settings | PUT | `/api/plugins/{plugin_id}/settings` | 已实现 | 更新非敏感配置 |
| Plugin Settings | PUT/DELETE | `/api/plugins/{plugin_id}/settings/{key}/secret` | 已实现 | 写入或删除 Secret Reference |
| Provider | 现有路径 | `/api/providers/*``POST /api/chat` | 扩展 | 补齐协议能力和统一行为 |
| Retrieval | GET/POST | `/api/index/status``/api/index/rebuild` | 扩展 | 暴露 Embedding 兼容状态并安全重建向量 |
| Benchmark | GET | `/api/benchmarks/datasets` | 计划新增 | 枚举受控 Dataset |
| Benchmark | POST | `/api/benchmarks/rag/runs` | 计划新增 | 创建 RAG Benchmark |
| Benchmark | POST | `/api/benchmarks/agent/runs` | 计划新增 | 创建 Agent Benchmark |
| Benchmark | GET | `/api/benchmarks/runs` | 计划新增 | 分页获取 Benchmark Run |
| Benchmark | GET/POST | `/api/benchmarks/runs/{run_id}/*` | 计划新增 | 查询、订阅、取消和读取报告 |
| Export | POST | `/api/exports` | 计划新增 | 创建 HTML/PDF/DOCX 导出任务 |
| Export | GET | `/api/exports` | 计划新增 | 分页获取导出任务 |
| Export | GET | `/api/exports/{job_id}` | 计划新增 | 查询导出任务 |
| Export | GET | `/api/exports/{job_id}/file` | 计划新增 | 下载已完成产物 |
| Export | POST | `/api/exports/{job_id}/cancel` | 计划新增 | 取消导出任务 |
| Theme | Host Contract | `ThemePackageService` | 计划新增 | 导入、预览、启停和卸载主题包 |
| Renderer | 内部 Contract | `StaticRenderer` | 计划新增 | Mermaid/Function Plot 预览和导出复用 |
---
## 4. 公共协议
### 4.1 JSON 与时间
- HTTP JSON 字段统一使用 `snake_case`
- TypeScript Wire DTO 与 JSON 保持同名,View Model 可在 Service 层转换。
- 时间使用 UTC ISO 8601,例如 `2026-08-31T10:30:00Z`
- 耗时统一使用 `duration_ms`,音视频时间使用秒数浮点值。
- ID 是不透明字符串,调用方不得解析前缀或依赖生成规则。
- 枚举新增值时前端必须提供 unknown fallback,不能使整个页面渲染失败。
### 4.2 Job 状态
第二阶段异步任务统一使用:
```text
queued
running
completed
failed
cancelled
```
第一阶段 Transcription 曾使用 `processing`。迁移期间前端 Wire DTO 同时接受 `processing``running`Service 统一映射为 `running`;所有消费者更新后,后端再停止返回旧值。
通用进度结构:
```json
{
"phase": "transcribing",
"current": 42,
"total": 100,
"percent": 42.0,
"message": "正在转写音频"
}
```
`percent` 可以为 `null`,调用方不能以 100 代替 `completed`。终态 Job 必须有 `completed_at`;失败 Job 必须提供项目错误码,不直接暴露第三方堆栈。
### 4.3 分页和游标
普通资源列表继续使用:
```json
{
"items": [],
"page": { "total": 0, "limit": 50, "offset": 0 }
}
```
事件和 Trace 使用序列游标:
```json
{
"items": [],
"next_sequence": 120,
"has_more": false
}
```
`after_sequence` 表示只返回大于该值的事件。事件序号只保证在一个资源内单调递增,客户端不得假设从 0 或 1 开始。
### 4.4 SSE
SSE 事件统一包含 `id``event` 和 JSON `data`
```text
id: 42
event: Progress
data: {"sequence":42,"resource_id":"job_123","data":{"percent":50},"timestamp":"2026-08-31T10:30:00Z"}
```
- `id` 等于可恢复的事件 sequence。
- 客户端重连时发送 `Last-Event-ID`;开发环境也可以使用 `?after_sequence=42`
- 服务端先回放缺失事件,再切换为实时事件。
- 重放与实时交界处允许重复,客户端按资源 ID 与 sequence 去重。
- `Completed``Failed``Cancelled` 是终止事件;正常断流但没有终止事件时,客户端按可重连处理。
- 心跳使用 SSE 注释行,不创建业务事件。
### 4.5 统一错误
沿用第一阶段结构:
```json
{
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "Resource was not found.",
"details": {}
}
}
```
`message` 用于用户提示,`details` 只放可安全展示和定位的结构化信息。前端业务分支只判断 `code`
---
## 5. Multimodal / Transcription
### 5.1 创建转写任务
`POST /api/media/transcriptions`,扩展现有接口,成功返回 `202 Accepted`
请求:
```json
{
"attachment_id": "attachment_123",
"language": null,
"diarization": true,
"word_timestamps": false,
"profile_id": "default",
"metadata": {}
}
```
约束:
- `attachment_id` 必须属于当前 Vault 的 Host 管理附件。
- `language = null` 表示自动识别。
- `profile_id` 引用服务端模型配置,不允许请求直接传 CUDA 路径或任意模型目录。
- `metadata` 只用于关联业务上下文,不进入模型参数。
响应:
```json
{
"job_id": "transcription_123",
"attachment_id": "attachment_123",
"status": "queued",
"progress": null,
"language": null,
"diarization_enabled": true,
"segments": [],
"text": null,
"warnings": [],
"error": null,
"created_at": "2026-08-31T10:30:00Z",
"started_at": null,
"updated_at": "2026-08-31T10:30:00Z",
"completed_at": null
}
```
### 5.2 Transcript Segment
```json
{
"segment_id": "segment_001",
"speaker": "SPEAKER_00",
"start_time": 12.4,
"end_time": 18.9,
"text": "这一部分介绍进程调度。",
"confidence": 0.93,
"words": []
}
```
`speaker``confidence``words` 可以为空。Segment 按 `start_time` 排序且不得重叠为负时长。没有 pyannote 能力时任务可以完成,但必须在 `warnings` 中返回 `DIARIZATION_UNAVAILABLE`
### 5.3 查询、事件和取消
| 方法 | 路径 | 响应 |
| --- | --- | --- |
| GET | `/api/media/transcriptions?status=&limit=&offset=` | `TranscriptionJobListResponse` |
| GET | `/api/media/transcriptions/{job_id}` | 完整 `TranscriptionJob` |
| GET | `/api/media/transcriptions/{job_id}/events` | `TranscriptionEvent` SSE |
| POST | `/api/media/transcriptions/{job_id}/cancel` | `OperationResponse` |
事件类型:
```text
Queued
ModelLoading
DiarizationStarted
DiarizationCompleted
TranscriptionStarted
Progress
Completed
Failed
Cancelled
```
取消必须向正在运行的模型任务传播。接口返回 accepted 只表示取消请求已记录,最终状态以 Job 或终止事件为准。
### 5.4 写入 Knowledge Core
`POST /api/media/transcriptions/{job_id}/notes`
```json
{
"title": "操作系统课程录音",
"folder": "课程/操作系统",
"include_timestamps": true,
"include_speakers": true,
"template_id": null
}
```
仅 completed Job 可以转换。响应返回第一阶段 `Note` Contract。生成的 Block Citation 需要保留 `source_audio``start_time``end_time``speaker`,使搜索与 Agent 引用能够跳回音频。
### 5.5 Multimodal 错误码
```text
ATTACHMENT_NOT_FOUND
UNSUPPORTED_AUDIO_FORMAT
TRANSCRIPTION_PROFILE_NOT_FOUND
TRANSCRIPTION_MODEL_UNAVAILABLE
DIARIZATION_MODEL_UNAVAILABLE
TRANSCRIPTION_FAILED
TRANSCRIPTION_CANCELLED
TRANSCRIPT_NOT_READY
```
---
## 6. Agent Trace
### 6.1 兼容现有 AgentEvent
保留现有事件:
```text
RunStarted
TextDelta
ThinkingDelta
ToolCall
ToolResult
PermissionRequired
Usage
Citation
RunCompleted
RunFailed
RunCancelled
```
第二阶段增加:
```text
ModelCallStarted
ModelCallCompleted
ModelCallFailed
PermissionResolved
```
`GET /api/agent/runs/{run_id}/events` 增加 `Last-Event-ID``after_sequence` 支持,不改变现有事件 envelope。
### 6.2 Trace 快照
`GET /api/agent/runs/{run_id}/trace?after_sequence=0&limit=200`
```json
{
"run_id": "run_123",
"status": "running",
"items": [],
"next_sequence": 42,
"has_more": false,
"summary": {
"model_calls": 2,
"tool_calls": 3,
"duration_ms": 1530,
"token_usage": 2048,
"errors": 0
},
"config_snapshot": {}
}
```
Trace API 返回事件事实,不返回前端树形布局。前端根据 parent ID 和 sequence 构建时间线或树,Benchmark 使用相同事件计算指标。
### 6.3 新增事件数据
`ModelCallStarted`
```json
{
"model_call_id": "model_call_01",
"provider_id": "deepseek",
"model": "deepseek-chat",
"step": 2
}
```
`ModelCallCompleted`
```json
{
"model_call_id": "model_call_01",
"duration_ms": 820,
"finish_reason": "tool_calls",
"input_tokens": 1200,
"output_tokens": 240
}
```
`PermissionResolved`
```json
{
"request_id": "permission_01",
"permission": "notes.write",
"decision": "allow_once"
}
```
ToolCall 和 ToolResult 增加可选 `parent_model_call_id``duration_ms` 和经过截断/净化的摘要字段。完整敏感参数不进入 Trace。
### 6.4 Trace 保留和脱敏
- `run_id + sequence` 是事件幂等键。
- 运行记录和事件由持久化 Repository 管理,不能只保存在 SSE 订阅队列。
- API Key、Authorization Header、Secret Setting、完整附件正文默认脱敏。
- 大型 Tool Result 保存摘要与受控 artifact 引用,不直接塞入事件 JSON。
- Trace 被 Benchmark 引用时保存运行配置快照,避免后续 Provider 配置变更导致报告不可解释。
### 6.5 Trace 错误码
```text
AGENT_RUN_NOT_FOUND
TRACE_NOT_AVAILABLE
TRACE_CURSOR_EXPIRED
TRACE_CURSOR_INVALID
```
---
## 7. MCP Bridge 与 Plugin Contribution
### 7.1 MCP 内部 Adapter
Agent Runtime 不使用 MCP 原始类型。MCP Bridge 必须完成:
```text
MCP tools/list
→ JSON Schema 校验
→ 项目 ToolDefinition
→ Tool Registry
项目 ToolCall
→ MCP tools/call
→ MCP Content / Error
→ 项目 ToolResult
```
进入 Tool Registry 的名称使用 `<plugin_id>.<tool_name>` 命名空间。重复名称、无效 Schema、未声明权限和超过结果大小上限时拒绝注册。
内部接口示意:
```python
class McpBridge(Protocol):
async def start(self, plugin: PluginManifest) -> "PluginHostStatus": ...
async def discover_tools(self, plugin_id: str) -> list[ToolDefinition]: ...
async def call_tool(self, plugin_id: str, call: ToolCall) -> ToolResult: ...
async def cancel(self, plugin_id: str, request_id: str) -> None: ...
async def stop(self, plugin_id: str) -> None: ...
```
首个实现已支持本地 `stdio`,按 MCP `2025-11-25` 发起 initialize,并兼容 `2025-06-18``2025-03-26``2024-11-05` 协商结果。Host 负责 capability negotiation、分页 `tools/list`、进程生命周期、超时取消、stderr 隔离和异常退出后的 Tool 注销。stdio 消息使用 UTF-8 单行 JSON-RPC,并在完整行进入内存前执行有界读取;当前不实现 Streamable HTTP。
MCP Plugin 的 `backend` 增加:
```yaml
backend:
type: mcp
transport: stdio
command: uvx
args: [--isolated, --from, example-mcp==1.2.3, example-mcp]
startup_timeout_seconds: 60
tool_timeout_seconds: 30
```
命令通过参数数组直接启动,不经过 Shell。带路径的 executable 必须位于 Plugin 包内;PATH 中的命令可以按名称引用。Python 包形式的 MCP 推荐使用固定版本的 `uvx --isolated --from`,但 `uvx` 只隔离依赖而不是文件/网络/系统调用安全沙箱,非 Python Server 不强制使用。开发模式首次运行未缓存的 uvx 包可能联网解析,因此 startup 示例使用 60 秒;生产安装阶段必须预取并验证,运行阶段不得临时解析依赖。子进程只继承运行所需的系统环境变量,不继承 `OPENAI_API_KEY``APP_DB_PATH`、Vault 路径等宿主状态。Secret 注入留给阶段 D 的专用引用接口。
在平台沙箱和可信命令许可完成前,`APP_ENVIRONMENT != development` 时启用 MCP Plugin 必须返回 `403 MCP_TRUST_APPROVAL_REQUIRED`,不得启动进程或注册 Tool。该门禁由后端执行,不能只依赖前端提示或文档约定。
远端 Tool 的可选项目权限放在 MCP `_meta`
```json
{ "_meta": { "notesagent/permission": "notes.read" } }
```
该权限必须属于项目已知权限并同时出现在 Plugin Manifest 中。发现结果必须与 `contributes.tools` 的命名空间 ID 完全一致;校验全部成功后才一次性发布到 Tool Registry。
当前 Web 开发接口 `POST /api/plugins/install` 使用 `package_path`。桌面 Host 接入后,`ExtensionInstallRequest` 增加 `package_id`,由文件选择器产生临时包句柄;`package_path` 只在明确的 development 环境保留并标记 deprecated,生产构建拒绝任意前端路径。
### 7.2 Plugin Host 状态
`GET /api/plugins/{plugin_id}/host`
```json
{
"plugin_id": "example-plugin",
"backend_type": "mcp",
"transport": "stdio",
"status": "ready",
"tools_count": 3,
"started_at": "2026-08-31T10:30:00Z",
"last_seen_at": "2026-08-31T10:31:00Z",
"error": null
}
```
Host 状态:
```text
stopped
starting
ready
unhealthy
error
```
`POST /api/plugins/{plugin_id}/host/restart` 返回 `202 OperationResponse`。重启期间先注销旧 Tool,发现和校验全部成功后再一次性发布新 Tool 集合,避免半注册状态。
当前实现还返回协商后的 `protocol_version``server_name``server_version`。单条协议消息上限为 2 MiB,单次 Tool Result 上限为 256 KiB;超限分别按 Host/Result 错误处理。Server 异常退出或发送无效 stdout 消息时,Host 进入 `unhealthy`Plugin 进入 `error`,相关 Tool 立即注销。取消会同时通知 Server、移除 pending request 并唤醒本地等待线程。Restart 不得把 `installed``disabled``permission_required` Plugin 隐式启用,这些状态必须走 Enable。
### 7.3 Command Contribution 列表
`GET /api/plugin-contributions/commands?location=command_palette`
```json
{
"items": [
{
"command_id": "example-plugin.open-search",
"plugin_id": "example-plugin",
"title": "打开外部搜索",
"description": "在插件服务中搜索当前选区",
"icon": "search",
"locations": ["command_palette", "context_menu"],
"when": ["workspace.has_vault", "editor.has_selection"],
"parameters": {
"type": "object",
"properties": {}
},
"enabled": true
}
]
}
```
`icon` 只能引用宿主图标 ID 或插件包内已验证资源。`when` 是宿主支持的条件 token 列表,不执行插件提供的 JavaScript 表达式。
### 7.4 执行 Command
`POST /api/plugin-contributions/commands/{command_id}/execute`
```json
{
"arguments": {},
"context": {
"note_id": "note_123",
"selection": "选中的文本"
}
}
```
宿主根据 Command 声明裁剪 Context,插件不能要求前端上传整个 Pinia 状态。响应:
```json
{
"command_id": "example-plugin.open-search",
"status": "completed",
"effect": {
"type": "notification",
"payload": { "level": "success", "message": "命令已完成" }
}
}
```
允许的 effect 首批为 `none``notification``navigate``refresh``job`。每类 payload 也是契约的一部分:`none` 必须为空;`notification` 只接受 `level``info/success/warning/error`)和非空 `message``navigate` 只接受宿主路由名 `route``refresh` 只接受 `workspace/commands/settings/plugins` 范围;`job` 只接受受限格式的 `job_id`。后端拒绝未知字段和不匹配的 payload,前端仍须按判别联合穷尽处理,不得把 effect 当作任意代码执行。
当前宿主只注册已启用且已满足权限授权的 Plugin Command。执行前按 JSON Schema 校验参数、按 `when` 校验上下文,再根据 Command 声明裁剪 Context;单次执行默认超时 30 秒,effect 的 JSON 编码结果不得超过 64 KiB。宿主保留最多 500 条轻量审计事件,仅记录 Command、Plugin、状态、耗时和错误码,不记录 arguments、Context、effect 或 Secret。
需要 Secret 的 Command 必须在 `commands.yaml` 的内部 `secrets` 数组中声明对应 Setting Key,并在 Plugin Manifest 声明 `secrets.use` 权限。安装时宿主校验该字段确实属于当前 Plugin Settings Schema 的 `secret` 类型;只有权限已授予并启用后,运行时才向受控 handler 或 MCP Command Target 提供声明过的 Secret。未声明字段返回 `PLUGIN_SECRET_ACCESS_DENIED`,必填 Secret 未配置返回 `PLUGIN_SECRET_REQUIRED``secrets` 不属于前端 `PluginCommand` DTOSecret 明文也不会并入普通 Settings 字典。
`commands.yaml` 中的执行目标必须在宿主白名单 `handler` 与当前 Plugin 命名空间的 `mcp_tool` 之间二选一。MCP Command Target 不注册为 Agent Tool;宿主用 `_notesagent` 保留包装传入 Command ID、参数、裁剪后的 Context、已校验的非敏感 Settings 和声明过的 Secret,并将 MCP structured result 再校验为白名单 effect。作为宿主协议标记,目标 MCP Tool 的 `inputSchema` 必须在顶层 `properties` 中直接声明 `_notesagent: { type: object }`;不得用顶层组合或引用替代该标记。`_notesagent` 对象内部仍可使用完整 Draft 2020-12 约束、文档内引用和组合 Schema。启用阶段只检查协议标记,不尝试求解 Schema 或伪造业务值;宿主会保留完整 Schema,并在每次调用前用官方 Validator 校验真实信封。Command 与 Tool Schema 仅允许 `#...` 文档内引用,任何通过 `$ref``$dynamicRef` 指向文件、HTTP 或其他外部资源的 Schema 都会在注册前被拒绝。文档内引用遵循 Draft 2020-12 的嵌套 `$id` 与 Anchor 资源作用域,不能解析的引用不得进入运行时。
### 7.5 Settings Schema
`GET /api/plugins/{plugin_id}/settings`
```json
{
"plugin_id": "example-plugin",
"schema_version": 1,
"fields": [
{
"key": "result_limit",
"label": "结果数量",
"description": "每次最多返回的结果数",
"type": "number",
"required": true,
"default": 10,
"minimum": 1,
"maximum": 100,
"options": []
},
{
"key": "api_key",
"label": "API Key",
"type": "secret",
"required": true,
"options": []
}
],
"values": { "result_limit": 10 },
"secrets": { "api_key": { "configured": false } }
}
```
字段类型首批固定为:
```text
string
number
boolean
select
secret
```
Number 字段的 `minimum``maximum` 必须是有限数值;`NaN`、正无穷和负无穷均视为无效 Settings Schema。
### 7.6 更新 Settings 和 Secret
`PUT /api/plugins/{plugin_id}/settings`
```json
{
"schema_version": 1,
"values": { "result_limit": 20 }
}
```
该接口拒绝 secret 字段。Schema 版本过期返回 `PLUGIN_SETTINGS_VERSION_CONFLICT` 并附当前版本。没有默认值的必填普通字段必须先通过该接口配置;否则 Plugin Enable 和 Command Execute 返回 `PLUGIN_SETTINGS_REQUIRED`MCP 或内部 handler 不会收到残缺配置。
Secret 使用:
```text
PUT /api/plugins/{plugin_id}/settings/{key}/secret
DELETE /api/plugins/{plugin_id}/settings/{key}/secret
```
写入请求:
```json
{ "secret": "仅在本次请求中出现的明文" }
```
响应只返回:
```json
{
"plugin_id": "example-plugin",
"key": "api_key",
"configured": true
}
```
Secret 明文不进入普通 Settings、日志、Trace、Benchmark Dataset 或前端持久化。
非敏感值按 `plugin_id` 写入 `APP_DATA_DIR/plugins/settings.json`。该文件只保存普通值、Schema 版本和确定性的定长 Secret Reference,格式为 `plugin.<sha256(plugin_id\\0setting_key)>`;Secret 本身由宿主凭据存储加密保存。卸载 Plugin 时同时清理它的 Settings 命名空间和 Secret Reference。当前开发阶段使用 Fernet 文件凭据存储,第三阶段接入桌面 Host 后应迁移到 Stronghold 或系统 Keychain。
`plugin.*` 为宿主保留凭据命名空间。`/api/credentials/{credential_id}`、Provider 持久配置、Provider 临时测试凭据和 Provider Resolver 均拒绝该前缀,防止通过 Provider 链路覆盖、删除或向外部 Base URL 发送 Plugin Secret。
### 7.7 Plugin/MCP 错误码
```text
PLUGIN_HOST_UNAVAILABLE
PLUGIN_HOST_START_FAILED
PLUGIN_HOST_UNHEALTHY
MCP_INITIALIZE_FAILED
MCP_CAPABILITY_UNSUPPORTED
MCP_TOOL_SCHEMA_INVALID
MCP_TOOL_CALL_FAILED
MCP_TOOL_RESULT_TOO_LARGE
MCP_TRUST_APPROVAL_REQUIRED
PLUGIN_COMMAND_NOT_FOUND
PLUGIN_COMMAND_CONFLICT
PLUGIN_COMMAND_INVALID
PLUGIN_COMMAND_ARGUMENT_INVALID
PLUGIN_COMMAND_CONTEXT_INVALID
PLUGIN_COMMAND_TIMEOUT
PLUGIN_COMMAND_EXECUTION_FAILED
PLUGIN_COMMAND_RESULT_INVALID
PLUGIN_COMMAND_RESULT_TOO_LARGE
PLUGIN_COMMAND_TARGET_SCHEMA_MISMATCH
PLUGIN_SETTINGS_SCHEMA_INVALID
PLUGIN_SETTINGS_VERSION_CONFLICT
PLUGIN_SETTINGS_FIELD_INVALID
PLUGIN_SETTINGS_REQUIRED
PLUGIN_SECRET_FIELD_NOT_FOUND
PLUGIN_SECRET_ACCESS_DENIED
PLUGIN_SECRET_REQUIRED
PLUGIN_SECRET_VALUE_INVALID
PLUGIN_SECRET_STORE_ERROR
PLUGIN_STORAGE_ERROR
CREDENTIAL_NAMESPACE_RESERVED
```
---
## 8. Provider Adapter 扩展
第二阶段不新增平行 Provider CRUD,继续使用第一阶段接口:
```text
GET /api/providers
GET /api/providers/presets
POST /api/providers
GET /api/providers/{provider_id}
PATCH /api/providers/{provider_id}
DELETE /api/providers/{provider_id}
GET /api/providers/{provider_id}/models
POST /api/providers/test
POST /api/chat
```
### 8.1 ModelInfo 扩展
`GET /api/providers/{provider_id}/models` 的 item 增加可选字段:
```json
{
"model": "example-model",
"display_name": "Example Model",
"capabilities": ["chat", "streaming", "tool_calling"],
"context_window": 128000,
"max_output_tokens": 8192,
"deprecated": false,
"metadata": {}
}
```
模型列表无法可靠获取 Capability 时返回保守集合,不能默认声称支持 Tool Calling 或 Vision。
### 8.2 Streaming 行为
继续使用现有 ModelEvent
```text
TextDelta
ThinkingDelta
ToolCallStart
ToolCallDelta
ToolCallEnd
Usage
Error
Done
```
第二阶段统一以下规则:
- 每个事件包含单调递增 `sequence`
- `ToolCallDelta``tool_call_id` 聚合,完成时参数必须是合法 JSON Object。
- `Usage` 字段统一为 `input_tokens``output_tokens``total_tokens`
- Model Stream 在成功或失败后都发送且只发送一个终止事件 `Done`;失败顺序为 `Error``Done``Done.data.status``completed``failed``cancelled`
- 浏览器取消 Fetch 或 SSE 后,服务端必须取消上游 Provider 请求。
- 不支持 reasoning 的 Provider 不发送伪造 ThinkingDelta。
### 8.3 Provider 一致性测试 Contract
每个 Adapter 使用相同 Case 描述:
```yaml
id: provider-tool-call-basic
capabilities:
- chat
- streaming
- tool_calling
request_fixture: tool_call_weather
assertions:
terminal_event: Done
tool_calls: 1
usage_required: false
```
测试报告记录 Provider 类型、模型、协议、运行时间和跳过原因。真实网络测试与离线 Adapter 单元测试分组,CI 默认不要求外部 API Key。
### 8.4 Provider 错误码
沿用并补齐:
```text
PROVIDER_AUTH_FAILED
PROVIDER_RATE_LIMITED
PROVIDER_TIMEOUT
PROVIDER_UNAVAILABLE
PROVIDER_PROTOCOL_ERROR
PROVIDER_STREAM_INTERRUPTED
MODEL_NOT_FOUND
MODEL_CAPABILITY_MISMATCH
MODEL_CONTEXT_LENGTH_EXCEEDED
```
---
## 9. RAG / Agent Benchmark
Benchmark Service 同时提供 Python 调用接口和本地 HTTP 接口。CLI、测试和前端报告页调用同一 Service,不各自实现指标。
### 9.1 Dataset 列表
`GET /api/benchmarks/datasets?kind=rag|agent`
```json
{
"items": [
{
"dataset_id": "rag-core-v1",
"kind": "rag",
"version": "1.0.0",
"description": "基础中文笔记检索集",
"case_count": 50,
"content_hash": "sha256:..."
}
]
}
```
Dataset 从仓库或受控导入目录注册。API 不接受调用方提交任意文件路径。
### 9.2 RAG Dataset Case
```json
{
"case_id": "rag-os-001",
"query": "死锁发生需要什么条件",
"expected_note_ids": ["note_deadlock"],
"expected_block_ids": ["block_conditions"],
"citation_required": true,
"tags": ["os", "deadlock"]
}
```
### 9.3 Agent Dataset Case
```json
{
"case_id": "agent-os-review-001",
"prompt": "查找死锁内容并创建三个复习任务",
"allowed_tools": ["rag.search", "notes.read", "tasks.create"],
"expected_tools": ["rag.search", "notes.read", "tasks.create"],
"expected_conditions": {
"citation_required": true,
"tasks_created": 3
},
"tags": ["os", "write"]
}
```
有写操作的 Agent Case 必须运行在隔离 Vault/数据库中,测试结束后清理 Fixture,禁止作用于用户真实知识库。
### 9.4 创建 RAG Benchmark
`POST /api/benchmarks/rag/runs`
```json
{
"dataset_id": "rag-core-v1",
"modes": ["fts", "vector", "hybrid"],
"retrieval": {
"top_k": 10,
"rrf_k": 60,
"rerank": true,
"rerank_candidates": 20,
"score_threshold": 0.0
},
"repeat": 1,
"metadata": {}
}
```
配置快照必须记录 Embedding model ID/version/dimension、Reranker、索引版本、Dataset Hash 和运行环境。
### 9.5 创建 Agent Benchmark
`POST /api/benchmarks/agent/runs`
```json
{
"dataset_id": "agent-core-v1",
"provider_id": "mock",
"model": "mock-1",
"skill_id": null,
"max_steps": 10,
"token_budget": 20000,
"concurrency": 1,
"metadata": {}
}
```
Benchmark Runner 通过正式 Agent Runtime 创建 Run,并从 Trace 计算结果,不能直接调用 Tool Executor 绕过权限和步骤控制。
### 9.6 Benchmark Run
RAG 和 Agent 创建接口均返回 `202 BenchmarkRun`
```json
{
"run_id": "benchmark_123",
"kind": "rag",
"dataset_id": "rag-core-v1",
"dataset_hash": "sha256:...",
"status": "queued",
"progress": null,
"metrics": null,
"config_snapshot": {},
"error": null,
"created_at": "2026-08-31T10:30:00Z",
"started_at": null,
"completed_at": null
}
```
公共接口:
| 方法 | 路径 | 用途 |
| --- | --- | --- |
| GET | `/api/benchmarks/runs?kind=&status=&limit=&offset=` | 分页获取运行记录 |
| GET | `/api/benchmarks/runs/{run_id}` | 获取状态和指标摘要 |
| GET | `/api/benchmarks/runs/{run_id}/events` | SSE 进度和 Case 结果 |
| POST | `/api/benchmarks/runs/{run_id}/cancel` | 取消运行 |
| GET | `/api/benchmarks/runs/{run_id}/report` | 获取结构化完整报告 |
### 9.7 指标 Contract
RAG
```json
{
"hit_at_1": 0.72,
"hit_at_5": 0.91,
"recall_at_k": 0.88,
"mrr": 0.81,
"citation_hit_rate": 0.89,
"p50_latency_ms": 24.5,
"p95_latency_ms": 67.3
}
```
Agent
```json
{
"task_success_rate": 0.80,
"tool_selection_accuracy": 0.92,
"tool_argument_accuracy": 0.86,
"invalid_tool_call_rate": 0.02,
"average_steps": 4.3,
"average_latency_ms": 2300,
"average_token_usage": 3200
}
```
报告同时返回逐 Case 结果、失败原因和 Trace Run ID,汇总指标不能覆盖失败样本。
### 9.8 Benchmark 错误码
```text
BENCHMARK_DATASET_NOT_FOUND
BENCHMARK_DATASET_INVALID
BENCHMARK_CONFIG_INVALID
BENCHMARK_INDEX_INCOMPATIBLE
BENCHMARK_RUN_NOT_FOUND
BENCHMARK_RUN_FAILED
```
### 9.9 Retrieval Profile 与索引兼容
Benchmark、Search 和 RAG Engine 共享内部 `RetrievalProfile`,不得各自硬编码参数:
```json
{
"profile_id": "default-v2",
"version": 2,
"fts_recall": 20,
"vector_recall": 20,
"rrf_k": 60,
"rerank": true,
"rerank_candidates": 20,
"context_top_k": 8,
"score_threshold": 0.0
}
```
第二阶段扩展 `GET /api/index/status`
```json
{
"status": "idle",
"pending_jobs": 0,
"active_job_id": null,
"embedding": {
"model_id": "bge-m3",
"model_version": "...",
"dimension": 1024,
"normalization": "l2"
},
"vector_index": {
"model_id": "hash-v1",
"dimension": 128,
"compatible": false
},
"last_completed_at": null,
"error_message": null
}
```
模型 ID、版本、维度或归一化方式不一致时,Vector/Hybrid 搜索返回 `INDEX_MODEL_MISMATCH`,前端降级到 FTS`POST /api/index/rebuild` 使用现有 `scope = vectors` 重建,不能在查询时混用旧向量。
相关错误码:
```text
EMBEDDING_MODEL_UNAVAILABLE
EMBEDDING_DIMENSION_MISMATCH
INDEX_MODEL_MISMATCH
VECTOR_INDEX_REBUILD_REQUIRED
```
---
## 10. Export Service
### 10.1 创建导出任务
`POST /api/exports`,返回 `202 ExportJob`
```json
{
"source": {
"type": "note",
"note_id": "note_123"
},
"format": "pdf",
"options": {
"theme_id": "light",
"include_title": true,
"include_metadata": false,
"page_size": "A4",
"code_theme": "github-light"
}
}
```
`source.type` 首批支持 `note``markdown``markdown` 来源用于尚未保存的预览,字段大小受限且不持久化到 Trace。`format` 固定为 `html``pdf``docx`
响应:
```json
{
"job_id": "export_123",
"status": "queued",
"format": "pdf",
"progress": null,
"file": null,
"warnings": [],
"error": null,
"created_at": "2026-08-31T10:30:00Z",
"started_at": null,
"completed_at": null
}
```
### 10.2 查询、取消和下载
| 方法 | 路径 | 响应 |
| --- | --- | --- |
| GET | `/api/exports?status=&format=&limit=&offset=` | `ExportJobListResponse` |
| GET | `/api/exports/{job_id}` | `ExportJob` |
| POST | `/api/exports/{job_id}/cancel` | `OperationResponse` |
| GET | `/api/exports/{job_id}/file` | 文件流 |
下载响应设置正确 `Content-Type`、经过清理的 `Content-Disposition` 文件名和 `Content-Length`。未完成、失败或过期 Job 不返回空文件。
完成 Job 的 file
```json
{
"file_name": "操作系统复习.pdf",
"mime_type": "application/pdf",
"size": 1048576,
"sha256": "...",
"expires_at": "2026-09-01T10:30:00Z"
}
```
### 10.3 Document AST 内部 Contract
```python
class DocumentExporter(Protocol):
async def export(
self,
document: Document,
options: ExportOptions,
) -> ExportResult: ...
```
Document Node 使用稳定判别字段:
```json
{
"node_id": "node_01",
"type": "heading",
"attributes": { "level": 2 },
"children": [],
"text": "进程调度"
}
```
首批 node type
```text
document
heading
paragraph
text
emphasis
strong
link
list
list_item
table
image
blockquote
code_block
math_inline
math_block
mermaid
function_plot
```
Exporter 对无法表示的节点添加 warning,不允许静默丢失。严重缺失由 `EXPORT_UNSUPPORTED_CONTENT` 失败。
### 10.4 Static Render Contract
Mermaid 和函数图像共享:
```ts
interface StaticRenderRequest {
kind: 'mermaid' | 'function_plot'
source: string
sourceHash: string
theme: 'light' | 'dark'
width?: number
height?: number
}
interface StaticRenderResult {
mimeType: 'image/svg+xml' | 'image/png'
content: string | Uint8Array
width: number
height: number
warnings: string[]
}
```
渲染实现可以位于前端共享 Renderer、Host 或后端适配进程,但 Export Service 只依赖上述 Contract。SVG 进入导出器前必须净化;缓存键至少包含 source hash、主题和 renderer version。
### 10.5 Export 错误码
```text
EXPORT_SOURCE_NOT_FOUND
EXPORT_FORMAT_UNSUPPORTED
EXPORT_OPTIONS_INVALID
EXPORT_RENDER_FAILED
EXPORT_UNSUPPORTED_CONTENT
EXPORT_JOB_NOT_FOUND
EXPORT_FILE_EXPIRED
```
---
## 11. Theme Package Host Contract
Theme 不经过 Python AI Core。正式桌面版通过 Tauri HostWeb 开发模式由相同 TypeScript Service 的 Mock Adapter 提供。
### 11.1 Theme Manifest
`theme.yaml` 对应:
```ts
interface ThemeManifest {
theme_id: string
name: string
version: string
author: string
description?: string
min_app_version: string
is_dark: boolean
css_entry: string
preview?: string
}
```
### 11.2 ThemePackageService
```ts
interface ThemePackageService {
selectPackage(): Promise<string | null>
inspectPackage(packageId: string): Promise<ThemePackageInspection>
install(packageId: string): Promise<InstalledTheme>
list(): Promise<InstalledTheme[]>
enable(themeId: string): Promise<InstalledTheme>
disable(themeId: string): Promise<void>
uninstall(themeId: string): Promise<void>
}
```
`packageId` 是 Host 通过文件选择器生成的临时句柄,不是前端传入的绝对路径。
### 11.3 Inspection
```json
{
"package_id": "theme_package_123",
"manifest": {},
"preview_url": "app-theme-preview://theme_package_123",
"warnings": [],
"compatible": true
}
```
安装前检查:
```text
Manifest Schema
Theme ID / Version
min_app_version
包内相对路径
CSS 语法与大小
禁止远程 URL、@import、脚本和越界资源
预览资源 MIME
```
预览运行在隔离容器,不能直接将未验证 CSS 注入主页面。
### 11.4 Theme 错误码
```text
THEME_PACKAGE_NOT_FOUND
THEME_MANIFEST_INVALID
THEME_PACKAGE_INCOMPATIBLE
THEME_RESOURCE_OUTSIDE_PACKAGE
THEME_CSS_UNSAFE
THEME_ALREADY_INSTALLED
THEME_NOT_FOUND
THEME_BUILTIN_PROTECTED
```
---
## 12. Mermaid 与 Function Plot 前端 Contract
### 12.1 MermaidRenderer
```ts
interface MermaidRenderer {
render(
source: string,
options: {
theme: 'light' | 'dark'
mode: 'interactive' | 'static'
},
): Promise<{
svg: string
width: number
height: number
warnings: string[]
}>
}
```
Renderer 返回净化后的 SVG 或明确错误,不直接操作调用组件之外的 DOM。组件销毁时释放事件监听和临时节点;编辑快速变化时以前一次请求的 AbortSignal 或 render version 丢弃旧结果。
### 12.2 FunctionPlot Model
```ts
interface FunctionPlot {
version: 1
expressions: Array<{
expression: string
label?: string
color?: string
}>
domain?: [number, number]
range?: [number, number]
axes: {
xLabel?: string
yLabel?: string
grid: boolean
}
}
```
Markdown parser 将 fenced block 转换为 `FunctionPlotParseResult`
```ts
interface FunctionPlotParseResult {
plot?: FunctionPlot
diagnostics: Array<{
severity: 'warning' | 'error'
code: string
message: string
line?: number
column?: number
}>
}
```
表达式解析使用白名单数学语法,不执行 `eval`、函数构造器、网络请求或对象属性访问。无法解析时保留原始 fenced block,并在预览中显示可定位诊断。
### 12.3 FunctionPlotRenderer
```ts
interface FunctionPlotRenderer {
renderInteractive(plot: FunctionPlot, target: HTMLElement): Promise<RenderHandle>
renderStatic(plot: FunctionPlot, options: StaticRenderOptions): Promise<StaticRenderResult>
}
interface RenderHandle {
update(plot: FunctionPlot): Promise<void>
resize(width: number, height: number): void
destroy(): void
}
```
编辑器、Markdown 预览和 Export Service 只能依赖上述 Model/Renderer,不直接依赖具体绘图库的数据结构。
### 12.4 可视化错误码
```text
MERMAID_PARSE_FAILED
MERMAID_RENDER_FAILED
MERMAID_SVG_UNSAFE
FUNCTION_PLOT_PARSE_FAILED
FUNCTION_PLOT_EXPRESSION_UNSAFE
FUNCTION_PLOT_RENDER_FAILED
```
---
## 13. 跨模块 Contract 对照
| Contract | 提供模块 | 消费模块 | 稳定字段 |
| --- | --- | --- | --- |
| `TranscriptSegment` | Multimodal | Knowledge、Frontend | speaker、time range、text |
| `AgentEvent` / Trace | Agent Runtime | Frontend、Agent Benchmark | run_id、sequence、event、timestamp、data |
| `ToolDefinition` / `ToolResult` | Tool Registry | Agent、MCP Bridge | name、parameters、permission、success/error |
| `PluginCommand` | Extension Core | Command Palette、Context Menu | command_id、locations、when、parameters |
| `PluginSettingsSchema` | Extension Core | Settings UI | schema_version、fields、values、secret status |
| `ModelEvent` | Provider Adapter | Chat、Agent、Frontend | event、sequence、data、timestamp |
| `BenchmarkRun` | Benchmark Service | CLI、报告页 | dataset hash、config snapshot、metrics、status |
| `Document AST` | Export Service | HTML/PDF/DOCX Exporter | node_id、type、attributes、children/text |
| `StaticRenderResult` | Mermaid/Function Renderer | Export Service | mime、content、size、warnings |
| `ThemeManifest` | Theme Host | Theme UI | ID、version、compatibility、entry、preview |
跨模块字段需要修改时:
1. 先修改本文和对应 Pydantic/TypeScript Contract。
2. 新字段优先可选并提供默认行为。
3. 同一提交增加 Provider/Consumer 两侧契约测试。
4. 若必须破坏兼容,增加版本字段或新路径并记录迁移窗口。
---
## 14. HTTP 状态码约定
| 状态码 | 使用场景 |
| --- | --- |
| 200 | 查询、同步更新、已完成命令 |
| 201 | 同步创建持久资源 |
| 202 | 创建异步 Job、请求取消或 Host 重启 |
| 204 | 删除成功且无响应正文 |
| 400 | 语义无效但 JSON 结构合法 |
| 401/403 | 本地 Session 或权限不足 |
| 404 | 资源不存在 |
| 409 | 状态冲突、依赖缺失、Schema 版本冲突 |
| 413 | 上传正文、结果或包超过限制 |
| 422 | Pydantic/Schema 校验失败 |
| 429 | 并发或速率容量已满 |
| 501 | Contract 已预留但当前宿主能力未实现 |
| 502/503/504 | 外部 Provider、MCP Host 或模型服务异常 |
`501` 只用于明确存在于当前版本 Contract、但运行环境尚不具备的能力;未定义路由仍返回普通 404。
---
## 15. 接口冻结与开发顺序
这里描述依赖顺序,不代表人员优先级。
### 15.1 公共契约先行
先冻结并实现:
```text
JobStatus / JobProgress
SSE sequence 与恢复规则
Agent Trace 新事件
Document AST
StaticRenderResult
Plugin Command / Settings Schema
Benchmark Dataset / Run
```
这些类型同时进入后端 Pydantic 和前端 Wire DTO,并增加序列化样例测试。
### 15.2 Adapter 和 Service
在公共 Contract 后分别实现:
```text
Whisper / Pyannote Adapter
MCP Bridge / Plugin Host
Provider 行为适配
RAG / Agent Benchmark Runner
HTML / PDF / DOCX Exporter
Mermaid / FunctionPlot Renderer
ThemePackageService
```
Service 先以 Fake/Mock Adapter 完成状态机和错误测试,再接真实运行时,便于其他模块并行联调。
### 15.3 前后端联调
- 前端 Service 只依赖本文 Wire DTO。
- 后端未完成时使用与 Contract 相同的 Mock,不在组件中硬编码第二套字段。
- SSE 联调覆盖分片、重复事件、断线恢复、取消和终止状态。
- Job 页面覆盖刷新后恢复,不依赖只存在于 Pinia 的进度。
- Secret 输入在提交后立即清空,不进入浏览器持久化。
### 15.4 集成验收链路
```text
Audio Attachment
→ Transcription Job / Trace
→ Transcript Note
→ Index / RAG
→ Agent Run
→ MCP Tool
→ Agent Trace
→ Markdown with Mermaid / Function Plot
→ HTML / PDF / DOCX
→ Community Theme Preview
```
链路中每个箭头都必须通过本文定义的 Contract 或已有第一阶段接口,禁止测试脚本直接写数据库来伪造完成状态。
---
## 16. Definition of Done
一个第二阶段接口完成需要同时满足:
- [ ] Pydantic Request/Response Model 已实现并进入 OpenAPI
- [ ] 前端 Wire DTO 与 Service 显式映射已实现;
- [ ] 成功、校验失败、资源不存在、状态冲突和运行时失败均有测试;
- [ ] 异步任务支持查询终态,取消语义明确;
- [ ] SSE 支持分片解析、sequence 去重、重连和唯一终止事件;
- [ ] 第三方错误已映射,响应和日志不包含 Secret 或堆栈;
- [ ] Mock/Fake 与真实 Adapter 遵守相同 Contract
- [ ] 接口已在 `/docs``/openapi.json` 可见;
- [ ] 本文状态由“计划新增”更新为“已实现”或“扩展完成”;
- [ ] 相关开发说明、测试手册和问题修复文档已同步。
---
## 17. 实现文件建议
在不破坏现有目录的前提下,第二阶段可以逐步拆分:
```text
backend/app/
├── contracts.py # 现有公共 Contract;稳定后可按域拆分
├── api/
│ ├── media.py
│ ├── agent_trace.py
│ ├── plugin_contributions.py
│ ├── benchmarks.py
│ └── exports.py
├── media/
│ ├── transcription.py
│ ├── whisper_adapter.py
│ └── diarization_adapter.py
├── extensions/
│ ├── mcp_bridge.py
│ ├── command_registry.py
│ └── settings.py
├── benchmarks/
│ ├── rag.py
│ ├── agent.py
│ └── reports.py
└── export/
├── document.py
├── service.py
└── exporters/
frontend/src/
├── contracts/
├── services/
│ ├── transcriptionService.ts
│ ├── pluginContributionService.ts
│ ├── benchmarkService.ts
│ ├── exportService.ts
│ └── themePackageService.ts
└── features/
├── agent-trace/
├── themes/
└── export/
```
目录调整应按实际代码规模渐进进行。Router 只做参数接收和错误映射,状态机、第三方 SDK 与文件处理继续放在 Service/Adapter 层。