扩展域 API 契约基线(V1)
状态:Decision — 扩展域 API 契约基线,约束前后端请求/响应与错误结构。 本文 2026-09-28 版按平台 API 响应契约与实现现状收口(#46):旧版「统一包装
code/message/data」的表述作废——实现(internal/common/response)一直是 成功直返业务 JSON,旧文档属于漂移而非超前。
更新时间:2026-09-28
1. 目的
该文档作为前后端与 Agent 联调的稳定基线,约束扩展域 API 的请求/响应与错误结构。
2. 通用约定
- 成功响应直返业务 JSON(
internal/common/response.Success即c.JSON(200, data)), 不带code/message/data包装。业务响应结构体(如列表返回{total, items}) 只含业务字段。 - 错误响应统一
{ "error": "<snake_case 稳定码>", "message": "<人类可读>", "details"?: {...} },HTTP 状态码表达错误类别(400/403/404/409/500,见平台 API 响应契约)。 - 契约字段一律 lowerCamelCase(
installationId、scopeType、releaseVersion)。 旧版文档中的 snake_case 字段名(installation_id等)是文档笔误,wire 上从来 不是这个形态。 - 列表接口统一分页参数:
page(默认 1)、pageSize(默认 20);列表响应统一{ total, items }。 - 扩展域路由挂
FlagExtensionssoft-flag 组(未启用时整域 404),不在 scoped 组(无GameDBMiddleware):扩展表是 meta 库模型,scopeType/scopeId是 行级业务标记而非分库路由。
3. 接口基线
canonical 路由形态如下(与 internal/handler/routes.go registerExtensionRoutes 一致)。兼容路由组 /api/v1/extensions/:id/* 已废弃:它是历史 URL 形态的 别名,唯一前端消费方(listExtensionPages)本就契约错位(见 §6),修复后该组 零消费方,按「禁止兼容旧键」纪律随实施批次删除。
3.1 Catalog
GET /api/v1/extensions/catalogGET /api/v1/extensions/catalog/:idGET /api/v1/extensions/catalog/:id/releasesPOST /api/v1/extensions/catalog(登记)PUT /api/v1/extensions/catalog/:id(更新/上下架)DELETE /api/v1/extensions/catalog/:id(移除)POST /api/v1/extensions/catalog/:id/releases(发布版本)POST /api/v1/extensions/packs/import(pack.tgz导入自动登记)
关键字段(ExtensionCatalogItem):
id(= extensionId,如official.notification)name/displayName/vendor/kind(official|community|private)summary/iconUrl/status(active|hidden|deprecated)latestVersion/installed(该 scope 是否有活跃安装)/defaultInstall/tags[]
detail 额外返回:releases[]、manifest(快照)、capabilities[]。 releases 关键字段(ExtensionReleaseItem):version / releaseChannel (stable|beta|experimental)/ minCoreVersion / publishedAt / changelog。
写路径(admin CRUD + pack 导入)已随 §7 批次 3/6 落地;catalog/release 读端点形态不变。
3.2 Installation
GET /api/v1/extensions/installationsGET /api/v1/extensions/installations/:idPOST /api/v1/extensions/installGET|PUT /api/v1/extensions/installations/:id/configGET /api/v1/extensions/installations/:id/config-schemaPOST /api/v1/extensions/installations/:id/test-connectionPOST /api/v1/extensions/installations/:id/enablePOST /api/v1/extensions/installations/:id/disablePOST /api/v1/extensions/installations/:id/upgradePOST /api/v1/extensions/installations/:id/reconcileDELETE /api/v1/extensions/installations/:id(卸载)GET /api/v1/extensions/installations/:id/events
安装项关键字段(ExtensionInstallationItem):
id/installationKey/extensionId/displayNamereleaseVersion/status/desiredState/enabledscopeType(global|game|env|node-group|node)/scopeIdtargetType(server|agent|hybrid)/targetIdhealthStatus(V1 恒unknown:无健康探测落库,extension_health表按 简化决策不建,见安装模型文档;health-check 端点的响应才是推导值—— enabled→healthy、否则 disabled、卸载→uninstalled)displayName(列表组装批量 join catalog:displayName→name→extensionId兜底;✅ #46 批次 2 已落地)lastError/updatedAt
detail 额外返回:installation、configSchema、config、secretRefs、 bindings[](bindingType/bindingKey/targetRef/status/lastError)、events[]。
动作响应统一 [{status: "<enabled|disabled|upgraded|uninstalled>"}] 直返; reconcile 额外 applied / failed 计数;health-check 返回 {status, checkedAt}。
升级语义:upgrade {releaseVersion} 要求目标版本存在于 release 目录、依赖图 校验通过、现有 config 对新版本 schema 校验通过;回滚 = upgrade 到旧版本 (同版本重复升级返回 409 conflict),不设独立 rollback 端点。
卸载保护:存在活跃依赖方(ensureNoActiveDependents)时拒绝,错误码 dependency_blocked + details.blockers。
3.3 Runtime 与可观测
GET /api/v1/extensions/installations/:id/capabilitiesPOST /api/v1/extensions/installations/:id/health-checkGET /api/v1/extensions/installations/:id/pages
capabilities 返回 {capabilities[], details[]};details 项含 type/key/capability/ provider/operations[]/permissions/configKeys/source。
pages 返回 {pages[]}(不是 items[]),页面项(ExtensionPageItem): type(binding|manifest)/ key / title / route / icon / group / order / requiredPermission / source / schema。数据来源优先 runtime page 绑定, 回退 release manifest 的 ui.pages(此时仅 route,title=route)。
事件查询参数:level / keyword / page / pageSize;事件项 {eventType, level, message, payload(string), createdBy, createdAt}。
3.4 Agent 同步(server ↔ agent wire)
GET /api/v1/agents/:agentId/extensions—— Agent 轮询拉取(HTTP,默认 30s,ExtensionSyncPuller);响应为{payload}包装,payload即AgentSyncPayload(installations + bindings 的 agent 运行时视图,字段 lowerCamelCase)。该结构是 agent wire 契约:修改需同步internal/app/agent/extension_sync_puller.go的解析端。GET /api/v1/extensions/agents/:agentId/sync-payload—— 同一 payload 的 管理端预览(前端 AgentSync 页使用)。
已知边界:Agent 同步走 HTTP 轮询而非自行开发 TCP 隧道推送。V1 保留该形态 (实现完整、间隔可配),隧道推送列为演进项不承诺批次。
4. 错误码基线
必须稳定支持(实现位于 internal/api/extension 的 mapServiceError / errorx):
extension_already_installeddependency_blockedmissing_dependencyversion_mismatchdependency_cycleforbiddennot_foundconflict(升级目标同版本等)
5. 结构化错误 details 基线
dependency_blocked:
details.code = dependency_blockeddetails.blockers = ["extension@version", ...]
extension_already_installed:
details.code = extension_already_installeddetails.installationIddetails.scopeType/scopeIddetails.targetType/targetId
missing_dependency / version_mismatch:
details.missing/details.expected/details.actual
6. 前端消费现状审计(2026-09-28,#46)
| 页面 | 消费端点 | 结论 |
|---|---|---|
Store(Extensions/Store) | catalog 列表/详情/releases、install | ✅ 一致;adapter 层(services/adapters/extensions.ts)薄归一,EXTENSION_ERROR_CODES 按 §4 分支已接 |
Installations(Extensions/Installations) | installations 列表/详情、enable/disable/reconcile/uninstall、events | ✅ 一致;详情/事件/升级三 overlay 受控组合 |
AgentSync(Extensions/AgentSync) | sync-payload 预览 | ✅ 一致(只读 JSON 预览) |
DomainEntry(Extensions/DomainEntry) | installations 列表 → listExtensionPages | ✅ 已修(#46 批次 1):canonical 端点 installations/:id/pages,先取首个安装实例再拉页面绑定;.catch 静默已移除,加载失败走页面统一错误提示。页面级真实渲染用例归批次 4 |
前端 services/api/extensions.ts 的类型/归一不消费 code/message (normalize 直取业务字段)——后端响应 DTO 去掉内嵌 code/message 对前端零影响; 唯一消费方是 agent puller 的 payload 包装(§3.4,随批次同步)。
7. 已知缺口与实施批次链(#46 收口路径)
- ✅ 契约收口批次(2026-09-28 已落地):16 个响应 DTO 去内嵌
code/message字段;agent puller 解析端同步(wrapper 仅存payload);删除 compat 路由组 (13 条)与resolveCompatInstallationID及其 18 个测试函数;listExtensionPages切 canonical 端点、DomainEntry 静默吞错移除。 已知边界:Extensions 四页面(Store/Installations/AgentSync/DomainEntry) 此前零测试文件,本批仅 API 层extensions.test.ts回归(16 用例),页面级 真实渲染用例归批次 4。 - ✅ 列表组装修正(2026-09-29 已落地):
displayName批量 join catalog 真名(CatalogRepo.GetByExtensionIDs单查避免 N+1,displayName → name → extensionId 兜底;查表失败静默回退不阻塞列表);healthStatus从 status/enabled 推导(deriveExtensionHealthStatus,与 HealthCheck 同语义: status/desired_state uninstalled → uninstalled、enabled → healthy、否则 disabled),替换恒unknown;detail 响应同步。引 runtime binding 状态的 深探测仍不承诺(无健康探测落库,见安装模型文档)。 - ✅ catalog 写路径批次(2026-09-29 已落地):admin catalog/release CRUD——
POST /extensions/catalog(登记,name/displayName/vendor/kind 兜底链,extensionId 形态校验,重复 409)、PUT /extensions/catalog/:id(非空覆盖 + status 上下架 active|delisted 闭集)、DELETE /extensions/catalog/:id(活跃安装实例阻止 → 409;卸载后物理删除并级联 清 releases,物理删避免软删行占用 extension_id 唯一索引)、POST /extensions/catalog/:id/releases(发布版本,semver 校验、渠道 stable/beta/alpha 闭集、manifest 必须对象、(extension,version) 应用层查重 409、latestVersion 仅在新版本 semver 更高时回填);官方扩展 seed 补齐official.notification/alerting/approval/backup-advanced(对齐统一模式: manifest 声明三层权限键/pages.requiredPermission/configSchema 属性 type+description,仓库守卫测试防漂移;official.external-platform 为先于 模式的连接器条目不回溯改造)。pack(.tgz,protoc-gen-croupier 产物) 导入自动登记列为后续。已知边界:Store 页管理动作(登记/上下架/发布 UI) 未接线,本批仅 API 面;catalog 写操作无独立审计事件(经 HTTP 层通用审计 链,catalog 表无 createdBy 列)。 - ✅ DomainEntry 恢复实测(2026-09-29 已落地):页面级真实渲染用例 3 例 (
web/src/pages/Extensions/DomainEntry/__tests__/)——有安装实例时 installations→pages 两跳拉取并渲染入口卡(title+route,断言 pages 端点以 安装实例 ID 调用);无安装实例时 Empty 引导且不调 pages 端点;拉取失败时 message.error 提示(批次 1 前被静默吞掉的路径现在有回归防护)且页面不崩。 - ✅ Store 页管理动作 UI(2026-09-29 已落地):批次 3 四端点的页面接线—— 工具栏「登记扩展」+ 行「更多」菜单(下架/上架/发布版本/删除),双受控弹窗 (登记表单 extensionId 形态校验、发布表单 semver + manifest JSON 对象 预检);409 三分支(登记重复/活跃安装阻止删除/版本重复)本地化文案。
- ✅ pack(.tgz)导入自动登记(2026-09-29 已落地):
POST /api/v1/extensions/packs/import(multipart,file字段,写权限 + 64MiB 上限)——服务端解包取manifest.json(包根或单层顶层目录,多层取 最浅;字段与手填写路径同语义:extensionId/version/manifest 必填,渠道 stable/beta/alpha 默认 stable)→ sha256 服务端计算 → 工件写入对象存储extension-packs/<extensionId>/<version>.tgz→ catalog 登记或复用 (已登记扩展再导入只补版本,catalogCreated=false)→ release 发布 (版本查重 409,latestVersion 仅更高 semver 回填,与手填发布同规则)。 Store 页「导入扩展包」Upload 按钮接线(成功提示带版本号,409 出冲突文案)。 已知边界:包内其余文件(descriptors/schemas)不解析,仅随工件整体存储; manifest 业务闭集校验以后端为准;导入复用既有登记时不改登记元数据 (改 displayName 等须走 PUT /catalog/:id)。 批次链至此全部收口(Store UI 批次 5 详见 OPEN-ISSUES #46)。
