2026-09-23 20:00:47 +08:00
|
|
|
|
# 客户端与服务端协议(v1)
|
|
|
|
|
|
|
|
|
|
|
|
> 契约本体在 `contracts/schemas/`(JSON Schema),TS 镜像在 `contracts/`,漂移由 `scripts/check-contracts.test.mjs` 卡。本文是给人看的同一份东西。
|
|
|
|
|
|
|
|
|
|
|
|
## 1 范围与所有权
|
|
|
|
|
|
|
|
|
|
|
|
服务端与客户端之间只认这一套接口。引擎协议(Pi 的 JSONL、Codex 的 app-server)只活在 `server/agent` 内部,客户端与 Core 都不感知——协议的所有权比协议的兼容性重要(架构设计 5.2)。
|
|
|
|
|
|
|
|
|
|
|
|
界面只依赖本文的事件模型:换引擎、换模型,事件不变。
|
|
|
|
|
|
|
|
|
|
|
|
## 2 版本与兼容规则
|
|
|
|
|
|
|
|
|
|
|
|
| 规则 | 说明 |
|
|
|
|
|
|
| --- | --- |
|
|
|
|
|
|
| 路径版本 | 前缀 `{PROTOCOL_PREFIX}` = `/v1`;改语义就换前缀,不原地改 |
|
|
|
|
|
|
| 事件版本 | 每个事件带 `v`,当前 `1`;字段只加不改不删 |
|
|
|
|
|
|
| 删除字段 | 先废弃(保留但不再填充)→ 下一个大版本再删 |
|
|
|
|
|
|
| 未知事件 | 客户端必须忽略不认识的 `type`,不能崩 |
|
|
|
|
|
|
| 未知字段 | 客户端必须忽略,不能因多字段报错 |
|
|
|
|
|
|
|
|
|
|
|
|
## 3 认证与身份
|
|
|
|
|
|
|
|
|
|
|
|
| 客户端 | 方式 | 说明 |
|
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
|
| 浏览器 | `Cookie: ap_session=<httpOnly>` | 沿用现有 JMS 登录;服务端不把业务系统 token 下发给客户端 |
|
|
|
|
|
|
| 桌面端 | `Authorization: Bearer <desktop-token>` | 授权码 + 设备绑定换来的短期令牌(沿用现有授权方式) |
|
|
|
|
|
|
|
|
|
|
|
|
两者都由 `server/entry` 解析成同一个 Subject(用户 + 租户),再交给应用服务与 Core。首次调用的返回值就是 `MeResponse`。
|
|
|
|
|
|
|
|
|
|
|
|
## 4 端点
|
|
|
|
|
|
|
|
|
|
|
|
| 方法 | 路径 | 请求 | 响应 | 说明 |
|
|
|
|
|
|
| --- | --- | --- | --- | --- |
|
|
|
|
|
|
| GET | `/v1/health` | — | `HealthResponse` | 健康检查 |
|
|
|
|
|
|
| GET | `/v1/me` | — | `MeResponse` | 当前身份与客户端形态 |
|
2026-09-23 20:00:59 +08:00
|
|
|
|
| GET | `/v1/projects` | — | `ProjectListResponse` | 可见项目 |
|
2026-09-23 20:00:47 +08:00
|
|
|
|
| GET | `/v1/sessions` | `?projectId=` | `SessionListResponse` | 会话列表 |
|
|
|
|
|
|
| POST | `/v1/sessions` | `CreateSessionRequest` | `CreateSessionResponse` | 建会话;写操作必须建在项目会话(ADR-0003) |
|
|
|
|
|
|
| GET | `/v1/sessions/{id}` | `?limit=` | `SessionDetailResponse` | 会话详情与最近消息 |
|
|
|
|
|
|
| GET | `/v1/sessions/{id}/events` | `Last-Event-ID` 或 `?since=` | SSE 事件流 | 回放 + 实时,见第 5 节 |
|
|
|
|
|
|
| POST | `/v1/sessions/{id}/messages` | `SendMessageRequest` | `SendMessageResponse` | 发消息;返回受理,结果走事件流 |
|
|
|
|
|
|
| POST | `/v1/sessions/{id}/cancel` | — | `CancelResponse` | 取消当前运行 |
|
|
|
|
|
|
| GET | `/v1/capabilities` | `?sessionId=` | `CapabilitiesResponse` | 按身份过滤的能力清单 |
|
|
|
|
|
|
| GET | `/v1/executions/{executionId}` | — | `ExecutionReceipt` | 执行状态与回执 |
|
|
|
|
|
|
| POST | `/v1/approvals/{approvalId}` | `ApprovalDecisionRequest` | `ApprovalDecisionResponse` | 确认卡响应 |
|
|
|
|
|
|
|
|
|
|
|
|
## 5 事件流(SSE)
|
|
|
|
|
|
|
|
|
|
|
|
| 规则 | 说明 |
|
|
|
|
|
|
| --- | --- |
|
|
|
|
|
|
| 传输 | `text/event-stream`;`event:` 填事件 `type`,`data:` 填整条事件 JSON |
|
|
|
|
|
|
| 序号 | `seq` 在会话内单调递增,从 1 开始;客户端按它去重与排序 |
|
|
|
|
|
|
| 断点续传 | 客户端带 `Last-Event-ID`(或 `?since=`)重连,服务端从该序号之后回放 |
|
|
|
|
|
|
| 回放 | 回放与实时用同一格式,客户端不需要两套解析 |
|
|
|
|
|
|
| 心跳 | 无事件时按固定间隔发 `heartbeat`,用于探活与代理保活 |
|
|
|
|
|
|
| 顺序 | 同一会话的事件严格有序;不同会话之间不保证 |
|
|
|
|
|
|
| 敏感 | 事件里只有参数摘要(`argsDigest`、`paramsDigest`),没有凭据与参数明文 |
|
|
|
|
|
|
|
|
|
|
|
|
## 6 事件清单
|
|
|
|
|
|
|
|
|
|
|
|
| 事件 | 含义 | `data` 关键字段 |
|
|
|
|
|
|
| --- | --- | --- |
|
2026-09-24 15:09:37 +08:00
|
|
|
|
| `message.user` | 用户消息(多端与重连回放用) | `messageId`、`text` |
|
2026-09-23 20:00:47 +08:00
|
|
|
|
| `message.delta` | 助手增量文本 | `messageId`、`text` |
|
|
|
|
|
|
| `message.completed` | 一条助手消息结束 | `messageId`、`finishReason` |
|
|
|
|
|
|
| `tool.call` | 引擎请求调用能力 | `callId`、`capabilityId`、`argsDigest` |
|
|
|
|
|
|
| `tool.result` | 能力调用的回执 | `callId`、`executionId`、`status` |
|
|
|
|
|
|
| `execution.updated` | 执行状态变化 | `executionId`、`status`、`errorKind` |
|
|
|
|
|
|
| `approval.requested` | 需要用户确认 | `approvalId`、`capabilityId`、`paramsDigest`、`impactSummary`、`revision`、`expiresAt` |
|
|
|
|
|
|
| `approval.resolved` | 确认结果 | `approvalId`、`decision` |
|
|
|
|
|
|
| `run.state` | 当前运行状态 | `state`(`idle`/`running`/`cancelling`/`cancelled`) |
|
|
|
|
|
|
| `session.updated` | 会话标题或状态变化 | `title`、`status` |
|
|
|
|
|
|
| `error` | 错误 | `code`、`message`、`retryable` |
|
|
|
|
|
|
| `heartbeat` | 保活 | 空对象 |
|
|
|
|
|
|
|
|
|
|
|
|
`status` 取值统一为 `accepted` / `pending-confirmation` / `succeeded` / `failed`。
|
|
|
|
|
|
|
|
|
|
|
|
## 7 审批时序
|
|
|
|
|
|
|
|
|
|
|
|
1. 引擎请求写能力 → Core 判定需确认 → 落一条待确认动作(ADR-0003、架构设计 4.2)
|
|
|
|
|
|
2. 服务端推 `approval.requested`,客户端渲染确认卡(影响范围、参数摘要、目标版本、有效期)
|
|
|
|
|
|
3. 用户点批准 → `POST /v1/approvals/{id}`,带 `revision` 与 `idempotencyKey`
|
|
|
|
|
|
4. 服务端校验版本与有效期 → 推 `approval.resolved` → 执行 → 推 `execution.updated`
|
|
|
|
|
|
5. 版本过期或已执行 → 返回 `expired`,客户端提示重新确认;确认人是会话发起人本人
|
|
|
|
|
|
|
|
|
|
|
|
## 8 错误模型
|
|
|
|
|
|
|
|
|
|
|
|
| 位置 | 形状 | 说明 |
|
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
|
| HTTP | `ErrorEnvelope` | 请求本身失败(鉴权、参数、找不到) |
|
|
|
|
|
|
| SSE | `error` 事件 | 运行中失败;`retryable` 表示可重试,客户端据此给"重试"按钮 |
|
|
|
|
|
|
|
|
|
|
|
|
## 9 幂等与取消
|
|
|
|
|
|
|
|
|
|
|
|
| 场景 | 规则 |
|
|
|
|
|
|
| --- | --- |
|
|
|
|
|
|
| 发消息 | 带 `idempotencyKey`;同一 key 重试不产生第二个 run |
|
|
|
|
|
|
| 审批 | 带 `idempotencyKey`;同一 key 只生效一次 |
|
|
|
|
|
|
| 取消 | 幂等:重复取消不报错;已结束的运行返回 `idle` |
|
|
|
|
|
|
| 结果未知 | 执行超时返回 `failed` 且 `errorKind = unknown`,由客户端提示查执行记录,不自动重试写操作 |
|
|
|
|
|
|
|
|
|
|
|
|
## 10 变更规则
|
|
|
|
|
|
|
|
|
|
|
|
改协议必须同时改三处,否则 `pnpm test` 会失败:
|
|
|
|
|
|
|
|
|
|
|
|
1. `contracts/schemas/*.json`(规范本体)
|
2026-09-23 20:52:24 +08:00
|
|
|
|
2. `contracts/events.ts` / `contracts/protocol.ts`(TS 镜像;第 4 节的端点表与 `ENDPOINT_SPECS` 由测试逐条比对)
|
2026-09-23 20:00:47 +08:00
|
|
|
|
3. 本文档(第 4、6 节的表)
|