Covel HTTP API 参考文档。通过这些端点,你可以在没有前端 UI 的情况下,仅通过 HTTP 请求完成一局完整的 AI RPG 游戏。
- 基础 URL:
http://localhost:3001/api/ - 协议: HTTP JSON API (Content-Type:
application/json) - 服务器框架: Hono
服务器端常用三种存储后端,通过环境变量 STORE_BACKEND 配置:
| 后端 | 值 | 用途 |
|---|---|---|
| SQLite | sqlite (默认) |
本机开发 / 单机部署,数据落到 ./data/covel.db |
| Memory | memory |
测试或一次性 demo,数据存于内存,重启丢失 |
| PostgreSQL | pg |
生产环境,需配置 DATABASE_URL;多实例部署自动启用 pg_advisory_lock 分布式会话锁,跨 Node 进程对同一 session 互斥 |
注意:
@covel/store工厂也支持idb,用于浏览器能力调用;常规服务器部署使用memory/sqlite/pg。
下面演示一个完整的纯 API 游戏流程:从启动服务器到完成多轮对话。
# 安装依赖
pnpm install
# 启动开发服务器 (SQLite 默认后端, 端口 3001)
pnpm dev:servercurl http://localhost:3001/api/healthcurl http://localhost:3001/api/worldscurl http://localhost:3001/api/pluginscurl -X POST http://localhost:3001/api/sessions \
-H "Content-Type: application/json" \
-d '{
"worldId": "cloudmere",
"locale": "zh-CN",
"plugins": ["pregame", "narrator", "codex"]
}'记下返回的 id(格式为 {worldId}-{uuid8},如 cloudmere-a1b2c3d4),后续请求都需要它。
curl -X POST http://localhost:3001/api/sessions/<sessionId>/turn \
-H "Content-Type: application/json" \
-d '{
"message": "我环顾四周,观察这个陌生的世界"
}'# 查看状态表
curl http://localhost:3001/api/sessions/<sessionId>/state
# 查看角色列表
curl http://localhost:3001/api/sessions/<sessionId>/characters
# 查看消息历史
curl http://localhost:3001/api/sessions/<sessionId>/messagescurl -X POST http://localhost:3001/api/sessions/<sessionId>/turn \
-H "Content-Type: application/json" \
-d '{
"message": "走向远处的城镇"
}'curl -X POST http://localhost:3001/api/sessions/<sessionId>/plugin-rpc \
-H "Content-Type: application/json" \
-d '{
"pluginId": "framework",
"action": "submit-form",
"payload": {
"turnId": "<turnId>",
"submissions": [
{
"interactionId": "<interactionId>",
"type": "choice",
"values": { "selectedId": "option_1", "selectedLabel": "接受任务" }
}
]
}
}'curl -X DELETE http://localhost:3001/api/sessions/<sessionId>| 方法 | 路径 | 描述 |
|---|---|---|
| GET | /api/health |
健康检查 |
| 方法 | 路径 | 描述 |
|---|---|---|
| GET | /api/worlds |
列出所有世界 |
| GET | /api/worlds/:id |
获取世界详情 |
| POST | /api/worlds |
创建/更新世界 |
| PATCH | /api/worlds/:id |
部分更新世界(支持顶层 dimensions,并与现有 metadata 合并) |
| GET | /api/worlds/:id/dimensions/export |
导出世界维度(YAML/JSON) |
| POST | /api/worlds/:id/dimensions/import |
导入世界维度 |
| POST | /api/worlds/:id/sync-dimensions |
将世界维度同步到活跃 session 的 plugin_data 与 lorebook 常量词条,并清理旧 key |
| POST | /api/worlds/:id/world-data/preflight |
只读构建 worldData import plan,返回 diagnostics、planned count 和目标摘要 |
| POST | /api/worlds/:id/sync-data |
基于 provenance ledger 同步 importer 管理的 worldData row,支持 dry-run 与 force |
| 方法 | 路径 | 描述 |
|---|---|---|
| GET | /api/sessions |
列出所有会话(可选 ?worldId= 过滤) |
| POST | /api/sessions |
创建新会话 |
| GET | /api/sessions/:id |
获取会话信息 |
| PATCH | /api/sessions/:id |
更新会话字段(status / runtimeModelOverrides) |
| DELETE | /api/sessions/:id |
删除会话 |
| 方法 | 路径 | 描述 |
|---|---|---|
| GET | /api/sessions/:id/snapshot |
获取完整会话快照(用于客户端恢复/重连) |
| 方法 | 路径 | 描述 |
|---|---|---|
| POST | /api/sessions/:id/turn |
执行玩家回合 |
| 方法 | 路径 | 描述 |
|---|---|---|
| POST | /api/sessions/:id/plugin-rpc |
统一插件 RPC 通道(action 级 / runtime 级,含 submit-form) |
| GET | /api/sessions/:id/approvals |
PR-7 列出该 session 的待批准 RPC 请求 |
| POST | /api/approvals/:approvalId/decision |
PR-7 提交玩家批准决定(allow/deny + once/session) |
| 方法 | 路径 | 描述 |
|---|---|---|
| GET | /api/sessions/:id/plugins |
列出会话的活跃/可用插件 |
| POST | /api/sessions/:id/plugins/enable |
启用插件(body: { pluginId }) |
| POST | /api/sessions/:id/plugins/disable |
禁用插件(body: { pluginId },core-plugin 返回 403) |
| 方法 | 路径 | 描述 |
|---|---|---|
| GET | /api/framework/capabilities |
框架级能力索引:manifest 枚举、工具、proposal、world-data URI |
| GET | /api/plugins |
列出所有已加载插件 |
| GET | /api/plugins/:id |
获取插件详情 |
| GET | /api/plugins/:id/contract |
获取插件完整开发契约 |
| GET | /api/plugins/:id/plugin-data-contract |
获取插件数据 namespace/schema 契约 |
| 方法 | 路径 | 描述 |
|---|---|---|
| GET | /api/sessions/:id/state |
获取所有状态表 |
| 方法 | 路径 | 描述 |
|---|---|---|
| GET | /api/sessions/:id/messages |
获取会话消息列表 |
| POST | /api/sessions/:id/messages/sync |
同步消息(LocalDataService 用) |
为跨 runtime 消费和观测接入而设计的规范记录。每次 runtime 执行产生一条 RuntimeOutput,每次外部输入(玩家消息、插件 UI、RPC 调用)产生一条 InteractionRecord。两张表与 trace_events 并存 —— 翻译层面向"被组件消费",trace 层面向"调试时钻取细节"。
接入状态(2026-04-27):服务端写入和查询 API 已实现并有测试覆盖;当前内置 Web UI 暂未直接消费这些 HTTP 查询端点,debug 页面主要使用
/api/traces/*与/api/sessions/:id/snapshot。这些端点保留为 observability / API client 能力,不应因 UI 暂未接入而删除。
| 方法 | 路径 | 描述 |
|---|---|---|
| GET | /api/sessions/:id/runtime-outputs |
列出该 session 的 runtime 输出记录,支持 ?runtimeId= / ?pluginId= / ?since= / ?limit= 过滤,按 timestamp 降序 |
| GET | /api/sessions/:id/runtime-outputs/:outputId |
获取单条 runtime 输出记录 |
| GET | /api/sessions/:id/runtime-outputs/:outputId/full-prompt |
从 turn_messages + rawPromptDelta 重建该次 LLM 调用的完整 prompt 历史(best-effort) |
| GET | /api/sessions/:id/interaction-records |
列出该 session 的外部输入记录,支持 ?type= / ?source= / ?targetPluginId= / ?limit= 过滤 |
RuntimeOutput 结构:
{
id: string,
sessionId: string,
turnId: string,
runtimeResultId?: string, // 关联到 runtime_results 表
pluginId: string,
runtimeId: string, // "{plugin}" 或 "{plugin}/{runtime}"
timestamp: string,
results: [{ text: string, structured?: unknown }],
metaData: {
turn: number,
preGameDone?: boolean, // Pre-Game runtime 声明自身初始化已完成
rawPromptDelta?: [{ role, content }], // 相对上次调用的 prompt 增量
outputResponses?: string[], // 流式输出合并后的裸文本
toolCallList?: [{ tool, input, output, status, durationMs }],
modelSlot?: string,
tokenUsage?: { input, output },
}
}InteractionRecord 结构:
{
id: string,
sessionId: string,
turnId?: string,
timestamp: string,
source: 'player' | 'plugin-ui' | 'external-api',
channel: 'web' | 'cli' | 'api' | 'external',
type: 'message' | 'click' | 'form-submit' | 'rpc-call' | 'skill-invoke',
targetPluginId?: string,
targetRuntimeId?: string,
payload: unknown,
metaData?: { selectionSnapshot?, userAgent? }
}注意:
- 翻译层写入是 best-effort。失败只打 warn,不阻塞 turn pipeline
rawPromptDelta在 PR-1 首迭代中不会被 turn-executor 自动填充。接入 LLM 调用链的 delta 采集在后续迭代完成- full-prompt 重建端点目前用 turn_messages + delta 做近似还原,对 compaction 后的 session 可能不完全精确 —— 调试用途足够,审计场景需要走 trace_events
接入状态(2026-04-27):内置 Web UI 目前使用 GET/list 读取 plugin-data(右侧面板、message UI specs、plugin data store)。PUT/DELETE 是管理/API 写入口,当前内置 Web UI 暂未直接调用;插件 runtime 推荐通过 plugin-data tools、plugin RPC 或 proposal 写入。PUT/DELETE 保持兼容,但若未来收窄攻击面,应先标记 deprecated 或加 admin/debug gate,而不是静默删除。
| 方法 | 路径 | 描述 |
|---|---|---|
| GET | /api/sessions/:id/plugin-data/:pluginId/:namespace |
列出某 namespace 下的数据 |
| GET | /api/sessions/:id/plugin-data/:pluginId/:namespace/:key |
获取单条数据 |
| PUT | /api/sessions/:id/plugin-data/:pluginId/:namespace/:key |
写入/更新数据 |
| DELETE | /api/sessions/:id/plugin-data/:pluginId/:namespace/:key |
删除数据 |
接入状态(2026-04-27):Working Memory 的 store / proposal / prompt injection 是运行时功能;下列 HTTP CRUD 是管理/调试接口,当前内置 Web UI 暂未直接消费。若未来收敛写路径,应保持 URL/响应兼容,优先替换内部实现而不是直接删除。
| 方法 | 路径 | 描述 |
|---|---|---|
| GET | /api/sessions/:id/working-memory |
列出该 session 的所有工作记忆条目 |
| PUT | /api/sessions/:id/working-memory/:scope/:key |
写入/更新工作记忆(scope: player | story | shared) |
| DELETE | /api/sessions/:id/working-memory/:scope/:key |
删除工作记忆条目 |
| GET | /api/sessions/:id/memory-blocks |
只读返回 Letta 风格的 memory blocks(story scope)。2026-04-27 从 /:id/memory 重命名以避免与 memory 插件 id 冲突。 |
Session 级 lorebook 词条的只读查看 + 启用/删除管理。Entries 由插件通过 proposal commit 管道写入 store 层的 lorebook_entries 表,这些端点提供玩家 UI 与程序化读取视图,不走提案系统(单项 toggle/删除为 MVP 级别的直接写入)。
| 方法 | 路径 | 描述 |
|---|---|---|
| GET | /api/sessions/:id/lorebook |
列出该 session 的所有 lorebook 词条(按 insertionOrder 升序) |
| PATCH | /api/sessions/:id/lorebook/:entryId |
切换单条词条的 enabled 标志,body: { enabled: boolean } |
| DELETE | /api/sessions/:id/lorebook/:entryId |
删除单条词条 |
响应:
GET→{ entries: LorebookEntryRecord[] }(见packages/store/src/types.ts)PATCH→{ success: true, entryId, enabled }DELETE→{ success: true }
404 场景:session 不存在、entryId 不存在。无 feature-flag 开关,始终启用。消费方:当前无内置 UI(右侧面板"世界"Tab 已切换为 WORLD.md 渲染,见 docs/reference/ui-panels.md);插件可通过 ui.right JSON spec 自行消费这些端点。
| 方法 | 路径 | 描述 |
|---|---|---|
| POST | /api/sessions/:id/resume |
用提交的 data 重新启动指定 suspensionId 对应的 runtime |
| GET | /api/sessions/:id/suspensions |
列出当前 session 所有未解决的挂起项 |
| DELETE | /api/sessions/:id/suspensions/:suspensionId |
放弃一个挂起项(删除记录) |
接入状态(2026-04-27):服务端手动快照、列表和 fork 能力已实现并有测试覆盖;当前内置 Web UI 只直接使用
GET /api/sessions/:id/snapshot做恢复/重连,暂未提供手动快照列表或 fork 操作界面。
| 方法 | 路径 | 描述 |
|---|---|---|
| POST | /api/sessions/:id/snapshot |
创建一份手动快照(kind=manual) |
| GET | /api/sessions/:id/snapshots |
列出当前 session 所有物化快照(auto / manual / fork) |
| POST | /api/sessions/:id/fork |
从指定 snapshotId 物化一个新 session,拷贝状态与截至 cursor 的消息 |
接入状态(2026-04-27):当前内置 Web UI 主要通过
GET /api/sessions/:id/snapshot获取角色快照;本节 REST 端点保留为轻量读取/管理 API。插件 runtime 推荐使用create-character/update-character/list-characters/get-character工具维护角色。POST /characters是兼容管理入口,后续若收敛角色写路径,应保持 URL/响应兼容并优先替换内部实现。
| 方法 | 路径 | 描述 |
|---|---|---|
| GET | /api/sessions/:id/characters |
获取会话角色列表 |
| POST | /api/sessions/:id/characters |
创建/更新角色 |
| 方法 | 路径 | 描述 |
|---|---|---|
| POST | /api/actions |
SSE 动作桥接(发送消息/执行命令 → Turn 执行 → SSE 事件流) |
| 方法 | 路径 | 描述 |
|---|---|---|
| GET | /api/events/stream?sessionId=xxx |
SSE 实时事件流(支持 topic 过滤和重放) |
| POST | /api/events/emit |
注入外部事件 |
| 方法 | 路径 | 描述 |
|---|---|---|
| POST | /api/ai/ping |
测试 LLM 提供商连通性 |
| POST | /api/ai/generate-world |
AI 生成世界包 |
| 方法 | 路径 | 描述 |
|---|---|---|
| GET | /api/model-db |
获取模型数据库信息 |
| GET | /api/model-db/search?q=xxx |
搜索模型 |
| GET | /api/model-db/lookup?model=xxx |
查找模型能力 |
| POST | /api/model-db/refresh |
刷新模型数据库 |
| 方法 | 路径 | 描述 |
|---|---|---|
| GET | /api/traces/:sessionId |
获取会话所有 trace 事件 |
| GET | /api/traces/:sessionId/turns |
按 Turn 分组的 trace 事件 |
| 方法 | 路径 | 描述 |
|---|---|---|
| GET | /api/media/:id?token=<signed> |
内容寻址媒体下载(HMAC token + 会话引用校验) |
| POST | /api/media/cleanup |
破坏性维护端点:默认禁用 (COVEL_MEDIA_CLEANUP_ENABLED),商业层 503,dryRun:false 需 X-Confirm-Cleanup: yes |
| 方法 | 路径 | 描述 |
|---|---|---|
| GET | /api/presets |
列出配置的模型预设 |
| GET | /api/packages |
列出已加载插件包(含 runtime/tool/userSettings/tags/relations 信息) |
| GET | /api/commands |
列出注册的命令 |
| GET | /api/block-schemas |
列出插件 block schema |
| GET | /api/ui-specs?sessionId=<id> |
列出插件 UI 声明(按 slot 分组);带 sessionId 时按会话激活集过滤,不带则返回全部插件 |
| GET | /api/llm-config |
返回 slot 配置与能力信息 |
| GET | /api/provider-keys |
桌面 bearer client 返回原始 provider key;其他请求返回 masked availability |
| GET | /api/config/info |
返回当前部署信息(isDesktop、covelHome、dataRoot 等) |
| GET | /api/config/keys |
仅桌面:列出已配置的 provider(不返回值) |
| PUT | /api/config/keys |
仅桌面:写入 <covelHome>/keys.env;body { provider: value } |
| GET | /api/config/settings |
仅桌面:读取 <covelHome>/settings.json(unified SettingsStore) |
| PUT | /api/config/settings |
仅桌面:原子写 settings.json;body { entries: Record<string, unknown> } |
| PUT | /api/config/data-root |
仅桌面:改写 config.toml 的 data_root 行,需要重启服务器 |
| POST | /api/config/open-folder |
仅桌面:打开 config/data/logs 目录或 llm.toml / keys.env |
返回插件的 UI 声明,按 slot 分组。前端在 boot 时调用,用于动态构建右侧面板 Tab 和消息区 block 渲染器。
查询参数:
| 参数 | 类型 | 说明 |
|---|---|---|
sessionId |
string(可选) | 指定后只返回该会话激活集中的插件;省略时返回全局所有已加载插件(向后兼容) |
响应格式:
{
"right": [
{
"pluginId": "codex",
"specs": [{
"id": "codex",
"group": "codex",
"label": { "zh": "知识图鉴", "en": "Codex" },
"icon": "book-open",
"dataSource": { "namespace": "entries" },
"view": { "component": "Stack", "children": [...] }
}]
}
],
"message": [...],
"left": [...]
}每个 slot 包含一个数组,元素为 { pluginId, specs[] }。specs 中每项是一个 json-render spec(从插件 ui/*.json 文件加载)。
| 方法 | 路径 | 描述 |
|---|---|---|
| POST | /api/runtime/invoke |
独立调用单个 Runtime(计划中) |
检查服务器是否正常运行。
响应:
{
"status": "ok",
"version": "1.0.0",
"bootId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"timestamp": "2025-01-15T10:00:00.000Z",
"storage": {
"data": {
"backend": "sqlite",
"durable": true,
"frontendMode": "remote"
},
"media": {
"backend": "sqlite",
"configuredBackend": "mirror",
"enabled": true,
"durable": true
},
"vector": {
"backend": "embedded",
"capable": true,
"driver": "sqlite-vec",
"modelCount": 0,
"tableCount": 0
},
"migrations": [
{
"id": "browser:idb:unified-storage",
"domain": "browser",
"backend": "idb",
"version": 11,
"status": "managed-by-backend",
"description": "Browser-local data, media, app-KV, and render cache share the covel-browser IndexedDB schema."
}
]
},
"vector": {
"capable": true,
"driver": "sqlite-vec",
"modelCount": 0,
"tableCount": 0
}
}| 字段 | 说明 |
|---|---|
bootId |
服务器启动 ID(UUID),每次重启变化,可用于检测服务器重启 |
storage.data.backend |
当前服务端 DataStore 后端(memory / sqlite / pg)。默认 sqlite。 |
storage.data.frontendMode |
前端数据模式:local 使用浏览器 IndexedDB;remote 使用服务端 API,由服务端 STORE_BACKEND 持久化。 |
storage.media |
当前 MediaStore 的配置值、实际后端、启用状态和持久化状态。 |
storage.migrations |
已注册迁移域、版本和状态;SQL 迁移由服务端迁移流程执行,IDB 迁移由浏览器数据库升级回调执行。 |
vector.capable |
当前后端是否支持向量检索(受 store 类型 + 编译时 vector 扩展可用性影响) |
vector.driver |
sqlite-vec / pgvector / in-memory / external / none |
vector.modelCount / vector.tableCount |
已注册的 embedding 模型数量及对应物理表数量(每个模型一张表) |
列出所有已加载的世界。世界数据从 worlds/ 目录读取并缓存在 Store 中。
响应:
{
"items": [
{
"id": "cloudmere",
"name": "云溟界",
"description": "一个漂浮于云层之上的奇幻世界...",
"locale": "zh-CN",
"metadata": {
"source": "file",
"requiredPlugins": ["pregame", "world-init", "char-creator"],
"recommendedPlugins": ["narrator", "guide"],
"excludedPlugins": ["chat-mode-narrator"],
"pluginPolicy": {
"preset": "traditional-story",
"preferTags": ["mode:traditional-story"],
"avoidTags": ["mode:dialogue"]
},
"worldDataPath": "data/world.data.yaml",
"worldData": {
"schemaVersion": 1,
"sources": [
{
"id": "dimensions",
"target": "world:metadata.dimensions",
"digest": "sha256:...",
"order": 0,
"origin": "world",
"diagnostics": { "info": 0, "warning": 0, "error": 0 }
}
]
}
},
"createdAt": "2025-01-15T10:00:00.000Z",
"updatedAt": "2025-01-15T10:00:00.000Z"
}
]
}获取单个世界的详细信息。
参数:
| 参数 | 位置 | 说明 |
|---|---|---|
id |
路径 | 世界 ID(如 cloudmere) |
响应 200:
{
"id": "cloudmere",
"name": "云溟界",
"description": "一个漂浮于云层之上的奇幻世界...",
"locale": "zh-CN",
"metadata": {
"source": "file",
"dimensions": {},
"requiredPlugins": ["pregame", "world-init", "char-creator"],
"recommendedPlugins": ["narrator", "guide"],
"excludedPlugins": ["chat-mode-narrator"],
"pluginPolicy": {
"preset": "traditional-story",
"preferTags": ["mode:traditional-story"],
"avoidTags": ["mode:dialogue"]
},
"worldDataPath": "data/world.data.yaml",
"worldData": {
"schemaVersion": 1,
"sources": [
{
"id": "dimensions",
"target": "world:metadata.dimensions",
"digest": "sha256:...",
"order": 0,
"origin": "world",
"diagnostics": { "info": 0, "warning": 0, "error": 0 }
}
]
}
},
"createdAt": "2025-01-15T10:00:00.000Z",
"updatedAt": "2025-01-15T10:00:00.000Z"
}响应 404:
{
"error": "World not found: unknown-world"
}创建或更新一个世界记录(upsert 语义)。
请求体:
{
"id": "my-world",
"name": "自定义世界",
"description": "我的自定义世界描述",
"locale": "zh-CN",
"metadata": {}
}响应:
{
"id": "my-world",
"name": "自定义世界",
"description": "我的自定义世界描述",
"locale": "zh-CN",
"metadata": {},
"createdAt": "2025-01-15T10:00:00.000Z",
"updatedAt": "2025-01-15T10:00:00.000Z"
}导出世界维度数据。支持 YAML 和 JSON 格式。
参数:
| 参数 | 位置 | 说明 |
|---|---|---|
id |
路径 | 世界 ID |
format |
查询 | yaml(默认)或 json |
响应: 以 Content-Disposition: attachment 返回维度数据文件。
导入维度数据到世界(全量替换 dimensions)。导入后自动通知使用该世界的活跃 session。
请求体:
{
"dimensions": {
"geography": { "overview": "...", "regions": [...] },
"factions": [...]
}
}响应: 更新后的 WorldRecord。
响应 422: 维度数据校验失败。
部分更新世界。支持更新基础字段,也支持直接提交顶层 dimensions。
行为:
- 顶层
dimensions会写入metadata.dimensions metadata采用 merge 语义,未提及的兄弟字段会保留dimensions与metadata.dimensions都会经过世界维度校验
请求体示例:
{
"description": "更新后的描述",
"dimensions": {
"geography": { "overview": "新的地理结构" },
"factions": { "groups": ["Guild"] }
}
}将世界最新维度数据同步到指定 session 的 plugin_data 与 lorebook 常量词条中。
行为:
- 覆盖同名维度 key
- 清理目标 session 中已经失效的旧维度 key
- 保持
SessionContextSnapshot.world.entries下一轮读取到最新世界词条
请求体:
{ "sessionId": "neonridge-abcd1234" }响应:
{ "success": true, "syncedKeys": ["geography", "factions", ...], "entryCount": 9 }只读预检 worldData 导入计划。请求可以传 sessionId 使用现有 session 的插件列表,也可以传 plugins 预检创建 session 前的插件选择。
请求体:
{ "plugins": ["character-blueprint", "char-creator", "living-world-rules"] }或:
{ "sessionId": "haruka-academy-abcd1234" }响应:
{
"imported": true,
"planned": 12,
"diagnostics": [],
"targets": [
{
"kind": "plugin-data",
"target": "plugin:character-blueprint/blueprints",
"sourceId": "cast",
"pluginId": "character-blueprint",
"namespace": "blueprints",
"key": "kamishiro-mio"
}
]
}同步已有 session 中由 worldData importer 管理的数据。默认 dry-run;传 dryRun:false 才写入。同步只处理 world_data_import_ledger.managed=true 的 row,并用 valueHash 检测玩家或插件是否修改过目标数据。
请求体:
{ "sessionId": "haruka-academy-abcd1234" }执行写入:
{ "sessionId": "haruka-academy-abcd1234", "dryRun": false }强制覆盖冲突:
{ "sessionId": "haruka-academy-abcd1234", "dryRun": false, "force": true }响应:
{
"imported": true,
"dryRun": true,
"diagnostics": [],
"planned": 12,
"upserted": 2,
"deleted": 1,
"unchanged": 9,
"conflicts": [
{
"target": "plugin:character-blueprint/blueprints",
"key": "kamishiro-mio",
"sourceId": "cast",
"reason": "modified"
}
]
}列出所有游戏会话。支持 ?worldId= 查询参数过滤。
响应:
{
"items": [
{
"id": "cloudmere-a1b2c3d4",
"worldId": "cloudmere",
"status": "active",
"turnCount": 0,
"preGameCompleted": [],
"locale": "zh-CN",
"activePlugins": ["pregame", "narrator"],
"createdAt": "2025-01-15T10:00:00.000Z",
"updatedAt": "2025-01-15T10:00:00.000Z"
}
]
}创建一个新的游戏会话。
请求体:
{
"worldId": "cloudmere",
"locale": "zh-CN",
"plugins": ["pregame", "narrator", "codex"]
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
worldId |
string | 否 | 关联的世界 ID(校验: /^[a-z0-9_-]{1,64}$/i) |
locale |
string | 否 | 语言区域,默认 zh-CN |
plugins |
string[] | 否 | 要激活的插件 ID 列表 |
id |
string | 否 | 客户端自定义会话 ID(如不提供则自动生成 {worldId}-{uuid8}) |
世界包字段会影响准备页和 session 初始化:
metadata.requiredPlugins:准备页锁定启用。metadata.recommendedPlugins:准备页默认启用。metadata.excludedPlugins:准备页默认关闭。metadata.pluginPolicy:准备页组合包策略。可包含preset、preferTags、avoidTags、requireCapabilities、requiredPlugins、recommendedPlugins、excludedPlugins、packs;旧的三组插件字段仍兼容并会参与合并。metadata.characterBlueprints:创建 session 时自动导入到character-blueprint插件数据,并实例化为 NPC character。
响应:
{
"id": "cloudmere-a1b2c3d4",
"worldId": "cloudmere",
"locale": "zh-CN",
"status": "active",
"turnCount": 0,
"preGameCompleted": [],
"activePlugins": ["pregame", "narrator", "codex"],
"createdAt": "2025-01-15T10:00:00.000Z",
"updatedAt": "2025-01-15T10:00:00.000Z"
}响应字段:
status('active' \| 'paused' \| 'ended') — 会话生命周期状态(turn-band 重构后替代原先的phase字段)turnCount(number) — 主循环轮数计数(从 0 开始,每次成功 turn +1)preGameCompleted(string[]) — 已完成 Pre-Game 初始化的 runtime id 集合,框架据此跳过后续轮次的 Pre-Game 调度
Session ID 格式: 自动生成的 ID 格式为
{worldId}-{uuid8}(如cloudmere-a1b2c3d4),使用crypto.randomUUID()后缀防止枚举。如未提供 worldId 则前缀为session。
获取会话的完整信息。
参数:
| 参数 | 位置 | 说明 |
|---|---|---|
id |
路径 | 会话 ID |
响应 200:
{
"id": "cloudmere-a1b2c3d4",
"worldId": "cloudmere",
"status": "active",
"turnCount": 3,
"preGameCompleted": [
"pregame",
"world-init/schema-gen",
"char-creator/player-init"
],
"locale": "zh-CN",
"activePlugins": ["pregame", "narrator"],
"createdAt": "2025-01-15T10:00:00.000Z",
"updatedAt": "2025-01-15T10:05:00.000Z"
}响应 404:
{
"error": "Session not found: <id>"
}更新会话字段。当前支持 status 与 runtimeModelOverrides。
参数:
| 参数 | 位置 | 说明 |
|---|---|---|
id |
路径 | 会话 ID |
请求体:
{
"status": "paused",
"runtimeModelOverrides": {
"narrator": "balance",
"codex/unlocker": "fast"
}
}字段说明:
status(可选,'active' \| 'paused' \| 'ended') — 会话生命周期状态。turn-band 重构后取代原先的phase字段——phase已从 SessionRecord 中移除,运行进度改由turnCount+preGameCompleted集合描述。非合法枚举值返回 400。runtimeModelOverrides(可选,object) — PR-6 引入。Per-runtime 模型 slot 覆盖,key 为 runtime ID(pluginId或pluginId/runtimeName,必须匹配/^[a-z][a-z0-9-]*(?:\/[a-z][a-z0-9-]*)?$/),value 为llm.toml中定义的 slot 名(如default/fast/balance)。框架在每次 turn 执行前快照该字段,resolver 优先查找 session override → 然后 fallback 到manifest.model→ 最后default。空对象{}清除所有覆盖。插件列表与 Session Prep 会暴露 runtime 的声明 slot;若声明 slot 未配置,UI 会提示补充[covel.<slot>],不会静默改绑到不相关的文本 slot。Provider 与 API key 仍走前端 localStorage +X-Provider-Keysheader,不入库,以保护隐私。
校验规则(runtimeModelOverrides):
- 必须是对象(
null/ 数组 / 非对象类型 → 400) - 最多 64 条目
- 空字符串 key / 非字符串 value / 不匹配 pattern 的 key 会被静默剥离
- 没传该字段 → 不动现有覆盖
响应 200: 返回合并后的会话对象(含 status / turnCount / preGameCompleted / runtimeModelOverrides)。
删除一个游戏会话。
参数:
| 参数 | 位置 | 说明 |
|---|---|---|
id |
路径 | 会话 ID |
响应 200:
{
"deleted": true
}响应 404:
{
"error": "Session not found: <id>"
}Turn 是游戏的核心交互单元。每次玩家发言触发一个 Turn,服务器调度所有活跃的 Runtime 按优先级执行,收集 LLM 输出并返回。
兼容的 headless/testing JSON 入口。Web UI 的主交互路径使用 /api/actions,因为它会返回 data-only SSE 流并转发回合内进度事件。保留该入口用于脚本、测试和不需要 SSE 的调用方;它和 /api/actions 共享 turn 执行、session lock、commit pipeline 与 turnCount 口径,但不会把回合内事件逐条流给客户端。
执行一个玩家回合。
参数:
| 参数 | 位置 | 说明 |
|---|---|---|
id |
路径 | 会话 ID |
请求体:
{
"message": "我拔出剑,准备迎战",
"locale": "zh-CN",
"model": "deepseek-chat"
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
message |
string | 是 | 玩家的文字输入 |
locale |
string | 否 | 覆盖会话语言 |
model |
string | 否 | 覆盖 LLM 模型(API 级别) |
响应:
{
"turnId": "a1b2c3d4-...",
"sessionId": "cloudmere-a1b2c3d4",
"runtimeResults": [
{
"pluginId": "narrator",
"runtimeId": "narrator-main",
"output": "你缓缓拔出腰间的长剑,剑刃在微弱的光芒中闪烁...",
"toolCalls": [],
"durationMs": 2340
}
],
"durationMs": 2500
}响应 404:
{
"error": "Session \"<id>\" not found"
}使用说明:
- Turn 执行是同步的,响应时间取决于 LLM 调用耗时
- 每个活跃 Runtime 按优先级依次执行(同优先级并行)
runtimeResults包含每个 Runtime 的输出,可能包含叙事文本、工具调用结果等session.turnCount表示主循环进度:setup-only Pre-Game 执行会保存turn_results,但不会计入主循环轮数;完成 Pre-Game 后最低推进到1- 服务端会对每个 runtimeResult 运行
processRuntimeResult提交管道(与/api/actions一致):normalize → state.commit → 触发后续 SessionEvent - 如果某个 Runtime 的输出包含
pendingInputs,需要通过plugin-rpc的framework.submit-formaction 提交玩家响应
当 Turn 执行后产生 pendingInputs(如表单、选择题、确认框),玩家需要通过 framework.submit-form 提交响应。框架会将玩家输入转化为自然语言叙事,追加到对话历史中。若该响应完成最后一个 Pre-Game runtime,随后发起的 /api/actions send_message 会在同一个请求里完成 Pre-Game 并立即补跑已触发的主循环 runtime,因此同一个 turnId 可能同时包含 setup completion 和第一段正式叙事。
提交一个或多个玩家交互响应。
参数:
| 参数 | 位置 | 说明 |
|---|---|---|
id |
路径 | 会话 ID |
请求体:
{
"pluginId": "framework",
"action": "submit-form",
"payload": {
"turnId": "a1b2c3d4-...",
"submissions": [
{
"interactionId": "char-creation-form",
"type": "form",
"values": {
"name": "艾尔文",
"class": "战士",
"background": "孤儿出身的流浪剑客"
}
}
]
}
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
pluginId |
string | 是 | 固定为 "framework" |
action |
string | 是 | 固定为 "submit-form" |
payload.turnId |
string | 是 | 产生该交互的 Turn ID |
payload.submissions |
Submission[] | 是 | 提交数组 |
Submission 对象:
| 字段 | 类型 | 说明 |
|---|---|---|
interactionId |
string | 交互 ID,来自 Turn 输出的 pendingInputs |
type |
"form" | "choice" | "confirmation" |
交互类型 |
values |
object | 玩家输入的值 |
三种交互类型的 values 格式:
表单 (form): 每个字段名对应一个值
{
"interactionId": "create-character",
"type": "form",
"values": { "name": "艾尔文", "class": "法师" }
}选择 (choice): 提供 selectedId 和可选的 selectedLabel
{
"interactionId": "path-choice",
"type": "choice",
"values": { "selectedId": "forest_path", "selectedLabel": "穿越黑暗森林" }
}确认 (confirmation): 提供 confirmed 布尔值
{
"interactionId": "accept-quest",
"type": "confirmation",
"values": { "confirmed": true }
}响应:
{
"status": "ok",
"result": {
"results": [
{
"interactionId": "char-creation-form",
"filledNarrative": "旅人自称艾尔文,是一名孤儿出身的流浪剑客,以战士之姿行走江湖。",
"accepted": true
}
],
"accepted": true
}
}错误响应:
{ "error": "turnId is required" } // 400
{ "error": "submissions[] is required" } // 400
{ "error": "Session \"<id>\" not found" } // 404使用说明:
filledNarrative是将玩家输入填入模板后的纯自然语言文本,不含 JSON 结构- 该文本作为玩家消息追加到对话历史,供叙事者在下一轮 Turn 中参考(不再生成合成的 assistant-role 消息)
- 模板由插件提供,使用
{{fieldName}}占位符语法 - 如果找不到模板,会生成一条简单的回退叙事(如
[玩家输入] name: 艾尔文, class: 战士)
统一的"结构化插件指令"通道。同时支持:
- Action 级:
{ pluginId, action, payload }— 调用插件在 PLUGIN.mdrpc字段中声明的 RPC handler,或框架默认 handler(如submit-form)。返回单次 JSON。 - Runtime 级:
{ pluginId, runtimeId, payload }— 手动触发一次 runtime 执行。通过完整 Turn pipeline(prompt 组装、工具循环、proposal 提交)跑一次目标 runtime,事件触发的下游 runtime 会按 priority 自动 chain。执行子模式由manifest.execution决定:'sync'(默认): 同步等待 runtime 完成,commit proposals 后返回汇总 JSON。'background': 立即返回 202 +jobId,后台通过setImmediate继续执行。进度/结果通过plugin_data表_jobs保留命名空间写回,前端经plugin-data.changedSSE 感知变化。
参数:
| 参数 | 位置 | 说明 |
|---|---|---|
id |
路径 | 会话 ID |
请求体:
{
"pluginId": "framework",
"action": "submit-form",
"payload": {
"turnId": "a1b2c3d4-...",
"submissions": [
{
"interactionId": "char-form",
"type": "form",
"values": { "name": "艾尔文" }
}
]
}
}或 action 级:
{
"pluginId": "codex",
"action": "regenerate",
"payload": { "cardId": "shrine-of-stars" }
}或 runtime 级:
{
"pluginId": "dashscope-image-gen",
"runtimeId": "dashscope-image-gen/prompt-generator",
"payload": { "style": "cinematic" }
}| 字段 | 类型 | 说明 |
|---|---|---|
pluginId |
string | 插件 ID(框架默认 handler 用 framework 占位即可) |
action |
string(可选) | RPC action 名,kebab-case。与 runtimeId 互斥 |
runtimeId |
string(可选) | runtime 全名(如 my-plugin/my-runtime)。与 action 互斥 |
payload |
unknown | handler 的输入数据 / agent runtime 的 manualPayload / function runtime 的 ctx.manualPayload |
expectsBackgroundFollower |
boolean(可选) | runtime 级 sync 入口若只是生成 prompt 并预计触发后台 follower,可设为 true。框架会立即写入 _jobs 占位并返回 202,随后在后台执行入口 runtime 与 follower,避免 UI 等 prompt LLM 完成后才出现任务。 |
解析顺序(action 级):
- 插件声明的 action(
manifest.rpc[action],通过pluginId命名空间隔离) - 框架默认 action(全局,如
submit-form/cancel)
框架默认 action:
| Action | 说明 |
|---|---|
submit-form |
持久化玩家输入、找模板消息、按 {{字段}} 填充自然语言 |
响应 200 — action 级:
{
"status": "ok",
"result": {
/* 取决于 handler */
}
}响应 200 — runtime 级,sync 模式:
{
"status": "ok",
"turnId": "7f3e-...",
"runtimeResults": [
{
"runtimeId": "dashscope-image-gen/prompt-generator",
"pluginId": "dashscope-image-gen",
"status": "ok",
"durationMs": 3421,
"output": {
/* runtime final output (parsed envelope) */
}
}
],
"durationMs": 3480
}图像/媒体生成输出:
Function runtime 的完成态可返回 assetGenerations[]。每一项会被提交为 asset.generate proposal,并以 MediaRef 形式进入 trace / SSE / 视图层。
{
"assetGenerations": [
{
"ref": {
"id": "media_01HX...",
"mime": "image/png",
"sha256": "..."
},
"modality": "image",
"meta": {
"prompt": "A misty harbor stairway",
"provider": "openai",
"model": "gpt-image-1"
}
}
],
"pluginData": [
{
"namespace": "images",
"key": "img_01HX...",
"value": {
"status": "done",
"ref": { "id": "media_01HX...", "mime": "image/png", "sha256": "..." }
}
}
]
}Provider-specific wire responses such as OpenAI b64_json, SDK base64, or expiring remote image URLs are transient handler inputs. The handler must call ctx.media.put() for bytes/base64 or ctx.media.ingestUrl() for remote URLs before returning, then expose the generated media through assetGenerations[].ref and ref-only business records.
For plugins with capabilities: ["image-generation"], a successful completed runtime must return at least one valid assetGenerations[] entry. pluginData records in the images namespace must store ref records; completed outputs with old url, base64, or dataUrl image fields are reported as runtime errors.
响应 202 — runtime 级,background 模式:
{
"status": "accepted",
"jobId": "a3c9-...",
"pending": true,
"turnId": "7f3e-...",
"runtimeId": "dashscope-image-gen/image-generator",
"phase": "prompt"
}当请求体包含 expectsBackgroundFollower: true 时,sync prompt-builder 也会使用同样的 202 形态立即返回(响应中 phase: "prompt");此时 _jobs/{jobId} 先以 phase: "prompt" / message: "Generating image prompt..." 写入,prompt 完成并排入后续 background follower 后会更新为 status: "done" 且包含 deferredJobs。如果 prompt-builder 返回但没有触发任何 background follower,框架会把 _jobs/{jobId} 标记为 status: "failed" 并附带 reason: "expected-background-follower-missing"。
_jobs 命名空间协议(background 模式):
框架在 plugin_data 表中为每个后台任务保留以下 row,插件 禁止直接写该命名空间,但可订阅 SSE 读取:
sessionId : <session>
pluginId : <触发插件>
namespace : "_jobs"
key : <jobId>
value : {
status: "pending" | "done" | "failed",
runtimeId: string,
turnId: string,
startedAt: ISO8601,
completedAt?: ISO8601,
durationMs?: number,
runtimeResults?: RuntimeResultSummary[], // status=done
deferredJobs?: { jobId: string; runtimeId: string }[], // prompt-builder follower 队列
phase?: "prompt" | string,
message?: string,
progress?: number,
error?: string, // status=failed
reason?: "expected-background-follower-missing" | string, // status=failed 时的细分原因
abortReason?: string
}
每次写入都会通过 store-proxy 发出 plugin-data.changed SSE 事件(见 protocol.md),前端据此刷新 loading / final UI。
错误响应:
| 状态码 | 触发条件 |
|---|---|
| 400 | pluginId 缺失 / action 与 runtimeId 同时设置或同时缺失 / RpcValidationError / plugin-mismatch(runtimeId 不属于 pluginId) |
| 404 | 会话不存在 / unknown-action(action 未注册) / runtime-not-active(runtimeId 未加载到该 session) |
| 429 | queue-full(community trust 的待批准队列满) |
| 500 | handler 抛出未处理异常 / handler 模块加载失败 / runtime-execution-failed / background-enqueue-failed |
注意: background 模式下 runtime 内部异常 不会映射为 5xx HTTP 状态 —— 202 已经发出,失败信息写入
_jobs/{jobId}.value.error,前端通过 SSE 感知。
插件 PLUGIN.md 中声明 RPC action:
---
name: my-plugin
description: ...
rpc:
regenerate:
handler: ./rpc/regenerate.js
input: ./rpc/regenerate.schema.json # 可选
streaming: false # 默认 false
description: 重新生成上一次叙事
cancel:
handler: ./rpc/cancel.js
trustLevel: builtin # 强制 builtin 信任
---Action 名不能以
framework-开头(保留给框架默认 handler)。所有 action 名必须是 kebab-case。
Handler 是
default export函数,签名(payload, context) => Promise<unknown>。context至少包含{ sessionId, pluginId, action, store }。模块在首次调用时按需import()。
第三方插件(community 信任级别)的 RPC action 在执行前需要玩家显式批准,防止未审计的代码自动操作 session。
┌─────────┐ POST plugin-rpc ┌────────┐
│ Client │ ───────────────────────────────────► │ Server │
└─────────┘ {pluginId, action, payload} └────────┘
│
▼
gate.evaluate()
│
┌───────────────────────────────┤
│ │
builtin/official community
+ 不在 cache + 不在 cache
│ │
▼ ▼
直接执行 → 200 ok 创建 pending → 202 approval-required
│
┌──────────┴──────────┐
│ Client 弹对话框 │
│ POST decision │
└──────────┬──────────┘
│
allow once / allow session / deny
│
重新发起 plugin-rpc
│
允许 → 200 ok
| 状态码 | 含义 |
|---|---|
| 202 | community 信任级别需要 approval。响应体见下 |
202 响应体:
{
"status": "approval-required",
"approvalId": "9d8c-...",
"pending": {
"approvalId": "9d8c-...",
"sessionId": "sess-...",
"pluginId": "third-party-plugin",
"action": "do-thing",
"payload": {
/* 原始 payload */
},
"trustLevel": "community",
"requestedAt": "2026-04-15T20:00:00.000Z",
"description": "Run the thing"
}
}列出该 session 当前所有未决批准。前端在刷新后用此重建对话框队列。
响应 200:
{
"pending": [
{ "approvalId": "9d8c-...", "pluginId": "...", "action": "...", ... }
]
}提交玩家决定。
请求体:
{
"decision": "allow",
"scope": "once"
}| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
decision |
"allow" | "deny" |
是 | 玩家选择 |
scope |
"once" | "session" |
仅 allow 时 | once(默认):允许这一次后过期,需重新批准;session:缓存到本 session 结束 |
响应 200:
{
"ok": true,
"decision": "allow",
"scope": "once",
"pending": {
/* 原始 pending 数据,已从队列移除 */
}
}错误响应:
| 状态码 | 触发条件 |
|---|---|
| 400 | decision 字段缺失 / 非 allow 或 deny / scope 非法 |
| 404 | approvalId 不存在或已被消费 |
| 信任等级 | 来源 | Approval 行为 |
|---|---|---|
builtin |
框架自带 / 框架默认 handler | 永远直接执行 |
official |
维护团队白名单 | 永远直接执行 |
community |
第三方,默认级别 | 每次都需要玩家批准,除非 session-cache 命中 |
One-time grant 的 TTL 为 60 秒。如果玩家批准后 60 秒内没有发起对应的 dispatch,grant 会过期,需要重新走 dialog 流程。
Approval 状态是进程内 + 内存,服务器重启后所有 pending / cache 全部清空。如果跨重启的持久化变得必要,可以把 gate 的状态写入
plugin_data(见packages/approval/src/rpc-approval.ts内联说明)。
返回框架级 discovery 索引,供第三方开发者、外部工具和 AI Agent 程序化判断“Covel 当前支持哪些字段、URI、工具和事件”。它描述框架能力,不依赖某个具体插件。
响应节选:
{
"schemaVersion": 1,
"framework": {
"pluginManifest": {
"triggerTypes": [
"auto",
"manual",
"scheduled",
"conditional",
"event",
"error-retry"
],
"outputKinds": ["story", "plugin", "system"],
"runtimeTypes": ["agent", "function"],
"executionModes": ["sync", "background"],
"inputInjectKinds": ["runtime", "plugin-data"],
"uiSlots": ["right", "message", "left"]
},
"pluginData": {
"scope": "(sessionId, pluginId, namespace, key)",
"reservedNamespaces": [
"_jobs",
"_logs",
"__ui_right__",
"__ui_message__"
],
"writePaths": [
"builtin-tool:plugin-data-set",
"function-output:pluginData[]"
]
},
"worldData": {
"targetUris": [
"world:metadata.<path>",
"plugin:<pluginId>/<namespace>",
"plugin:<pluginId>/<namespace>+lorebook"
],
"schemaUris": [
"covel://world/dimensions",
"plugin://<pluginId>/<namespace>",
"<local-json-schema-path>"
]
}
}
}列出所有已加载的插件。name 与 description 是 I18nText(可能是字符串或 { "<locale>": "..." } 字典)。capabilities 为该插件所有 runtime 声明的 capability 并集;tags 是玩家/作者筛选标签;relations 是目录关系元数据;outputKind 取首个 runtime 的输出类别(story / plugin / system);source 是框架根据加载路径派定的信任层级(builtin / official / community)。
UI 与第三方调用方应优先按 capabilities / outputKind / source 派发,不要根据 id 字符串硬编码(违反框架/插件隔离规则)。
响应:
{
"plugins": [
{
"id": "narrator",
"name": { "zh-CN": "核心叙事者", "en-US": "Narrator" },
"description": "主要叙事生成插件",
"pluginType": "core-plugin",
"runtimeCount": 1,
"status": "active",
"source": "builtin",
"capabilities": ["narrative"],
"tags": ["mode:traditional-story", "role:narrator", "cost:llm"],
"relations": {
"provides": ["narrative-engine"],
"conflicts": ["chat-mode-narrator"]
},
"outputKind": "story"
},
{
"id": "dashscope-image-gen",
"name": "DashScope Image",
"description": "DashScope 文生图(wan2.x)",
"pluginType": "plugin",
"runtimeCount": 2,
"status": "active",
"source": "community",
"capabilities": ["image-generation", "image-prompt", "manual-invoke"],
"tags": ["role:image", "cost:llm"],
"outputKind": "plugin"
}
]
}获取单个插件的详细信息。返回字段与列表项一致。
参数:
| 参数 | 位置 | 说明 |
|---|---|---|
id |
路径 | 插件 ID(如 narrator) |
响应 200:
{
"id": "narrator",
"name": { "zh-CN": "核心叙事者", "en-US": "Narrator" },
"description": "主要叙事生成插件",
"pluginType": "core-plugin",
"runtimeCount": 1,
"status": "active",
"source": "builtin",
"capabilities": ["narrative"],
"tags": ["mode:traditional-story", "role:narrator", "cost:llm"],
"relations": {
"provides": ["narrative-engine"],
"conflicts": ["chat-mode-narrator"]
},
"outputKind": "story"
}响应 404:
{
"error": "Plugin \"narrator\" not found"
}返回单个插件从 PLUGIN.md manifest 聚合出的开发契约。多 runtime 插件会把所有 runtime 合并为 plugin-level 视图,同时保留 runtimes[] 明细。该端点用于回答“这个插件声明了哪些 capabilities、工具、RPC action、UI slot、dataSchemas 和 plugin-data namespace”。
响应节选:
{
"id": "codex",
"capabilities": ["codex"],
"declaredPluginDataNamespaces": ["entries"],
"dataSchemas": {
"entries": {
"namespace": "entries",
"schemaVersion": 1,
"acceptsWorldData": true,
"schema": "./schemas/entries.schema.json"
}
},
"tools": {
"builtin": [],
"local": [
{
"runtimeId": "codex",
"path": "./tools/unlock-codex-entries.js",
"name": "unlock-codex-entries"
}
]
},
"ui": {
"right": [{ "runtimeId": "codex", "path": "./ui/codex-panel.json" }],
"message": [{ "runtimeId": "codex", "path": "./ui/codex-message.json" }],
"left": []
},
"runtimes": [
{
"id": "codex",
"runtimeType": "agent",
"readablePluginDataNamespaces": ["entries"],
"writablePluginDataNamespaces": ["entries"],
"input": {
"inject": [
{
"kind": "plugin-data",
"namespace": "entries",
"as": "<existing-entries>"
}
]
}
}
]
}declaredPluginDataNamespaces 来自 dataSchemas 和 input.inject: plugin-data。运行时动态 key(如 entries/<entryId>、images/<turnId>)不会在这里枚举;需要结合 schema、插件文档或 _index 端点查看当前 session 的实际 key。
返回 GET /api/plugins/:id/contract 的 plugin-data 子集,适合只关心数据保存/导入契约的工具。
响应节选:
{
"pluginId": "codex",
"dataSchemas": {
"entries": {
"namespace": "entries",
"schemaVersion": 1,
"acceptsWorldData": true,
"schema": "./schemas/entries.schema.json"
}
},
"declaredPluginDataNamespaces": ["entries"],
"writablePluginDataNamespaces": ["entries"],
"readablePluginDataNamespaces": ["entries"]
}列出会话的活跃插件和所有可用插件。available[] 同样暴露插件 capabilities、tags、relations,供前端筛选和组合包状态说明使用。
响应:
{
"active": ["pregame", "narrator"],
"available": [
{
"id": "narrator",
"name": "核心叙事者",
"description": "主要叙事生成插件",
"pluginType": "runtime",
"active": true,
"capabilities": ["narrative"],
"tags": ["mode:traditional-story", "role:narrator", "cost:llm"],
"relations": {
"provides": ["narrative-engine"],
"conflicts": ["chat-mode-narrator"]
}
}
]
}启用一个插件。
请求体:
{ "pluginId": "codex" }响应:
{ "ok": true, "active": ["pregame", "narrator", "codex"] }禁用一个插件。如果目标插件 pluginType === "core-plugin",返回 403 拒绝禁用(核心插件由框架保护)。
请求体:
{ "pluginId": "codex" }响应 200:
{ "ok": true, "active": ["pregame", "narrator"] }响应 403:
{ "error": "Cannot disable core plugin \"narrator\"" }状态系统以结构化表格形式存储游戏世界的各类事实(如角色属性、世界状态、任务进度)。每个表由插件通过 StateManager 注册。
获取会话的所有状态表及其数据。
参数:
| 参数 | 位置 | 说明 |
|---|---|---|
id |
路径 | 会话 ID |
响应 200:
{
"tables": {
"character_stats": {
"schema": {
"name": "character_stats",
"fields": [
{ "name": "hp", "type": "number", "default": 100 },
{ "name": "mp", "type": "number", "default": 50 }
]
},
"data": {
"hp": 85,
"mp": 42
}
},
"world_flags": {
"schema": {
"name": "world_flags",
"fields": [
{ "name": "gate_opened", "type": "boolean", "default": false }
]
},
"data": {
"gate_opened": true
}
}
}
}响应 404:
{
"error": "Session not found: <id>"
}获取会话的所有消息列表。
参数:
| 参数 | 位置 | 说明 |
|---|---|---|
id |
路径 | 会话 ID |
响应:
{
"items": [
{
"id": "msg-001",
"sessionId": "cloudmere-a1b2c3d4",
"role": "user",
"content": "我环顾四周",
"createdAt": "2025-01-15T10:00:00.000Z"
},
{
"id": "msg-002",
"sessionId": "cloudmere-a1b2c3d4",
"role": "assistant",
"content": "你发现自己站在一片广阔的草原上...",
"createdAt": "2025-01-15T10:00:05.000Z"
}
]
}插件的 session 级持久化 KV 存储。数据按 (sessionId, pluginId, namespace, key) 隔离。
接入状态(2026-04-27):内置 Web UI 目前使用 GET/list 读取 plugin-data(右侧面板、message UI specs、plugin data store)。PUT/DELETE 是管理/API 写入口,当前内置 Web UI 暂未直接调用;插件 runtime 推荐通过 plugin-data tools、plugin RPC 或 proposal 写入。PUT/DELETE 保持兼容,但若未来收窄攻击面,应先标记 deprecated 或加 admin/debug gate,而不是静默删除。
列出某插件某 namespace 下的所有数据条目。
参数:
| 参数 | 位置 | 说明 |
|---|---|---|
id |
路径 | 会话 ID |
pluginId |
路径 | 插件 ID(如 world-init) |
namespace |
路径 | 数据命名空间(如 schema, entries) |
响应:
{
"items": [
{ "namespace": "schema", "key": "attributes", "value": { ... }, "updatedAt": "..." },
{ "namespace": "schema", "key": "skills", "value": { ... }, "updatedAt": "..." }
]
}列出某 session 下某插件已经存在的 plugin-data namespace 和 key,不返回 value。这个端点用于调试、AI Agent 自动发现当前 session 数据形态、或 UI 构建轻量索引。
响应:
{
"sessionId": "cloudmere-a1b2c3d4",
"pluginId": "codex",
"namespaces": [
{
"namespace": "entries",
"count": 2,
"latestUpdatedAt": "2026-05-07T10:00:00.000Z",
"keys": [
{
"key": "codex-001",
"createdAt": "...",
"updatedAt": "...",
"valueType": "object"
},
{
"key": "codex-002",
"createdAt": "...",
"updatedAt": "...",
"valueType": "object"
}
]
}
]
}获取单条插件数据。
响应:
{ "namespace": "schema", "key": "attributes", "value": { ... }, "updatedAt": "..." }写入或更新单条插件数据。Value 最大 64KB。
请求体:
{ "value": { "dimensions": ["strength", "agility", "wisdom"] } }响应:
{ "success": true, "namespace": "schema", "key": "attributes" }删除单条插件数据。
响应:
{ "success": true }接入状态(2026-04-27):Working Memory 的 store / proposal / prompt injection 是运行时功能;下列 HTTP CRUD 是管理/调试接口,当前内置 Web UI 暂未直接消费。若未来收敛写路径,应保持 URL/响应兼容,优先替换内部实现而不是直接删除。
列出该 session 的所有工作记忆条目,按 scope(player → story → shared)和 key 排序。
响应:
{
"entries": [
{
"id": "wm_abc123",
"sessionId": "world-uuid8",
"key": "mood",
"scope": "player",
"value": "cautious",
"schemaRef": null,
"updatedAt": "2026-04-12T00:00:00.000Z"
}
]
}写入或更新工作记忆条目(upsert)。scope 必须是 player、story 或 shared 之一。
请求体:
{ "value": <any JSON>, "schemaRef": "optional-schema-id" }响应:
{ "success": true }删除工作记忆条目。
响应:
{ "success": true }用玩家提交的数据重新启动一个被 suspend 工具暂停的 runtime。
请求必须带 X-Provider-Keys header(API key 不在服务端持久化,每次 resume 都要重新提供)。
请求体:
{
"suspensionId": "susp_abc123",
"data": {
/* shape 必须匹配 suspension.resumeSchema */
}
}服务器会用 suspension.resumeSchema 对 data 做最小 JSON Schema 校验(type / required / 顶层 properties type);不匹配返回 400。
响应:
{ "result": <ResumeResult> }错误码:
| 状态 | 触发条件 |
|---|---|
400 |
缺少 X-Provider-Keys、JSON body 错误、suspensionId 缺失或 schema 校验失败 |
404 |
session、suspension 不存在,或 suspension 已 resolved,或 runtime manifest 找不到 |
500 |
resumeSuspendedRuntime() 抛出错误 |
列出指定 session 当前所有挂起项,按 createdAt asc 排序。
响应:
{
"suspensions": [
{
"id": "susp_abc123",
"sessionId": "world-uuid8",
"turnId": "turn-...",
"runtimeId": "core-foo/bar",
"pluginId": "core-foo",
"reason": "需要玩家选择路线",
"resumeSchema": {
"type": "object",
"required": ["choice"],
"properties": { "choice": { "type": "string" } }
},
"pendingContinuation": {
/* runtime-internal serialized state */
},
"createdAt": "2026-04-12T00:00:00.000Z",
"resolvedAt": null
}
]
}放弃一个挂起项 —— 直接从 suspensions 表删除。用于玩家放弃当前选择 / 管理界面清理。
响应:
{ "deleted": true, "suspensionId": "susp_abc123" }接入状态(2026-04-27):服务端手动快照、列表和 fork 能力已实现并有测试覆盖;当前内置 Web UI 只直接使用
GET /api/sessions/:id/snapshot做恢复/重连,暂未提供手动快照列表或 fork 操作界面。
物化快照是存档 / 读档 / 时间线分叉的核心 —— 每个快照把一个回合结束时的完整 session 状态序列化为 payload,保存在 state_snapshots 表。kind 取三种值:auto(每回合结束自动写入)、manual(POST /snapshot 显式创建)、fork(POST /fork 写到子 session 上记录来源)。
从当前 session 状态物化一份 kind="manual" 的快照。payload 包含 characters、stateEntries、pluginData、workingMemory、lorebookEntries、suspensions(未解决的挂起项)以及 messagesCursor(最后一条 turn_message.id)。
响应:
{
"snapshot": {
"id": "<uuid>",
"sessionId": "cloudmere-a1b2c3d4",
"turnId": "turn-42",
"kind": "manual",
"payload": {
"schemaVersion": 1,
"turnId": "turn-42",
"characters": [
/* ... */
],
"stateEntries": [
/* ... */
],
"pluginData": [
/* ... */
],
"workingMemory": [
/* ... */
],
"lorebookEntries": [],
"suspensions": [
/* 未解决的 SuspensionRecord[] */
],
"messagesCursor": "tm_abc"
},
"createdAt": "2026-04-13T00:00:00.000Z"
}
}返回 201 Created;session 不存在时返回 404。
列出指定 session 的所有快照(auto / manual / fork),按 createdAt 升序。
{
"snapshots": [
/* SnapshotRecord[] */
]
}session 不存在时返回 404。
基于指定 snapshot 物化一个新的 session。请求体:
{ "fromSnapshotId": "<snapshot-uuid>" }服务端会:
- 创建新 sessionId(
{worldId}-{uuid8}); - 复用父 session 的 locale / activePlugins / status / turnCount / preGameCompleted;
- 拷贝 characters / state entries / plugin data / working memory / state schemas / unresolved suspensions 到新 session;
- 从
turn_messages中按顺序拷贝消息直到payload.messagesCursor(含),超过 cursor 的消息不拷贝。cursor 在父 session 中已丢失(compact / 删除等)时返回409 { code: 'cursor_missing' }; - 写入一个
kind="fork"的快照到子 session,parentId指向源 snapshot,供 provenance 追踪; - 在 eventBus 上广播
session.forked(SSE topic=session)。
响应:
{
"sessionId": "cloudmere-<new-uuid8>",
"parentSessionId": "cloudmere-a1b2c3d4",
"fromSnapshotId": "<parent-snapshot-id>",
"forkSnapshotId": "<child-fork-snapshot-id>"
}返回 201 Created;快照不属于该 session、快照不存在、或父 session 不存在均返回 404;fromSnapshotId 缺失返回 400;payload.messagesCursor 指向的消息已不在父 session 中返回 409 { code: 'cursor_missing' };内部写入失败返回 500。
整个 fork 在 beginTx / commitTx 下写入,中途任何失败都会 rollback,不会留下半成品子 session。
接入状态(2026-04-27):当前内置 Web UI 主要通过
GET /api/sessions/:id/snapshot获取角色快照;本节 REST 端点保留为轻量读取/管理 API。插件 runtime 推荐使用create-character/update-character/list-characters/get-character工具维护角色。POST /characters是兼容管理入口,后续若收敛角色写路径,应保持 URL/响应兼容并优先替换内部实现。
获取会话中的所有角色。角色在游戏过程中动态创建和演化。
参数:
| 参数 | 位置 | 说明 |
|---|---|---|
id |
路径 | 会话 ID |
响应:
{
"items": [
{
"id": "char-001",
"sessionId": "cloudmere-a1b2c3d4",
"name": "艾尔文",
"type": "player",
"description": "一名孤儿出身的流浪剑客",
"fields": { "class": "战士", "level": 3 },
"version": 2,
"createdAt": "2025-01-15T10:00:00.000Z",
"updatedAt": "2025-01-15T10:10:00.000Z"
}
]
}创建或更新一个角色(upsert 语义)。该兼容管理端点内部通过 character.upsert proposal 提交,因此后续可继续接入 commit pipeline 的 hook / trace 策略,同时保持原 URL 和响应形状。
参数:
| 参数 | 位置 | 说明 |
|---|---|---|
id |
路径 | 会话 ID |
请求体:
{
"id": "char-001",
"name": "艾尔文",
"type": "player",
"description": "一名孤儿出身的流浪剑客",
"fields": { "class": "战士", "level": 3 },
"version": 2
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
id |
string | 是 | 角色 ID |
name |
string | 是 | 角色名称 |
type |
string | 是 | 角色类型(如 player, npc) |
description |
string | 否 | 角色描述 |
fields |
object | 否 | 自定义属性(JSON) |
version |
number | 是 | 版本号 |
响应:
{
"id": "char-001",
"sessionId": "cloudmere-a1b2c3d4",
"name": "艾尔文",
"type": "player",
"description": "一名孤儿出身的流浪剑客",
"fields": { "class": "战士", "level": 3 },
"version": 2,
"createdAt": "2025-01-15T10:00:00.000Z",
"updatedAt": "2025-01-15T10:10:00.000Z"
}事件系统基于 EventBus,支持 SSE (Server-Sent Events) 实时推送和外部事件注入。
订阅指定会话的实时事件流(SSE 长连接)。支持 topic 过滤和事件重放。
查询参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
sessionId |
string | 是 | 会话 ID |
topics |
string | 否 | 逗号分隔的 topic 过滤(如 runtime,state) |
lastEventId |
string | 否 | 从此 ID 之后重放事件(用于断线重连) |
示例:
curl -N "http://localhost:3001/api/events/stream?sessionId=<sessionId>"SSE 事件格式:
连接成功后收到的第一条消息:
event: connected
data: {"sessionId":"cloudmere-a1b2c3d4","timestamp":"2025-01-15T10:00:00.000Z"}
后续事件:
event: turn.completed
data: {"turnId":"a1b2c3d4-...","durationMs":2500}
id: evt-001
event: state.updated
data: {"table":"character_stats","field":"hp","value":85}
id: evt-002
使用说明:
- 使用
curl -N(禁用缓冲)来正确显示 SSE 流 - 连接保持活跃直到客户端断开
- 服务器每 30 秒发送心跳维持连接
- 只接收
sessionId匹配的事件
错误响应:
{
"error": "sessionId query parameter required"
}从外部注入事件到 EventBus 中。可用于触发特定 Runtime 或模拟游戏事件。
请求体:
{
"topic": "combat.start",
"payload": { "enemyId": "goblin-01", "terrain": "forest" },
"sessionId": "cloudmere-a1b2c3d4",
"targetRuntime": "combat"
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
topic |
string | 是 | 事件主题 |
payload |
object | 否 | 事件负载数据 |
sessionId |
string | 是 | 目标会话 ID |
targetRuntime |
string | 否 | 指定接收事件的 Runtime |
响应:
{
"id": "evt-a1b2c3d4",
"emitted": true
}错误响应:
{
"error": "topic and sessionId are required"
}前端主要使用此端点进行游戏交互。将动作请求(发送消息、执行命令等)翻译为 Turn 执行,并通过 SSE 流式返回结果。
请求体:
{
"requestId": "req-001",
"type": "send_message",
"sessionId": "cloudmere-a1b2c3d4",
"locale": "zh-CN",
"payload": {
"content": "我拔出剑,准备迎战"
}
}payload 字段 |
适用 type |
说明 |
|---|---|---|
content |
send_message |
玩家自然语言输入。actions.ts 优先读取此字段。 |
command |
execute_command |
以 / 开头的命令(如 /look),与 content 互斥。 |
旧版示例曾使用
payload.message,但服务端从未读取该字段,已统一为content/command。
响应: SSE 事件流,使用 data-only SSE 帧(无 event: 命名头),每条 data: 是一个 SseEnvelope 对象(不是 ProtocolEvent):
interface SseEnvelope {
type: string; // 事件子类型,参见下方「SSE 协议」
requestId: string; // 请求关联 ID(来自请求体)
traceId: string; // 本回合的 trace ID
sessionId: string;
turnId?: string;
flowId: string; // 等于 traceId
seq: number; // 该流内自增
timestamp: string;
payload: Record<string, unknown>;
}客户端用 fetch() + ReadableStream 解析(见 apps/web/src/services/api/actions.ts: sendAction),不能用 EventSource.addEventListener('<type>', …) —— 因为帧没有命名 event 头。/api/events/stream 才使用命名事件。
测试 LLM 提供商连通性。
请求体:
{ "presetId": "default" }响应:
{
"ok": true,
"latencyMs": 0,
"text": "Preset default (deepseek/deepseek-chat) configured"
}AI 生成世界包。LLM 自主决定世界的所有细节(id、name、tags、dimensions、lore)。服务器会把模型输出的 dimensions 写入 data/dimensions.yaml,生成 data/world.data.yaml descriptor,并在 world.yaml 中写入 worldData: data/world.data.yaml。
这个接口使用 SSE 返回进度和最终世界。客户端通过 fetch() + ReadableStream 解析 data: {...}\n\n 帧。
请求体:
{
"concept": "一个被永恒暴风雪笼罩的冰封大陆",
"locale": "zh-CN",
"model": "deepseek-chat",
"saveTarget": "server-file"
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
concept |
string | 是 | 世界概念描述(最多 4000 字符)。兼容旧字段名 prompt |
locale |
string | 否 | 语言区域,默认 zh-CN |
model |
string | 否 | 覆盖 LLM 模型 |
saveTarget |
string | 否 | 保存目标,默认 server-file。可选值见下表 |
saveTarget |
保存位置 | 持久性来源 | 适用场景 |
|---|---|---|---|
server-file |
服务端 COVEL_USER_WORLDS_DIR 世界包 |
服务端文件系统 | Electron、本机开发、可信私有服务 |
server-store |
服务端 DataStore.upsertWorld() |
STORE_BACKEND=sqlite 或 pg |
自部署 Web 服务希望复用服务端数据库,并避免长期保留世界包文件 |
return-only |
SSE 响应体 | 调用方自行保存 | 浏览器本地 IndexedDB、预览生成结果、公开服务避免写服务端持久层 |
server-store 和 return-only 会先把世界包写入临时目录做校验,然后删除临时目录。因为当前 worldData 文件导入依赖包内相对路径,这两个模式保存的 WorldRecord.metadata 会移除 worldDataPath、worldData 和 dimensionSources,保留已经归一化到 metadata.dimensions 的世界维度数据。
响应里的 world.metadata.storage 标注真实保存位置:
{
"scope": "server",
"backend": "pg",
"durable": true
}Web 前端根据 /api/health.storage.data.frontendMode 选择生成世界保存目标。local 模式使用 saveTarget: "return-only",然后通过 packages/store 的 IndexedDB DataStore.upsertWorld() 保存到用户浏览器,并把 world.metadata.storage 标注为 { "scope": "browser", "backend": "indexeddb", "durable": true }。remote 模式使用 saveTarget: "server-store",并通过服务端 packages/store 后端持久化。缺少 frontendMode 时使用 server-file。
响应 200: SSE 帧。
data: {"type":"progress","phase":"generating"}
data: {"type":"progress","phase":"validating"}
data: {"type":"progress","phase":"saving"}
data: {"type":"done","world":{"id":"frost-continent","name":"冰封大陆","metadata":{"storage":{"scope":"server","backend":"file","durable":true}}}}
响应 422: { "error": "World generation failed", "details": [...] }
响应 400: { "error": "concept (string) is required" } 或 { "error": "saveTarget must be \"server-file\", \"server-store\", or \"return-only\"" }
获取会话的所有 trace 事件。
响应:
{
"sessionId": "cloudmere-a1b2c3d4",
"count": 42,
"events": [
{
"type": "runtime.started",
"requestId": "req-001",
"traceId": "trace-001",
"sessionId": "cloudmere-a1b2c3d4",
"turnId": "turn-001",
"flowId": "flow-001",
"seq": 0,
"timestamp": "2025-01-15T10:00:00.000Z",
"payload": {}
}
]
}按 Turn 分组的 trace 事件。
响应:
{
"sessionId": "cloudmere-a1b2c3d4",
"turnCount": 3,
"turns": [
{
"turnId": "turn-001",
"flowId": "flow-001",
"traceId": "trace-001",
"startedAt": "2025-01-15T10:00:00.000Z",
"completedAt": "2025-01-15T10:00:05.000Z",
"eventCount": 12,
"events": [...]
}
]
}独立调用单个 Runtime,用于测试和调试。
当前状态: 返回 501 Not Implemented。
{
"error": "Not implemented"
}短时鉴权下载内容寻址媒体。token 通过 GET /api/sessions/:id/media-token?id=<mediaId> 颁发。详细鉴权语义见路由实现 doc-comment。
破坏性维护端点:扫描 messages / plugin_data / runtime_outputs / trace_events / snapshots / turn_results / runtime_results 收集仍被引用的 mediaId,再调用 MediaStore.cleanup() 删除未引用的资产。
默认禁用。 必须显式启用:
| 条件 | 行为 |
|---|---|
COVEL_MEDIA_CLEANUP_ENABLED 未设为 truthy(true / 1) |
403 { "error": "cleanup endpoint disabled", "code": "forbidden" } |
DEPLOYMENT_TIER=commercial |
503 { "error": "cleanup endpoint not available in this deployment tier", "code": "unavailable" } — 在管理员鉴权中间件就绪前永远不在商业部署可用 |
dryRun:false 且无 X-Confirm-Cleanup: yes 请求头 |
400 { "error": "confirmation header missing: …", "code": "invalid_request" } |
| 同一时刻第二次并发请求 | 429 { "error": "Operation already in progress" } (singleFlight) |
任一会话的扫描行数超过 scanLimit(默认 1000) |
400 { "error": "scan limit exceeded for session …", "code": "limit_exceeded" } |
请求体:
{
"dryRun": true,
"maxBytes": 0,
"maxAgeMs": 0,
"keepRecentBytes": 0,
"scanLimit": 1000
}dryRun(boolean, defaulttrue) —true仅返回拟删除清单;false真正删除,需配合X-Confirm-Cleanup: yes。maxBytes/maxAgeMs/keepRecentBytes(non-negative number, optional) — 透传MediaLifecyclePolicy。scanLimit(positive integer, optional, default 1000) — 单个 session 累计扫描行数上限。超过即拒绝,宁可让运维显式抬高也不静默截断。
成功响应(200):
{
"policy": { "dryRun": true, "maxBytes": 0 },
"result": {
"scanned": 12,
"protected": 7,
"retained": 7,
"deleted": 5,
"totalBytes": 1048576,
"bytesDeleted": 524288,
"bytesRetained": 524288,
"protectedIds": ["..."],
"deletedIds": ["..."]
}
}示例:
# Dry-run only — 安全
COVEL_MEDIA_CLEANUP_ENABLED=true \
curl -X POST http://localhost:3001/api/media/cleanup \
-H "content-type: application/json" \
-d '{"dryRun": true}'
# 真正删除 — 必须带确认头
COVEL_MEDIA_CLEANUP_ENABLED=true \
curl -X POST http://localhost:3001/api/media/cleanup \
-H "content-type: application/json" \
-H "X-Confirm-Cleanup: yes" \
-d '{"dryRun": false, "maxBytes": 0}'大型部署(>100 sessions)会自动按 50 个一批迭代并通过
console.info输出[cleanup] scanned X/Y sessions进度。
Covel 有两条独立的 SSE 流,信封格式和帧格式都不同:
| 端点 | 帧形态 | 信封 | 客户端 | 说明 |
|---|---|---|---|---|
POST /api/actions |
data-only(data: {...},无 event: 头) |
SseEnvelope(带 requestId/traceId/flowId/seq) |
fetch() + ReadableStream(api.ts:sendAction) |
回合内主流:narrative / runtime lifecycle / 工具调用 trace 等 |
GET /api/events/stream |
命名事件(event: <type>\ndata: {...}) |
ProtocolEvent(带 id/source),并由 server 经 EventBus 广播 |
EventSource + addEventListener('<type>')(subscription.ts) |
回合外辅助通道:跨 session 通知 / 持久订阅 / 重连补放 |
关键差异:
/api/actions使用 data-only 帧,前端无法通过EventSource.addEventListener订阅;/api/events/stream才是命名事件。
完整定义见 packages/shared/src/types/protocol.ts。
| 类型 | 分类 | 说明 |
|---|---|---|
narrative.delta |
叙事 | 叙事文本增量(逐 token 流式) |
narrative.completed |
叙事 | 叙事文本完成 |
interaction.requested |
交互 | 请求玩家输入(表单/选择/确认) |
interaction.completed |
交互 | 玩家交互完成 |
ui.rendered |
UI | ui.render proposal commit 后发出 |
ui.part.update |
UI | UI part 状态更新(每个 part 一条) |
state.changed |
状态 | 游戏状态变更 |
state.snapshot |
状态 | 状态快照 |
state.snapshot.created |
状态 | 自动 / 手动 / fork 写入 snapshot 后发出 |
session.forked |
会话 | POST /api/sessions/:id/fork 物化子 session 后发出 |
execution.started |
执行生命周期 | Turn 执行开始 |
runtime.started |
执行生命周期 | 单个 Runtime 开始执行 |
runtime.completed |
执行生命周期 | 单个 Runtime 执行完成 |
runtime.failed |
执行生命周期 | Runtime 执行失败 |
execution.completed |
执行生命周期 | Turn 执行完成 |
record.updated |
会话生命周期 | 记录更新(角色、任务等) |
event.emitted |
会话生命周期 | 事件发射 |
asset.progress |
资产 | 多模态生成进度(0..100) |
asset.generated |
资产 | asset.generate proposal commit 后发出 |
world.dimensions.changed |
世界 | 世界维度文件变更(热更新) |
plugin-data.changed |
插件数据 | plugin-data-set / DELETE / batch 等所有写路径 |
turn.suspended |
流程控制 | suspend() 工具序列化 pendingContinuation |
turn.resumed |
流程控制 | POST /api/sessions/:id/resume 重启 runtime |
error.occurred |
系统 | 执行错误 |
connection.restored |
系统 | 连接恢复 |
下列事件只经 /api/actions 流转发,未来稳定后才会进 enum。当前消费方需做兼容性处理:
| 事件 | 来源 | 说明 |
|---|---|---|
runtime.skipped |
actions.ts |
runtime 因 cooldown / startTurn / maxTriggerCount 跳过 |
character.upserted |
session-commit-handlers.ts |
character.upsert proposal commit 后发出(与 record.updated 平行) |
tool.calling / tool.completed / tool.failed |
TurnEmitter | 工具调用 trace(debug timeline 用) |
llm.calling / llm.responded / message.completed |
TurnEmitter | LLM 调用 trace |
block.emitted / state.patch.applied |
TurnEmitter | 块发出 / state patch 应用 trace |
hook.fired / hook.rewrote / hook.aborted |
TurnEmitter | Hook 行为 trace |
// /api/actions data-only 帧每条 data: 的形态
interface SseEnvelope {
type: string; // 见上表 + 实现私有事件
requestId: string;
traceId: string;
sessionId: string;
turnId?: string;
flowId: string; // = traceId
seq: number; // 该流内自增
timestamp: string;
payload: Record<string, unknown>;
}
// /api/events/stream 命名事件帧的 data 形态
interface ProtocolEvent {
id: string;
type: ProtocolEventType;
sessionId: string;
turnId?: string;
source?: { pluginId: string; runtimeId: string };
payload: Record<string, unknown>;
timestamp: string;
}完整协议定义参见
packages/shared/src/types/protocol.ts。
Covel 支持三种部署层级 (Deployment Tier),每种层级使用不同的存储策略。浏览器模式可切换:local 保存到用户浏览器 IndexedDB,remote 通过服务端 API 保存到 STORE_BACKEND 指定的后端。
- 服务器存储: SQLite(默认,
./data/covel.db);可显式切换 Memory(仅用于一次性测试) - 前端存储: 默认 remote 使用服务端 SQLite;local 模式使用浏览器 IndexedDB
- 数据流: remote 模式下前端通过 API 读写服务端 store;local 模式下业务记录持久保存在浏览器
covel-browserIndexedDB 中,并在执行 turn 前通过syncToServer()向本地服务器补齐临时执行上下文;生成世界等服务器能力通过return-only响应交给前端保存 - API 密钥: 用户自行管理,存储在浏览器 localStorage
- 认证: 无
# 启动方式
pnpm dev:server # SQLite 后端(默认)
STORE_BACKEND=memory pnpm dev:server # 临时 Memory 后端(重启即丢失)- 服务器存储: Memory 或 SQLite
- 前端存储: 演示公开写入可用 local IndexedDB;私有演示也可用 remote 服务端存储
- API 密钥: 用户自行管理,HTTPS 传输必需
- 认证: 无
- 服务器存储: PostgreSQL(需配置
DATABASE_URL) - 前端存储: 无本地缓存,所有 CRUD 委托给服务器 API
- API 密钥: 平台 + 用户双层管理
- 认证: 必需
# 启动方式
STORE_BACKEND=pg DATABASE_URL=postgresql://covel:pass@localhost:5432/covel pnpm dev:server| 能力 | Memory | SQLite | PostgreSQL |
|---|---|---|---|
| 持久化 | 否 (重启丢失) | 是 (本地文件) | 是 (远程数据库) |
| 并发 | 单进程 | 单进程 | 多进程(pg_advisory_lock 实现 session 级跨进程串行化) |
| 适用场景 | 测试 / 一次性 demo | 单机部署(默认) | 生产环境 |
| 配置 | STORE_BACKEND=memory |
默认(STORE_BACKEND=sqlite,可显式指定) |
STORE_BACKEND=pg + DATABASE_URL |
多进程部署 session 锁:当
STORE_BACKEND=pg时,服务器启动日志会输出session lock: pg-advisory。每次/api/actions//api/sessions/:id/turn//api/sessions/:id/resume都会在专用 PG 连接上拿到pg_advisory_lock(hash(sessionId)),确保同一 session 在任意时刻只有一个 Node 进程执行 turn。Memory / SQLite 后端使用进程内Map锁,足以覆盖单进程场景。
完整 registry 见 docs/guide/env-registry.md。
| 变量 | 说明 | 默认值 |
|---|---|---|
STORE_BACKEND |
存储后端类型 | sqlite |
SQLITE_PATH |
SQLite 数据库路径 | ./data/covel.db |
DATABASE_URL |
PostgreSQL 连接字符串 | - |
SERVER_PORT |
服务器端口 | 3001 |
DEPLOYMENT_TIER |
部署层级 | - |
CORS_ORIGIN |
CORS 允许的源 | - |
ENABLE_DEBUG_PAGE |
启用调试页面 | - |
RATE_LIMIT_RPM |
速率限制 (请求/分钟) | - |