# 客户端与服务端协议(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=` | 沿用现有 JMS 登录;服务端不把业务系统 token 下发给客户端 | | 桌面端 | `Authorization: Bearer ` | 授权码 + 设备绑定换来的短期令牌(沿用现有授权方式) | 两者都由 `server/entry` 解析成同一个 Subject(用户 + 租户),再交给应用服务与 Core。首次调用的返回值就是 `MeResponse`。 ## 4 端点 | 方法 | 路径 | 请求 | 响应 | 说明 | | --- | --- | --- | --- | --- | | GET | `/v1/health` | — | `HealthResponse` | 健康检查 | | GET | `/v1/me` | — | `MeResponse` | 当前身份与客户端形态 | | GET | `/v1/projects` | — | `ProjectSummary[]` | 可见项目 | | 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` 关键字段 | | --- | --- | --- | | `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`(规范本体) 2. `contracts/events.ts` / `contracts/protocol.ts`(TS 镜像) 3. 本文档(第 4、6 节的表)