文档HTTP/SSE API 参考

24 · HTTP/SSE API 参考

pi-web 把所有会话、配置、附件、状态桥、视觉/AIGC 操作统一暴露为标准 REST + SSE 接口,底层由框架无关的 createPiWebHandler 工厂驱动。本章是全书端点的收敛参考——各特性章(会话列表、消息队列、附件、扩展、AIGC/视觉、Canvas)散落的端点在此汇总;建议先读对应特性章理解语义,再回本章查具体请求/响应契约。


架构概览

Next.js 已从 main 删除。服务端宿主是 Honoserver/index.ts),@hono/node-server 仅作 fetch↔Node 适配器,不引入框架级抽象。整个 /api/* 面收敛为一条 app.all('/api/*') 转发到 getHandler() 单例——但在它之前,宿主单独注册了几条不经 handler 的端点(webext 资源、/api/bootstrap),因为通用转发会抢先匹配它们。

浏览器 (Vite SPA, dev :5173 / prod 同端口)
         │  fetch /api/**

server/index.ts  (Hono 宿主, 默认 PORT=3000)
  ├── app.get('/api/webext/singletons/:name')  ┐ 先注册:webext 资源
  ├── app.get('/api/webext/resolve')           │ (须早于通用 /api/* 转发,
  ├── app.get('/api/webext/dist/:dir/*')       │  否则被 handler 抢匹配)
  ├── app.get('/api/bootstrap')                ┘ SPA 运行时配置
  └── app.all('/api/*')  ─────────────┐  其余全部 API

                        getHandler()  ← lib/app/pi-handler.ts 单例


                        createPiWebHandler(opts)
                        packages/server/src/http/create-handler.ts
                                       ├── Router (方法 + 路径分发)
                                       ├── 内置端点 (sessions / config / attachments)
                                       └── 注入端点 (config / attachment / agent-sources /
                                            favorites / session-list / aigc-models /
                                            vision-models / bash / extensions …)
  • c.req.raw 是标准 Request;handler 返回的 Response(含 SSE ReadableStream body)原样透传,不重写 status/headers/body、不缓冲(server/index.ts:75-91)。
  • 宿主是常驻进程:它 spawn 会话子进程、持有 SSE 长连接,因此不能跑在 Edge/无状态 Serverless 上。这是与框架无关的运行时约束,不再由任何 runtime="nodejs" 声明强制。
  • 服务端由 esbuild 打成单文件 dist/server.mjs(入口必须在产物根,详见 19 · 部署与运维)。

端口:默认 PORT=3000server/index.ts:100process.env.PORT ?? 3000)。开发期 pnpm dev 并发拉起 API(:3000) 与 Vite dev(:5173),浏览器开 5173、/api 由 Vite 代理到 3000;直连 API(如本章 curl 示例)打 3000。生产是同一端口的单进程。

端点速查(按用途分组,详见对应小节):

用途端点
SPA 引导GET /bootstrap(运行时配置,宿主直挂,不经 handler)
会话生命周期POST /sessionsDELETE /sessions/:id
会话列表GET /sessions(列出历史会话,分页)
agent 源枚举GET /agent-sourcesGET·PUT /agent-sources/favorites
事件订阅GET /sessions/:id/stream(SSE)
发消息 / 引导POST /sessions/:id/messages/steer/follow_up/abort
会话控制POST /sessions/:id/models/thinking/fork/ui-response/ui-rpc
状态注入桥POST /sessions/:id/state(写回;下行经 SSE control:state 帧)
会话查询GET /sessions/:id/state/stats/messages/commands/models/fork-messages/completion
Agent 声明式 routesGET /sessions/:id/agent-routesGET·POST /sessions/:id/agent-routes/:name
配置GET·PUT /config/:domainGET /config/models
模型枚举(按类型筛选)GET /config/models?input=&output=
附件POST /sessions/:id/attachmentsGET /attachments/:id/raw

所有端点在浏览器/curl 侧的完整前缀是 /api/**(handler 内部路由不含 /api,由 sse.basePath 对齐)。webext 与 /bootstrap 由宿主直挂,其余经 app.all('/api/*') → handler。


通用约定

响应结构

成功响应返回 JSON 对象,HTTP 状态码视端点而定(见下文)。
所有响应(成功与错误)均携带协议版本响应头与响应体字段(当前协议版本为 0.1.0,定义于 packages/protocol/src/version.ts):

X-Pi-Protocol-Version: 0.1.0

成功响应体也会注入 protocolVersion 字段(由 jsonResponse 统一附加)。

错误响应统一结构:

{
  "error": {
    "code": "SESSION_NOT_FOUND",
    "message": "Session \"abc\" not found.",
    "fields": ["source"]
  },
  "protocolVersion": "0.1.0"
}

fields 仅在请求体校验失败(400)时出现,值为出错字段路径列表。

错误码映射

场景HTTP 状态code
SessionNotFoundError / :id 不存在404SESSION_NOT_FOUND
SessionStoppedError409SESSION_STOPPED
UnknownExtensionUIError409UNKNOWN_EXTENSION_UI
MissingInputError400MISSING_INPUT
body 非 JSON400INVALID_JSON
body DTO 校验失败400VALIDATION_FAILED(带 fields
停机中(不再接受新会话)503SHUTTING_DOWN
上游 RPC 命令失败502UPSTREAM_ERROR
路径无匹配404NOT_FOUND
路径匹配但方法不符405METHOD_NOT_ALLOWED
未知异常500INTERNAL

code 字面量来源:会话引擎错误码见 packages/server/src/session/session.errors.ts:7SESSION_STOPPED / SESSION_NOT_FOUND / UNKNOWN_EXTENSION_UI / MISSING_INPUT);HTTP 层 code 见 packages/server/src/http/error-map.ts 与各 route handler。

版本不兼容(客户端声明 X-Pi-Protocol-Version 主版本与服务端 0 不符;未声明则放行):
→ 426 PROTOCOL_VERSION_MISMATCH

鉴权接缝(默认放行):
authResolver 拒绝:401 UNAUTHORIZEDauthorizeSession 返回 false:403 FORBIDDEN


Bootstrap API — /api/bootstrap

GET /api/bootstrap — SPA 运行时配置

SPA 启动时首先拉取此端点,取回渲染选源页/会话页所需的运行时配置。它由宿主直挂(server/index.ts:67不经 createPiWebHandler),取代了 Next 时代的两处注入:server component 的 props,以及 15 个 NEXT_PUBLIC_* 门控——后者在 Next 下是构建期内联(故 CLI 运行时设这些 env 无效),收进本端点后它们变成真正的运行时配置pi-web --canvas 之类的运行时开关由此才真正生效。

查询参数

参数说明
sessionId可选。冷加载 /session/:id 时带上,响应附带该会话的 agent source 恢复结果(resumeSource),否则刷新后 webext 扩展表面会静默消失

成功响应 200(BootstrapPayload,见 server/bootstrap.ts:28):

{
  "defaultCwd": "/workspace",
  "autoStart": false,
  "multiTenant": false,
  "hostApiVersion": "0.1.0",
  "defaultSource": "/path/to/agent",   // 可选(配置了默认源时)
  "defaultModel": "claude-opus-4-5",   // 可选
  "resumeSource": "/path/to/agent",    // 可选(?sessionId= 命中且能恢复时)
  "features": {
    "canvas": false,          // NEXT_PUBLIC_PI_WEB_CANVAS
    "sourcePicker": false,    // NEXT_PUBLIC_PI_WEB_SOURCE_PICKER
    "launcherRail": false,    // NEXT_PUBLIC_PI_WEB_LAUNCHER_RAIL
    "bashEnabled": false,     // NEXT_PUBLIC_PI_WEB_BASH_ENABLED
    "sessionsManage": true,   // 仅显式 false/0 才关
    "sessionsSlot": "sidebar",
    "extensionCommands": "",
    "extensionAllowlist": "",
    "extensionBaseUrl": "",
    "disableReadinessHandshake": false
  }
}

Provider 密钥永不出现在响应里supabase 字段仅在 PI_WEB_MULTI_TENANT=1 且配了 URL/anon key 时下发(anon key 本就是公开的浏览器端密钥)。各门控 env 详见 06 · 配置14 · 会话列表

实现参考server/bootstrap.ts


Sessions API — /api/sessions/**

POST /api/sessions — 创建会话

建立新的 agent 会话,返回服务端生成的 sessionId(由主进程 randomUUID() 主导,下传给 agent 以对齐持久化文件 id)。

请求体 (CreateSessionRequestSchema,见 packages/protocol/src/transport/rest-dto.ts:38):

{
  "source": "/path/to/agent",
  "cwd": "/working/dir",
  "model": "claude-opus-4-5",
  "env": { "MY_VAR": "value" }
}
字段类型必填说明
sourcestringagent 源路径或标识
cwdstring工作目录
modelstring覆盖默认模型
envobject(string→string)额外环境变量
trustboolean显式项目信任意图,门控 .pi/ 扩展/子代理/技能加载;缺省由服务端信任策略决定
resumeIdstring给定即”恢复已有会话”而非新建;服务端据持久化元数据恢复,缺失即新建

成功响应 201:

{ "sessionId": "550e8400-e29b-41d4-a716-446655440000", "protocolVersion": "0.1.0" }

sessionId 是 UUID(示例中其他端点用 sess_abc 仅为占位)。

错误:400(缺 source 或 DTO 校验失败)、503(服务停机中)

curl 示例

curl -X POST http://localhost:3000/api/sessions \
  -H "Content-Type: application/json" \
  -d '{"source": "/path/to/.pi", "cwd": "/workspace"}'

GET /api/sessions — 列出历史会话

列出本机持久化的历史会话(仅会话头部轻量元数据,不读正文),用于会话列表面板的浏览与恢复。经 routes: 注入接缝挂载(createSessionListRoutes()),与内置 sessions 端点共存。按 updatedAt ?? createdAt 倒序、keyset 游标分页。

查询参数 (ListSessionsRequestSchema,见 packages/protocol/src/transport/rest-dto.ts:187):

参数类型必填说明
scope"cwd" | "all"缺省 cwd(当前目录);all(系统/全机器)受全局门控
limit正整数单页上限,默认 50,硬 clamp 到 200
cursorstring不透明 keyset 游标(base64url(JSON.stringify({ ts, id }))),续取下一页
qstring名称搜索关键字(sidebar-launcher-rail):非空时按会话名称/标识子串(大小写不敏感)过滤,置于排序/分页前;缺省/空串行为不变(向后兼容)。限长 100。仅匹配名称,不检索正文

成功响应 200(ListSessionsResponse,见 rest-dto.ts:222):

{
  "sessions": [
    {
      "sessionId": "550e8400-...",
      "name": "重构 auth 模块",   // 可选
      "cwd": "/workspace",
      "createdAt": "2025-06-01T08:00:00.000Z",
      "updatedAt": "2025-06-01T09:30:00.000Z"  // 可选(部分存储后端无此值)
    }
  ],
  "nextCursor": "eyJ0cyI6...",  // 缺省表示无更多页
  "protocolVersion": "0.1.0"
}

错误

状态code触发
400INVALID_REQUESTscope / limit / cursor 非法(响应含出错字段)
500INTERNAL存储读取异常
curl "http://localhost:3000/api/sessions?limit=50"

会话列表恒为全局:返回本机全部工作目录下的会话,不按项目目录区分(原 scope=cwd|all 二视图与 NEXT_PUBLIC_PI_WEB_SESSIONS_GLOBAL 门控已移除)。列表项仍带 cwd,故「哪个项目的会话」在项上仍可见。分页、前端三态与重定位等完整机制详见 14 · 会话列表

实现参考packages/server/src/session-list/session-list-routes.ts

GET /api/agent-sources — 列出可用的 agent source

只读枚举「当前环境下可用的 agent source」,供新建会话选择器(AgentSourcePicker)浏览、点选后以其 source 直接创建会话(等价手输)。数据来源为两路合并:目录扫描(PI_WEB_SOURCES_ROOT 下的一级子目录,复用源探测语义判定 custom/cli)∪ 注册表文件(PI_WEB_SOURCES_REGISTRY JSON),按 id 去重(注册表覆盖扫描)。经 routes: 注入接缝挂载(createAgentSourcesRoutes())。

严格只读:处理请求时不写文件、不 clone git、不 resolve/spawn 会话子进程。未配任何来源时返回空列表(成功)。

查询参数 (ListAgentSourcesRequestSchema,见 packages/protocol/src/transport/rest-dto.ts):

参数类型必填说明
limit正整数单页上限,默认 100,硬 clamp 到 500
cursorstring不透明 keyset 游标(base64url(JSON.stringify({ id }))),续取下一页

成功响应 200(ListAgentSourcesResponse):

{
  "sources": [
    {
      "id": "/abs/examples/hello-agent",   // 稳定标识:dir→realpath;git→url@ref
      "source": "/abs/examples/hello-agent", // 直接提交给 POST /sessions 的 source
      "name": "hello-agent",                // 技术名:package.json name > 目录/repo 末段
      "kind": "dir",                        // "dir" | "git"
      "origin": "scan",                     // "scan" | "registry"
      "mode": "custom",                     // "custom"(含入口)| "cli"
      "title": "Hello Agent",               // 可选展示标题(pi-web.title / registry.title);列表用 title ?? name
      "description": "…",                   // 可选(pi-web.description / registry.description / package.json description)
      "avatar": "🤖"                        // 可选头像:图片 URL/data-URI→<img>;否则短文本/emoji;缺省用标题首字母
    }
  ],
  "nextCursor": "eyJpZCI6...",  // 缺省表示无更多页
  "protocolVersion": "0.1.0"
}

展示元数据来源:目录扫描的源从其 package.jsonpi-web 字段(与 pi-web.entry 同处)取 title / description / avatar,name 仍取 package.json 顶层 name,description 回退顶层 description。注册表登记项可直接声明 title / description / avatar。前端源列表以宽屏卡片网格展示,每卡片含头像 + title ?? name + 模式徽标 + 描述 + 收藏星标。

// 示例源的 package.json 片段
{
  "name": "hello-agent",
  "pi-web": {
    "entry": "index.ts",
    "title": "Hello Agent",
    "description": "最简回声 agent,用于上手演示",
    "avatar": "🤖"
  }
}

错误

状态code触发
400INVALID_REQUESTlimit / cursor 非法(响应含出错字段)
500INTERNAL装配/序列化等意外失败(来源缺失/损坏不算,退化为空贡献)
curl "http://localhost:3000/api/agent-sources?limit=100"

前端是否展示源列表由 NEXT_PUBLIC_PI_WEB_SOURCE_PICKER=1 门控——现由 GET /api/bootstrap 服务端读 env 后经 features.sourcePicker 运行时下发(不再是 Next 时代的构建期内联);后端来源由 PI_WEB_SOURCES_ROOTpath.delimiter 分隔多个)与 PI_WEB_SOURCES_REGISTRY(默认 <agentDir>/sources.json)配置。三者详见 06 · 配置

实现参考packages/server/src/agent-source-list/

GET·PUT /api/agent-sources/favorites — agent source 收藏(读写)

收藏是用户偏好(sidebar-launcher-rail),独立于只读源枚举 /agent-sources;持久化在 <agentDir>/agent-source-favorites.json,供侧栏启动导航区渲染一键启动锚点。经 createFavoritesRoutes() 作为注入路由挂在 /agent-sources/favorites 路径(GET+PUT)。收藏/取消收藏不修改源枚举来源(扫描目录/注册表)。

  • GETListFavoritesResponse{ "favorites": [ { "source": "...", "name": "..." } ] }。文件缺失/损坏容错返回其余可用项。
  • PUT { favorites }ListFavoritesResponse(回显落盘结果):全量替换(幂等),原子 tmp+rename 写。body 非法 → 400 INVALID_REQUEST
curl -X PUT "http://localhost:3000/api/agent-sources/favorites" \
  -H "Content-Type: application/json" \
  -d '{"favorites":[{"source":"./examples/hello-agent","name":"hello-agent"}]}'
状态code触发
400INVALID_REQUESTPUT body 非法 JSON / 结构不符
500INTERNAL读/写偏好文件异常

实现参考packages/server/src/agent-source-list/favorites-store.tsfavorites-routes.ts


GET /api/sessions/:id/stream — SSE 事件流

建立长连接,实时接收会话事件(文本增量、工具调用、控制帧等)。该订阅是**按轮(per-turn)**建立的:每轮回复由客户端为该轮新开一条 /stream 连接来承接,并非一条会话级常驻连接;空闲期没有流是正常的。

调用顺序约定(重要):客户端必须先创建会话、先打开本轮 /stream 订阅,再 POST /messages 提交 prompt——该轮回复帧经这条已建立的流回来。顺序不可颠倒:回复帧经服务端 EventEmitter 瞬时广播、无缓冲,若流尚未连上,连接窗口之前广播的帧会永久丢失(见下文”竞态注意”)。

响应头

Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
X-Accel-Buffering: no
Content-Encoding: identity
X-Pi-Protocol-Version: <semver>

SSE 帧格式

event: uiMessageChunk
id: 42
data: {"kind":"uiMessageChunk","protocolVersion":"0.1.0","chunk":{"type":"text-delta","id":"t1","delta":"Hello"}}

event: control
id: 43
data: {"kind":"control","protocolVersion":"0.1.0","payload":{"control":"error","message":"session ended: stopped","code":"stopped"}}

: keep-alive
  • event: 行 = 帧种类(uiMessageChunkcontrol,即帧的 kind 字段)
  • id: 行 = 单调帧序号,供断线重连时携带 Last-Event-ID
  • 心跳帧(: keep-alive)每 15 秒发送一次(DEFAULT_HEARTBEAT_MS = 15_000),防止代理超时
  • control 帧负载在 payload 字段内,以 payload.control 判别(不是 type);会话结束时服务端发一帧 payload.control = "error"message 描述原因、code 为结束 reason)后关闭连接

断线重连与回放边界:携带 Last-Event-ID 头重新 GET 此端点,服务端重新订阅并续推后续帧。Last-Event-ID 仅作续推的续号起点startSeq),网关不缓存历史帧、也不按序号回放历史消息帧。迟到订阅者(含重连)能”补回”的仅有:日志 ring-buffer,以及 session-status / session-state / queue / 状态桥 state(按 key 登记)等**粘性(sticky)**帧(packages/server/src/session/pi-session.ts:271,396,436,578,640);本轮已广播的 uiMessageChunk 回复帧不会重放——它们经 EventEmitter 瞬时广播、无缓冲。要取回错过的回复正文,须走历史接口 GET /sessions/:id/messages

curl -N "http://localhost:3000/api/sessions/sess_abc/stream" \
  -H "Last-Event-ID: 42"

错误:404(会话不存在)、409 SESSION_ENDED(会话已结束,返回明确响应而非空流挂起)

重要:会话 stats(用量统计)不通过独立的 control:"stats" 帧推送。SSE control 帧的 schema 虽定义了 stats 类型,但 pi-session 实际从不发送 payload.control = "stats" 帧;用量数据须通过 GET /sessions/:id/stats REST 端点主动拉取(或从粘性 control:"session-state" 快照帧的 snapshot.stats 读取)。实际发送的 control 帧种类见下文 SSE 帧完整参考

竞态注意POST /messages 触发 agent 产出首帧可能极快(实测 ~32ms),而同轮 /stream 在 dev 冷编译或高负载下可能数秒才连上(实测冷态 ~3237ms、热态仅 ~79ms)。若 POST 早于流连上,连接窗口之前广播的 uiMessageChunk 因服务端无缓冲而永久丢失,该轮回复只有刷新(走 GET /sessions/:id/messages 历史接口)后才可见——表现为”发送消息后需手动刷新才看到回复”,且因取决于流是否抢在 agent 首帧前连上而呈间歇性。规避方式即严格遵守上文的调用顺序约定:先建立本轮 /stream 订阅,再 POST /messages


POST /api/sessions/:id/messages — 发送消息

向会话发送用户消息,触发 agent 推理。推理结果通过本轮已建立的 /stream 连接异步推送。必须先为本轮打开 /stream 订阅,再调用本端点:回复帧经服务端 EventEmitter 瞬时广播、不缓冲,若本端点早于 /stream 连上,连接窗口之前的回复帧会永久丢失,只能刷新经 GET /sessions/:id/messages 历史接口恢复(详见 GET /sessions/:id/stream 的”竞态注意”)。

请求体 (PromptRequestSchema,见 packages/protocol/src/transport/rest-dto.ts:67):

{
  "message": "请帮我分析这段代码",
  "images": [],
  "attachmentIds": ["att_xyz789"],
  "streamingBehavior": "steer"
}
字段类型必填说明
messagestring用户消息文本(注意字段名是 message,不是 prompt
imagesarrayvision 图像内容(base64)
attachmentIdsstring[]已落库附件公开 id(att_<nanoid>),服务端注入结构化文本引用
streamingBehavior"steer" | "followUp"推理进行中提交时的行为

成功响应 200:{ "ok": true }(消息已转发给 agent)

错误:400(校验失败)、404(会话不存在)、409(会话已停止)

curl -X POST http://localhost:3000/api/sessions/sess_abc/messages \
  -H "Content-Type: application/json" \
  -d '{"message": "Hello, agent!"}'

POST /api/sessions/:id/steer — 引导输出

在推理进行中注入引导文本。

请求体 (SteerRequestSchema):{ "message": "请用中文回答", "images": [] }images 可选;字段名是 message,不是 text

成功响应 200:{ "ok": true }
错误:400、404、409


POST /api/sessions/:id/follow_up — 追问

请求体:与 steer 同构(SteerRequestSchema):{ "message": "继续" }

成功响应 200:{ "ok": true }
错误:400、404、409


POST /api/sessions/:id/abort — 中止推理

中止当前进行中的推理轮次。

请求体:无(空体)

成功响应 200:{ "ok": true }

错误:404、409


POST /api/sessions/:id/models — 切换模型

已变更(spec multi-gateway-providers 任务 6.5):旧路径 POST /api/sessions/:id/model(单数)已废弃,恒返回 410 ENDPOINT_MOVED 并在消息里给出新路径——不静默 404,既有集成方能立刻辨识出是接口变更而非会话不存在。

新路径与查询端点 GET /api/sessions/:id/models 同路径、仅方法不同GET 取该会话可用的模型清单,POST 设定当前模型。

请求体 (SetModelRequestSchema):{ "provider": "anthropic", "modelId": "claude-sonnet-4-5" }(两字段均必填;注意是 provider + modelId,不是单字段 model

成功响应 200:{ "ok": true }
错误:400、404、409;旧路径 410


POST /api/sessions/:id/thinking — 设置扩展思考

请求体 (SetThinkingRequestSchema):{ "level": "high" }

level 取值为 ThinkingLevel 枚举:"minimal" | "low" | "medium" | "high" | "xhigh"(见 packages/protocol/src/rpc/model.ts:19)。没有 enabled / budget 字段。

成功响应 200:{ "ok": true }
错误:400、404、409


POST /api/sessions/:id/ui-response — 扩展 UI 响应

将用户在扩展 UI 交互中产生的响应回传给 agent。请求体即 pi 的 RpcExtensionUIResponseUiResponseRequestSchema 别名,见 rest-dto.ts:118),其中的 id 字段标识对应的 UI 请求。

成功响应 200:{ "ok": true }
错误:400(校验失败)、404(会话不存在)、409(未知 UI 请求 id 或会话已停止)


POST /api/sessions/:id/ui-rpc — Tier3 UI↔agent RPC

Web UI 扩展(Tier3)的上行 RPC 请求(UiRpcRequestSchema)。响应不在此端点返回,而是经 SSE control 帧(payload.control = "ui-rpc")按 correlationId 配对回流。

成功响应 200:{ "ok": true }
错误:400、404、409


POST /api/sessions/:id/state — 写回会话共享状态(状态注入桥)

状态注入桥(state-injection-bridge)是一条独立于 LLM 对话历史之外的会话级共享 KV 路线,权威态在 agent 子进程。本端点是其 UI→agent 写回方向:校验 StateSetRequestPiSession.setState(经 stdin 内部行下发子进程)→ 200 同步 ack。前端的收敛靠下行 SSE control:"state" 帧(不在本端点等待),见下文 SSE 帧完整参考。作者面读写 API(getSessionState())见 04 · Surface 权威表面栈08 · 自定义 Agent 开发

请求体 (StateSetRequestSchema,见 packages/protocol/src/web-ext/state.ts:27):

{ "key": "aigc.selectedModel", "value": "gemini-3.1-flash-image", "op": "set" }
字段类型必填说明
keystring状态键(非空)
value任意可 JSON 值op=set 时的新值;传输无关,不限文本
op"set" | "delete"缺省 setdelete 时忽略 value

成功响应 200:{ "ok": true }(同步 ack;实际状态变更经 control:state 帧回流)
错误:400(负载不合契约,不改权威态)、404(会话不存在)

curl -X POST http://localhost:3000/api/sessions/sess_abc/state \
  -H "Content-Type: application/json" \
  -d '{"key":"aigc.selectedModel","value":"gemini-3.1-flash-image"}'

实现参考packages/server/src/http/routes/state-routes.ts:25


POST /api/sessions/:id/fork — 分叉会话

从指定历史条目分叉。请求体 (ForkRequestSchema):{ "entryId": "..." }

成功响应 200:{ "text"?: string, "cancelled"?: boolean }
错误:400、404、409、502(上游命令失败)


GET /api/sessions/:id/state — 查询会话状态

成功响应 200(stateRpcSessionState,见 session-state.ts:18):

{
  "state": {
    "sessionId": "550e8400-...",
    "thinkingLevel": "high",
    "isStreaming": false,
    "isCompacting": false,
    "steeringMode": "...",
    "followUpMode": "...",
    "autoCompactionEnabled": true,
    "messageCount": 12,
    "pendingMessageCount": 0,
    "model": { "...": "..." }
  },
  "protocolVersion": "0.1.0"
}

错误:404、502(上游命令失败)


GET /api/sessions/:id/stats — 查询用量统计

注意:stats 数据仅通过此端点拉取,SSE 流不推送用量帧。

成功响应 200(statsSessionStats,见 session-state.ts:54):

{
  "stats": {
    "sessionId": "550e8400-...",
    "userMessages": 6,
    "assistantMessages": 6,
    "toolCalls": 5,
    "toolResults": 5,
    "totalMessages": 12,
    "tokens": { "input": 3200, "output": 800, "cacheRead": 0, "cacheWrite": 0, "total": 4000 },
    "cost": 0.0042
  },
  "protocolVersion": "0.1.0"
}

错误:404、502(上游命令失败)

curl http://localhost:3000/api/sessions/sess_abc/stats

GET /api/sessions/:id/messages — 查询消息历史

成功响应 200:{ "messages": [...] }
错误:404、502


GET /api/sessions/:id/commands — 查询可用命令

返回会话当前可用命令列表(纯查询,无安装/信任语义)。

成功响应 200:{ "commands": [...] }
错误:404、502


GET /api/sessions/:id/models — 查询可用模型

返回会话 agent 可用的模型列表({ models: Model[] },元素为 pi 的 Model 形状),受 PI_WEB_HIDE_PROVIDERS 环境变量过滤(剔除被隐藏 provider 的模型;与设置页 /config/models 用同一名单)。

成功响应 200:{ "models": [...] }
错误:404、502


GET /api/sessions/:id/fork-messages — 查询可分叉条目

返回可作为 fork 起点的历史条目列表。

成功响应 200:{ "messages": [{ "entryId": "...", "text": "..." }] }
错误:404、502


GET /api/sessions/:id/completion — 触发符补全

触发符补全框架(如 @file: 引文件)的查询端点。配套 GET /api/sessions/:id/completion/triggers 返回已注册的触发符。详见 02 · 核心概念

成功响应 200:补全结果 JSON
错误:404


GET /api/sessions/:id/agent-routes — agent 声明式 route 清单

agent 在 AgentDefinition.routes 中声明的 HTTP routes(声明面契约见 08 · 自定义 Agent 开发指南)随会话创建自动挂载到会话命名空间。本端点返回该会话声明的 route 清单——纯数据投影name / methods / description),handler 函数只存在于 agent 子进程、从不跨进程。

成功响应 200(无声明返回空数组,是成功不是错误):

{
  "routes": [
    {
      "name": "gallery-stats",
      "methods": ["GET"],
      "description": "Canvas 画廊统计(资产计数/来源分布/是否生成中)"
    }
  ],
  "protocolVersion": "0.1.0"
}

错误:404(会话不存在)、401/403(既有 :id 鉴权接缝拒绝)。运维关断时(PI_WEB_AGENT_ROUTES_DISABLED=1,见下文 env 表)返回通用 404 NOT_FOUND,不泄露端点存在性。

curl -s http://localhost:3000/api/sessions/sess_abc/agent-routes

GET·POST /api/sessions/:id/agent-routes/:name — 调用声明 route

将一次 HTTP 调用转发进该会话的 agent 子进程,由声明绑定的 handler 处理,并在同一 HTTP 请求-响应周期内同步返回结果——外部系统(curl / webhook / 第三方服务)无需订阅 SSE 流即可调用 agent 能力。底层为声明帧 + 专用请求/结果帧,骑既有 stdin/stdout JSONL 通道,不新增 SSE 帧。

调用语义

  • handler 只在 agent 子进程内执行;调用不触发 LLM 推理、不进对话历史、不产生任何 UI 变化
  • 会话推理进行中(busy)照常受理,与对话互不干扰。
  • GET 调用忽略请求体(不读);POST 空体宽松放行(body 以 undefined 传入 handler),非空但非法 JSON → 400。
  • 成功响应体 = handler 返回的原始 JSON(对象/数组/标量皆可,undefined 归一为 null),不包 protocolVersion 信封;协议版本仅经响应头 X-Pi-Protocol-Version 承载。

错误(检查顺序:门控 → 会话/鉴权 → 名称 → 方法 → 体积 → JSON → 转发):

状态code触发
404NOT_FOUNDPI_WEB_AGENT_ROUTES_DISABLED=1 运维关断(不泄露端点存在性)
404SESSION_NOT_FOUND会话不存在
401 / 403UNAUTHORIZED / FORBIDDEN既有 :id 鉴权接缝拒绝
404ROUTE_NOT_FOUNDroute 名未在该会话的 agent 定义中声明
405METHOD_NOT_ALLOWED方法不在该 route 声明的 methods 白名单(缺省 ["GET"]
413PAYLOAD_TOO_LARGEPOST 请求体超上限(默认 1 MiB;先按 Content-Length 头提前拒,头缺失/不可信时读后按实际字节兜底复核)
400INVALID_BODYPOST 非空请求体不是合法 JSON
502ROUTE_HANDLER_ERRORhandler 抛错(错误消息带 handler 侧 message)
504ROUTE_TIMEOUT子进程应答超时(默认 20000 ms)
409SESSION_STOPPED会话已停止

环境变量

env默认说明
PI_WEB_AGENT_ROUTES_DISABLED未设置(功能开启)=1 服务端权威关断,全部 agent-routes 端点返回通用 404;按请求时读取
PI_WEB_AGENT_ROUTE_TIMEOUT_MS20000转发进子进程的应答超时(毫秒),超时 → 504
PI_WEB_AGENT_ROUTE_BODY_LIMIT1048576(1 MiB)POST 请求体上限(字节),超限 → 413
# 调用(GET;查询参数拍平为单值传给 handler)
curl -s "http://localhost:3000/api/sessions/sess_abc/agent-routes/gallery-stats?verbose=1"
 
# 调用(POST;须该 route 声明 methods 含 "POST")
curl -s -X POST http://localhost:3000/api/sessions/sess_abc/agent-routes/my-route \
  -H "Content-Type: application/json" \
  -d '{"key": "value"}'

可跑演示见 examples/aigc-canvas-agent(README「Agent Routes 演示(gallery-stats)」小节:声明只读统计 route,curl 直调拿结构化 JSON);声明面契约(名称格式、缺省 methods、handler 约束、装配期校验)详见 08 · 自定义 Agent 开发指南

实现参考packages/server/src/http/routes/agent-route-routes.ts


DELETE /api/sessions/:id — 删除会话

停止并移除会话。handler 返回后,宿主层server/index.ts:80-88app.all DELETE 分支)在响应 res.ok 时额外调用 forgetSessionSource(id) 清除 sessionId → source 的 app 级映射(best-effort,不改写 handler 响应;防止映射表无限累积)。子资源删除(多余路径段)不触发。

成功响应 200:{ "ok": true }
错误:404

curl -X DELETE http://localhost:3000/api/sessions/sess_abc

Config API — /api/config/**

配置域(domain)读写接口。已知域有五个:authsettingssandboxloggingaigcpackages/server/src/config/config-routes.ts:30);models 是特殊端点。schema 驱动的设置界面见 13 · 配置 UI

GET /api/config/:domain — 读取配置

路径参数domain = auth | settings | sandbox | logging | aigc

成功响应 200:

{
  "formSchema": { "...": "..." },
  "values": { "apiKey": "sk-***", "model": "claude-opus-4-5" },
  "protocolVersion": "0.1.0"
}

values 中的 secret 字段返回掩码值(sk-***),不回传明文。

错误:404 DOMAIN_NOT_FOUND(未知域)、401 UNAUTHORIZED / 403 FORBIDDEN(管理员鉴权接缝拒绝)


PUT /api/config/:domain — 写入配置

请求体

{ "values": { "apiKey": "sk-new-key", "model": "claude-opus-4-5" } }

掩码值(sk-***)在写入时自动合并为磁盘原值(不覆盖未改动的 secret)。

成功响应 200:{ "ok": true }
错误:400 INVALID_JSON(JSON 解析失败)/ VALIDATION_FAILED(DTO 校验失败)、422 SCHEMA_VALIDATION_FAILED(域 schema 校验失败,含 fields)、404 DOMAIN_NOT_FOUND、401/403


GET /api/config/models — 列出可用模型(配置侧)

为设置页 provider/model 下拉控件提供数据。受 PI_WEB_HIDE_PROVIDERS 环境变量过滤(逗号分隔的 provider 名,大小写敏感)。

成功响应 200:

{
  "providers": ["anthropic", "openai"],
  "models": [
    { "id": "claude-opus-4-5", "provider": "anthropic" },
    { "id": "gpt-4o", "provider": "openai" }
  ]
}

未配置 listModelOptions 接缝时返回 { "providers": [], "models": [] },前端回退到自由文本输入。

PI_WEB_HIDE_PROVIDERS=anthropic 时,anthropic 的全部 provider 与模型从结果中剔除。此过滤与聊天区的 GET /sessions/:id/models 使用相同名单。

实现参考packages/server/src/config/config-routes.tspackages/server/src/config/model-options-filter.ts


Model Enumeration API — 工具侧模型枚举

已变更(spec multi-gateway-providers 任务 4.3)GET /api/aigc/modelsGET /api/vision/models 已删除。两者的能力由统一目录端点 GET /api/config/models 的类型筛选覆盖 —— 模型目录不再按用途分裂成多个端点,而是同一份清单按输入/输出类型查询。

按类型筛选模型(顶替上述两个端点)

GET /api/config/models 接受 input / output 两个查询参数,取值域 text / image / video / audio。条目字段统一为 { provider, id, name, input, output, source }

旧端点等价查询说明
GET /api/aigc/modelsGET /api/config/models?output=image图像生成模型(产出图像)
GET /api/vision/modelsGET /api/config/models?input=image&output=text视觉理解模型(读图产出文本)

★ 视觉清单必须同时限定 output=text:只按 input=image 会把图生图/图像编辑模型 (inputimageoutputimage)一并纳入,那不是「视觉理解」要的东西。

★ 旧端点里 value 形如 provider/modelId 的复合标识不再由服务端产出,改由消费面 自行拼 ${provider}/${id}aigc.json 中已存的 visionModel 值格式因此保持不变。

成功响应 200:

{
  "providers": ["anthropic", "cloudflare", "blksails-ai"],
  "models": [
    {
      "provider": "anthropic",
      "id": "claude-opus-4-5",
      "name": "Claude Opus 4.5",
      "input": ["text", "image"],
      "output": ["text"],
      "source": "self"
    }
  ]
}

降级:取数抛错(如 models.json 损坏)→ 返回 200 + 空清单,而非把 500 透给前端。

curl 'http://localhost:3000/api/config/models?input=image&output=text'

实现参考packages/core/src/http/routes/config-routes.tspackages/core/src/model-catalog/service.ts


Attachments API — /api/attachments/**

POST /api/sessions/:id/attachments — 上传附件

此端点注册在 sessions 命名空间(POST /sessions/:id/attachments,不是 attachments 路由),复用 Router 的 :id 会话门控(会话不存在→404、未授权→401/403)。

请求multipart/form-data,文件字段名 file

大小限制:默认 25 MiB(DEFAULT_MAX_UPLOAD_BYTES)。超限在读取 body 前通过 Content-Length 头预检拒绝(413)。

成功响应 200:

{
  "attachment": {
    "id": "att_xyz789",
    "name": "screenshot.png",
    "mimeType": "image/png",
    "size": 102400,
    "origin": "upload",
    "sessionId": "550e8400-..."
  },
  "displayUrl": "/api/attachments/att_xyz789/raw?exp=1750000000000&sig=abc...",
  "protocolVersion": "0.1.0"
}

attachmentAttachment 形状(id/name/mimeType/size/origin/sessionId,见 packages/protocol/src/attachment/attachment-dto.ts)。displayUrl 是即时签名的分发 URL(presignUrl),有效期有限。附件 id 形如 att_<base64url>

错误:400 NO_FILE(无文件部分或文件为空)、413 PAYLOAD_TOO_LARGE(超大小限制)、404(会话不存在)、401/403(鉴权接缝拒绝)

curl -X POST http://localhost:3000/api/sessions/sess_abc/attachments \
  -F "file=@/path/to/image.png"

GET /api/attachments/:id/raw?exp=&sig= — 下载附件

签名自洽鉴权,不绑会话,可直接在浏览器中访问(<img src="..."> 等)。

查询参数

参数说明
exp过期时刻(epoch ms)
sigHMAC-SHA256 签名(hex),通过 PI_WEB_ATTACHMENT_SECRET 生成

安全策略(防枚举):先校验签名,签名缺失/无效/过期一律 401(不查存在性,攻击者无法据响应判断 id 是否存在)。仅签名有效才读取并流式返回字节。

成功响应 200:字节流
响应头:Content-Type: <附件 mime>Cache-Control: private, max-age=300

错误:401 INVALID_SIGNATURE(签名缺失/无效/过期)、404 ATTACHMENT_NOT_FOUND(附件不存在,仅签名有效时才可能返回此码)

实现参考packages/server/src/http/routes/attachment-routes.ts


会话来源映射(sessionId → source)

sessionId → agent source 的 app 级映射用于冷加载(直接访问 /session/:id)时恢复 .pi/web UI 扩展配置,读取发生在 GET /api/bootstrap?sessionId=(见 resolveResumeSource)。

注意:Next 时代的独立 POST /api/session-source 端点已随 app/ 删除——main 上没有注册该路由(recordSessionSource 仅存在于测试)。当前恢复路径优先读该映射、回退到持久化的会话元数据(resume-meta),故新会话即便未显式 POST 也能按 id 恢复。清理由 DELETE /api/sessions/:id 顺带完成(forgetSessionSource,见上文)。

实现参考lib/app/session-source-map.tsserver/bootstrap.ts:46


createPiWebHandler — 框架无关集成

框架无关工厂,返回标准 Web Fetch 处理器 (Request) => Promise<Response>,可挂载到任意兼容 Web Fetch 的框架。main 上的宿主用 Hono 挂载它(见 server/index.ts):

import { Hono } from "hono";
import { serve } from "@hono/node-server";
import {
  createPiWebHandler,
  createConfigRoutes,
  createAttachmentRoutes,
} from "@blksails/pi-web-server";
 
const handler = createPiWebHandler({
  manager,          // SessionManager(来自 session-engine)
  store,            // SessionStore
  authResolver,     // 可选,默认放行
  authorizeSession, // 可选,默认放行
  routes: [         // 可选,注入外部路由(config / attachment / vision-models …)
    ...createConfigRoutes({ listModelOptions }),
    ...createAttachmentRoutes(attachmentStore),
  ],
  sse: {
    heartbeatMs: 15_000,  // 心跳间隔(毫秒)
    basePath: "/api",     // 路由前缀(与浏览器侧 /api/** 对齐)
  },
});
 
const app = new Hono();
// c.req.raw 是标准 Request;handler 返回的 Response(含 SSE ReadableStream body)原样透传。
app.all("/api/*", (c) => handler(c.req.raw));
serve({ fetch: app.fetch, port: Number(process.env.PORT ?? 3000) });

生产宿主实际还在 app.all('/api/*') 之前注册了 webext 资源与 /api/bootstrap 端点(见架构概览),并在 DELETE 成功后清理来源映射。上面是最小可运行骨架。

注入接缝说明

  • opts.routes 中的外部路由与内置路由合并,内置路由优先(外部路由无法覆盖/遮蔽精确 method+path 冲突的内置端点)
  • authResolver(req) 拒绝 → 401;authorizeSession(ctx) 返回 false → 403
  • 需要在 SIGTERM 优雅停机时,改用 createPiWebHandlerBundle(opts),它额外返回 shutdown: () => Promise<void>(透传 manager.shutdown()),handler 行为与 createPiWebHandler 一致

实现参考packages/server/src/http/create-handler.ts


SSE 帧完整参考

SSE 流包含两类顶层帧,由 @blksails/pi-web-protocolSseFrameSchema 定义:

kind: uiMessageChunk

增量内容帧,负载在 chunk 字段,chunk.type 为 AI SDK v5 标准块子类型(见 packages/protocol/src/transport/ui-message-chunk.ts),主要包括:

chunk.type说明
text-start / text-delta / text-end文本流(text-deltadelta 字段携带增量,配 id
reasoning-start / reasoning-delta / reasoning-end思考过程流
tool-input-start / tool-input-delta / tool-input-available工具调用输入
tool-output-available / tool-output-error工具调用输出
start / finish / start-step / finish-step / error / abort消息生命周期标记
data-${string}(如 data-pi-queue自定义结构化 data-part(见 data-part.ts

注意:finishuiMessageChunk 的一个 chunk.type(消息流结束标记),不是 control 帧类型。

kind: control

控制帧,负载在 payload 字段,以 payload.control 判别。ControlPayloadSchema九类判别联合packages/protocol/src/transport/sse-frame.ts:21-54):

payload.control说明实际是否发送
error出错 / 会话结束(message + 可选 code
ui-rpcTier3 UI↔agent RPC 下行响应(按 correlationId 配对)
session-status会话就绪握手态(SessionLifecycleState:initializing/ready/error/ended),粘性、订阅时回放
session-state会话权威快照(lifecycle/busy/turn/stats/model/title 六字段),粘性是(需开启快照权威)
state状态注入桥:权威 KV 变更 agent→UI 下行镜像(key/value/rev/deleted),按 key 粘性
logs结构化日志批推到前端面板(entries
queue排队状态(steering / followUp 数组),粘性按 message-queue 场景发
extension-ui扩展 UI 请求(需 POST /ui-response 回传)按扩展场景发
stats用量统计从不发送(用量走 REST 或 session-state.snapshot.stats

前端/集成方写 SSE control 解析器时须覆盖上述判别值(未知 control 走 default 分支安全忽略即可向后兼容)。session-status(就绪握手)见 packages/protocol/src/transport/session-status.tssession-state(权威快照)见 session-state.tsstate(状态桥)见 packages/protocol/src/web-ext/state.ts:15

每帧 JSON 结构:

{
  "kind": "uiMessageChunk",
  "protocolVersion": "0.1.0",
  "chunk": { "type": "text-delta", "id": "t1", "delta": "Hello" }
}

完整主链路示例

以下步骤演示从建会话到接收响应的完整流程:

  1. 创建会话

    SESSION=$(curl -s -X POST http://localhost:3000/api/sessions \
      -H "Content-Type: application/json" \
      -d '{"source": "/path/to/.pi"}' | jq -r .sessionId)
    echo "Session: $SESSION"
  2. 订阅 SSE 流(后台运行;必须先于下一步的 POST /messages,否则本轮回复帧会因无缓冲而丢失,见 /stream 的”竞态注意”):

    curl -N "http://localhost:3000/api/sessions/$SESSION/stream" &
    STREAM_PID=$!
  3. 发送消息

    curl -X POST "http://localhost:3000/api/sessions/$SESSION/messages" \
      -H "Content-Type: application/json" \
      -d '{"message": "Hello, agent! What can you do?"}'
  4. 查询用量(推理结束后):

    curl "http://localhost:3000/api/sessions/$SESSION/stats"
  5. 删除会话(顺带清理 sessionId → source 映射):

    kill $STREAM_PID
    curl -X DELETE "http://localhost:3000/api/sessions/$SESSION"

跑不通时的常见对策:连接被立即关闭并收到一帧 payload.control = "error" → 多为 source 路径不存在或 agent 启动失败;/messages 返回 409 → 会话已停止,需重建;SSE 流无任何帧 → 多为流晚于 POST /messages 连上(见”竞态注意”),或宿主被部署到了 Edge/无状态 Serverless(不支持子进程驻留 + SSE 长连接)。更多见 23 · 故障排查 FAQ


下一步 / 相关