补齐外部契约版本策略与历史抠图模型口径
Project CI / Repository checks (pull_request) Failing after 8s
Project CI / Backend tests (pull_request) Failing after 10s
Project CI / Frontend tests (pull_request) Successful in 2m31s
Project CI / Native shell tests (pull_request) Successful in 12m16s

External v1 从四个生成响应移除原本 required 的 provider 时未升 info.version,属
breaking change 而非文档同步。接入方案新增「版本与兼容策略」,记录当前无外部存量调用方
的豁免前提、失效条件与今后的 breaking 判定;openapi info.description 补充面向集成方的
兼容性说明。API Key 由用户自助发放,「无调用方」不是受控状态,改契约前需先确认活跃密钥。

用户可见性过滤是标题精确匹配「处理模型」,不做语义识别,历史素材中标题为「抠图模型」的
字段会继续出现在图片信息弹窗与画布 ZIP。收敛 MVP 接入方案原有的全覆盖表述,明确该例外
属已知且接受的产品决策:历史素材不迁移、不回溯清理,约束只对新写入生效。

两条决策记入 decision-log,排查要点记入 pitfalls。本次均为文档变更,不修改响应结构、
请求 DTO、过滤实现或素材数据。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 07:49:13 +00:00
parent 2e0f1f41ad
commit 7b9433c23f
5 changed files with 76 additions and 2 deletions
@@ -3,7 +3,7 @@
"info": {
"title": "陶泥儿外部编辑器 OpenAPI",
"version": "1.0.0",
"description": "外部系统调用陶泥儿图片画布项目、画布布局、素材库,以及图片、视频、音效、音乐等编辑器素材生成/编辑能力的 v1 契约。新建 projectId 使用 proj- 前缀,新建 taskId / operationId 使用 task- 前缀;历史 editor-project-*、aitask_*、extgen-* ID 仍可作为既有资源标识传入。"
"description": "外部系统调用陶泥儿图片画布项目、画布布局、素材库,以及图片、视频、音效、音乐等编辑器素材生成/编辑能力的 v1 契约。新建 projectId 使用 proj- 前缀,新建 taskId / operationId 使用 task- 前缀;历史 editor-project-*、aitask_*、extgen-* ID 仍可作为既有资源标识传入。\n\n兼容性说明:v1 当前处于无外部存量调用方阶段,正式对外发放 API Key 之前,契约可能在不升 info.version、不设弃用期的情况下发生包含字段移除在内的破坏性变更。生成客户端时请勿假定本文档已冻结。"
},
"servers": [
{
@@ -37,6 +37,26 @@
---
## 2026-07-31 接受外部 OpenAPI v1 的未版本化 breaking change
- 背景:2026-07-31「修正抠图内部元数据的普通用户读取边界」把 OpenAPI 更新列为影响范围内的机械同步,但实际动作是从已发布的 `/api/external/v1` 契约中删除四个生成响应里原本 `required``provider`,另从 `EditorProjectResource` / `EditorAsset` 删除可选 `provider`,而 `info.version` 仍为 `1.0.0`、路径前缀未变。严格反序列化或由 OpenAPI 生成的调用方会在服务端上线瞬间直接失败,且不需要调用方做任何动作。脱敏目标本身成立,但契约处理方式当时没有单独定性。
- 决策:确认这是 breaking change 而非文档同步,并接受本次不升版本、不提供兼容字段、不设弃用期。唯一依据是截至 2026-07-31 `external_api_key` 无属于外部第三方的存量调用方。该豁免不具一般性:API Key 由用户在个人中心自助发放,`/api/external/v1/openapi.json` 又是该批路由中唯一免鉴权端点,因此「无外部调用方」不是受控状态,出现非内部账号活跃密钥、对外公布契约或与外部团队联调后立即失效。今后删除响应字段、把字段移出 `required`、收窄类型或取值、改变字段语义、新增请求必填字段均视为 breaking,存量调用方出现后必须按兼容值、弃用期或 `/api/external/v2` 三选一处理,只更新 JSON 不构成合规变更流程。本次不追加代码改动。
- 影响范围:`docs/【后端架构】外部OpenAPI与APIKey接入方案-2026-06-19.md` 新增「版本与兼容策略」一节;`docs/openapi/genarrative-external-v1.openapi.json``info.description` 补充面向集成方的兼容性说明;`pitfalls.md` 记录「无调用方」不可当长期前提。不修改响应结构、请求 DTO、路由或 `external_editor_api.rs` 断言。
- 验证方式:`info.version` 保持 `1.0.0` 且 JSON 仍可被 `serde_json` / `json.load` 解析;`external_editor_api.rs` 既有 openapi 断言继续通过。该断言只校验 schema 形状、不校验兼容性,因此通过不等于契约安全,判定仍以上述 breaking 清单为准。正式对外发放第一个外部密钥前需复核本条是否仍成立。
- 关联文档:`docs/【后端架构】外部OpenAPI与APIKey接入方案-2026-06-19.md``docs/openapi/genarrative-external-v1.openapi.json``docs/project-memory/shared-memory/pitfalls.md`
---
## 2026-07-31 历史素材的内部处理模型不做回溯清理
- 背景:用户可见性过滤 `isEditorUserVisibleGenerationInputField` 的实现是标题精确匹配 `处理模型`,既不识别语义也不探测取值;服务端 User/Public mapper 只清理 `generationInputs` 顶层的 `screenColorHex` / `mattingProvider` / `mattingModel`,不遍历 `fields` 数组。历史资产中存在标题为「抠图模型」、取值形如 `动漫风格 anime-seg` 的字段,两侧都拦不住,因此仍会出现在图片信息弹窗和画布 ZIP 导出元数据中。MVP 接入方案原文「前端同时过滤历史项目中已持久化的处理模型字段」读起来是全覆盖保证,与实际实现和历史数据存在显式冲突。
- 决策:维持产品决策——历史素材不迁移、不回溯清理,本次不扩大过滤范围,不修改 `isEditorUserVisibleGenerationInputField`。改为收敛文档口径:明确过滤是标题精确匹配而非语义识别,明确「抠图模型」为已知例外且属于接受状态,不得据此判定为缺陷。约束只对新写入生效,新产生的 `generationInputs.fields` 不得再写入任何内部处理模型字段,无论标题为何。若将来决定扩大过滤范围,必须先对生产 `generation_inputs_json` 做标题去重查询枚举真实存在的历史标题,不得仅凭测试夹具推断清单。
- 影响范围:`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md` 收敛该句表述并补充已知例外;`pitfalls.md` 记录过滤口径与排查方式。不修改前端过滤实现、服务端 mapper、素材数据或导出结构。
- 验证方式:图片信息弹窗与画布 ZIP 共用同一过滤口径,任何一侧改动必须同时覆盖另一侧;既有 `ImageCanvasMetadataModalView``ImageCanvasExportModel` 测试保持通过,不新增针对历史「抠图模型」的过滤断言,以免与本决策冲突。
- 关联文档:`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md``docs/project-memory/shared-memory/pitfalls.md`
---
## 2026-07-30 图片画布搜索不得索引内部模型和 Provider
- 背景:素材和图层详情虽已把内部处理模型显示为 `-`,搜索仍索引原始 `model``provider`,导致 `BgFilter complex``segment-common-image``BgFilter``Aliyun Matting` 等隐藏信息可被查询命中,并出现“命中但无可见匹配字段”的异常体验。
@@ -3948,3 +3948,19 @@
- 现象:为了让 Runtime 调用中立 trait,在 core 或 AGC 内再拼一次 URL/header/request body,或自己消费 SSE;它会与 `platform-llm` 的重试、脱敏、工具分片和错误分类迅速漂移。
- 处理:adapter 只做 core DTO 与现有 `Llm*` DTO 转换,网络调用唯一落到 `LlmClient::run/stream_run`。流式 sink 保留累计文本、当前增量和 finish reasontool calls 继续从最终 response 读取。
- 验证:三种 descriptor/capability、request/response round-trip、stream callback 和稳定 error kind 单测后,仍必须运行 `platform-llm` 全量 parser 测试;只有 adapter fake 通过不能证明 wire 协议没有回归。
## 内部处理模型的可见性过滤是标题精确匹配,不是语义识别
- 现象:读文档以为「内部处理模型不会展示给普通用户」是全覆盖保证,实际历史素材的图片信息弹窗和画布 ZIP 导出里仍能看到抠图模型,例如标题“抠图模型”、取值 `动漫风格 anime-seg`
- 原因:`isEditorUserVisibleGenerationInputField` 的实现是 `field.title.trim() !== '处理模型'`,只按这一个标题字符串精确排除,既不识别语义也不探测取值。历史数据里存在标题不同但语义相同的字段,直接穿过过滤器;服务端 User/Public mapper 只清理 `generationInputs` 顶层的 `screenColorHex` / `mattingProvider` / `mattingModel`,不遍历 `fields` 数组,所以两侧都不会拦。
- 处理:本条目前不修——历史素材不迁移、不回溯清理是明确的产品决策,不得据此判定为缺陷或提交「修复」。约束只对新写入生效:新产生的 `generationInputs.fields` 不得再写入任何内部处理模型字段,无论标题叫什么。若将来要扩大过滤范围,先对生产 `generation_inputs_json` 做一次标题去重查询枚举真实存在的历史标题,不要仅凭测试夹具推断清单。
- 验证:`ImageCanvasMetadataModalView``ImageCanvasExportModel` 共用同一过滤口径,改动其一必须同时覆盖另一侧;新增过滤标题时需同时确认图片信息弹窗与画布 ZIP 两条路径。
- 关联:`src/components/image-editor/ImageCanvasGenerationModel.ts``isEditorUserVisibleGenerationInputField`)、`src/components/image-editor/ImageCanvasExportModel.ts``src/components/image-editor/ImageCanvasMetadataModalView.tsx``src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts``docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`
## 外部 OpenAPI v1 的「无调用方」豁免不是受控状态
- 现象:认为外部 v1 契约可以随内部脱敏需要直接改,因为「反正没人接」;`docs/openapi/genarrative-external-v1.openapi.json` 被当成内部文档同步,不走版本流程。
- 原因:API Key 由用户在个人中心 `我的 → 开发者 API Key` 自助发放,`/api/external/v1/openapi.json` 又是该批路由里唯一不要求鉴权的端点,任何登录用户都能拉规格并生成客户端。因此「无外部调用方」随时可能在无人决策的情况下变为假,不能当作长期前提。
- 处理:改外部 v1 响应前先确认 `external_api_key` 是否已有非内部账号的活跃密钥。仍无调用方时可按现行豁免直接改,但必须同步更新接入方案的「版本与兼容策略」;已有调用方时按该节规则择一处理(兼容值 / 弃用期 / 升 v2),只改 JSON 不构成合规变更。
- 验证:`external_editor_api.rs` 的 openapi 断言只校验 schema 形状,不校验兼容性,通过不等于契约安全;判定 breaking 与否以「删字段、移出 required、收窄类型、改语义、新增必填」为准。
- 关联:`docs/【后端架构】外部OpenAPI与APIKey接入方案-2026-06-19.md``docs/openapi/genarrative-external-v1.openapi.json``server-rs/crates/api-server/src/external_editor_api.rs``server-rs/crates/api-server/src/modules/external_api.rs`
@@ -175,7 +175,7 @@
- 工程刷新后能从后端恢复资源、图层布局和 viewport。
- “我的”页项目入口能进入 `/project`;项目页能列出工程、重命名 / 删除单个工程、批量选择和批量删除;点击工程后进入 `/editor/canvas?projectid=<projectId>` 并按 query 加载该工程。
- 用户可见的 `generationInputs.fields` 不得写入或展示“处理模型”;角色抠图、图标 / UI 图集抠图及自动拆分形成的派生资产,其 `Model` 继续继承并展示源生图模型,不得改写为 `birefnet``anime-seg``connected-components`、阿里云抠图或本地键色等内部后处理模型。前端同时过滤历史项目中已持久化的“处理模型”字段;历史资源若只剩内部处理模型而无法恢复源生图模型,保留 `Model` 行并显示 `-`。画布 ZIP 的用户可见导出元数据使用同一过滤口径且保持既有结构。
- 用户可见的 `generationInputs.fields` 不得写入或展示“处理模型”;角色抠图、图标 / UI 图集抠图及自动拆分形成的派生资产,其 `Model` 继续继承并展示源生图模型,不得改写为 `birefnet``anime-seg``connected-components`、阿里云抠图或本地键色等内部后处理模型。前端历史项目中已持久化的字段按**标题精确匹配 `处理模型`** 过滤,不做语义识别,也不探测字段取值;历史资源若只剩内部处理模型而无法恢复源生图模型,保留 `Model` 行并显示 `-`。画布 ZIP 的用户可见导出元数据使用同一过滤口径且保持既有结构。已知例外:历史资产中存在标题为“抠图模型”(取值形如 `动漫风格 anime-seg`)的字段,不在上述精确匹配范围内,会继续出现在图片信息弹窗与画布 ZIP 导出元数据中。该状态是已知且接受的产品决策——历史素材不迁移、不回溯清理,不得据此判定为缺陷;本条约束只对新写入生效,新产生的 `generationInputs.fields` 不得再写入任何内部处理模型字段,无论标题为何。
- `generationInputs` 整体是可扩展 JSON 包络,不等同于全量用户可见快照;图片信息只消费 `fields` / `references`。后端可仿照 `screenColorHex` 在顶层保存 `mattingProvider` / `mattingModel`,记录角色、图标图集和 UI 图集抠图实际成功的 BgFilter、阿里云或本地键色结果;前端规范化和图片信息展示不得把这些顶层内部字段映射成可见字段,正式 `Model` 仍取源生图模型。普通用户(包括 owner)与匿名精选资源响应均不返回素材顶层 `provider`、内部处理 `model``screenColorHex``mattingProvider``mattingModel`;正常用户可见 `model` 与合法功能性顶层字段继续保留。只有后台管理和服务端审计读取原始值。
## 后续扩展点
@@ -61,6 +61,44 @@ DELETE /api/profile/api-keys/{keyId}
前端入口位于登录后个人中心的 `我的 → 开发者 API Key`,用于查看当前 Key、创建新 Key、复制一次性明文和撤销已创建 Key。外部 OpenAPI 只描述 `/api/external/v1` 下可由 API Key 调用的接口,不混入登录态 API Key 管理接口。
## 版本与兼容策略
### 当前状态:v1 尚无外部存量调用方
截至 2026-07-31`external_api_key` 表内没有属于外部第三方的存量调用方,v1 处于「已发布但无存量集成」阶段。本节记录的豁免只在该前提成立时有效。
### 已接受的未版本化 breaking change
2026-07-31「收紧编辑器内部处理元数据边界」从下列响应中移除了原本 `required``provider``info.version` 保持 `1.0.0`,路径前缀保持 `/api/external/v1`
- `EditorImageGenerationResponse``EditorIconSpritesheetGenerationResponse``EditorVideoGenerationResponse``EditorAudioGenerationResponse``provider` 原为 `required` 且非空 `string`,属性整体移除,JSON key 不再出现。
- `EditorProjectResource``EditorAsset``provider` 原为可选 nullable,属性移除后同样不再出现在响应中。
受影响端点为 `POST /api/external/v1/editor/images/generations``.../images/edits``.../icon-spritesheets/generations``.../ui-designs/assets/extractions``.../videos/generations``.../audios/sound-effects/generations``.../audios/background-music/generations`,以及全部引用上述两个资源 schema 的读取端点。
这是 breaking change,不是文档同步:严格反序列化的调用方(OpenAPI Generator 生成的 Java / Kotlin / C#、pydantic、serde 非 `Option` 字段)在 `required` 字段缺失时直接失败,且失败发生在服务端上线瞬间,不需要调用方做任何动作。脱敏目标本身成立,接受不升版本、不设弃用期的唯一依据是当前无存量调用方。
### 豁免的失效条件
API Key 由用户在个人中心自助发放,因此「无外部调用方」不是受控状态,可能在无人决策的情况下变为假。本节豁免在下列任一条件出现后立即失效:
- `external_api_key` 出现属于非内部账号的活跃密钥;
- 对外公布 v1 契约、接入文档或示例代码;
- 与外部团队开始基于 v1 的联调。
正式对外发放第一个外部密钥之前,必须先确认下节规则已生效且 v1 契约已冻结。
### 今后的变更规则
下列改动视为 breaking,不得在同一 `info.version` 与同一路径前缀下直接发布:
- 删除响应字段,或把响应字段移出 `required`
- 收窄字段类型、取值枚举或长度约束;
- 在不改字段名的前提下改变字段语义;
- 新增请求必填字段,或收紧既有请求字段的校验。
存量调用方出现后,上述改动按以下顺序择一处理:保留字段并返回兼容值(可为 `null` 或占位值);标注 `deprecated` 并公告弃用期后再移除;或开设 `/api/external/v2` 并冻结 v1。仅更新 `docs/openapi/genarrative-external-v1.openapi.json` 不构成合规的变更流程。
## 鉴权
外部调用使用 Bearer API Key