# API 与事件契约草案 > 本文是拟建接口,不是已经可请求的服务。实现时生成 OpenAPI 3.1 与前端客户端,禁止前后端各手写一套不一致 DTO。 > 当前基础工程仅实现 `GET /api/v1/health` 存活检查及非生产环境的 `/api/docs`、`/api/docs-json`,尚未实现下列业务接口。存活检查使用 Terminus 原生响应,不宣称业务响应封装、鉴权或资金边界已完成。 ## 1. 通用约定 - 前缀 `/api/v1`;时间为 UTC ISO-8601;积分、法币金额、BigInt 与 Decimal 作为字符串传输。 - 公共业务接口使用同源安全会话;HttpOnly、Secure、SameSite cookie,写入配套 CSRF/Origin 校验。 - 工作区由服务端验证会话与 Membership;URL 中 workspace ID 只是选择器,不是授权。 - 资源越权一般返回 404,管理员权限不足返回 403;前端隐藏菜单不构成鉴权。 - 分页使用 cursor/limit,限制最大页长;所有列表默认作用于当前工作区。 - 编辑采用对象版本或 `If-Match`;冲突返回 409,不静默覆盖另一浏览器标签页的修改。 - 生成、充值、退款、确认等有副作用的操作要求 `Idempotency-Key`;用户双击/网络重连复用同一 key。 ### 成功与失败 ```json { "data": { "taskId": "opaque-task-id", "status": "queued" }, "requestId": "opaque-request-id" } ``` ```json { "error": { "code": "QUOTE_EXPIRED", "message": "报价已过期,请重新确认。", "retryAction": "refreshQuote", "details": { "quoteId": "opaque-quote-id" } }, "requestId": "opaque-request-id" } ``` 失败 HTTP 状态与错误码一致;不能 HTTP 200 加 `success:false` 混淆缓存、监控和客户端。错误不包含平台 key、连接地址、原始请求头、内部堆栈或未经清理的上游正文。 ## 2. 用户端资源接口 | 方法 | 路径 | 权限/语义 | | --- | --- | --- | | GET | `/me` | 登录用户与可访问工作区 | | GET/POST | `/workspaces/{w}/projects` | 项目列表/创建 | | GET/PATCH | `/projects/{p}` | 归属校验;PATCH 要版本 | | DELETE | `/projects/{p}` | 逻辑删除,检查活动执行 | | GET/POST | `/projects/{p}/episodes` | 分集列表/创建 | | PATCH | `/episodes/{e}` | 排序、标题等可变属性 | | GET/POST | `/episodes/{e}/script-versions` | 列出/人工保存新版本;AI 生成走计划接口 | | POST | `/script-versions/{v}/confirm` | 确认明确版本 | | GET/POST | `/projects/{p}/assets` | 资产列表/人工建资产 | | GET | `/assets/{a}/versions` | 含选用/来源信息,不暴露永久公网 URL | | POST | `/assets/{a}/select-version` | 切换当前指针,不覆写文件 | | GET/POST | `/episodes/{e}/shots` | 镜头列表/人工创建 | | POST | `/shots/{s}/versions` | 保存新镜头版本与引用 | | POST | `/shot-versions/{v}/confirm` | 确认信息准备 | | GET | `/shot-versions/{v}/readiness` | 能力、引用、参数、价格缺口的字段级结果 | | GET/POST | `/episodes/{e}/timeline-versions` | 时间线读取/保存不可变版本 | | GET | `/projects/{p}/impact-preview` | 版本变化影响预览,不触发付费 | | GET | `/catalog/models` | 当前用户有效的非敏感模型投影 | | GET | `/catalog/voices` | 已验证音色与能力,不含平台凭据 | 查询参数必须白名单;导出、筛选和排序不能允许任意数据库字段注入。 ## 3. 统一付费计划与执行接口 | 方法 | 路径 | 说明 | | --- | --- | --- | | POST | `/plans` | operation + 输入引用 + 创作参数;服务端规范化、验证与报价 | | GET | `/plans/{id}` | 已冻结计划、报价、有效期和错误修正建议 | | POST | `/executions` | quoteId + planHash + 批准上限;原子冻结并排任务 | | GET | `/executions/{id}` | 业务执行、批次项目、财务状态和可用操作 | | POST | `/executions/{id}/cancel-pending` | 只取消仍 queued 的项,返回未取消的已运行项 | | POST | `/executions/{id}/regeneration-plans` | 显式新生成:创建新计划,不复用已消费授权 | | GET | `/tasks` | 工作区通用任务列表,支持项目与状态过滤 | | GET | `/tasks/{id}` | 当前快照、safe error、结果引用、允许的操作 | | POST | `/tasks/{id}/cancel` | 只允许 queued;原子条件更新,运行中返回 409 | | GET | `/tasks/{id}/events` | SSE:授权、游标、保留窗口与快照补偿 | ### 建议 operation 枚举 `text.outline`、`text.script`、`text.extractAssets`、`text.shotBreakdown`、`image.generate`、`image.edit`、`video.generate`、`audio.synthesizeSpeech`、`render.export`。P1 再启用 `audio.generateMusic`、`audio.generateSfx`、`audio.transcribe`、`video.extend`;没有能力时拒绝而不是假成功。 ### 视频计划示例 ```json { "operation": "video.generate", "projectId": "project-id", "target": { "shotVersionId": "shot-version-id" }, "productModelRevisionId": "product-model-revision-id", "input": { "mode": "firstLastFrame", "prompt": "缓慢推进,海风吹动衣角。", "durationSeconds": 5, "aspectRatio": "16:9", "resolution": "1080p", "references": [ { "role": "firstFrame", "assetVersionId": "image-version-a" }, { "role": "lastFrame", "assetVersionId": "image-version-b" } ] } } ``` 这组参数只有模型明确支持时才通过。服务端从资产读取合法文件身份,不能接受用户随便传入的远程 URL。请求不含渠道 ID、Base URL、密钥或客户端自报价格。 ### 确认执行示例 ```json { "quoteId": "quote-id", "planHash": "canonical-plan-hash", "approvedMaximumMicrocredits": "120000000", "acceptanceVersion": "billing-terms-version" } ``` 例如 `120000000` microcredits 等于 120 积分,仅用于说明单位,不是正式售价。返回 `202 Accepted`,带 Execution ID、Task 列表、Reservation 摘要和财务状态。 同一幂等 key + 同一 canonical hash 返回原执行;同一 key 不同 hash 返回 `IDEMPOTENCY_CONFLICT`。执行失败后也不能复用同一 key 发起新的付费生成。 ## 4. 文件与产物接口 | 方法 | 路径 | 说明 | | --- | --- | --- | | POST | `/uploads/intents` | 归属与配额检查,返回绑定服务端所分配 key 的七牛短期上传凭证及必要上传参数 | | POST | `/uploads/{id}/complete` | 服务端校验对象、MIME、大小、解码和 hash | | GET | `/asset-versions/{id}/preview` | 短期访问凭证,先鉴权,不接受任意 objectKey | | POST | `/asset-versions/{id}/download-links` | 授权下载或新签名,不重新生成 | | GET | `/episodes/{e}/exports` | 成片与打包结果列表 | | POST | `/episodes/{e}/package-plans` | 素材包作为独立受控本地任务,明确是否收费 | 浏览器上传成功不等于文件可用。JSON 中不返回存储主密钥、不直接公开桶,视频下载支持 Range 时仍保持同一权限与过期规则。 七牛直传响应可含 `uploadIntentId`、`uploadToken`、`uploadUrl`、服务端分配的 `objectKey`、`expiresAt` 与上传限制,不包含独立的 AK/SK 配置;上传目标由 `QINIU_BUCKET`、`QINIU_REGION` 和服务端 SDK 确定,不接受浏览器自选空间或任意上传地址。完成接口按 UploadIntent 查询七牛对象并验证实际内容,不信任客户端自报 key、文件 hash 或时长。预览/下载基于 `QINIU_PUBLIC_DOMAIN` 与实际鉴权配置返回短期受控链接,不返回永久裸链;仅记录对象身份,不记录访问 token。 ## 5. 钱包和订单 | 方法 | 路径 | 说明 | | --- | --- | --- | | GET | `/wallet` | 可用、冻结、状态、单位和版本 | | GET | `/wallet/transactions` | 用户安全流水投影与业务回跳 | | GET | `/wallet/transactions/{id}` | 报价、结算、释放明细;不暴露上游进价 | | GET | `/recharge-products` | 后台已发布套餐及法币/积分规则 | | POST | `/payment-orders` | 服务端生成商户订单,冻结套餐快照 | | GET | `/payment-orders/{id}` | 数据库支付事实,前端回跳不等于已付 | | POST | `/payment-orders/{id}/reconcile` | 有频率限制的核对请求,不直接加余额 | | POST | `/payment-orders/{id}/refund-requests` | 创建申请,不代表退款已成功 | | POST | `/payment-webhooks/{provider}` | 独立入口验证官方签名、金额、币种与订单 | 支付商尚未指定;不在文档中假设 Stripe/微信/支付宝都已接入,也不提供不安全的“模拟已付”生产接口。 ## 6. 管理后台 API | 方法 | 路径(均在 `/admin` 下) | 权限与语义 | | --- | --- | --- | | GET/POST | `/channels` | channel.read/write;初始列表为空 | | GET/PATCH | `/channels/{id}/draft` | 仅编辑草稿,乐观锁,字段白名单 | | POST | `/channels/{id}/credentials` | credential.rotate;write-only,生成新版本 | | DELETE | `/channels/{id}/credentials/{v}` | credential.revoke;影响活动任务需二次确认 | | POST | `/channels/{id}/connection-checks` | 只读检测;不自动做收费生成 | | POST | `/channels/{id}/capability-tests` | 有上游费用风险,记录显式测试预算 | | GET | `/channels/{id}/model-candidates` | 仅候选,不自动发布 | | POST/PATCH | `/models`、`/models/{id}` | 产品模型与能力,不把上游响应直接暴露用户 | | POST | `/models/{id}/bindings` | 映射指定发布渠道与协议 | | POST | `/prices/drafts` | pricing.write;明确单位、档位、成本与售价 | | POST | `/catalog/publications` | pricing.publish;整体 readiness 校验并生成版本 | | POST | `/channels/{id}/disable` | 默认停止新计划,保留旧任务查询 | | GET | `/reconciliation-cases` | 排查未知受理、交付失败、资金待结 | | POST | `/reconciliation-cases/{id}/actions` | 受控枚举动作、原因、证据和审批 | | POST | `/wallet-adjustments` | 财务补偿,产生新账本交易,不能 UPDATE 余额 | | POST | `/refund-orders/{id}/approve` | 超额审批/分权;幂等调用支付商 | | GET | `/audit-events` | 脱敏审计,独立权限 | ### 草稿与凭据语义 新增草稿可以只填名称,`baseUrl:null`、`credentialConfigured:false`,不会加入有效目录。保存普通配置不传 key 就保持不变;替换或撤销走专用操作。任何 GET 都不返回密钥明文或密文。 管理员能读取经权限保护的连接地址以维护服务,但普通用户 API 不能读它。审计 diff 中 key 字段只记“新增/轮换/移除”和版本引用,不记录值。 ## 7. 内部接口与供应商回调 供应商回调入口使用受限路径身份和签名,不能直接调用内部 wallet settle。内部 Worker 调用使用独立服务身份,禁止将管理员 cookie 或平台 API key 作为通用内部信任标识。回调若没有可靠签名,只唤醒主动查询,不能依据 body 宣布成功或退款。 ## 8. SSE 事件 ```json { "eventId": "opaque-event-cursor", "type": "task.phaseChanged", "taskId": "task-id", "objectVersion": 7, "occurredAt": "2026-10-16T12:00:00Z", "data": { "status": "waitingProvider", "phase": "providerProcessing" } } ``` 事件类型包括 `plan.approvalRequired`、`task.phaseChanged`、`task.reconciliationRequired`、`asset.versionReady`、`wallet.changed`、`execution.completed`。事件按授权过滤;重新连接使用 Last-Event-ID 或受控游标。游标失效返回重同步信号,客户端重新取快照,不盲目重放 UI 的付费命令。 ## 9. 错误分类 | 错误码 | HTTP | 用户/系统动作 | | --- | ---: | --- | | UNAUTHENTICATED | 401 | 登录,不创建执行 | | FORBIDDEN | 403 | 无权限,不做资金和网络动作 | | RESOURCE_NOT_FOUND | 404 | 不泄露跨工作区对象存在性 | | INPUT_INVALID | 422 | 字段级修正 | | VERSION_CONFLICT / IDEMPOTENCY_CONFLICT | 409 | 重新获取状态,不能自动换 key 重发 | | QUOTE_EXPIRED / PLAN_STALE | 409 | 重新计划与确认 | | INSUFFICIENT_CREDITS | 409 | 显示积分缺口,无副作用 | | CAPABILITY_UNAVAILABLE | 503 | 无有效配置/暂停服务,保留编辑 | | RATE_LIMITED | 429 | 限制频率;是否可恢复由请求性质决定 | | PROVIDER_AUTH_FAILED / PROVIDER_BALANCE_LOW | 502 | 平台运维问题,不混同用户余额 | | PROVIDER_CONTENT_REJECTED | 422 | 解释政策,不自动换渠道 | | PROVIDER_OUTCOME_UNKNOWN | 202 或任务状态 | 执行已存在;返回核对状态,不创建第二次提交 | | DELIVERY_PENDING | 202 或任务状态 | 远程已完成,恢复入库 | | SETTLEMENT_PENDING | 202 或任务状态 | 产物成功,财务处理中 | HTTP POST 已返回 Task 后的异步失败通过 Task 资源表达,不靠后续某个 GET 随意改成 500。表中“202 或任务状态”取决于同步提交响应还是已存在任务查询,不能一个错误码机械覆盖所有阶段。 ## 10. 契约验收 schema 同源、OpenAPI 生成客户端可编译;跨工作区引用被拒绝;新安装无渠道;幂等重放与冲突可测;所有金额以字符串传输;SSE 断线不触发新生成;后台测试声明费用;供应商正文与密钥不外泄;文档示例仅为协议草案,不被导入为实际数据库配置。