Merge branch 'master' into fix/middle-button-canvas-pan

This commit is contained in:
2026-07-20 10:25:15 +08:00
119 changed files with 11499 additions and 5383 deletions
@@ -4,7 +4,7 @@
## 背景
画布Agent对话需要保存历史记录并支持新开会话。消息正文是流式追加、体量随对话增长的 JSON 数据SpacetimeDB 是后端唯一结构化真相存储,但表行不适合承载不断增长的长文本;画布工程快照已有独立保存链路,对话流式更新频率远高于布局保存
画布Agent对话需要保存历史记录并支持新开会话。消息正文会随对话和工具结果持续增长SpacetimeDB 是后端唯一结构化真相存储,但表行不适合承载不断增长的长文本;画布工程快照已有独立保存链路,对话消息也不应与布局保存互相竞争
## 决策
@@ -14,7 +14,7 @@
- 浏览器禁止直接上传、覆盖或签名写入 `editor-agent/` 对象;前端只通过 `api-server` 的会话接口创建会话、发送消息、读取历史,OSS 读写由服务端完成。
- 消息文档序列化后的读写上限为 2 MiB;超过上限时后端拒绝继续读写该会话消息文档,并返回 payload too large 语义错误。
- 同一会话内的消息追加采用 `conversationId` 级串行锁,避免同一会话的“读-改-写”整对象过程互相覆盖。
- Agent 工具调用失败时,仍保留 assistant 消息和 `failed` generation record,包括 tool call、模型错误信息等排障字段;失败记录是会话历史的一部分,不因 SSE 已返回错误而丢弃
- Agent 规划失败使用 `role=system`、正文以 `ERROR ` 开头的消息持久化;首次响应和同 `clientMessageId` 重放都通过 `deltaMessages` 返回该消息,`errorMessage` 不重复携带。前端隐藏前缀并显示红色错误气泡,后端构建后续 LLM memory 时仍保留该消息,让 Agent 获取上一轮失败上下文。工具调用失败继续保留 `status=failed` 工具记录、模型错误信息;失败记录是会话历史的一部分。
- 不把消息明细写入 SpacetimeDB 表,不把对话混入画布工程快照,不在 api-server 内存中保存会话真相。
## 备选方案与取舍
@@ -31,5 +31,5 @@
- 会话表仍只保存元数据和 `messagesObjectKey`;API 可以在读取时把 OSS 消息文档拼装为会话详情返回,但完整消息正文的持久化真相仍是 OSS JSON 文档。
- 消息文档最大 2 MiB;该限制用于阻止单个会话无限增长。后续如果需要更长历史,应引入归档、分页对象或摘要压缩,不应把正文回填进 SpacetimeDB 表。
- 会话软删只打表标记,OSS 对象保留,便于恢复与审计。
- 工具生成失败也写入消息文档:assistant 消息可标记为失败 / 错误类型,`generations` 中保留 `failed` record、模型和错误信息,便于用户回看失败原因和后续排障。
- 规划或工具生成失败也必须写入消息文档:规划失败保存 `ERROR ` system 消息,工具失败保存失败状态、模型和错误信息,便于用户回看失败原因和后续排障。
- 若未来出现跨会话消息检索需求,需另建投影或索引,不回退为消息入表。
@@ -2569,7 +2569,7 @@
"type": "array",
"items": {
"type": "string",
"description": "图片 Data URL。quick-edit 最多 9 张,其它模式最多使用前 5 张。"
"description": "当前账号的 objectKey、项目资源 ID 或素材 ID;本地临时图必须先上传 OSS 再提交。禁止 Data URL / Blob URL。普通生成最多使用前 5 张,数组上限为 9。"
},
"maxItems": 9
},
@@ -2691,7 +2691,7 @@
},
"sourceImageSrc": {
"type": "string",
"description": "待重绘/调整图片 Data URL。"
"description": "待重绘/调整图片的稳定引用:当前账号的 objectKey、项目资源 ID 或素材 ID;本地临时图必须先上传 OSS。禁止 Data URL / Blob URL。"
},
"projectId": {
"type": [
@@ -2763,7 +2763,7 @@
"type": "array",
"items": {
"type": "string",
"description": "图片 Data URL、asset://、OSS object key 或公网 URL。"
"description": "当前账号的 objectKey、项目资源 ID 或素材 ID;本地临时图必须先上传 OSS。禁止 Data URL / Blob URL。"
},
"maxItems": 8
},
@@ -2892,13 +2892,13 @@
"properties": {
"referenceImageSrc": {
"type": "string",
"description": "规范图/参考图 Data URL 或 objectKey;上传参考图优先提交 objectKey。"
"description": "图标规范的稳定引用:当前账号的 objectKey、项目资源 ID 或素材 ID;本地临时图必须先上传 OSS。禁止 Data URL / Blob URL。"
},
"referenceImageSrcs": {
"type": "array",
"items": {
"type": "string",
"description": "额外图标素材参考图 Data URL 或 objectKey;上传参考图优先提交 objectKey。"
"description": "额外图标素材参考图的稳定引用:objectKey、项目资源 ID 或素材 ID;本地临时图必须先上传 OSS。禁止 Data URL / Blob URL。最多 8 张。"
},
"maxItems": 8
},
@@ -2988,7 +2988,7 @@
"properties": {
"sourceImageSrc": {
"type": "string",
"description": "UI 设计图 Data URL 或 objectKey。"
"description": "带红框标注的 UI 设计图稳定引用:当前账号的 objectKey、项目资源 ID 或素材 ID;浏览器内合成后必须先上传 OSS 再提交。禁止 Data URL / Blob URL。"
},
"screenColor": {
"type": [
@@ -3006,7 +3006,7 @@
"type": "array",
"items": {
"type": "string",
"description": "额外 UI 素材参考图 Data URL 或 objectKey;上传参考图优先提交 objectKey。"
"description": "额外 UI 素材参考图的稳定引用:objectKey、项目资源 ID 或素材 ID;本地临时图必须先上传 OSS。禁止 Data URL / Blob URL。最多 5 张。"
},
"maxItems": 5
},
@@ -3273,7 +3273,8 @@
"type": "string"
},
"sourceImageSrc": {
"type": "string"
"type": "string",
"description": "角色源图的稳定引用:当前账号的 objectKey、项目资源 ID 或素材 ID;本地临时图必须先上传 OSS。禁止 Data URL / Blob URL。"
},
"sourceWidth": {
"type": "integer",
@@ -3557,7 +3558,7 @@
"type": "array",
"items": {
"type": "string",
"description": "公网 URL、asset://、OSS object key 或图片 Data URL。仅 Seedance 2.0 系列支持。"
"description": "公网 URL、asset://、当前账号的 objectKey、项目资源 ID 或素材 ID。本地临时图必须先上传 OSS。禁止 Data URL / Blob URL。仅 Seedance 2.0 系列支持。"
},
"maxItems": 9
},
@@ -3565,7 +3566,7 @@
"type": "array",
"items": {
"type": "string",
"description": "公网 URL、asset:// 或 OSS object key。仅 Seedance 2.0 系列支持。"
"description": "公网 URL、asset://、当前账号的 objectKey、项目资源 ID 或素材 ID。本地临时视频必须先上传 OSS。禁止 Data URL / Blob URL。仅 Seedance 2.0 系列支持。"
},
"maxItems": 3
},
@@ -3573,7 +3574,7 @@
"type": "array",
"items": {
"type": "string",
"description": "公网 URL、asset://、OSS object key 或音频 Data URL。仅 Seedance 2.0 系列支持。"
"description": "公网 URL、asset://、当前账号的 objectKey、项目资源 ID 或素材 ID。本地临时音频必须先上传 OSS。禁止 Data URL / Blob URL。仅 Seedance 2.0 系列支持。"
},
"maxItems": 3
},
@@ -16,6 +16,83 @@
---
## 2026-07-18 图片生成 K 档由 provider 直接生成
- 背景:旧 gpt-image-2 尺寸表会把 2K 竖版回落到 `1024x1536`,图标入口又使用固定 `360x360 / 512x512` 占位;角色去背景结果变小时还会直接放大整张透明成品,导致 UI 显示的 2K 与模型实际生成清晰度不一致。
- 决策:用户选择的模型、比例和 K 档先映射为 provider 可直接接受的真实像素,前端占位、api-server 请求和 VectorEngine request body 保持一致。带显式尺寸选项的用户生成不再用回图后缩放恢复 K 档;宣发素材固定交付尺寸与旧无尺寸请求保留原有兼容恢复。角色、图标和 UI 去背景降采样时只重采样 alpha 蒙版并应用回 provider 原始 RGB,不放大低分辨率后处理 RGB。
- 影响范围:普通图片、角色形象、图标图集、UI 设计图的占位与生成请求,gpt-image-2 尺寸矩阵,以及角色透明后处理。
- 验证方式:前端尺寸矩阵和入口占位测试、api-server 生成参数与 alpha 合成测试、platform-image 最终 request body 测试、类型检查、Rust check、编码和 diff 门禁。
- 关联文档:`docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md``docs/【编辑器】画板图标素材生成入口设计-2026-06-15.md``docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`
## 2026-07-18 阿里云 URL 抠图链路按外部调用阶段审计
- 背景:阿里云 URL 抠图先从源 OSS GET,再解码、校验尺寸、归一化并上传临时 OSS;此前解码和尺寸失败仍使用普通 `InvalidRequest`,被错误标记为 `externalCallAttempted=false`,无法满足阿里云失败统一审计约定。
- 决策:真正开始外部调用前的本地预检不写 `external_api_call_failure`;源 OSS GET 成功后发生的解码、尺寸、归一化、临时上传、阿里云请求和结果处理失败均进入审计。`platform-matting` 使用结构化 `LocalProcessing` 分类和 `failureStage`,由 api-server 映射为 `source_decode``source_validate``source_normalize``temp_upload``result_decode` 等阶段,不再把这些错误统称为“发请求前本地预检”。
- 影响范围:`server-rs/crates/platform-matting/src/lib.rs``server-rs/crates/api-server/src/aliyun_matting.rs``server-rs/crates/api-server/src/external_api_audit.rs``server-rs/crates/api-server/src/editor_project.rs`、后端架构文档。
- 验证方式:运行 `cargo test -p platform-matting --manifest-path server-rs/Cargo.toml`、阿里云抠图与外部审计定向测试、`cargo check -p api-server --manifest-path server-rs/Cargo.toml``npm run check:encoding``git diff --check`
## 2026-07-18 手动去背景稳定媒体引用校验收口
- 背景:当前分支与 `master` 分别增加手动去背景专用 Data URL 校验和编辑器通用稳定媒体引用校验,直接叠加会让 API 与 worker 重复执行语义相同的 helper,并造成 `data:` / `blob:` 覆盖范围和错误文案漂移。
- 决策:删除手动去背景专用校验。HTTP API 在入队前统一调用 `ensure_editor_reference_image_source_is_stable`,立即拒绝 `data:` / `blob:`;worker 不重复调用该入口校验,只通过 `resolve_editor_reference_object_key_for_owner` 完成稳定引用解析和 owner 归属校验。底层 `resolve_editor_reference_object_key` 在尝试 object key、项目资源 ID 或素材 ID 解析前统一拒绝内联媒体,作为历史任务和内部直接调用的最终边界。
- 影响范围:`server-rs/crates/api-server/src/editor_project.rs`、图片画布手动去背景测试和图片画布技术方案;不改变队列 DTO、BgFilter `image_url` 协议或 SpacetimeDB 的编辑器任务 payload 门禁。
- 验证方式:覆盖 API 入队前拒绝 `data:` / `blob:`、解析器拒绝内联媒体、worker 只调用稳定引用解析与归属校验;运行 api-server 编辑器定向测试、`cargo check -p api-server --manifest-path server-rs/Cargo.toml``npm run check:encoding``git diff --check`
- 关联文档:`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md``docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`
## 2026-07-17 画布 Agent 普通消息不提供客户端停止
- 背景:普通消息进入 LLM 前,后端已经把用户消息写入 OSS;前端中断 fetch 只能停止本地等待,不能保证后端停止规划,且会保留无法与后端消息对齐的 optimistic message。
- 决策:移除画布 Agent 普通消息的“停止”按钮和 `stopCurrentTurn`,发送期间保持按钮禁用并等待后端响应。待确认工具调用的“取消”仍保留,不受本决策影响。
- 影响范围:画布 Agent 对话 hook、发送区交互、前端测试和专题文档。
- 验证方式:运行画布 Agent hook / 面板定向测试、`npm run typecheck``npm run check:encoding``git diff --check`
- 关联文档:`docs/【编辑器】画布Agent对话面板-2026-07-03.md`
## 2026-07-10 画布 Agent 工具确认分离执行参数与展示投影
- 背景:画布 Agent 已在实际生成前进入 `pending_confirmation`,但 `EditorAgentToolCall.args` 只保存工具私有 JSON,其中图片参数是保护真实 data key 的 SHA-256 opaque ID。前端直接解析 raw args 只能显示内部哈希或图片数量,无法向用户准确展示即将使用的目标图、参考图和完整参数;若直接把图片 URL 或对象塞回 raw args,又会破坏确认执行反序列化和 LLM 不可见真实 data key 的安全边界。
- 决策:`EditorAgentToolCall.args` 继续作为确认执行唯一真相,不允许前端改写或回传替代参数;新增必填 `displayArgs` 只读展示投影,内含 `stringArgs``imageArgs``extras.priceMudPoints``stringArgs` 承载提示词与规格等用户可见字段,`imageArgs.refs` 承载 `imageId` 及后端解析出的 `objectKey``imageSrc`、可选缩略图、标签和尺寸;`extras.priceMudPoints` 由 api-server 在创建待确认消息时使用后端运行时模型定价快照计算,前端只显示“预计消耗 N泥点”,不自行计算或回传价格。api-server 必须按已注册 tool 白名单,从已校验 args 与 OSS 会话文档的附件 / 历史生成结果构建该投影;前端只渲染投影,以 `ResolvedAssetImage` 换签显示图片,不解析 tool 私有 schema、不展示 SHA-256 ID。展示价格不参与确认执行或实际扣费,确认后仍由既有生成 BFF 按后端运行时定价预扣费。删除只重复 `args` 且没有稳定语义的 `EditorAgentToolCall.summary`。模块尚未上线,不保留缺少 `displayArgs` 时读取 raw `args` 的旧消息降级路径。
- 影响范围:`shared-contracts` / `packages/shared``editorAgent` DTO、`api-server/src/editor_agent/api.rs` 的待确认消息构建、画布 Agent 待确认卡、OSS 会话消息文档与相关测试。
- 验证方式:`cargo test -p shared-contracts --manifest-path server-rs/Cargo.toml editor_agent``cargo test -p api-server --manifest-path server-rs/Cargo.toml editor_agent``npm run test -- src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.test.tsx src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.test.tsx src/services/image-editor/editorAgentClient.test.ts``npm run typecheck``npm run check:encoding``git diff --check`
- 关联文档:`docs/【编辑器】画布Agent对话面板-2026-07-03.md``docs/adr/【ADR】画布Agent会话消息存OSS-2026-07-03.md`
## 2026-07-18 图片多产物任务的原图进入正常完成画布
- 背景:角色形象、图标 spritesheet 和 UI 素材提取会同时持久化带纯色背景的 provider 原图与透明后处理结果;这些原图都需要在画布中可直接查看和复用。任务与扣费实际仍只有一次。
- 决策:provider 原图继续写入 OSS、`asset_object`、项目资源和账号素材库。三类任务透明处理正常成功时,透明结果作为主图并保持生成器 `generatedLayerId` 锚点,provider 原图作为第二个图层放在透明主结果右侧;图标和 UI 的业务拆分素材从原图右侧继续排列。只有透明背景处理最终失败时,才把 provider 原图作为唯一主图完成占位并返回 warning。
- 影响范围:角色形象、图标 spritesheet、UI 素材提取的画布完成快照,以及多产物持久化与画布展示边界。
- 验证方式:后端定向测试断言三类任务正常成功都落透明主图与右侧 provider 原图、`generatedLayerId` 仍指向透明主图,图标 / UI 拆分素材继续排列在原图右侧,同时保留 source-only 失败降级测试;并运行 `cargo check -p api-server --manifest-path server-rs/Cargo.toml``npm run check:encoding``git diff --check`
- 关联文档:`docs/technical/【后端架构】外部生成Worker化方案-2026-06-03.md``docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`
## 2026-07-17 生成后抠图原图以 OSS 作为内存生命周期边界
- 背景:角色形象、图标图集、UI 素材图集和角色动作抽取帧的带背景原图虽然已先落私有 OSS,但 api-server 仍可能把原图字节保留到 BgFilter / 阿里云 / 本地 fallback 结束,造成并发任务下的内存峰值叠加。
- 决策:目标链路的带背景原图上传 OSS 时消费 `DownloadedImage` 或动作帧字节所有权,不为上传克隆整张字节缓冲;上传完成后不再跨 BgFilter 调用常驻。手动去背景直接复用已有 OSS object key,不下载原图。BgFilter 只读取 600 秒签名 URL;进入阿里云 fallback 时由 `platform-matting` 新 URL 接口下载原图、上传 `AuthorizeFileUpload` 临时对象,并在开始阿里云推理前结束下载缓冲作用域;阿里云继续失败时,api-server 再从私有 OSS 独立下载原图供本地键色,产出后释放本次原图下载缓冲。
- 边界:不改变接口 DTO、资源记录、画布原图展示、图集切分行为和降级顺序;“释放”指 Rust 所有权和 `Vec<u8>` 析构,RSS 不保证同步下降。
- 验证方式:`platform-matting` 测试覆盖 URL 下载缓冲在临时上传后结束、降尺寸 Alpha 回贴;`api-server` 结构测试覆盖带背景原图 owned 上传、URL 阿里云 fallback、本地重新下载与原图释放;随后运行两个 crate 的测试与编译检查。
## 2026-07-17 图片改造保持源图与所选清晰度
- 背景:图片画布从已生成的 2K 角色图重新打开生成器时,面板恢复逻辑会优先采用新建面板的 1K 默认值;即使用户重新选择 2K,角色透明化链路也可能接受 BgFilter / 阿里云返回的 1K 后处理图,并因 `nanobanana2` 使用标量清晰度档位而跳过几何尺寸恢复,最终把 2K provider 原图降为 1K 透明图。
- 决策:从既有图片重新打开普通图片、角色、UI 或宣发生成器时,在没有仍存活的生成对话框快照时按当前图层真实 `originalWidth / originalHeight` 恢复比例与清晰度,并按目标模型支持范围归一;恢复或切换比例 / 清晰度后,普通图片、角色、图标图集和 UI 设计图的待生成及生成中占位框必须同步使用目标像素尺寸,不能保留新建 draft 的默认 1K 框。UI 素材提取的占位按框选数量对应的 1K / 2K 计划生成,旧图片修改入口按源图真实尺寸占位。角色形象去背景完成后必须保持去背景前 provider 原图的像素尺寸;若去背景供应商返回较小结果,只把 alpha 蒙版重采样回原图并保留原始 RGB,不放大低分辨率透明成品。
- 影响范围:图片画布生成对话框恢复、生成中占位尺寸、UI 素材提取与旧图片修改的 `canvasCompletion`、角色形象 BgFilter / 阿里云 / 本地去背后处理、项目资源与账号素材尺寸元数据。
- 验证方式:覆盖“持久化 2K 角色图重开仍为 2K”“普通图片 / 角色 / 图标 / UI 改造的 2K 占位与目标一致”“普通生图、规范图和角色图生成中占位不回退 1K”“UI 提取和旧修改入口的完成占位使用业务目标尺寸”以及“较小去背结果只提供 alpha、最终 RGB 仍来自 2K provider 原图”的前后端定向测试,并运行前端类型检查、`cargo check -p api-server --manifest-path server-rs/Cargo.toml``npm run check:encoding``git diff --check`
- 关联文档:`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md``docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md`
## 2026-07-15 BgFilter 输入改用私有 OSS 短期签名 URL
- 背景:角色形象、图标图集、UI 素材图集、角色动作抽取帧和手动去背景在调用 BgFilter 前都已有私有 OSS object key;继续由 api-server 下载或保留图片并作为 multipart `file` 再上传,会重复传输图片字节并占用 API 进程网络与内存。
- 决策:上述抠图链路统一复用 object key,签发 600 秒 OSS GET URL,并通过 BgFilter multipart 的 `image_url` 字段提交;请求中不再携带 `file`。签名 URL 只交给 BgFilter,不写日志或持久化。2026-07-17 起,生成原图和动作帧上传后不再保留图片字节;进入“阿里云通用抠图 → 本地键色”兜底链时按阶段从私有 OSS 重新下载。
- 影响范围:`server-rs/crates/api-server/src/editor_project.rs``server-rs/crates/api-server/src/character_animation_assets.rs`、相关测试与文档;不改变 BgFilter endpoint、鉴权、`screen_color``seg_model`、输出校验、熔断规则、阿里云上传协议或降级顺序。
- 验证方式:定向测试必须断言 BgFilter 请求函数包含 `image_url` 与 600 秒 OSS 换签,不包含 multipart `file` 或源图字节读取;随后运行 `cargo check -p api-server --manifest-path server-rs/Cargo.toml`
## 2026-07-15 阿里云通用抠图上传切换到 AuthorizeFileUpload 正式链路
- 背景:`platform-matting` 原先通过 `viapiutils/GetOssStsToken` 获取临时 AK/SK,再向固定 `viapi-customer-temp` 共享桶执行 OSS V1 PUT。阿里云官方文档将该显式生成 URL 的共享临时桶通道标记为不保证 SLA、仅便于调试且不推荐生产使用;动作视频逐帧抠图会把这条风险放大到每任务 32 至 48 次。
- 决策:非上海地域图片字节统一按新版官方 SDK `AdvanceRequest` 的实际协议处理:调用 `openplatform.aliyuncs.com``AuthorizeFileUpload` 获取单对象 `Bucket``Endpoint``AccessKeyId``EncodedPolicy``Signature``ObjectKey`;再以 multipart Policy POST 上传到动态返回的上海临时 OSS,表单字段为 `OSSAccessKeyId`= AccessKeyId)、`policy`= EncodedPolicy)、`Signature``key`= ObjectKey)、`success_action_status=201``file`,最后把临时对象 URL 交给 `SegmentCommonImage`。移除 `GetOssStsToken`、固定 `viapi-customer-temp`、AccessKeySecret/SecurityToken 临时凭证组合和 OSS V1 SHA-1 签名;Policy POST 仍需要授权响应中的 `AccessKeyId`,不再下发可独立签名的完整临时密钥。图片归一化、结果下载、原尺寸 Alpha 回贴和上层降级顺序保持不变。该链路仍会让图片字节经过执行任务的 api-server / worker 并上传临时 OSS,不把它描述成阿里云服务端直接抓取任意公网 URL。
- 影响范围:`server-rs/crates/platform-matting`、阿里云抠图冒烟示例、后端架构与开发运维文档;不改变 api-server DTO、动作拆帧、BgFilter 或业务降级契约。
- 验证方式:`cargo test -p platform-matting --manifest-path server-rs/Cargo.toml``cargo check -p api-server --manifest-path server-rs/Cargo.toml`,并用真实图片运行 `segment_smoke`,确认授权上传 host 来自动态上海 OSS 且 `SegmentCommonImage` 成功返回。
- 关联文档:`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md``docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`、阿里云“通用图像分割”与“文件 URL 处理”官方文档。
## 2026-07-15 角色动作 BgFilter 请求超时按帧数扩展
- 背景:角色动作全部序列帧会并发进入 BgFilter,而服务端可能在自身进程内排队;固定 `180000ms` 会把排队时间和单帧推理共用同一预算,靠后的请求可能在服务仍正常处理时被 api-server 提前取消。
@@ -34,6 +111,8 @@
## 2026-07-14 手动去背景迁移到 BgFilter complex 模式
> 后续更正:本条关于 multipart 图片文件输入的描述已由 2026-07-15「BgFilter 输入改用私有 OSS 短期签名 URL」和 2026-07-17「生成后抠图原图以 OSS 作为内存生命周期边界」取代;当前手动路径不下载原图,只提交 `image_url`。下文保留作历史记录。
- 背景:图片画布手动“去除背景”此前单独代理 BiRefNet 服务;BgFilter 已增加 `background_mode=complex`,可直接处理非纯色背景,继续保留独立服务会形成重复的上游、配置和错误处理链路。
- 决策:`POST /api/editor/images/background-removals` 保持前端与 BFF 契约不变,worker 改用现有 BgFilter 地址、token、超时和共享 HTTP client。multipart 提交图片文件、`background_mode=complex``seg_model=birefnet``cross_check=off`,不提交 `screen_color`。标准纯色背景的角色形象、图标 spritesheet、UI 素材提取和角色动作逐帧抠图继续使用 `background_mode=flat`。删除独立 BiRefNet base URL / timeout 配置;旧 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN` 仅作为 `GENARRATIVE_EDITOR_BGFILTER_TOKEN` 的兼容回退别名。
- 影响范围:图片画布手动去背景 worker、BgFilter HTTP 协议、api-server 配置、资源元数据、前端 provider 展示和相关文档。
@@ -96,7 +175,7 @@
- 背景:角色形象、图标 spritesheet 和 UI 素材提取会先得到带纯色背景的原图,再执行抠图或拆分;图片修改会先得到模型对齐尺寸的原始输出,角色动作会先得到绿幕预览视频,再抽帧和抠图。此前部分原始产物只登记到 OSS,或者要等后处理成功后才进入项目资源,用户无法在失败后找回已经生成成功的内容。
- 决策:凡一次资产生成任务产生多个具有独立复用价值的产物,后端必须把上游已返回的中间产物写入 OSS、`asset_object`、项目资源和账号素材库,再执行抠图、抽帧或拆分;未指定素材文件夹时进入默认“项目”文件夹。角色形象、图标 spritesheet 和 UI 素材提取在透明背景处理正常成功时同时保留纯色背景原图与透明后处理结果;透明背景处理最终失败时只保留已经持久化的 provider 原图,并按下一条降级规则收口。普通图片和图片修改的纯尺寸变换不属于独立产物:provider 回图保留在内存,变换成功只上传变换结果,变换失败只上传 provider 原图,整个流程只写一次 OSS 并只创建一个素材,不能制造重复“原始输出”。`nanobanana2` 使用标量清晰度档位和独立比例,保留 provider 输出尺寸,不按 `WIDTHxHEIGHT` 解析。角色动作把绿幕预览视频作为一个可复用素材保存,逐帧源图继续留在同一任务 OSS 路径,不把 32 至 48 帧逐张灌入素材库。去背景、音频等没有独立上游中间产物的任务不制造重复副本。
- 画布、成本与降级:有项目上下文的图片多产物继续由同一次 `canvasCompletion` 写入权威画布快照,正常成功时生成器 `generatedLayerId` 锚定主后处理结果角色形象、图标 spritesheet UI 素材提取已经保存 provider 原图、但透明背景处理最终失败时,任务以 `completed + warning` 收口,原图作为唯一主图完成画布占位;不写入不存在的透明处理图,图标和 UI 也不继续拆分。透明处理成功后的图标和 UI 图集自动拆分仍是 best-effort;识别或切片持久化失败继续完成整张透明图集,并在 inline、队列轮询和刷新后任务列表中提示非阻断 warning,不得借用失败错误字段。provider 原图或角色动作预览视频承载该任务的模型生成成本,抠图、逐帧处理、透明图集和切片等后处理派生产物的 `generation_cost_mud_points = 0`,避免把生图成本误显示成抠图成本;所有中间产物沿用所属任务的真实 `asset_kind`,角色原图仍为 `character`、图标和 UI 图集原图仍为 `icon-spritesheet`、角色动作预览仍为 `character-animation`,不得再写新的“原图类型”。后台素材查询按任务分页,最终产物作为父行并显示任务总成本,每个中间产物作为可展开的独立子行显示阶段生成器和阶段成本。扣费确认边界保持为 provider 成功,OSS、尺寸恢复和画布回填不延长退款保护。
- 画布、成本与降级:有项目上下文的图片多产物继续由同一次 `canvasCompletion` 写入权威画布快照,正常成功时生成器 `generatedLayerId` 锚定主后处理结果角色形象、图标 spritesheet UI 素材提取都把 provider 原图作为第二个图层放在透明主结果右侧;图标和 UI 的业务拆分素材从原图右侧继续排列。三类任务已经保存 provider 原图、但透明背景处理最终失败时,任务以 `completed + warning` 收口,原图作为唯一主图完成画布占位;不写入不存在的透明处理图,图标和 UI 也不继续拆分。透明处理成功后的图标和 UI 图集自动拆分仍是 best-effort;识别或切片持久化失败继续完成整张透明图集,并在 inline、队列轮询和刷新后任务列表中提示非阻断 warning,不得借用失败错误字段。provider 原图或角色动作预览视频承载该任务的模型生成成本,抠图、逐帧处理、透明图集和切片等后处理派生产物的 `generation_cost_mud_points = 0`,避免把生图成本误显示成抠图成本;所有中间产物沿用所属任务的真实 `asset_kind`,角色原图仍为 `character`、图标和 UI 图集原图仍为 `icon-spritesheet`、角色动作预览仍为 `character-animation`,不得再写新的“原图类型”。后台素材查询按任务分页,最终产物作为父行并显示任务总成本,每个中间产物作为可展开的独立子行显示阶段生成器和阶段成本。扣费确认边界保持为 provider 成功,OSS、尺寸恢复和画布回填不延长退款保护。
- 2026-07-16 告警契约补充:inline / external v1 继续返回结构化原始诊断;queue 有意把通用 `warning``sliceWarning` 归一为展示就绪字符串,通用 `warning.reason` 原样保留,`sliceWarning.reason` 由 worker 添加“图集已生成,但自动拆分未完成:”前缀,摘要与 BFF 原样投影,Web 直接展示。历史值保留写入时快照,不按新格式回填或推断;该内部字符串契约通过 API/worker 与 Web 同一维护窗口、同版本发布收口,不增加混部兼容层。
- 影响范围:`server-rs/crates/api-server/src/editor_project.rs``character_animation_assets.rs`、外部生成任务摘要、图片画布完成快照、账号素材库和前端生成提示。
- 验证方式:覆盖中间产物登记先于后处理、默认素材文件夹、图集拆分降级、inline / queue warning 和主结果锚定的定向测试,并运行 `cargo check -p api-server --manifest-path server-rs/Cargo.toml``npm run check:spacetime-schema`、前端定向测试、`npm run check:encoding``git diff --check`
@@ -225,6 +304,8 @@
## 2026-07-05 BgFilter 失败时本地纯色去背兜底
> 后续更正:本条关于手动去背景仍使用独立 BiRefNet BFF 的描述已由 2026-07-14「手动去背景迁移到 BgFilter complex 模式」、2026-07-15 OSS 签名 URL 决策和 2026-07-17 内存生命周期决策取代。下文保留作历史记录。
- 背景:曾用一次性 BgFilter live probe 稳定复现 BgFilter 对 2K 输入返回 `HTTP 500 {"detail":"inference failed"}`,浏览器生成链路会因此收到“BgFilter 服务返回非成功状态”。
- 决策:角色形象生成、图标 spritesheet 生成和 UI 设计图素材提取仍优先调用独立 BgFilter;若 BgFilter 请求失败、返回非成功状态、返回空图片或非法图片,api-server 记录 warning 后使用本地 `editor_green_screen` 按解析后的纯色背景执行确定性去背兜底,不中断生成。手动任意图片去背景仍只走独立 BiRefNet BFF,不使用该兜底。
- 更正(截至 2026-07-10 实现):BgFilter 失败/熔断后不再直接本地兜底,而是先走阿里云通用抠图,仅阿里云也失败才本地 `editor_green_screen` 键色兜底;口径统一见 2026-07-09「角色动作视频…阿里云抠帧」决策,并已扩展到本条的角色形象/图标/UI 三条静态生图链路。
@@ -234,6 +315,8 @@
## 2026-07-03 图片画布生成纯色背景资产接入 BgFilter
> 后续更正:本条关于 multipart `file`、手动去背景独立 BiRefNet 配置以及 BgFilter 失败后直接本地兜底的描述,已分别由 2026-07-14、2026-07-15 OSS 签名 URL 决策和 2026-07-17 内存生命周期决策取代。下文保留作历史记录。
- 背景:独立 BgFilter 服务已部署在 image host,并提供 `POST /bgfilter/remove-background`,支持显式 `screen_color``seg_model`。手动去背景已有独立 BiRefNet BFF,不能把两个服务的配置或语义混在一起。
- 决策:角色形象生成、图标 spritesheet 生成和 UI 设计图素材提取在保存带纯色背景源图后,统一调用 BgFilter 生成透明 PNG;请求 multipart 字段为 `file``screen_color=<screenColor>` 和内部固定的 `seg_model=birefnet``segModel` 虽是后端可识别的内部兼容字段(另保留 `anime-seg`),但不向用户或外部 OpenAPI 暴露:当前 BgFilter 的内存与并发容量不适合由调用方自由切换模型。BgFilter 使用独立配置 `GENARRATIVE_EDITOR_BGFILTER_BASE_URL``GENARRATIVE_EDITOR_BGFILTER_TOKEN``GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS`,默认 base URL 为 `http://58.87.105.82/bgfilter`,默认请求超时 `180000ms`token 未配置时复用 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN`。手动 `POST /api/editor/images/background-removals` 继续使用独立 BiRefNet 配置 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_BASE_URL`,不受 BgFilter 影响。BgFilter 参数里的 `seg_model=birefnet` 只表示 BgFilter 内部分割后端,不等于手动去背景的独立 BiRefNet 服务。若 BgFilter 失败,api-server 对这些标准纯色背景生成图使用本地 `editor_green_screen` 兜底;连续失败达到 `GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD`(默认 `3`)后,`GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS`(默认 `300`)内直接本地兜底。角色动作背景色和抠帧口径已由 2026-07-09 决策取代:角色动作同样使用多色自动决策,抽帧后优先阿里云通用抠图,失败再按选定背景色本地兜底。更正(截至 2026-07-10 实现):BgFilter 失败与熔断期本条描述的「直接本地兜底」已过时——角色形象/图标/UI 三条静态生图链路同样先走阿里云通用抠图,仅阿里云也失败才本地 `editor_green_screen` 兜底。
- 影响范围:`server-rs/crates/api-server/src/config.rs``server-rs/crates/api-server/src/editor_project.rs`、图片画布 MVP 文档和角色形象生成设计文档。
@@ -275,10 +358,10 @@
## 2026-07-03 画布Agent会话元数据入 SpacetimeDB、消息正文存 OSS
- 背景:图片画布工程需要对话式编辑历史,但消息正文随流式输出增长,不适合放入表行或画布布局快照;同时画布 Agent 只属于编辑器画布域,不能复用拼图 `creative-agent` 内存会话。
- 决策:新增 `module-editor-agent` 承载纯领域规则,`editor_agent_conversation` 只保存会话元数据,完整消息以 `editor-agent/{conversationId}.json` 会话粒度存 OSS`api-server` 负责编排 LLM、SSE、OSS 读写和既有生成工具调用。
- 背景:图片画布工程需要对话式编辑历史,但消息正文随对话和工具结果增长,不适合放入表行或画布布局快照;同时画布 Agent 只属于编辑器画布域,不能复用拼图 `creative-agent` 内存会话。
- 决策:`module-editor-agent` 承载可供 SpacetimeDB WASM 使用的纯领域规则;Agent runner、工具实现和资产 DTO 迁入原生 `platform-editor-agent`,仅由 `api-server` 依赖。`editor_agent_conversation` 只保存会话元数据,完整消息以 `editor-agent/{conversationId}.json` 会话粒度存 OSS`api-server` 负责编排 LLM、普通 JSON 消息、OSS 读写和既有生成工具调用。用户消息以独立 `clientMessageId` 在会话锁内幂等,数字 `message.id` 只作后端定位;旧 OSS 消息允许缺失幂等键,早期用户消息字符串 `id` 在读取时迁入 `clientMessageId`。画布 Agent 只与任务侧栏互斥,不与左侧素材 / 图层栏互斥。
- 影响范围:图片画布右侧 Agent 面板、`shared-contracts` / `packages/shared``editorAgent` 契约、`spacetime-module` / `spacetime-client``platform-oss` 内部读签名边界、画布生成落板规则。
- 验证方式:`npm run spacetime:generate``npm run check:spacetime-schema``cargo test -p module-editor-agent --manifest-path server-rs/Cargo.toml``cargo test -p api-server --manifest-path server-rs/Cargo.toml editor_agent`、前端 Agent 面板与 SSE client 定向测试、`npm run check:encoding``git diff --check`
- 验证方式:`npm run spacetime:generate``npm run check:spacetime-schema``cargo test -p module-editor-agent --manifest-path server-rs/Cargo.toml``cargo test -p api-server --manifest-path server-rs/Cargo.toml editor_agent`、前端 Agent 面板与 JSON client 定向测试、`npm run check:encoding``git diff --check`
- 关联文档:`docs/【编辑器】画布Agent对话面板-2026-07-03.md``docs/adr/【ADR】画布Agent会话消息存OSS-2026-07-03.md``docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`
## 2026-07-01 认证工作集只经 typed projection 同步正式表
@@ -291,6 +374,8 @@
## 2026-06-29 图片画布手动抠图走远端 BiRefNet BFF
> 后续更正:本条独立 BiRefNet 服务、专用 base URL 以及 api-server 下载并解析原图的实现,已由 2026-07-14「手动去背景迁移到 BgFilter complex 模式」、2026-07-15 OSS 签名 URL 决策和 2026-07-17 内存生命周期决策取代。下文保留作历史记录。
- 背景:用户手动“去除背景”面对任意图片,前端 `chromaKey` 和标准绿幕后处理不适合复杂人物、自然背景或非纯色背景;远端 image host 已部署 BiRefNet 服务,需要让手动抠图走高质量模型,同时避免把服务令牌暴露到浏览器。
- 决策:画布手动“去除背景”默认调用登录态同源 BFF `POST /api/editor/images/background-removals`。api-server 解析当前图片后代理到 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_BASE_URL/remove-background`,默认指向 `http://58.87.105.82/remove-background`,可选 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN` 只在服务端注入。api-server 对上游结果做字节和尺寸上限保护,并先落 OSS / asset object 再返回给前端。编辑器自己生成的标准绿幕资产不属于该决策,见 2026-06-30 绿幕契约收口。
- 影响范围:api-server 编辑器图片接口、图片画布手动去背景、画布右上角任务侧栏、图片画布 MVP 技术文档。
@@ -421,13 +506,15 @@
## 2026-06-18 图片画布 UI 设计图提取素材保留图集
- 背景:UI 设计图需要从成图中继续抽取可复用独立素材;原图标素材生成只把拆分后的图标放入画布,spritesheet 原图没有保留,后续追溯和二次切图不方便。
- 决策:`assetKind="ui-design"` 图层浮动工具栏新增 `提取素材`,点击后先进入红框素材框选编辑态,默认矩形框选,并支持椭圆框选和画笔自由框选。至少存在一个框选区域后才能提交;前端把红色轮廓绘入原 UI 设计图并将合成图作为 `/api/editor/ui-designs/assets/extractions` 的参考图。后端固定 `gpt-image-2` 和纯色背景素材提取提示词,返回结构复用图标 spritesheet 响应。透明背景处理正常成功时,UI 提取把透明 spritesheet 图集作为 `assetKind="icon-spritesheet"` 图层放到画布,再放拆分成功的 `assetKind="icon"` 素材。2026-07-03 起,UI 提取的纯色背景由 `screenColor` 选择并经 BgFilter 透明化。2026-07-13 起,图标素材生成在透明背景处理正常成功时把带背景原图和透明 spritesheet 同时写入项目资源、账号素材库并回填画布,未指定文件夹时落默认“项目”文件夹,再 best-effort 按 alpha 连通域拆分独立图标;拆分失败不改变生成成功状态,响应以空 `iconImageSrcs` 和结构化 `sliceWarning` 返回原因,用户可从图集工具栏手动重试。2026-07-16 起,透明背景处理最终失败时只把已经持久化的 provider 原图作为唯一主图放入画布,以 `completed + warning` 收口,不创建透明图集,也不继续拆分。手动拆分不计费,限制单边 `4096`、总像素 `2048×2048`、最多 `64` 个切片,所有切片用 `sourceResourceId` 指向透明图集。`icon-spritesheet` 图集继续显示并允许快速编辑,只有拆分后的 `assetKind="icon"` 单图标隐藏并拒绝快速编辑;工具栏、右键菜单、打开流程和提交兜底必须共用同一判定。本条新决策取代“图标素材生成只保留图集”的旧口径。
- 决策:`assetKind="ui-design"` 图层浮动工具栏新增 `提取素材`,点击后先进入红框素材框选编辑态,默认矩形框选,并支持椭圆框选和画笔自由框选。至少存在一个框选区域后才能提交;前端把红色轮廓绘入原 UI 设计图并将合成图作为 `/api/editor/ui-designs/assets/extractions` 的参考图。后端固定 `gpt-image-2` 和纯色背景素材提取提示词,返回结构复用图标 spritesheet 响应。透明背景处理正常成功时,UI 提取把透明 spritesheet 图集作为 `assetKind="icon-spritesheet"` 图层放到画布,再放 provider 原图和拆分成功的 `assetKind="icon"` 素材。2026-07-03 起,UI 提取的纯色背景由 `screenColor` 选择并经 BgFilter 透明化。2026-07-13 起,图标素材生成在透明背景处理正常成功时把带背景原图和透明 spritesheet 同时写入项目资源、账号素材库画布,未指定文件夹时落默认“项目”文件夹,再 best-effort 按 alpha 连通域拆分独立图标;拆分素材从 provider 原图右侧继续排列。拆分失败不改变生成成功状态,响应以空 `iconImageSrcs` 和结构化 `sliceWarning` 返回原因,用户可从图集工具栏手动重试。2026-07-16 起,透明背景处理最终失败时只把已经持久化的 provider 原图作为唯一主图放入画布,以 `completed + warning` 收口,不创建透明图集,也不继续拆分。手动拆分不计费,限制单边 `4096`、总像素 `2048×2048`、最多 `64` 个切片,所有切片用 `sourceResourceId` 指向透明图集。`icon-spritesheet` 图集继续显示并允许快速编辑,只有拆分后的 `assetKind="icon"` 单图标隐藏并拒绝快速编辑;工具栏、右键菜单、打开流程和提交兜底必须共用同一判定。本条新决策取代“图标素材生成只保留图集”的旧口径。
- 影响范围:图片画布浮动工具栏、编辑器图片生成 BFF、`platform-image` 图集连通域拆分、画布图层类型和编辑器文档。
- 验证方式:运行图片画布工具栏 / 图集落层 / 生成提交相关前端测试,`cargo test -p platform-image generated_asset_sheets --manifest-path server-rs/Cargo.toml`,以及 `cargo test -p api-server editor_ui_design_asset_extraction_prompt_is_fixed --manifest-path server-rs/Cargo.toml`
- 关联文档:`docs/【编辑器】画板UI设计图生成入口设计-2026-06-17.md``docs/【编辑器】画板图标素材生成入口设计-2026-06-15.md`
## 2026-06-30 图片画布标准绿幕契约收口
> 后续更正:本条关于生成资产只使用本地透明化、手动路径继续使用独立 BiRefNet 的描述,已由 2026-07-14、2026-07-15 OSS 签名 URL 决策和 2026-07-17 内存生命周期决策取代;当前 flat 链路为 `BgFilter → 阿里云 → 本地键色`。下文保留作历史记录。
- 背景:角色形象、图标 spritesheet、UI 提取 spritesheet 和角色动作帧都要求模型生成标准绿幕,但提示词片段和后处理入口分散在多个模块中,容易把标准绿幕资产误接到远端 BiRefNet。
- 决策:编辑器标准绿幕提示词和本地确定性绿幕透明化统一收口到 `server-rs/crates/api-server/src/editor_green_screen.rs`。手动 `POST /api/editor/images/background-removals` 继续面向用户任意图片并走 BiRefNet;编辑器自己生成的标准绿幕资产统一复用 `platform-image::generated_asset_sheets` 的本地透明化能力,不再依赖 BiRefNet。角色图、图标 spritesheet、UI 提取 spritesheet 和角色动作抽帧源图必须在绿幕透明化前先保存一份带绿幕原图到 OSS,便于追溯和重处理。
- 影响范围:`editor_project.rs` 的角色图 / 图标图集 / UI 提取图集、`character_animation_assets.rs` 的编辑器角色动作帧、编辑器绿幕相关文档。
@@ -2388,7 +2475,7 @@
## 2026-05-07 server-rs Cargo 依赖集中到 workspace
- 背景:`server-rs` 多 crate 已稳定成 DDD workspace,成员 `Cargo.toml` 中重复散写第三方版本和本地 path 依赖,升级 SpacetimeDB SDK、`serde``reqwest``tokio` 等依赖时容易漂移。
- 决策:`server-rs/Cargo.toml``[workspace.dependencies]` 统一维护第三方依赖版本和 workspace 内部 crate path;成员 crate 默认使用 `{ workspace = true }`,只保留自身 feature、optional 或 target-specific 差异;OSS 与阿里云 OpenAPI 签名统一走 `sha2::Sha256` 对应的 V4/V3 口径。例外:`platform-matting` VIAPI 官方临时 OSS 桶上传抠图输入时,因该临时桶接口要求 OSS V1 头签名,允许在该 crate 内部受限使用 `sha1` / `Hmac<sha1::Sha1>` 生成 V1 签名;该例外不得扩展到自有 OSS、通用阿里云 OpenAPI 或其它新链路
- 决策:`server-rs/Cargo.toml``[workspace.dependencies]` 统一维护第三方依赖版本和 workspace 内部 crate path;成员 crate 默认使用 `{ workspace = true }`,只保留自身 feature、optional 或 target-specific 差异;OSS 与阿里云 OpenAPI 签名统一走 `sha2::Sha256` 对应的 V4/V3 口径。2026-07-15 起,`platform-matting` 已移除 VIAPI 共享临时桶的 OSS V1 SHA-1 例外,改用 `AuthorizeFileUpload` 返回的 Policy POST 授权;后续不得恢复 crate 内 SHA-1 签名
- 影响范围:`server-rs/Cargo.toml`、所有 `server-rs/crates/*/Cargo.toml``platform-oss``platform-auth`、后续新增 Rust crate 或新增 Rust 依赖的开发流程。
- 验证方式:修改 Cargo 配置后先执行 `cargo metadata --manifest-path server-rs\Cargo.toml --format-version 1 --no-deps`,再按影响范围执行 `cargo check`、DDD 边界检查和编码检查。
- 关联文档:`docs/technical/RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md`
@@ -3233,6 +3320,13 @@
- 影响范围:`server-rs/crates/api-server/src/editor_generation_queue.rs``server-rs/crates/api-server/src/external_generation_worker.rs``server-rs/crates/api-server/src/editor_project.rs``server-rs/crates/api-server/src/character_animation_assets.rs``server-rs/crates/api-server/src/vector_engine_audio_generation/generation.rs``src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts``src/services/image-editor/editorProjectClient.ts`
- 验证方式:`cargo test -p api-server external_generation_worker --manifest-path server-rs/Cargo.toml``cargo test -p api-server editor_canvas_generation --manifest-path server-rs/Cargo.toml``cargo test -p shared-contracts --manifest-path server-rs/Cargo.toml``npm run test -- src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx src/services/image-editor/editorProjectClient.test.ts``npm run typecheck``npm run check:encoding``git diff --check`
## 2026-07-13 画布 Agent 工具执行状态复用外部生成任务
- 决策:画布 Agent 的 OSS 工具消息使用 `status=not_completed|completed|failed|cancelled` 和可选 `externalJobId`;不使用 `cancelledAt`,不新增关联表。`external_generation_job` 是排队、执行、lease 与计费结算的唯一真相;OSS status 只表达该消息回填结果,不复制 queued / running。确认接口按 `conversationId + messageId + toolName` 稳定去重并复用既有编辑器 worker job kind。
- 懒回填:`GET /conversation` 会在持有 conversation lock 后扫描 `status=not_completed` 且有 `externalJobId` 的工具消息,按 job id 定向读取主任务;完成时复用原工具 formatter 更新 system text、写入轻量媒体引用并标记 `completed`,任务本身失败时写入 `error` 并标记 `failed`。任务结果读取或 completed payload 解析 / formatter 首次失败后,在同一次 GET 内最多重试 3 次,每次等待 100ms 并重新读取主任务;读取失败或 completed 任务暂缺 `result_payload_json` 时,本次重试耗尽后保留 `not_completed + externalJobId` 供下次 GET 继续 reconcile,确定性的 payload 损坏、结构不兼容或 formatter 错误才在重试耗尽后写为 `failed`,避免致命错误永久循环。排队 / 执行保持 `not_completed`。worker 的 `result_payload_json` 只保留 formatter 与媒体引用所需的轻量生成回包,不向通用 summary 状态接口投影。
- 影响范围:画布 Agent 共享契约、确认/取消接口、编辑器生成入队 helper、对话状态展示与恢复。
- 验证方式:`cargo check -p api-server -p shared-contracts --manifest-path server-rs/Cargo.toml`、画布 Agent 定向前端测试、`npm run typecheck``npm run check:encoding``git diff --check`
## 2026-06-18 原生壳 WebView 刷新能力只保留受控当前页刷新
- 背景:Expo 移动壳和 Tauri 桌面壳都需要一个真实的宿主级刷新入口,供 H5 在检测到资源、登录态或运行态需要重新载入时请求宿主刷新当前容器;该能力不能演变成任意 URL 导航或原生 WebView ref 透传。
@@ -4055,7 +4149,7 @@
- 背景:VectorEngine Apifox `api-349239079` 暴露 OpenAI-compatible `POST /v1/chat/completions`;创意 Agent 和通用 LLM 代理需要统一到 VectorEngine 文本服务,并将默认文本模型切换为 `gpt-5.4-mini`
- 决策:创意 Agent 的 `CREATIVE_AGENT_GPT5_MODEL` 固定为 `gpt-5.4-mini`,协议切到 Chat Completions,不再携带旧 APIMart `official_fallback` 字段;画布 Agent 侧边栏聊天规划请求也复用该模型和 Chat Completions 协议,不再显式使用 `gpt-4o` / Responses。通用 `/api/llm/chat/completions` 代理使用 `GENARRATIVE_LLM_PROVIDER=openai-compatible``GENARRATIVE_LLM_BASE_URL=https://api.vectorengine.cn/v1``GENARRATIVE_LLM_MODEL=gpt-5.4-mini`。未单独配置 `GENARRATIVE_LLM_API_KEY` 时,api-server 可复用 `VECTOR_ENGINE_API_KEY`;前端 LLM 客户端必须兼容 OpenAI `choices`、api-server raw `{content}` 和项目 envelope `{ok,data:{content}}` 三种非流式响应,以及 OpenAI SSE delta 和 api-server `event: delta` 两种流式响应。
- 决策补充:画布 Agent 的 planning prompt 必须自动注入上一条已完成生成结果的 `latestGeneratedImage`,来源为上一轮 generation 的 `summary` / `toolName` / `resourceId` / `objectKey` 等轻量摘要。用户用「这张」「刚才那个」「上一张」「把衣服换成……」等方式指代上一张图或继续编辑时,规划默认调用 `edit_image` 并引用该结果;不能因为本轮没有手动附件而退回 `generate_image`
- 决策补充:画布 Agent 的 planning prompt 必须自动注入上一条已完成生成结果的 `latestGeneratedImage`,来源为上一轮 generation 的 `toolName` / `resourceId` / `objectKey` 等轻量元数据。用户用「这张」「刚才那个」「上一张」「把衣服换成……」等方式指代上一张图或继续编辑时,规划默认调用 `edit_image` 并引用该结果;不能因为本轮没有手动附件而退回 `generate_image`
- 决策补充:画布 Agent 侧边栏的“规范图 / 视觉规范图 / 风格规范图 / 素材规范展板”是 Agent 规划 prompt 和 function-calling 工具选择约束,不是侧边栏 UI 说明文案。此类请求默认走 `generate_image`,prompt 必须要求规范展板包含统一视角、线条粗细、色卡、材质、阴影、圆角、状态层级、尺寸标注等视觉规范元素;角色规范图若是规范展板也走 `generate_image`,只有实际角色立绘才走 `generate_character`,多个图标素材 / 图集才走 `generate_icon_spritesheet`
- 影响范围:`server-rs/crates/platform-agent``server-rs/crates/api-server/src/config.rs``src/services/llmClient.ts``.env.example``deploy/env/api-server.env.example``scripts/test-ve-llm.mjs`
- 验证方式:`npm run test -- src/services/llmClient.test.ts``cargo test -p api-server --manifest-path server-rs/Cargo.toml from_env_reads_non_public_models_and_urls app_state_builds_creative_agent_gpt5_client_from_vector_engine_settings llm_chat_completions editor_agent_llm_request_uses_vector_engine_chat_model``cargo test -p platform-agent --manifest-path server-rs/Cargo.toml``npm run check:encoding``git diff --check`
@@ -1,6 +1,6 @@
# 文档地图与阅读索引
更新时间:`2026-07-03`
更新时间:`2026-07-16`
## 当前文档入口
@@ -18,7 +18,7 @@
| 微信小程序虚拟支付 | `docs/【技术方案】微信虚拟支付接入-2026-05-26.md` |
| UI 像素资产与 9-slice 规范 | `UI_CODING_STANDARD.md` |
| 图片画布生成面板与模型定价 | `docs/【编辑器】生成类面板Lovart统一改造方案-2026-06-17.md``docs/【编辑器】模型定价配置管理方案-2026-06-22.md` |
| 图片画布右侧 Agent 对话、会话消息 OSS 持久化、SSE 事件契约、与任务侧栏 / 左侧栏互斥规则 | `docs/【编辑器】画布Agent对话面板-2026-07-03.md``docs/adr/【ADR】画布Agent会话消息存OSS-2026-07-03.md` |
| 图片画布右侧 Agent 对话、会话消息 OSS 持久化、普通 JSON 消息、工具任务懒回填及侧栏并存规则 | `docs/【编辑器】画布Agent对话面板-2026-07-03.md``docs/adr/【ADR】画布Agent会话消息存OSS-2026-07-03.md` |
## 阅读顺序
+34 -2
View File
@@ -14,6 +14,22 @@
- 关联:相关文件、文档、提交或 Issue
```
## 图片生成的 K 档不能靠回图后缩放实现
- 现象:用户选择 2K 时占位框看起来是 2K,最终资源元数据也显示为 2K,但模型请求实际仍是固定 1K 或竖版回落尺寸;画面只是后端放大后的低分辨率结果。
- 原因:前端占位尺寸、api-server 的模型尺寸映射和 VectorEngine provider 合法尺寸各自维护;同时通用交付恢复与角色去背景恢复会直接缩放整张回图,掩盖了上游请求尺寸错误。
- 处理:`model + imageSize + aspectRatio` 必须先解析为 provider 可直接生成的真实尺寸,前端占位和后端请求使用同一矩阵。带新尺寸字段的用户生成不执行回图后交付放大;角色、图标和 UI 的去背景服务若降采样,只缩放 alpha 蒙版并应用回模型原始 K 档 RGB。
- 验证:覆盖 nanobanana2 / gpt-image-2 的比例与 K 档尺寸矩阵、VectorEngine 最终请求体、普通图片 / 角色 / 图标 / UI 占位,以及低分辨率去背景结果只贡献 alpha、不贡献被放大的 RGB。
- 关联:`src/components/image-editor/ImageCanvasGenerationModel.ts``server-rs/crates/api-server/src/editor_project.rs``server-rs/crates/platform-image/src/vector_engine/request.rs`
## 多产物任务的中间原图不能只落资源库
- 现象:角色形象、图标 spritesheet 或 UI 素材提取正常成功后,画布只有透明结果,用户无法直接对照和复用 provider 原图。
- 原因:后端虽然为追溯和后处理失败恢复持久化了 provider 原图,但完成画布只写入透明主结果和拆分素材,把“资源已保存”错误等同为“用户已拿到原图”。
- 处理:provider 原图始终写入 OSS、项目资源和账号素材库;三类任务正常成功时都同时落透明主结果与右侧 provider 原图,生成器 `generatedLayerId` 仍指向透明主结果,图标 / UI 拆分素材从原图右侧继续排列。仅当透明处理最终失败时,才用 provider 原图作为唯一主图完成占位。
- 验证:覆盖三类任务正常成功都落透明结果与原图、原图位于透明图右侧、`generatedLayerId` 指向透明图、图标 / UI 拆分素材位于原图右侧,以及透明处理失败仍由原图单独完成占位。
- 关联:`server-rs/crates/api-server/src/editor_project.rs``docs/technical/【后端架构】外部生成Worker化方案-2026-06-03.md`
## phase 上报的业务拒绝与传输失败不能共用字符串错误
- 现象:provider 已经返回并保存原图,worker 上报 `processing` 时一次断连或超时就直接把任务判为失败;或者为了规避误杀而重试所有错误,导致 stale lease 的旧 worker 继续执行后处理。
@@ -106,8 +122,8 @@
## 画板参考图 objectKey 必须先做归属校验
- 现象:画板生成、快速编辑、图标素材或 UI 素材提取如果允许直接提交 generated objectKey,用户只要知道其他账号的私有 objectKey,就可能让 api-server 签名读取并送给外部生成供应商。
- 原因:Data URL 参考图可以直接解析,但 objectKey 是服务端私有对象引用;只校验 generated 前缀、mime 和大小不能证明它属于当前账号
- 处理:所有编辑器参考图入口统一走 `parse_editor_reference_image(state, owner_user_id, source)`objectKey 分支必须先在当前账号的项目资源、素材库资产或 `asset_object` 中匹配 owner / bucket / key,再读取 OSS。图标素材等额外参考图必须真实传到 provider,不只写 metadata;图片快速编辑当前不开放额外参考图,若后续重开入口也必须沿用同一归属校验。
- 原因:Data URL/Blob URL 只允许停留在浏览器临时态,正式编辑器引用必须先上传并经统一 resolver 校验归属
- 处理:所有私有对象引用在读取字节或签发 URL 前统一走 `resolve_editor_reference_object_key_for_owner(state, owner_user_id, source)`先在当前账号的项目资源、素材库资产或 `asset_object` 中匹配 owner / bucket / key。只有确实需要图片字节的入口(生成 / 重绘 / 图标 / UI 提取等交给 provider 的路径)再走 `parse_editor_reference_image`(内部仍先 resolve,再下载 OSS 字节);手动去背景等只签发短期 URL 的入口不要 `parse` 整图。图标素材等额外参考图必须真实传到 provider,不只写 metadata;图片快速编辑当前不开放额外参考图,若后续重开入口也必须沿用同一归属校验。
- 验证:`cargo test -p api-server --manifest-path server-rs/Cargo.toml editor_reference`,并用前端 workflow 测试覆盖 `referenceImageSrcs` 进入图标生成请求;若快速编辑重开额外参考图,再补对应请求覆盖。
- 关联:`server-rs/crates/api-server/src/editor_project.rs``server-rs/crates/spacetime-client/src/assets.rs``src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts`
@@ -400,6 +416,14 @@
- 验证:`npm run test -- src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx` 覆盖来源参数继承、模型参数切换、目标尺寸提交和图层回填;`cargo test -p api-server editor_image_edit --manifest-path server-rs/Cargo.toml` 覆盖图标类拒绝、provider 尺寸对齐和回图恢复。
- 关联:`src/components/image-editor/ImageCanvasGenerationSubmissionModel.ts``src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts``src/components/image-editor/ImageCanvasGenerationLayerModel.ts``server-rs/crates/api-server/src/editor_project.rs`
## 图片画布生成中占位必须同步业务目标尺寸
- 现象:用户选择 `2K` 生成或从 2K 普通图、角色图再次改造时,最终成品仍是 2K,但待生成 / 生成中的灰色框保持 1K 大小,完成后突然放大;UI 素材提取也可能始终显示 512 方框。
- 原因:同来源改造先按默认 1K 创建 draft,再只恢复 `imageModel / aspectRatio / imageSize`,没有重新计算 placeholder;UI 提取和旧修改入口则分别写死图标展示尺寸与 `1024x1024`
- 处理:所有共享图片参数恢复和面板比例 / 清晰度切换都经过 `resizeGenerationPlaceholderToImageSelection(...)`,保持占位中心不变并同步 `width/height/originalWidth/originalHeight`UI 提取按 `resolveUiAssetExtractionGenerationPlan(...)` 的 1K / 2K 计划计算占位,旧修改入口使用源图真实 Resolution。快速编辑覆盖源图,不另建生成占位,仍按目标尺寸更新原图层。
- 验证:`npm run test -- src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx --reporter=dot`
- 关联:`src/components/image-editor/ImageCanvasGenerationModel.ts``src/components/image-editor/ImageCanvasGenerationDialogModel.ts``src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts``docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`
## 图片画布快速编辑模型必须在后端选择正确的 provider 协议
- 现象:快速编辑继承或选择 `nanobanana2` 后,上游返回 `not supported model for image generation`;图集开放快速编辑后尤其容易触发。
@@ -3157,3 +3181,11 @@
- 处理:在调用 `try_with_tx` 前通过 `let caller = ctx.sender()` 捕获真实调用者,再把 `caller` 显式传入事务函数;迁移、后台账号、runtime profile、外部生成等现有 procedure 已采用这一模式。`npm run check:spacetime-runtime-access` 禁止编辑器 runtime writer 鉴权重新直接读取事务 `ctx.sender()`。运行环境升级到 2.6.1 后继续保留显式 caller,避免鉴权依赖 SDK 版本细节。
- 验证:运行 `npm run check:spacetime-runtime-access``cargo test -p spacetime-module --manifest-path server-rs/Cargo.toml``npm run check:spacetime-schema`;发布使用 SpacetimeDB 2.6.1 构建的新模块后,以 runtime writer identity 重试 `POST /api/editor/icon-spritesheets/slices`,确认来源查询、分片素材写入与 cohort 完成不再返回 identity 403。
- 关联:`server-rs/crates/spacetime-module/src/editor_project_storage.rs``scripts/check-spacetime-runtime-access.mjs``docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`
## Portal 弹窗必须在遮罩根节点携带平台主题
- 现象:项目库点击“重命名”后,标题、输入框和按钮仍显示,但弹窗面板及遮罩背景变透明,看起来像“改名界面的背景没了”。
- 原因:`UnifiedModal` 默认 portal 到 `document.body`;若业务入口只在页面内层继承 `platform-theme`,portal 根节点不会继承该容器的 CSS 变量。此时 `.platform-modal-shell``background: var(--platform-modal-fill)``.platform-overlay` 的背景声明都会失效。
- 处理:平台白底工具弹窗优先复用 `PlatformToolModalShell`,由共享壳读取当前 `AuthUiContext.platformTheme`,并把 `platform-theme platform-theme--<light|dark>` 挂到 portal overlay;不要用硬编码白底掩盖主题变量缺失。必须直接使用 `UnifiedModal` 的特殊场景,也要在 `overlayClassName` 显式传递当前平台主题。
- 验证:在 light / dark 主题下打开 portal 弹窗,断言 dialog 的 overlay 携带对应主题类,并在真实浏览器核对 panel 与遮罩的 computed background 均非透明。
- 关联:`src/components/project/ProjectGalleryView.tsx``src/components/common/PlatformToolModalShell.tsx``src/components/common/UnifiedModal.tsx`
@@ -1,6 +1,6 @@
# SSE 客户端传输层收口约定
更新时间:`2026-06-03`
更新时间:`2026-07-16`
## 背景
@@ -13,7 +13,6 @@
- `readSseStream` 负责读取 `Response.body`、识别 `\n\n``\r\n\r\n` 事件边界、合并多行 `data:`、flush `TextDecoder` 尾部缓冲,并支持事件处理函数返回 `false` 后取消 reader。
- `readSseJsonStream` 只在传输事件基础上解析 JSON object,空 data 与异常 JSON 继续按旧口径静默跳过。
- 各业务 client 只保留领域事件归一化、最终结果聚合和中文错误文案,不再重复实现 SSE 边界扫描、reader 循环或 UTF-8 flush。
- `src/services/image-editor/editorAgentSse.ts` 消费画布 Agent 的 JSON SSE,只校验并归一 `stage``message_delta``tool_started``tool_completed``generation_result``error``done` 事件,不持有 reader 循环或事件边界解析。
- OpenAI 兼容流、`[DONE]` 哨兵或其它非 JSON SSE 可直接使用 `readSseStream`;业务 JSON 事件优先使用 `readSseJsonStream`
## 落地范围
@@ -26,27 +25,15 @@
- `src/services/visual-novel-runtime/visualNovelRuntimeSse.ts`
- `src/services/rpg-entry/rpgProfileClient.ts`
- `src/services/llmClient.ts`
- `src/services/image-editor/editorAgentSse.ts`
后续新增 SSE client 时不得复制 `findSseEventBoundary``parseSseEventBlock` 或手写 reader 循环;若确实需要特殊 framing,应先扩展 `sseStream.ts` 的传输能力,再在业务 client 中处理领域语义。
## 画布 Agent SSE client 口径
`src/services/image-editor/editorAgentSse.ts` 是画布 Agent 的业务事件归一层,只允许做以下事情:
- 调用 `readSseJsonStream(response, handler)` 读取 JSON SSE。
- 白名单识别 `stage``message_delta``tool_started``tool_completed``generation_result``error``done`
- 支持两种兼容输入形态:优先使用 SSE `event:` 字段作为事件名;若 `data` 内含 `{ "event": "...", "data": {...} }`,则归一为同一 `EditorAgentSseEvent`
- 对未知事件名、缺失 `data` 或非对象 JSON 静默跳过,保持业务 client 的容错口径。
`editorAgentSse.ts` 不得实现 `Response.body.getReader()``TextDecoder`、SSE 边界扫描、JSON 字符串拼接、fetch、鉴权、错误文案、会话状态聚合或画布聚焦逻辑;这些职责分别留在 `sseStream.ts``editorAgentClient.ts``useEditorAgentConversation.ts` 和画布视图层。
画布 Agent 已改为 `POST /api/editor/agent-conversations/{conversationId}/messages` 普通 JSON 请求,不属于本 SSE 传输层的落地范围;其客户端只通过 `requestJson` 读取 `EditorAgentMessageResponse`,不得为了恢复旧文档口径重新增加私有 SSE parser。
## 验收
- `src/services/sseStream.test.ts` 覆盖 CRLF / LF 边界、UTF-8 尾部 flush、异常 JSON 跳过和提前停止取消 reader。
- `src/services/llmClient.test.ts` 覆盖 OpenAI 兼容文本流、异常 JSON 跳过和 `[DONE]` 后提前停止。
- `src/services/image-editor/editorAgentSse.test.ts` 覆盖画布 Agent 的 `stage``generation_result``done` 事件解析,以及 `{ event, data }` fallback 形态归一
- `src/services/image-editor/editorAgentClient.test.ts` 覆盖会话 CRUD 路由和 `/messages/stream` 发起后能收到类型化 SSE 事件。
- 后续修改 `editorAgentSse.ts` 时,若出现手写 reader、`TextDecoder`、SSE 边界扫描或 fetch 逻辑,应视为违反本收口约定。
- `src/services/image-editor/editorAgentClient.test.ts` 覆盖会话 CRUD 和 `/messages` 普通 JSON 路由;画布 Agent 不纳入 SSE parser 验收
- 已有 OpenAI 兼容文本流、NPC 聊天流、创作 Agent、创意互动 Agent、视觉小说运行态和充值订单状态测试继续通过。
- `npm run typecheck` 不产生新的类型错误。
File diff suppressed because one or more lines are too long
@@ -118,7 +118,7 @@
- `ImageCanvasGenerationLayerModel.ts`
- 承载生成结果落画布的纯数据规则:普通生图、修改图片、快速编辑和图标素材批量结果如何生成图层 id、临时 resourceId、标题、位置、原始分辨率尺寸、zIndex、source metadata、`assetKind`、源图关联和 `generationInputs`
- 主视图继续负责生成提交 API、生成对象 active / archived 状态、资源持久化、图层选择、侧栏切换、对话框收起和适合视图等副作用,避免把多个画布生成对象的生命周期拆成浅 wrapper。
- 该模块用独立单测锁定“图片显示尺寸跟随原始 Resolution”“生成占位框只作为定位参考”“图标素材沿用当前行宽换行规则”和“快速编辑保留源图分组 / 类型”的规则。
- 该模块用独立单测锁定“图片显示尺寸跟随原始 Resolution”“生成占位框的位置和目标像素尺寸共同作为完成落图契约”“图标素材沿用当前行宽换行规则”和“快速编辑保留源图分组 / 类型”的规则。
## 第十三阶段模块
@@ -31,6 +31,7 @@
- `acknowledge_external_generation_job_summaries_and_return`:按当前账号确认已终态任务的完成 / 失败提示,写入摘要投影的 `notification_acknowledged_at` 并追加审计事件。
- `get_external_generation_queue_stats_and_return`controller 读取队列积压、运行中任务和过期 lease 数量,用于计算 worker 目标实例数;该 procedure 只读 `external_generation_job`,不直接操作 systemd。
- `get_external_generation_job_summary_and_return`:按 `job_id` 从轻量摘要投影读取单个任务状态,给 BFF 和生成页展示使用;必须只返回调用者有权读取的任务,不能暴露其它用户的 payload、错误详情或 worker 内部字段。
- `get_external_generation_job_result_and_return`:仅供后端内部回填异步编辑器 Agent 工具调用;按 `job_id + owner_user_id` 返回 `status``last_error_message` 和已持久化的 `result_payload_json`,不返回请求 payload、lease 或其它 worker 字段。该 procedure 不替代摘要状态读取接口,也不经 BFF 暴露给前端。
不带 `summary / summaries` 的旧 `get / list / acknowledge_external_generation_job*` procedure 只保留给受控内部兼容,不是 BFF 正式读取入口。
@@ -196,6 +197,8 @@ controller 配置:
角色形象、图标 spritesheet 和 UI 素材提取在 provider 原图已经持久化后,如果透明背景处理最终失败,只用原图完成 `canvasCompletion`,不创建或回填透明处理图,图标和 UI 也不继续拆分,任务保持 `completed`。这个 source-only 降级只包住透明背景处理的最终失败;phase 上报、provider 原图持久化、透明处理图持久化或画布写回失败仍按任务错误传播。
透明背景处理正常成功时,角色形象、图标 spritesheet 和 UI 素材提取的画布都同时放透明主结果与 provider 原图:透明主结果保持生成器 `generatedLayerId` 主锚点,provider 原图作为第二个图层放在其右侧;图标和 UI 实际拆分出的业务素材从 provider 原图右侧继续排列。
inline 与 external v1 成功响应继续使用结构化 `warning.code/reason`;图标 / UI 的透明图已经成功、只有自动拆分失败时,继续返回结构化 `sliceWarning.code/reason`,其中 `sliceWarning.reason` 保留原始诊断。queue worker 把两类告警归一为有界的 `result_payload_json.warning`:通用 `warning` 优先并原样保留完整 `reason`;只有不存在通用 `warning` 时,才给 `sliceWarning.reason` 添加“图集已生成,但自动拆分未完成:”前缀。任务摘要将该展示就绪的 `reason` 原样提取到 `warning_message`,单 job 状态和刷新后的任务列表 BFF 再以 `warning: string` 返回;Web 必须直接展示,不再补前缀或按 code 推断类型。历史任务保留写入时的 `reason` 快照,摘要 backfill 不按当前格式重新解释或补写前缀。该字符串语义是 worker / BFF / Web 的内部同版本契约,三者必须协调发布,不承诺滚动混部或旧 Web 缓存下的跨版本字符串兼容。
## 验收
File diff suppressed because one or more lines are too long
@@ -68,7 +68,11 @@ HTTP 角色的 `GENARRATIVE_SPACETIME_POOL_SIZE` 只表示 procedure / reducer
lease 过期后不代表任务一定再次执行:claim transaction 只有在 `attempt < max_attempts` 时才会递增 attempt 并返回 worker;如果过期的是最终 attempt,则直接把 job 收口为 `failed`、清理 lease,并按入队冻结价格为当前 attempt 原子退款或写 cancellation intent。该终态任务不会再次进入 provider executor,迟到 consume 会被 settlement intent 拒绝。
图片画布角色图、图标素材、UI 素材提取和角色动作逐帧去背景时优先调用 BgFilter;当前全部调用都显式传 `background_mode=flat`,保持单一纯色背景抠图语义。默认 `GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS=180000`,该值是所有路径的基准请求超时;角色动作逐帧请求的每一次 HTTP attempt 额外增加 `2000ms × 本次实际帧数`,默认 `32 / 40 / 48` 帧对应 `244000 / 260000 / 276000ms`,角色形象单图、图标、UI 和手动去背景继续使用基准值。该 request timeout 不是整批帧或整项任务超时,首次失败后的重试会重新计时;角色动作整项任务仍受默认 `GENARRATIVE_EXTERNAL_GENERATION_WORKER_LONG_JOB_TIMEOUT_SECONDS=1800` 预算约束,排查时以 `editor_bgfilter_request_start.timeout_ms` 确认实际值。连续失败达到 `GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD=3` 后熔断 `GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS=300` 秒。BgFilter 调用失败和熔断期均先走阿里云通用抠图,只有阿里云失败才走本地幕布色去背景兜底。阿里云这层默认 `GENARRATIVE_ALIYUN_MATTING_ENABLED=true`,但必须在 `api-server.env` 填入 `GENARRATIVE_ALIYUN_MATTING_ACCESS_KEY_ID` / `GENARRATIVE_ALIYUN_MATTING_ACCESS_KEY_SECRET`(或标准 SDK 命名 `ALIBABA_CLOUD_ACCESS_KEY_ID` / `ALIBABA_CLOUD_ACCESS_KEY_SECRET`)才会真正启用;AccessKey 缺失时启动日志会打印「阿里云抠图 AccessKey 未配置,跳过抠图客户端初始化」,抠图直接塌成 BgFilter→本地两级,`npm run check:api-server-env` 也会给出对应告警。修改这些变量后需要重启对应 `api-server` / worker 进程;排查时先从 worker 启动日志确认 lease 和 job timeout,再看带 `background_mode=flat``editor_bgfilter_request_start``editor_bgfilter_fallback_to_aliyun_matting``editor_bgfilter_circuit_open_fallback_to_aliyun_matting`,以及阿里云失败后的 `editor_aliyun_matting_fallback_to_local_screen_background_removal` 日志。
手动 `POST /api/editor/images/background-removals` 同样调用 BgFilter,但固定传 `background_mode=complex``seg_model=birefnet``cross_check=off`,不传背景色首次失败后立即重试 `1` 次,两次都失败则返回最终错误。它不进入只适用于已知纯色背景的阿里云 / 本地键色兜底链,也不改变 flat 路径的熔断状态。标准纯色背景四条链路仍固定传 `background_mode=flat`。两种模式统一使用 `GENARRATIVE_EDITOR_BGFILTER_BASE_URL``GENARRATIVE_EDITOR_BGFILTER_TOKEN``GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS` 和共享 HTTP client;旧 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN` 只保留为 token 兼容别名。
手动 `POST /api/editor/images/background-removals` 同样调用 BgFilter,但 worker 只解析并校验已有 OSS object key,直接签发 600 秒 URL,不下载原图;multipart 固定传 `image_url``background_mode=complex``seg_model=birefnet``cross_check=off`,不传 `file`背景色首次失败后立即重试 `1` 次,两次都失败则返回最终错误。它不进入只适用于已知纯色背景的阿里云 / 本地键色兜底链,也不改变 flat 路径的熔断状态。标准纯色背景四条链路仍固定传 `background_mode=flat`。两种模式统一使用 `GENARRATIVE_EDITOR_BGFILTER_BASE_URL``GENARRATIVE_EDITOR_BGFILTER_TOKEN``GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS` 和共享 HTTP client;旧 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN` 只保留为 token 兼容别名。
阿里云通用抠图的非上海地域输入使用 `AuthorizeFileUpload → Policy POST → SegmentCommonImage` 正式链路,上传 Bucket / Endpoint / ObjectKey 由阿里云动态返回;不得恢复 `GetOssStsToken`、固定 `viapi-customer-temp`、临时 AK/SK 或 OSS V1 PUT。该切换不新增环境变量;真实链路冒烟可运行 `cargo run -p platform-matting --example segment_smoke --manifest-path server-rs/Cargo.toml -- <图片路径>`,预期日志中的输入 host 为授权响应返回的上海 OSS host,并完成结果下载。图片字节仍经过执行任务的 api-server / worker,排障时不要把 Advance 路径误判为阿里云直接抓取任意公网 URL。
BgFilter 对已经落入私有 OSS 的生成原图、动作抽取帧和手动去背景源图直接使用 600 秒签名 URL:`api-server` 的 multipart 提交 `image_url` 及对应模式参数,不再提交 `file`,也不会在 BgFilter 调用前重新下载 OSS 对象。生成原图和动作帧上传完成后应已消费并释放字节所有权;手动路径从始至终不读取原图字节。flat 路径的 BgFilter 失败或熔断打开后,阿里云 fallback 才单独下载源对象并上传动态临时桶,临时上传完成即释放本次下载缓冲;阿里云继续失败时本地 fallback 再独立下载,并在本地处理产出后释放本次原图缓冲。排障日志只应出现 object key 与签名有效期,不得记录带 `x-oss-*` 查询参数的完整 URL。这里的释放是 Rust 缓冲析构,不以操作系统 RSS 立即下降作为判据。
`我的` 页签或排障面板展示队列等待时,只读取 BFF 队列接口:`GET /api/runtime/external-generation/queue-overview` 查看当前用户可见队列概览,`GET /api/runtime/external-generation/jobs/{jobId}` 查看单 job 状态。生成页 / 进度页不承接队列概览,只展示当前玩法业务进度;队列接口只提供等待 / 运行 / 失败 / 完成状态补充,最终草稿、作品和结果页仍要轮询对应玩法 session/detail 接口收敛到 ready 或 failed;不要直接查询 `external_generation_job` private table,也不要把 worker 内部 payload 暴露到前端。
@@ -55,7 +55,7 @@ Prompt 输入摘要与 Prompt 约束只作为内部生成契约维护,不在 U
- 宣发素材请求携带 `kind: publication-material`,前端提交和后端 handler 都固定归一为 `gpt-image-2`;即使旧前端或外部请求传入 `nanobanana2`,后端也按 `gpt-image-2` 生成和计费,但价格仍来自运行时模型定价配置,不写死数值。
- `publication-detail-gallery` 不再携带 `candidateCount: 5`;后端仍兼容多候选请求,但当前宣发素材入口不主动批量生成。
- 尺寸请求和成品交付规格必须使用明确像素值:游戏首图 `720x540`、详情图 `720x1280`、运营海报 `1280x720`。这些值是画布图层与成品的业务规格;由于三种规格并非都满足 `gpt-image-2` 的上游尺寸约束,VectorEngine 适配层会在发送前等比归一到合法请求尺寸(最大边 `3840`、两边为 `16` 的倍数、长短边比不超过 `3:1`、总像素 `655360..8294400`),不能将上游归一后的尺寸当作宣发成品规格。只有 `16:9``9:16``2k` 等比例 / 档位别名才走 provider 预设映射。
- 参考图在提交 `/api/editor/images/generations`由前端压缩成适合生成理解的图片 Data URL,避免原图 Data URL 撑爆 JSON 请求体;后端该路由保留 `12MB` body limit 作为兼容兜底
- 参考图在提交 `/api/editor/images/generations`必须先落稳定引用:本地图先上传 OSS 并确认 asset object,提交时使用 `objectKey`、项目资源 ID 或素材 ID;禁止把 Data URL / Blob URL 写入生成请求或外部生成队列载荷。浏览器内可对临时图做压缩等处理,但压缩结果也必须上传后再提交
- 扣费通过现有钱包资产操作封装执行;上游生成失败或未返回图片时按现有补偿逻辑退款。
- 生成成功后,成品作为图片画布生成图层加入画布,并保留游戏输入和参考图摘要供图层信息使用。
@@ -110,6 +110,10 @@
- 查看信息按钮固定使用圆形 `i` 图标,不使用中括号、花括号或文本符号样式。
- 视频 / 角色 / 角色动作 / 音效 / 背景音乐待生成占位的角标同样按 viewport 反向缩放,不随画布缩放变小。
- 已生成角色图、角色动作图或其它生成结果图被点击时只选中图层并收起已有生成输入框;重绘、快速编辑和生成动画面板必须由对应工具栏按钮或右键菜单显式打开。
- 角色图层打开“生成动作”后再点击“改造”,必须重新打开角色形象生成器;动作生成对话框只把角色图层作为输入来源,不得被识别为该角色图层自身的来源生成器。
- 角色动作结果图层点击“改造”时,必须通过 `sourceResourceId` 找回原角色图层并重新打开角色动作生成器;关联原角色已不存在时应显示明确提示,不得无响应或降级成图片生成器。
- 恢复已生成图层的来源生成器时,角色、动作、规范、图标、UI、宣发、视频、音效和背景音乐必须按 `assetKind / mediaType` 恢复对应面板;去背景或快速编辑产生的派生 `quick-edit` 对话框不得遮住图层原有来源生成器。
- 任何会移除画布图层的入口,包括删除、右键剪切和素材库删除关联素材,都必须同步清理该图层关联的生成面板和派生状态,不得在保存或刷新后恢复成孤立占位。
## 画布保存
@@ -1,6 +1,6 @@
# 画布Agent对话面板
日期:`2026-07-03`
日期:`2026-07-16`
## 定位与边界
@@ -8,9 +8,9 @@
- 它是画布域工具,**不承接玩法创作**、不产出玩法作品或模板,与 `CONTEXT.md` 中「表单/图片输入创作工作台」的 Avoid 边界不冲突。
- 独立于拼图专用的 `/api/runtime/creative-agent/sessions`(该会话为 api-server 内存态、拼图领域专用,不复用)。
## 能力范围(第一期)
## 能力范围
对话 Agent 可通过 function-calling 触发以下类工具,全部复用既有计费收口接口:
对话 Agent 可通过 function-calling 触发以下类工具,全部复用既有计费收口接口:
| 工具 | 后端接口 |
| --- | --- |
@@ -19,19 +19,22 @@
| 生成角色形象 | 既有角色形象生成入口对应接口 |
| 生成图标素材 | `POST /api/editor/icon-spritesheets/generations` |
| 生成 UI 设计图 | 既有 UI 设计图生成入口对应接口 |
| 生成视频 | `POST /api/editor/videos/generations` |
| 生成游戏音效 | `POST /api/editor/audios/sound-effects/generations` |
| 生成背景音乐 | `POST /api/editor/audios/background-music/generations` |
- 意图解析与工具编排在后端 api-server,前端只渲染状态,不承接业务规则。
- 下面的工具选择口径属于 Agent 规划 prompt / function-calling 约束,不是侧边栏 UI 说明文案;侧边栏面板不展示这些规则解释。
- 用户要求“规范图 / 视觉规范图 / 风格规范图 / 素材规范展板”时,规划默认选择 `generate_image`,并在 prompt 中明确要求生成规范展板,包含统一视角、线条粗细、色卡、材质、阴影、圆角、状态层级、尺寸标注等可落地的视觉规范元素。
- 用户要求“角色规范图”且语义是角色的规范展板、风格展板或设定板时,仍走 `generate_image`,不要误分流到 `generate_character`;只有实际生成角色立绘、角色主形象或角色视觉资产时才走 `generate_character`。用户要求多个图标素材、图集或 spritesheet 时才走 `generate_icon_spritesheet`
- 所有生成必须走 `execute_billable_asset_operation_with_cost` 与模型定价配置,禁止绕过定价收口。
- 视频 / 音频 / 图层操作等其余画板功能第一期不进入对话工具面,仍走现有面板。
- 图层操作及其他未注册的画板功能第一期不进入对话工具面,仍走现有面板。
## 当前分支落地状态
- 已落地:会话元数据、OSS 消息文档、会话 CRUD、SSE 消息、后端 LLM 工具规划、右侧对话面板、会话历史、新建 / 软删会话、停止当前 SSE 回合、附件从画布资源 / 账号素材库选择,以及类图片工具对既有生成入口的复用。
- 已落地:`tool_started` / `tool_completed` 事件携带 `status`;工具失败时也会写入失败 generation record,并随后发送 `stage=failed``error`,前端应保留消息内失败条目
- 未落地:附件弹窗末尾上传格、跨刷新异步生成恢复、external generation task 轮询回填。未落地前,对话消息状态只表示本次 SSE 回合记录,不作为后台任务队列真相
- 已落地:会话元数据、OSS 消息文档、会话 CRUD、`clientMessageId` 幂等键的普通 JSON 消息请求、后端 LLM 工具规划、右侧对话面板、会话历史、新建 / 软删会话、附件从画布资源 / 账号素材库选择,以及类图片 / 音视频工具对既有生成入口的复用。
- 已落地:工具确认 / 取消、external generation task 轮询与会话懒回填。LLM 未配置、请求失败或规划结果解析失败时,后端把 `role=system`、正文以 `ERROR ` 开头的消息写入 OSS,并通过 `deltaMessages` 返回,`errorMessage` 保持为空;前端隐藏 wire 前缀并以红色错误气泡展示。工具执行失败继续保存 `status=failed`、模型和错误信息,不能只返回瞬时错误
- 未落地:附件弹窗末尾上传格`external_generation_job` 继续作为后台任务队列真相,对话消息只保存确认、回填状态和轻量媒体结果引用
## 会话与持久化
@@ -39,24 +42,27 @@
- SpacetimeDB 新表 `editor_agent_conversation` 只存会话元数据:会话 ID、projectId、ownerUserId、标题、软删标记、聊天记录 OSS 对象引用、创建/更新时间。
- 完整消息内容存 OSS`editor-agent/{conversationId}.json`,**会话粒度整体读写**(追加消息=重写对象),不按消息拆对象。
- 不把对话塞进画布工程快照 payload,不在 api-server 内存中保存会话真相。
- 会话标题:新会话默认「新对话」,首条用户消息发出后自动截取前 N 字作为标题;第一期不做手动重命名
- 会话标题:新会话默认「新对话」,首条含文本的用户消息发出后自动截取前 N 字作为标题;列表摘要、详情和消息回包均携带同一必填标题,前端只展示该标题,不以会话 ID 或本地推导兜底。标题写入失败会使该消息请求失败,不能静默继续
- 会话删除:列表项 hover 出删除按钮 + 确认;软删(表打 deleted 标记,OSS 对象保留)。
- 每次用户主动发送生成一个最长 128 字符的 `clientMessageId``editorAgentClient` 对网络错误和通用瞬时状态码显式启用 1 次 POST transport 重试,重试复用同一个已序列化 body、`clientMessageId``x-request-id`。该字段独立于数字 `message.id` 并随用户消息写入 OSS。旧消息缺失时按 `None` 兼容;早期 SSE 文档若把客户端键存成用户消息字符串 `id`,读取时将其迁入 `clientMessageId`,同时重建数字定位符。后端在会话锁内检查重复键:内容一致时返回已持久化的同一回合结果,尚无结果时复用原用户消息继续规划;文本或附件身份不同则返回 `409`,不得再次追加用户消息或调用 LLM。
## 生成结果落画板(对现有占位规则的例外)
- 对话入口触发的生成**不创建"即将生成"画布占位**(区别于其余生成面板);生成中状态由对话消息流承载。
- 生成完成后:结果图按统一 placement 避让模型(视口中心就近、避开现有图层、32px 间距)落画板为新图层,同时登记到默认项目素材库,并在对话消息内显示纯缩略图;前端收到 `generation_result` 后立即刷新工程快照与素材库,缩略图本身不显示名称也不承担图层跳转。
- 对话入口触发的生成**不创建"即将生成"画布占位**(区别于其余生成面板);生成中状态由工具消息和外部任务状态承载。
- 生成完成后:结果图按统一 placement 避让模型(视口中心就近、避开现有图层、32px 间距)落画板为新图层,同时登记到默认项目素材库;前端轮询到任务终态并重新读取会话后,以回填的轻量媒体引用显示纯缩略图并刷新工程快照与素材库,缩略图本身不显示名称也不承担图层跳转。
- 消息内生成结果缩略图必须携带并优先使用 `objectKey` / `assetObjectId`,前端通过 `ResolvedAssetImage` / `/api/assets/read-url` 换签后渲染,不能把裸 `/generated-*` 私有路径直接交给 `<img>`
- 当前第一阶段通过既有编辑器生成 BFF 的 `canvasCompletion` 写回工程快照;刷新后异步任务恢复和轮询回填属于后续能力,不在本阶段声明为已完成
- 既有编辑器 worker 通过 `canvasCompletion` 写回工程快照;刷新后由 external generation task 状态和会话懒回填恢复结果
- 该例外已同步登记在《生成类面板Lovart统一改造方案-2026-06-17》「画布占位落点」节。
## 右侧布局
- 对话框与既有任务侧栏(`ImageCanvasTaskSidebarView`)**互斥展开**:展开一个自动收起另一个;各自收起后保留入口按钮。
- 对话框与左侧素材 / 图层侧栏也互斥:打开画布 Agent 时收起左侧栏;再次打开素材、图层或任务侧栏时收起 Agent 面板。
- 对话框与左侧素材 / 图层侧栏**不互斥**,允许同时展开,便于在对话中选取和核对画布素材;左侧栏切换不改变 Agent 面板开关状态
- 桌面端对话框固定宽约 360–400px;移动端抽屉式全宽覆盖;收起态为胶囊/圆形入口按钮。
- 会话管理入口在对话框头部:当前会话标题 + 历史会话下拉(按更新时间倒序)+ 新建对话按钮,全部包在对话框内。
- 收起对话框只是隐藏面板,不卸载当前会话 hook;流式回复、`生成中` 阶段和停止按钮状态必须在收起 / 重新打开之间保持一致
- 快速切换会话或会话轮询刷新产生并发详情请求时,前端只允许最后发起的请求更新当前会话、消息、错误和加载态;旧响应不得覆盖用户最新选择
- 普通 JSON 消息请求的回包必须绑定发送时的会话:用户在等待期间切换到其他会话后,只更新原会话的列表摘要,不得把原会话的 `deltaMessages` 、错误或画布刷新副作用应用到当前面板。
- 收起对话框只是隐藏面板,不卸载当前会话 hook;普通 JSON 消息请求的等待态和外部生成任务状态必须在收起 / 重新打开之间保持一致。
## 附件
@@ -68,11 +74,29 @@
- 应用后附件以胶囊 chip 挂在输入框上方;发出的消息内附件渲染为纯文本胶囊 chip(名称 + 小图标),**默认无缩略图,鼠标悬浮才浮出缩略图预览**。
- 附件领域形状:统一为画布资源 / 素材库对象引用(`resourceId` / `assetId` + 可选 `objectKey`),不存在只属于对话的第三种图;单条消息上限 9 张(前后端共同校验)。前端可携带展示用 `imageSrc` / `thumbnailSrc`,后端必须按当前工程和当前账号重新归一、校验归属与 `objectKey`
## 工具调用确认展示契约
- Agent 规划出生成或编辑工具后,先把 `status=not_completed` 且没有 `externalJobId` 的工具消息持久化为待确认记录;确认卡必须在真正调用生成 provider 前展示本次提示词、规格参数、目标图和参考图缩略图,用户确认后才执行,取消后保留同一条 `status=cancelled` 记录。内部 system 文本和图片哈希 ID 不直接展示给用户。
- 工具消息只保存 `status``not_completed` / `completed` / `failed` / `cancelled`)和可选 `externalJobId``external_generation_job` 仍是队列、执行、lease 与计费结算真相;OSS status 仅表示该条对话消息是否已经回填完成结果或失败,不复制 queued / running。
- 确认接口必须先把工具参数转换为既有编辑器 worker payload,再使用 `editor-agent:{conversationId}:{messageId}:{toolName}` 稳定 dedupe key 入队;同一确认的请求重试只能得到同一个 external job。入队成功后把返回的 job id 写回同一条 OSS 工具消息,不新增 Agent 工具执行关联表。
- 前端根据 `externalJobId` 查询通用 external-generation job 状态;worker 继续通过 `canvasCompletion` 把生成结果写回工程与素材库。浏览器断线、刷新或 api-server 重启不得导致确认接口重新扣费或重新提交 provider。
- `GET /conversation` 会在同一个 conversation lock 内扫描 `status=not_completed` 且已有 `externalJobId` 的工具消息:只对这些消息按 job id 定向读取主任务;任务完成后复用对应工具的 `format_execute_message` 替换 system text、回填轻量图片 / 视频 / 音频引用并写为 `completed`,任务失败则回填 `error` 并写为 `failed`。任务结果读取或 completed payload 解析 / formatter 回填失败时,必须在同一次 GET 内完成首次尝试及最多 3 次重试,三次重试各间隔 100ms 并重新读取任务结果;仍失败才把该工具消息写为 `failed` 并保存最后错误。该重试不依赖前端再次刷新。排队和执行中都保持 `not_completed`,整轮扫描结果一次性写回 OSS。
- `EditorAgentToolCall.args` 保留为工具返回的原始 JSON,是确认接口重新反序列化并执行工具的唯一参数真相。图片参数继续只保存由真实 data key 计算出的 opaque SHA-256 `imageId`;不得为了前端预览把 `args` 中的图片 ID 改写成 `objectKey`、URL 或展示对象,也不得由前端重组或回传一份新的执行参数。
- `EditorAgentToolCall.displayArgs` 是必填、只读的用户确认展示投影,与 `args` 分离:
- `stringArgs` 保存提示词、比例、清晰度、模型、时长等可展示参数的稳定名称、用户可见标题和值;
- `imageArgs` 按“目标图片 / 参考图片”等参数分组,每个 `refs` 项包含与原始参数对应的 `imageId`,以及后端从已校验会话上下文解析出的 `objectKey``imageSrc`、可选 `thumbnailSrc` / `label` / `width` / `height`
- `extras.priceMudPoints` 保存创建待确认消息时按后端运行时模型定价快照计算的预计泥点消耗;前端统一展示为“预计消耗 N泥点”,不自行计算价格。
- `displayArgs` 只能由 api-server 按已注册 tool 白名单,基于已经通过 ToolArgs 校验的 `args` 和当前 OSS 会话文档中的附件 / 历史生成结果构建;不能信任 LLM 自报的展示地址、标题或素材元数据。展示投影不参与确认执行,确认接口仍只读取同一条持久化 tool call 的 `args`,避免“看到的素材”和“实际执行的素材”分叉。
- `extras.priceMudPoints` 同样只属于展示投影,不作为扣费输入;确认后仍由既有生成 BFF 按后端运行时定价执行预扣费,因此该字段表达用户确认时看到的价格快照,而不是前端可提交或覆盖的计费真相。
- `EditorAgentToolCall.summary` 只是 `args` 的重复字符串且没有稳定语义,当前契约删除该字段,不再作为展示或执行输入。
- 前端待确认卡只消费必填 `displayArgs`,不解析各 tool 私有的 snake_case / camelCase schema,也不把 `sha256:*` ID 当标题或图片地址。图片统一通过 `ResolvedAssetImage` 使用 `objectKey` 换签后显示,签名 URL 不进入消息文档。模块尚未上线,不保留缺少 `displayArgs` 时读取 raw `args` 的旧消息降级路径。
## LLM 与计费
- 编排复用 `creative_agent_gpt5_client` 的 LLM 接入配置(同 provider/env,独立用途标识),画布 Agent 规划请求固定使用 VectorEngine `gpt-5.4-mini` Chat Completionsfunction-calling 注册类工具。
- 每个用户回合必须由 LLM 返回结构化计划;LLM 未配置、请求失败或返回格式不可解析时,后端写入明确错误消息,不使用本地关键词或“收到:...”回显兜底
- 规划 prompt 必须自动带入上一条已完成生成结果的 `latestGeneratedImage` 引用,内容只包含上一轮 generation 的 `summary` / `toolName` / `resourceId` / `objectKey` / `assetObjectId` 等轻量元数据,不把私有签名 URL 或大图内容塞进 prompt。
- 编排复用 `creative_agent_gpt5_client` 的 LLM 接入配置(同 provider/env,独立用途标识),画布 Agent 规划请求固定使用 VectorEngine `gpt-5.4-mini` Chat Completionsfunction-calling 注册类工具。
- 每个用户回合必须由 LLM 返回结构化计划;LLM 未配置、请求失败或返回格式不可解析时,后端写入正文为 `ERROR <错误内容>` 的 system 消息,不使用本地关键词或“收到:...”回显兜底。该错误消息与其它 system 消息一样进入后续 LLM memory,使 Agent 能看到上一轮失败上下文
- 规划 prompt 必须自动带入上一条已完成生成结果的 `latestGeneratedImage` 引用,内容只包含上一轮 generation 的 `toolName` / `resourceId` / `objectKey` / `assetObjectId` 等轻量元数据,不把私有签名 URL 或大图内容塞进 prompt。
- 工具参数中的图片 ID 是由真实 object key 或图片地址计算的稳定 SHA-256 标识;真实 data key 仅存于 api-server 的工具上下文映射,所有图片工具在执行时查表恢复,不能把 object key 或图片地址作为 LLM 可见的工具 ID。
- 用户使用「这张」「刚才那个」「上一张」「把衣服换成……」等方式指代或编辑上一张结果图时,LLM 默认选择 `edit_image` 并引用 `latestGeneratedImage` 作为源图;除非用户明确要求全新生成,否则不能因为本轮没有重新上传附件而降级为 `generate_image`
- 规划 prompt 必须显式区分“规范展板”和“实际素材产出”:规范图、视觉规范图、风格规范图、素材规范展板、角色规范图等规范展板请求走 `generate_image`,并补齐统一视角、线条粗细、色卡、材质、阴影、圆角、状态层级、尺寸标注等要求;实际角色立绘才走 `generate_character`,多个图标素材 / 图集才走 `generate_icon_spritesheet`
- 画布 Agent 规划请求使用 Chat Completions、1024 `max_tokens` 和 60 秒 Agent 专用请求超时;生成图片/编辑图片仍走对应生成工具和模型计费。
@@ -83,13 +107,13 @@
做(第一期):
1. 助手文本 SSE 流式输出
2. 阶段提示行(思考中 → 思考完成 → 生成中 → 完成/失败)
1. 助手文本随普通 JSON 消息响应一次性返回
2. 消息请求等待态,以及工具任务的待确认、生成中、完成 / 失败状态
3. 工具/模型标注行(生成时显示模型名 + 图标);
4. 消息内生成结果缩略图(纯预览,不显示名称,不点击聚焦图层);
5. 生成中的进行中动画;
6. 错误气泡(失败/余额不足,带原因);
7. 发送中断:进行中时发送按钮变「停止」,可中断当前回合(已提交的生成任务不追回,照常落画板)
7. 普通消息请求等待期间禁用发送按钮,不提供客户端停止操作;前端持续等待后端响应,避免后端已持久化消息但前端中断请求后产生会话状态错位
不做(明确排除,防止后人补齐):
@@ -113,22 +137,22 @@
- `api-server`
- `GET/POST /api/editor/projects/{projectId}/agent-conversations`(列表/新建);
- `GET/DELETE /api/editor/agent-conversations/{conversationId}`(详情/软删);
- `POST /api/editor/agent-conversations/{conversationId}/messages/stream`SSE);
- `POST /api/editor/agent-conversations/{conversationId}/messages`JSON);
- Agent 编排(function-calling 循环、工具内部调既有生成执行链路)放 api-server 编排层,独立文件,不复用 `creative_agent.rs` 内存会话。
- `shared-contracts` + `packages/shared``editorAgent` DTO 与 SSE 事件契约(`stage``message_delta``tool_started``tool_completed``generation_result``error``done`
- `shared-contracts` + `packages/shared``editorAgent` 会话、消息、工具确认展示与轻量媒体结果 DTO;消息响应返回 `conversation``deltaMessages` 和可选 `errorMessage`
## 实施顺序
1. 契约与领域规则(shared-contracts / packages/shared + module-editor-agent);
2. 存储层(spacetime-module 表 + procedure + migration + spacetime-client + schema check);
3. api-server 会话 CRUD + 消息 OSS 读写 + SSE 回显桩(不接 LLM,先保证会话链路端到端真实落库);
3. api-server 会话 CRUD + 消息 OSS 读写 + JSON 回显桩(不接 LLM,先保证会话链路端到端真实落库);
4. 前端最小纵切(对话框、会话管理、消息流、附件弹窗、侧边栏/小地图默认值)——可与 3 并行:3 只碰 `server-rs/`4 只碰 `src/` 且先以契约 mock 客户端联调,汇合点在 4 末接真实 API;
5. LLM 编排 + 类工具接入 + 生成落画板 + 停止/错误态(替换回显桩这一个点);
5. LLM 编排 + 类工具接入 + 生成落画板 + 请求等待 / 错误态(替换回显桩这一个点);
6. 验证与文档:定向测试、类型检查、`npm run check:encoding``git diff --check``npm run check:spacetime-schema`、api-server smoke `/healthz`;同步 `CONTEXT.md` 与相关文档。
## 第一阶段验收补充
- 打开画布 Agent 后任务侧栏和左侧素材 / 图层面板应关闭再次打开任务侧栏素材 / 图层面板时,Agent 面板应关闭
- 发送消息时先本地追加用户消息,再消费 SSE 增量;停止按钮只中断当前 SSE 回合,不追回已经提交的生成工具调用
- Agent 消息内生成结果缩略图只用于预览,不显示名称,也不点击跳转图层;收到 `generation_result`统一刷新工程快照和素材库。
- 打开画布 Agent 后任务侧栏应关闭再次打开任务侧栏时 Agent 面板应关闭;素材 / 图层面板Agent 可同时展开,互不改写开关状态
- 发送消息时先本地追加用户消息,再应用 JSON 响应中的 `deltaMessages`;请求等待期间发送按钮保持禁用,前端不主动中断当前回合
- Agent 消息内生成结果缩略图只用于预览,不显示名称,也不点击跳转图层;轮询到任务终态并完成会话懒回填后统一刷新工程快照和素材库。
- 对话内容可被用户选中复制;用户从输入框或对话内容点击回画布图层 / 生成器时,焦点应回到画布对象,Backspace / Delete 等画布快捷键继续生效。
@@ -54,14 +54,14 @@
- 框选状态参考微信截图工具栏布局,默认矩形框选,并支持矩形框选、椭圆框选和画笔自由框选三种工具。
- 用户可在同一张 UI 设计图上多次框选;未至少框选一个区域时,工具栏 `提取` 按钮不可点击。
- 可上传普通参考图辅助 UI 素材提取,用于约束被框选素材的风格、配色或材质;普通参考图会以 `objectKey` 随合成后的红框 UI 设计图一起提交。
- 点击 `提取` 后,前端把所有框选的红色轮廓绘入原 UI 设计图生成合成图Data URL,再提交到 `POST /api/editor/ui-designs/assets/extractions`
- 点击 `提取` 后,前端把所有框选的红色轮廓绘入原 UI 设计图生成合成图Data URL / Blob URL 只允许停留在浏览器临时态,必须先上传 OSS 并确认 asset object,再以返回的 `objectKey`(或项目资源 / 素材 ID)作为 `sourceImageSrc` 提交到 `POST /api/editor/ui-designs/assets/extractions`
- 后端固定使用 `gpt-image-2` 图片编辑链路,并固定提示词:
```text
仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用后端自动决策出的抠图背景色。纯色背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现与背景色相同或相近的描边、底板、投影或反光。
```
- 后端收到 spritesheet 后先把带解析后纯色背景的源图写入 OSS、项目资源和账号素材库,再调用 BgFilter,固定传 `background_mode=flat``cross_check=off` 并按默认 `segModel=birefnet` 透明化。透明背景处理正常成功时,透明 spritesheet 同样先进入 OSS、项目资源和账号素材库,再复用图标素材的连通域拆分能力;调用方未指定素材文件夹时落默认“项目”文件夹。透明背景处理最终失败、但 provider 原图已经持久化时,任务以 `completed + warning` 收口,只把 provider 原图作为唯一主图放入画布,`generatedLayerId` 指向原图,不创建透明图集,也不继续拆分。该收口只捕获透明背景处理本身的最终失败;phase 上报、provider 原图持久化、透明处理图持久化和 `canvasCompletion` 写回错误仍正常传播,不能被原图降级吞掉。
- 后端收到 spritesheet 后先把带解析后纯色背景的源图 owned 上传私有 OSS(消费图片字节所有权,上传完成后释放原图缓冲,不克隆保留),写入项目资源和账号素材库;随后只持 object key。每次 BgFilter attempt 重新签发 600 秒 GET URLmultipart 固定传 `image_url``screen_color=<screenColor>``seg_model=<segModel>``background_mode=flat``cross_check=off`,不包含 `file`默认 `segModel=birefnet`。BgFilter 主路径不重新下载原图;首次失败后立即重试 `1` 次(重试同样重新换签),第二次仍失败进入“阿里云通用抠图(按签名 URL 单独下载)→ 本地键色(再按 object key 独立下载一次原图并在产出后释放)”降级链。透明背景处理正常成功时,透明 spritesheet 同样先进入 OSS、项目资源和账号素材库,再复用图标素材的连通域拆分能力;调用方未指定素材文件夹时落默认“项目”文件夹。透明背景处理最终失败、但 provider 原图已经持久化时,任务以 `completed + warning` 收口,只把 provider 原图作为唯一主图放入画布,`generatedLayerId` 指向原图,不创建透明图集,也不继续拆分。该收口只捕获透明背景处理本身的最终失败;phase 上报、provider 原图持久化、透明处理图持久化和 `canvasCompletion` 写回错误仍正常传播,不能被原图降级吞掉。
- UI 素材自动拆分只在透明图集成功后执行,与图标图集一致,属于 best-effort 附加动作。未知素材数量时按从上到下、从左到右自动命名为 `素材 1``素材 2`;识别或切片持久化失败仍返回整张透明图集和 `sliceWarning`,前端显示非阻断 warning toast,用户可手动重试。`sliceWarning` 与透明背景最终失败使用的通用 `warning` 互斥,前者只表示透明图集成功但自动拆分失败,`sliceWarning.reason` 原始契约保持不变。
- 正常透明化成功时,前端先把透明 spritesheet 作为 `assetKind: "icon-spritesheet"` 图集图层放在 UI 设计图右侧,再把拆分成功的独立素材作为 `assetKind: "icon"` 图标图层继续放到画布;透明背景处理最终失败时只消费后端快照中的 provider 原图。透明图集图层提供 `拆分图集` 工具栏按钮,可使用相同连通域规则重新拆分。
@@ -10,9 +10,9 @@
- 底部 AI 画布工具栏新增 `生成图标素材` 按钮。
- 点击后立即在画布中心创建图标素材占位图,不复用普通“单张空白图片”图标;占位图表现为一叠空白素材图标卡片。
- 图标素材占位图使用 `360x360` 的画布展示尺寸和 `512x512` 的原始图集尺寸;面板中的模型、比例和尺寸仍按生成契约独立提交,不用通用图片生成的 `1K` 画布外框
- 图标素材占位图必须按当前模型、比例和 K 档对应的真实 provider 请求像素初始化;切换参数后继续保持占位尺寸与请求尺寸一致,不得用固定 `360x360 / 512x512` 框代替生成目标
- 图标素材面板锚定在占位图下方,和现有生成输入框同一层级展示。
- 透明背景处理正常成功后删除占位态,把后端返回的透明 spritesheet 作为 `assetKind: "icon-spritesheet"` 的图集图层放到画布,并把按 alpha 连通域成功拆出的 `assetKind: "icon"` 素材铺到图集右侧;透明背景处理最终失败时,后端完成快照只用 provider 原图替换占位态。
- 透明背景处理正常成功后删除占位态透明 spritesheet 作为主图(`assetKind: "icon-spritesheet"``generatedLayerId` 锚点)放入画布,provider 带背景原图作为第二个同类型图层放在透明主图右侧,按 alpha 连通域成功拆出的 `assetKind: "icon"` 素材从原图右侧继续铺放;透明背景处理最终失败时,后端完成快照只用 provider 原图替换占位态。
- 选中 `assetKind: "icon-spritesheet"` 图层时,图片浮动工具栏显示 `拆分图集`;手动拆分只追加独立素材,不复制原图集。
- 图标规范图写入 `assetKind: "icon-spec"`,用于刷新后保留标签和限制点选来源。
@@ -38,15 +38,15 @@
- 前端提交到 `POST /api/editor/icon-spritesheets/generations`
- 请求字段:
- `referenceImageSrc`:图标规范 Data URL。
- `referenceImageSrc`:图标规范的稳定引用(当前账号的 `objectKey`、项目资源 ID 或素材 ID);本地临时图必须先上传 OSS,禁止 Data URL / Blob URL。
- `iconDescriptions`:过滤空文本后的图标描述数组,`1..100`
- `model`:支持 `gemini-3.1-flash-image-preview`UI 显示 `nanobanana2`)和 `gpt-image-2`,默认 `nanobanana2`
- `aspectRatio`:按 `x:y` 展示,选项跟随模型。
- `imageSize`:按 `0.5K / 1K / 2K` 展示,选项跟随模型。
- `priceMudPoints`:按当前模型和尺寸从编辑器生成计费配置计算;`nanobanana2 1K``12``gpt-image-2 1K``3``gpt-image-2 2K``5`。前端只提交配置函数计算值,后端用 `editor_generation_config` 校验,不允许素材生成面板自行写死价格。
- 模型与尺寸选项:
- `nanobanana2`:比例 `1:1 / 2:3 / 3:2 / 9:16 / 16:9`;大小 `0.5K / 1K / 2K`。后端走 `/v1beta/models/{model}:generateContent`,把图标规范图作为 `inline_data`,并把 `aspectRatio` / `imageSize` 写入 `generationConfig.imageConfig``0.5K` 按 VectorEngine 文档传 `"512"`
- `gpt-image-2`:比例 `1:1 / 2:3 / 3:2 / 9:16 / 16:9`;大小 `1K / 2K`。后端走 `/v1/images/edits`,把图标规范图作为 multipart `image`,按 `size` 映射:`1K 1:1 -> 1024x1024``1K 2:3/9:16 -> 1024x1536``1K 3:2/16:9 -> 1536x1024``2K 1:1 -> 2048x2048``2K 3:2/16:9 -> 2048x1152`;文档未列出 `2K` 竖版,`2K 2:3/9:16` 后端回落到 `1024x1536`
- `nanobanana2`:比例 `1:1 / 4:3 / 3:2 / 2:3 / 9:16 / 16:9`;大小 `0.5K / 1K / 2K`。后端走 `/v1beta/models/{model}:generateContent`,把图标规范图作为 `inline_data`,并把 `aspectRatio` / `imageSize` 写入 `generationConfig.imageConfig``0.5K` 按 VectorEngine 文档传 `"512"`
- `gpt-image-2`:比例 `1:1 / 4:3 / 3:2 / 2:3 / 9:16 / 16:9`;大小 `1K / 2K`。后端走 `/v1/images/edits`,把图标规范图作为 multipart `image`。K 档按最长边计算,并转换为 provider 可直接生成的合法像素:`1K``1:1 / 4:3 / 3:2 / 2:3 / 9:16 / 16:9` 分别为 `1024x1024 / 1024x768 / 1024x688 / 688x1024 / 608x1088 / 1088x608``2K` 分别为 `2048x2048 / 2048x1536 / 2048x1376 / 1376x2048 / 1152x2048 / 2048x1152`。其中 9:16 的 1K 尺寸按 provider 最小总像素和 16 对齐约束修正。禁止把 2K 竖图回落为 1K 请求,也禁止在回图后放大伪造所选 K 档
- 用户在角色或图标素材面板中切换过模型后,下一次打开这两类面板继续使用上次模型。
- 不展示抠图背景色或抠图模型选择;前端用户路径固定提交 `screenColor=auto``segModel=birefnet`。后端在组装 prompt 前把 `auto` 自动决策为具体 hex,最多重试 3 次,失败后兜底 `#CFEFFF`,最终 prompt 和 BgFilter 不透传 `auto`
- Prompt 固定为:
@@ -59,17 +59,17 @@
## 去背与保存
- 后端收到 spritesheet 后先把带解析后纯色背景的源图写入 OSS,调用 BgFilter 透明化;BgFilter multipart 固定传 `background_mode=flat``cross_check=off`请求字段同时包含 `screenColor``segModel`。前端用户路径固定提交 `screenColor=auto` 与默认 `birefnet`,后端仍识别内部保留的 `anime-seg`,但这些内部参数不对用户可见。
- 透明背景处理正常成功时,带背景原图和去背后的透明 spritesheet 都先写入 OSS、项目资源和账号素材库,再按 alpha 连通域和素材描述顺序执行附加拆分;调用方未指定素材文件夹时统一落默认“项目”文件夹。每个成功切片单独写入 OSS、项目资源和账号素材库,`sourceResourceId` 指向透明图集资源。透明背景处理最终失败、但 provider 原图已经持久化时,任务以 `completed + warning` 收口,只把 provider 原图作为唯一主图放入画布,`generatedLayerId` 指向原图,不创建透明图集,也不继续拆分,`iconImageSrcs=[]`。该收口只捕获透明背景处理本身的最终失败;phase 上报、provider 原图持久化、透明处理图持久化和 `canvasCompletion` 写回错误仍正常传播,不能被原图降级吞掉。
- 后端收到 spritesheet 后先把带解析后纯色背景的源图写入私有 OSS并在上传完成后释放原图缓冲,再签发 600 秒 GET URL 调用 BgFilter 透明化;BgFilter multipart 固定传 `image_url``screen_color=<screenColor>``seg_model=<segModel>``background_mode=flat``cross_check=off`不包含 `file`。前端用户路径固定提交 `screenColor=auto` 与默认 `segModel=birefnet`,后端仍识别内部保留的 `anime-seg`,但这些内部参数不对用户可见。
- 透明背景处理正常成功时,带背景原图和去背后的透明 spritesheet 都先写入 OSS、项目资源和账号素材库,再按 alpha 连通域和素材描述顺序执行附加拆分;若 BgFilter 返回较小图集,只把 alpha 蒙版重采样到 provider 原图尺寸并应用回原始高分辨率 RGB,不放大低分辨率后处理成品。画布完成快照同时写入透明主图与右侧 provider 原图(二者均已登记为 project resource / 账号素材),`generatedLayerId` 仍锚定透明主图;成功拆出的切片从 provider 原图右侧继续排列。调用方未指定素材文件夹时统一落默认“项目”文件夹。每个成功切片单独写入 OSS、项目资源和账号素材库,`sourceResourceId` 指向透明图集资源。透明背景处理最终失败、但 provider 原图已经持久化时,任务以 `completed + warning` 收口,只把 provider 原图作为唯一主图放入画布,`generatedLayerId` 指向原图,不创建透明图集,也不继续拆分,`iconImageSrcs=[]`。该收口只捕获透明背景处理本身的最终失败;phase 上报、provider 原图持久化、透明处理图持久化和 `canvasCompletion` 写回错误仍正常传播,不能被原图降级吞掉。
- 自动拆分只在透明图集成功后执行,属于 best-effort 附加动作,不参与图集生成的成功判定。连通域识别或切片持久化失败时,接口仍返回并回填整张透明图集,`iconImageSrcs=[]`,并通过 `sliceWarning.code/reason` 暴露非阻断原因;`sliceWarning` 与透明背景最终失败使用的通用 `warning` 互斥,前者只表示透明图集成功但自动拆分失败,`sliceWarning.reason` 原始契约保持不变。前端在 inline、worker 队列完成和刷新恢复三条路径统一显示对应 warning toast,用户可在图集工具栏手动重试。
- 响应通过 `iconImageSrcs` 返回成功切片素材;自动生成使用用户输入的素材描述命名,UI 设计提取和手动拆分按从上到下、从左到右自动命名为 `素材 N`
- 手动拆分调用 `POST /api/editor/icon-spritesheets/slices`,只允许读取当前用户项目中的 `icon-spritesheet` 资源,不调用图片生成 provider,不扣除泥点。输入限制为单边最多 `4096` 像素、总像素最多 `2048×2048`,单次最多持久化 `64` 个切片;超限在任何切片写入前拒绝。
## 前端铺放规则
- spritesheet 图放在原占位图位置附近。
- 自动拆分素材从图右侧开始换行铺放;手动拆分使用相同布局,但保留原图集不变。
- 生成成功后关闭图标素材面板,选中 spritesheet 图,并打开图层面板。
- 透明 spritesheet 图放在原占位图位置附近;provider 带背景原图放在透明主图右侧
- 自动拆分素材从 provider 原图右侧开始换行铺放;手动拆分使用相同布局,但保留原图集不变。
- 生成成功后关闭图标素材面板,选中透明 spritesheet 图,并打开图层面板。
## 验收
@@ -78,7 +78,7 @@
- 默认 6 个素材描述会进入 prompt;用户在单个文本框中继续输入时最多解析 100 个素材描述。
- 默认打开图标素材面板时选中 `nanobanana2 / 1:1 / 1K`;模型切换后,角色和图标素材面板之间沿用上次选择的模型。
- 图标素材生成请求必须带 `model``aspectRatio``imageSize``nanobanana2` 请求体必须包含 `generationConfig.imageConfig.aspectRatio/imageSize``gpt-image-2` 请求必须包含文档映射后的 `size`
- 图标素材生成可以上传普通参考图;提交时图标规范图仍走 `referenceImageSrc`,普通参考图走 `referenceImageSrcs`上传参考图优先提交 `objectKey`,并写入 `generationInputs.references`
- 透明背景处理和自动拆分都成功后,画布同时出现透明 spritesheet 图集和按描述命名的独立图标图层;透明图集成功但拆分失败时出现透明图,透明背景处理最终失败时只出现 provider 原图。
- 选中图集图层时显示 `拆分图集`点击后不新增第二张图集,只在原图右侧追加自动识别的独立素材,并同步写入素材库。
- 图标素材生成可以上传普通参考图;提交时图标规范图仍走 `referenceImageSrc`,普通参考图走 `referenceImageSrcs`二者都必须是稳定引用(`objectKey` / 项目资源 ID / 素材 ID),禁止 Data URL / Blob URL,并写入 `generationInputs.references`
- 透明背景处理和自动拆分都成功后,画布同时出现透明 spritesheet 主图、其右侧的 provider 原图,以及从原图右侧铺开的按描述命名的独立图标图层;透明图集成功但拆分失败时出现透明主图与右侧原图,透明背景处理最终失败时只出现 provider 原图。
- 选中透明图集图层时显示 `拆分图集`点击后源图集显示扫描蒙层与 `拆图中` 状态,工具栏按钮同步切换为旋转图标和 `拆图中` 并禁用重复提交。完成后恢复工具栏,不新增第二张图集,只在 provider 原图右侧追加自动识别的独立素材,并同步写入素材库。
- 生成图标素材提交体包含按模型和尺寸计算的 `priceMudPoints``nanobanana2 1K` 应为 `12``gpt-image-2 1K` 应为 `3``gpt-image-2 2K` 应为 `5`。若前端传入与后端计费配置不一致的值,后端返回 `priceMudPoints` 校验错误,不继续调用上游生成。
@@ -55,8 +55,8 @@
- 前端用户路径固定提交 `screenColor=auto``segModel=birefnet`,不从生成器快照或输入快照恢复旧手动背景色 / 抠图模型。后端在组装 prompt 前把 `auto` 自动决策为具体 hex,最多重试 3 次,失败后兜底 `#CFEFFF`;调用 BgFilter 时只把解析后的具体 hex 作为 `screen_color` 传入。后端仍识别内部保留的 `anime-seg`,但该选项不对用户可见。
- 比例按 `x:y` 展示;大小按 `0.5K / 1K / 2K` 展示。
- 尺寸选项来源以 VectorEngine 接入文档为准:
- `nanobanana2`:比例 `1:1 / 2:3 / 3:2 / 9:16 / 16:9`;大小 `0.5K / 1K / 2K`。后端走 `/v1beta/models/{model}:generateContent`,把比例写入 `generationConfig.imageConfig.aspectRatio`,把大小写入 `generationConfig.imageConfig.imageSize`;其中 `0.5K` 按文档传 `"512"`
- `gpt-image-2`:比例 `1:1 / 2:3 / 3:2 / 9:16 / 16:9`;大小 `1K / 2K`。后端走 `/v1/images/generations``/v1/images/edits`,按文档尺寸映射:`1K 1:1 -> 1024x1024``1K 2:3/9:16 -> 1024x1536``1K 3:2/16:9 -> 1536x1024``2K 1:1 -> 2048x2048``2K 3:2/16:9 -> 2048x1152`;文档未列出 `2K` 竖版,`2K 2:3/9:16` 后端回落到 `1024x1536`
- `nanobanana2`:比例 `1:1 / 4:3 / 3:2 / 2:3 / 9:16 / 16:9`;大小 `0.5K / 1K / 2K`。后端走 `/v1beta/models/{model}:generateContent`,把比例写入 `generationConfig.imageConfig.aspectRatio`,把大小写入 `generationConfig.imageConfig.imageSize`;其中 `0.5K` 按文档传 `"512"`
- `gpt-image-2`:比例 `1:1 / 4:3 / 3:2 / 2:3 / 9:16 / 16:9`;大小 `1K / 2K`。后端走 `/v1/images/generations``/v1/images/edits`。K 档按最长边计算,并转换为 provider 可直接生成的合法像素:`1K``1:1 / 4:3 / 3:2 / 2:3 / 9:16 / 16:9` 分别为 `1024x1024 / 1024x768 / 1024x688 / 688x1024 / 608x1088 / 1088x608``2K` 分别为 `2048x2048 / 2048x1536 / 2048x1376 / 1376x2048 / 1152x2048 / 2048x1152`。其中 9:16 的 1K 尺寸按 provider 最小总像素和 16 对齐约束修正。禁止把 2K 竖图回落为 1K 请求,也禁止在回图后放大伪造所选 K 档
- 后端如果收到参考图,`nanobanana2` 把参考图作为 `inline_data` 传入原生 `generateContent``gpt-image-2` 走带多参考图的图片编辑链路。没有参考图时按所选模型走纯文本生成链路。
- `kind = "character"` 时,后端不直接把前端文本当完整生图提示词,而是把文本作为 `角色设定` 填入固定提示词骨架:
@@ -65,7 +65,7 @@
角色设定:<用户输入的角色设定>
```
- 角色图生成完成后,编辑器后端必须先把带自动决策纯色背景的源图写入 OSS,再调用共享 BgFilter 服务透明化:multipart 字段包含 `file``screen_color=<screenColor>``seg_model=<segModel>``background_mode=flat``cross_check=on`,用户路径默认并只提交 `seg_model=birefnet``flat` 明确表示单一纯色背景抠图模式,`birefnet` 是 BgFilter 管线内部后端。首次请求失败后立即重试 `1` 次,第二次仍失败进入“阿里云通用抠图 → 本地键色”降级链。角色图 prompt 按 `screenColor` 写入颜色名称、hex 和 RGB。该流程不再调用 RPG / 资产工坊的角色主图专用 `character_visual_assets` 后处理,也不复用手动去背景的 `background_mode=complex` 路径。透明背景处理正常成功时,输出透明背景 PNG,随后写入 OSS 私有对象并确认 `asset_object`;接口回包返回透明 PNG Data URL 及 `objectKey` / `assetObjectId`。三段透明背景处理最终仍失败、但 provider 原图已经持久化时,任务以 `completed + warning` 收口,只把 provider 原图作为唯一主图放入画布,`generatedLayerId` 指向原图,不创建不存在的透明处理图;通用 `warning.code/reason` 携带完整降级原因。该收口只捕获透明背景处理本身的最终失败;phase 上报、provider 原图持久化、透明处理图持久化和 `canvasCompletion` 写回错误仍正常传播,不能被原图降级吞掉。前端创建图层和画板资源记录时必须保存最终回包对应的媒体引用。
- 角色图生成完成后,编辑器后端必须先把带自动决策纯色背景的源图 owned 上传私有 OSS(消费图片字节所有权,上传完成后释放原图缓冲),再对 object key 签发 600 秒 GET URL 调用共享 BgFilter 服务透明化。BgFilter 主路径不重新下载原图;每次 HTTP attempt 重新换签,multipart 字段包含 `image_url``screen_color=<screenColor>``seg_model=<segModel>``background_mode=flat``cross_check=on`不包含 `file`用户路径默认并只提交 `seg_model=birefnet``flat` 明确表示单一纯色背景抠图模式,`birefnet` 是 BgFilter 管线内部后端。首次请求失败后立即重试 `1` 次,第二次仍失败进入“阿里云通用抠图(按签名 URL 单独下载)→ 本地键色(再按 object key 独立下载一次原图并在产出后释放)”降级链。角色图 prompt 按 `screenColor` 写入颜色名称、hex 和 RGB。该流程不再调用 RPG / 资产工坊的角色主图专用 `character_visual_assets` 后处理,也不复用手动去背景的 `background_mode=complex` 路径。透明背景处理正常成功时,输出透明背景 PNG,随后写入 OSS 私有对象并确认 `asset_object`;接口回包返回 `imageSrc: "/<objectKey>"``objectKey``assetObjectId` 及资源快照,画布同时写入透明主结果和 provider 原图,生成器 `generatedLayerId` 锚定透明主结果,provider 原图作为第二个图层放在其右侧。三段透明背景处理最终仍失败、但 provider 原图已经持久化时,任务以 `completed + warning` 收口,只把 provider 原图作为唯一主图放入画布,`generatedLayerId` 指向原图,不创建不存在的透明处理图;通用 `warning.code/reason` 携带完整降级原因。该收口只捕获透明背景处理本身的最终失败;phase 上报、provider 原图持久化、透明处理图持久化和 `canvasCompletion` 写回错误仍正常传播,不能被原图降级吞掉。前端创建图层和画板资源记录时必须保存最终回包对应的媒体引用。
-`assetKind: "character"` 的角色图层执行 `重绘` 时,前端仍使用原图作为参考图,但请求 `kind` 必须传 `character`,让后端继续套用上述角色提示词限定、角色图后处理和角色资产持久化;透明背景正常成功与最终失败保留 provider 原图的收口规则和角色新生成一致。普通图片图层重绘仍保持 `kind: "quick-edit"`
## 生成规范参考图
@@ -114,7 +114,7 @@
- 角色生成提交统一走 `/api/editor/images/generations`,按 `角色规范 -> 常规参考图` 顺序传 `referenceImageSrcs`,并写入 `assetKind: "character"`
- 角色图层重绘同样走 `/api/editor/images/generations``kind: "character"` 分支,原图作为参考图提交,生成结果继续保留 `assetKind: "character"`
- 角色和图标素材生成已接入 `nanobanana2` / `gpt-image-2` 模型切换、上次模型记忆,以及按模型归一的比例 / 大小尺寸;`nanobanana2` 使用原生 `generateContent``imageConfig.aspectRatio/imageSize``gpt-image-2` 使用文档列出的 `size` 字符串。
- 角色生成后端已按固定 prompt 骨架补入 `角色设定` 和自动决策纯色抠图背景,并在生成成功后先保存纯色背景源图,再通过 BgFilter 按用户路径默认 `segModel=birefnet` 执行透明化;透明化成功时把处理图写入 `generated-character-drafts/editor/character-images/<taskId>/image.png` 路径下的 OSS 私有对象,最终失败时则保留并返回已经持久化的 provider 原图和通用 warning。最终回包的 `objectKey` / `assetObjectId` 会随画板资源记录保存。
- 角色生成后端已按固定 prompt 骨架补入 `角色设定` 和自动决策纯色抠图背景,并在生成成功后先保存纯色背景源图,再通过 BgFilter 按用户路径默认 `segModel=birefnet` 执行透明化;透明化成功时把处理图写入 `generated-character-drafts/editor/character-images/<taskId>/image.png` 路径下的 OSS 私有对象,并把透明主结果与其右侧 provider 原图一起写入画布,`generatedLayerId` 仍锚定透明主结果;最终失败时则保留并返回已经持久化的 provider 原图和通用 warning。若 BgFilter 返回较小图片,只允许把其 alpha 蒙版重采样到 provider 原图尺寸并应用回原始高分辨率 RGB,不得放大低分辨率透明成品。最终回包的 `objectKey` / `assetObjectId` 会随画板资源记录保存。
- `Esc` 只退出角色规范画布点选状态,不关闭角色生成面板。
- 已补充回归测试覆盖角色形象生成、点选退出、角色动画入口隔离和快速编辑入口。
- 本次验证命令:
@@ -162,7 +162,7 @@
- 视频生成完成后,后端先把带纯色背景的预览视频登记为 OSS 私有对象、`asset_object`、项目资源和账号素材,再按面板选择抽取对应帧数:`32``40``48`。未传 `assetFolderId` 时进入默认“项目”素材文件夹;后续抽帧或抠图失败不能抹掉这份已经生成成功的可恢复视频。
- 抽帧采样必须按目标帧数预留视频尾部安全步长,例如 `32帧·4秒` 最后一帧采 `3.875s`,避免 FFmpeg 在尾点附近返回成功但输出 `0` 帧。
- 每帧必须先把带自动决策纯色背景的源图写入 OSS,再走 `BgFilterbackground_mode=flatseg_model=birefnetcross_check=on)→ 阿里云通用抠图 → 本地 editor_green_screen`,并按同一次生成已选定的 `screenColor` 去背。BgFilter 每一次 HTTP attempt 的 timeout 使用“`GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS` 基准值 + `2000ms × 本次实际帧数`”,默认 `32 / 40 / 48` 帧分别为 `244000 / 260000 / 276000ms`;首次失败后重试 `1` 次。
- 每帧绿幕源图字节由上传 owned 消费(`frame.bytes` 移入 put,上传完成后释放原帧缓冲,不克隆保留);后续只持 object key。每次 BgFilter attempt 重新签发 600 秒 GET URLmultipart 仅传 `image_url`(加 `background_mode=flat``seg_model=birefnet``cross_check=on` 与同一次生成已选定的 `screenColor`),不传 `file`。BgFilter 主路径不重新下载原帧;失败后走 `阿里云通用抠图(按签名 URL 单独下载)→ 本地 editor_green_screen(再按 object key 独立下载一次并在产出后释放)`。BgFilter 每一次 HTTP attempt 的 timeout 使用“`GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS` 基准值 + `2000ms × 本次实际帧数`”,默认 `32 / 40 / 48` 帧分别为 `244000 / 260000 / 276000ms`;首次失败后重试 `1` 次。
- 全部 `32 / 40 / 48` 帧以覆盖本次所有帧的无序在途集合连续发射,允许乱序完成并最终按 `frameIndex` 排序;任一帧最终失败时先排空全部已启动 Future,再让整项任务失败退款,不发布缺帧动画。
- 抽帧结果写入 OSS,并返回帧路径、帧尺寸、帧数、fps、预览视频路径、模型、价格和实际 prompt。
- 画板前端回填角色动作结果时,必须以 `frames[0].imageSrc` 创建 `mediaType: "image-sequence"``assetKind: "character-animation"` 图层,并把完整 `frames` 保存为图层 `imageSequenceFrames``previewVideoPath` 只保留为上游预览视频来源,不作为画布主媒体。
+83 -98
View File
@@ -1,38 +1,18 @@
// 画布Agent对话契约:会话元数据存 SpacetimeDB,消息正文整体存 OSSeditor-agent/{conversationId}.json)。
export const EDITOR_AGENT_MAX_ATTACHMENTS = 9;
export const EDITOR_AGENT_TITLE_MAX_CHARS = 20;
export const EDITOR_AGENT_DEFAULT_CONVERSATION_TITLE = '新对话';
export const EDITOR_AGENT_MESSAGES_DOCUMENT_VERSION = 1;
export const EDITOR_AGENT_ERROR_MESSAGE_PREFIX = 'ERROR ';
export type EditorAgentStage =
| 'idle'
| 'thinking'
| 'responding'
| 'generating'
| 'completed'
| 'failed';
export type EditorAgentMessageRole = 'user' | 'assistant' | 'system';
export type EditorAgentMessageRole = 'user' | 'assistant';
export type EditorAgentMessageKind = 'chat' | 'stage' | 'error';
export type EditorAgentMessageStatus =
| 'streaming'
| 'generating'
export type EditorAgentToolCallStatus =
| 'not_completed'
| 'completed'
| 'failed'
| 'stopped';
| 'cancelled';
export type EditorAgentAttachmentSource = 'canvas_resource' | 'library_asset';
export type EditorAgentToolName =
| 'generate_image'
| 'edit_image'
| 'generate_character'
| 'generate_icon_spritesheet'
| 'generate_ui_design';
export interface EditorAgentAttachmentRef {
source: EditorAgentAttachmentSource;
referenceId: string;
@@ -45,36 +25,87 @@ export interface EditorAgentAttachmentRef {
}
export interface EditorAgentGeneratedImage {
resourceId: string | null;
resourceId?: string | null;
objectKey?: string | null;
assetObjectId?: string | null;
imageSrc: string;
thumbnailSrc: string | null;
width: number | null;
height: number | null;
thumbnailSrc?: string | null;
width?: number | null;
height?: number | null;
}
export type EditorAgentGenerationStatus = 'generating' | 'completed' | 'failed';
export interface EditorAgentGeneratedVideo {
resourceId?: string | null;
objectKey?: string | null;
assetObjectId?: string | null;
videoSrc: string;
thumbnailSrc?: string | null;
width?: number | null;
height?: number | null;
}
export interface EditorAgentGenerationRecord {
toolCallId: string;
toolName: EditorAgentToolName;
summary?: string | null;
taskId: string | null;
status: EditorAgentGenerationStatus;
model: string | null;
export interface EditorAgentGeneratedAudio {
resourceId?: string | null;
objectKey?: string | null;
assetObjectId?: string | null;
audioSrc: string;
}
export interface EditorAgentToolCallStringArg {
name: string;
label: string;
value: string;
}
export interface EditorAgentToolCallImageRef {
imageId: string;
imageSrc: string;
objectKey?: string | null;
thumbnailSrc?: string | null;
label?: string | null;
width?: number | null;
height?: number | null;
}
export interface EditorAgentToolCallImageArg {
name: string;
label: string;
refs: EditorAgentToolCallImageRef[];
}
export interface EditorAgentToolCallDisplayExtras {
priceMudPoints: number;
}
export interface EditorAgentToolCallDisplayArgs {
stringArgs: EditorAgentToolCallStringArg[];
imageArgs: EditorAgentToolCallImageArg[];
extras: EditorAgentToolCallDisplayExtras;
}
export interface EditorAgentToolCall {
toolName: string;
status: EditorAgentToolCallStatus;
args: unknown;
displayArgs: EditorAgentToolCallDisplayArgs;
externalJobId?: string | null;
images: EditorAgentGeneratedImage[];
// Older persisted conversation documents do not contain these media fields.
videos?: EditorAgentGeneratedVideo[];
audios?: EditorAgentGeneratedAudio[];
error?: string | null;
}
export interface EditorAgentMessage {
id: string;
// Frontend must not use this to organize messages.
// It is an opaque backend locator for pending tool-call operations.
id: number;
// Present only on user messages created from a client send request.
clientMessageId?: string | null;
role: EditorAgentMessageRole;
kind: EditorAgentMessageKind;
text: string;
attachments: EditorAgentAttachmentRef[];
generations: EditorAgentGenerationRecord[];
status: EditorAgentMessageStatus;
toolCall: EditorAgentToolCall | null;
createdAt: string;
}
@@ -82,12 +113,15 @@ export interface EditorAgentConversationSummary {
conversationId: string;
projectId: string;
title: string;
createdAt: string;
updatedAt: string;
}
export interface EditorAgentConversationDetail
extends EditorAgentConversationSummary {
export interface EditorAgentConversationDetail {
conversationId: string;
projectId: string;
title: string;
createdAt: string;
updatedAt: string;
messages: EditorAgentMessage[];
}
@@ -109,63 +143,14 @@ export interface EditorAgentConversationResponse {
conversation: EditorAgentConversationDetail;
}
export interface StreamEditorAgentMessageRequest {
export interface EditorAgentMessageRequest {
clientMessageId: string;
text: string;
attachments?: EditorAgentAttachmentRef[];
}
export interface EditorAgentStageEvent {
conversationId: string;
stage: EditorAgentStage;
export interface EditorAgentMessageResponse {
conversation: EditorAgentConversationSummary;
deltaMessages: EditorAgentMessage[];
errorMessage: string | null;
}
export interface EditorAgentMessageDeltaEvent {
conversationId: string;
messageId: string;
role: EditorAgentMessageRole;
kind: EditorAgentMessageKind;
textDelta: string;
}
export interface EditorAgentToolEvent {
conversationId: string;
messageId: string;
toolCallId: string;
toolName: EditorAgentToolName;
summary?: string | null;
taskId?: string | null;
model?: string | null;
status?: EditorAgentGenerationStatus | null;
error?: string | null;
}
export interface EditorAgentGenerationResultEvent {
conversationId: string;
messageId: string;
toolCallId: string;
toolName: EditorAgentToolName;
model: string | null;
images: EditorAgentGeneratedImage[];
}
export interface EditorAgentErrorEvent {
conversationId: string | null;
code: string;
message: string;
recoverable: boolean;
}
export interface EditorAgentDoneEvent {
conversationId: string;
title: string | null;
}
export type EditorAgentSseEvent =
| { event: 'stage'; data: EditorAgentStageEvent }
| { event: 'message_delta'; data: EditorAgentMessageDeltaEvent }
| { event: 'tool_started'; data: EditorAgentToolEvent }
| { event: 'tool_completed'; data: EditorAgentToolEvent }
| { event: 'generation_result'; data: EditorAgentGenerationResultEvent }
| { event: 'error'; data: EditorAgentErrorEvent }
| { event: 'done'; data: EditorAgentDoneEvent };
+2 -1
View File
@@ -921,7 +921,8 @@ function readLinuxApiServerProcessSnapshot(pid) {
if (
error?.code === 'ENOENT' ||
error?.code === 'EACCES' ||
error?.code === 'EPERM'
error?.code === 'EPERM' ||
error?.code === 'ESRCH'
) {
return null;
}
+14 -3
View File
@@ -238,6 +238,7 @@ dependencies = [
"platform-agent",
"platform-audio",
"platform-auth",
"platform-editor-agent",
"platform-hyper3d",
"platform-image",
"platform-llm",
@@ -4441,6 +4442,19 @@ dependencies = [
"urlencoding",
]
[[package]]
name = "platform-editor-agent"
version = "0.1.0"
dependencies = [
"hmac",
"platform-agent",
"platform-llm",
"serde",
"serde_json",
"sha2",
"shared-contracts",
]
[[package]]
name = "platform-hyper3d"
version = "0.1.0"
@@ -4481,18 +4495,15 @@ dependencies = [
name = "platform-matting"
version = "0.1.0"
dependencies = [
"base64 0.22.1",
"dotenvy",
"hex",
"hmac",
"httpdate",
"image",
"platform-oss",
"reqwest 0.12.28",
"serde",
"serde_json",
"serde_urlencoded",
"sha1",
"sha2",
"time",
"tokio",
+2
View File
@@ -42,6 +42,7 @@ members = [
"crates/platform-wechat",
"crates/platform-speech",
"crates/platform-agent",
"crates/platform-editor-agent",
"crates/pingora-gateway",
"crates/server-manager-panel",
"crates/shared-contracts",
@@ -84,6 +85,7 @@ module-square-hole = { path = "crates/module-square-hole", default-features = fa
module-story = { path = "crates/module-story", default-features = false }
module-visual-novel = { path = "crates/module-visual-novel", default-features = false }
platform-agent = { path = "crates/platform-agent", default-features = false }
platform-editor-agent = { path = "crates/platform-editor-agent", default-features = false }
platform-auth = { path = "crates/platform-auth", default-features = false }
platform-audio = { path = "crates/platform-audio", default-features = false }
platform-hyper3d = { path = "crates/platform-hyper3d", default-features = false }
+1
View File
@@ -38,6 +38,7 @@ module-square-hole = { workspace = true }
module-story = { workspace = true }
module-visual-novel = { workspace = true }
platform-agent = { workspace = true }
platform-editor-agent = { workspace = true }
platform-audio = { workspace = true }
platform-auth = { workspace = true }
platform-hyper3d = { workspace = true }
+122 -13
View File
@@ -1,21 +1,20 @@
//! 阿里云通用抠图在 api-server 侧的适配层。
//!
//! 输入 URL 策略:抠图服务只认上海地域 OSS URL,而我们没有上海地域自有 OSS
//! 统一由 platform-matting 上传 VIAPI 官方临时桶(1 天自动过期,无需清理)。
//! 输入统一由 platform-matting 通过 AuthorizeFileUpload 单对象 Policy 上传动态临时 OSS
use axum::http::StatusCode;
use platform_matting::MattingError;
use serde_json::json;
use serde_json::{Value, json};
use crate::{
http_error::AppError, openai_image_generation::DownloadedOpenAiImage, state::AppState,
};
/// 图片字节 → 阿里云通用抠图 → 原尺寸透明 PNG。
/// 未配置抠图客户端时返回错误,由调用方决定是否降级本地算法
pub(crate) async fn segment_image_with_aliyun_matting(
/// 私有 OSS 签名 URL → 延迟下载 → 阿里云临时桶 → 原尺寸透明 PNG。
/// 下载和临时上传由 platform-matting 收口,源图缓冲不会跨越整次阿里云推理常驻
pub(crate) async fn segment_image_url_with_aliyun_matting(
state: &AppState,
image: &DownloadedOpenAiImage,
image_url: &str,
log_label: &str,
) -> Result<DownloadedOpenAiImage, AppError> {
let matting_client = state
@@ -25,7 +24,7 @@ pub(crate) async fn segment_image_with_aliyun_matting(
let file_name = format!("{log_label}.png");
let started_at = std::time::Instant::now();
let output_bytes = matting_client
.segment_image_to_transparent_png(image.bytes.as_slice(), &file_name)
.segment_image_url_to_transparent_png(image_url, &file_name)
.await
.map_err(|error| {
aliyun_matting_failure_to_app_error(&error, started_at.elapsed().as_millis() as u64)
@@ -34,7 +33,7 @@ pub(crate) async fn segment_image_with_aliyun_matting(
provider = "aliyun-matting",
log_label,
elapsed_ms = started_at.elapsed().as_millis() as u64,
"阿里云通用抠图完成"
"阿里云通用抠图 URL 输入完成"
);
Ok(DownloadedOpenAiImage {
@@ -48,18 +47,36 @@ pub(crate) async fn segment_image_with_aliyun_matting(
///
/// 分类(是否外部调用、超时、传输层故障、上游 HTTP 状态)由 platform-matting 在错误发生处
/// 结构化捕获,这里只做协议中立的读取,不再从中文 message 反推——外部供应商协议归属留在
/// platform-* 层。`InvalidConfig` / `InvalidRequest` / `Sign` 是发请求前的本地预检失败
/// `external_call_attempted()` 为 false,据此跳过外部失败审计。
/// platform-* 层。`InvalidConfig` / `InvalidRequest` / `Sign` 是尚未开始外部调用的本地预检失败
/// URL 链路在 OSS GET 成功后发生的解码、尺寸或其它本地处理失败由 `LocalProcessing` 表示,
/// 仍然需要进入外部失败审计,但不得包装成可重试的上游 5xx。
fn aliyun_matting_failure_to_app_error(error: &MattingError, latency_ms: u64) -> AppError {
let message = error.message();
if !error.external_call_attempted() {
// 本地预检失败(未配置 / 图片解码失败 / 尺寸过小 / 签名构造失败),未触达阿里云
// 本地预检失败(未配置 / 尚未下载源图前的参数或签名错误),未触达外部调用链路
return AppError::from_status(StatusCode::UNPROCESSABLE_ENTITY).with_details(json!({
"provider": "aliyun-matting",
"message": message,
"timeout": false,
"transport": false,
"localProcessing": false,
"externalCallAttempted": false,
"failureStage": error.failure_stage(),
"latencyMs": latency_ms,
"rawExcerpt": message.chars().take(500).collect::<String>(),
}));
}
// 外部调用已开始,但失败在本地解码 / 校验 / 归一等阶段:要审计,不能标成上游 5xx / 可重试。
if matches!(error, MattingError::LocalProcessing(_)) {
return AppError::from_status(StatusCode::UNPROCESSABLE_ENTITY).with_details(json!({
"provider": "aliyun-matting",
"message": message,
"timeout": false,
"transport": false,
"localProcessing": true,
"upstreamStatus": Value::Null,
"externalCallAttempted": true,
"failureStage": error.failure_stage(),
"latencyMs": latency_ms,
"rawExcerpt": message.chars().take(500).collect::<String>(),
}));
@@ -75,7 +92,10 @@ fn aliyun_matting_failure_to_app_error(error: &MattingError, latency_ms: u64) ->
"message": message,
"timeout": timeout,
"transport": error.is_transport(),
"localProcessing": false,
"upstreamStatus": error.upstream_status(),
"externalCallAttempted": true,
"failureStage": error.failure_stage(),
"latencyMs": latency_ms,
"rawExcerpt": message.chars().take(500).collect::<String>(),
}))
@@ -86,6 +106,7 @@ fn aliyun_matting_unconfigured_error() -> AppError {
"provider": "aliyun-matting",
"message": "阿里云抠图客户端未配置或未启用。",
"externalCallAttempted": false,
"failureStage": "preflight",
}))
}
@@ -100,6 +121,21 @@ mod tests {
assert!(!crate::external_api_audit::matting_failure_external_call_attempted(&error));
}
#[test]
fn url_input_path_delegates_download_and_temp_upload_to_platform_matting() {
let source = include_str!("aliyun_matting.rs");
let start = source
.find("pub(crate) async fn segment_image_url_with_aliyun_matting")
.expect("URL input adapter should exist");
let tail = &source[start..];
let end = tail
.find("/// 把 platform-matting 的错误映射")
.expect("URL input adapter should end before error mapper");
let body = &tail[..end];
assert!(body.contains("segment_image_url_to_transparent_png"));
}
#[test]
fn local_preflight_failures_do_not_count_as_external_call() {
for error in [
@@ -108,7 +144,7 @@ mod tests {
),
MattingError::InvalidRequest("解析待抠图图片失败:invalid png".to_string()),
MattingError::InvalidConfig("endpoint 为空".to_string()),
MattingError::Sign("初始化 OSS V1 签名失败".to_string()),
MattingError::Sign("构造 AuthorizeFileUpload 签名失败".to_string()),
] {
let mapped = aliyun_matting_failure_to_app_error(&error, 3);
@@ -129,6 +165,79 @@ mod tests {
}
}
#[test]
fn local_processing_after_source_download_is_audited_with_failure_stage() {
for (error, expected_stage) in [
(
MattingError::InvalidRequest("解析待抠图图片失败:invalid png".to_string())
.with_failure_stage("source_decode"),
"source_decode",
),
(
MattingError::InvalidRequest("待抠图图片尺寸 16x16 过小".to_string())
.with_failure_stage("source_validate"),
"source_validate",
),
] {
let mapped = aliyun_matting_failure_to_app_error(&error, 3);
assert!(crate::external_api_audit::matting_failure_external_call_attempted(&mapped));
// 本地处理失败要审计,但 HTTP 包装不得落成可重试 5xx。
assert_eq!(mapped.status_code(), StatusCode::UNPROCESSABLE_ENTITY);
let details = mapped.details().expect("details present");
assert_eq!(
details
.get("externalCallAttempted")
.and_then(|v| v.as_bool()),
Some(true)
);
assert_eq!(
details.get("localProcessing").and_then(|v| v.as_bool()),
Some(true)
);
assert_eq!(
details.get("transport").and_then(|v| v.as_bool()),
Some(false)
);
assert!(
details
.get("upstreamStatus")
.is_none_or(|value| value.is_null())
);
assert_eq!(
details.get("failureStage").and_then(|v| v.as_str()),
Some(expected_stage)
);
assert_eq!(
crate::external_api_audit::matting_failure_audit_failure_stage(
&mapped,
"aliyun_segment",
),
expected_stage
);
assert_eq!(
crate::external_api_audit::matting_failure_audit_status_code(&mapped),
None,
"本地处理失败没有上游 HTTP 状态,不能回退包装码"
);
let draft = crate::external_api_audit::build_matting_external_api_failure_draft(
"aliyun-matting",
"imageseg.example".to_string(),
"editor-screen-background-removal",
expected_stage,
crate::external_api_audit::matting_failure_audit_status_code(&mapped),
crate::external_api_audit::matting_failure_audit_timeout(&mapped),
crate::external_api_audit::matting_failure_audit_is_transport(&mapped),
crate::external_api_audit::matting_failure_audit_latency_ms(&mapped),
mapped.message().to_string(),
crate::external_api_audit::matting_failure_audit_raw_excerpt(&mapped),
&crate::external_api_audit::ExternalApiAuditContext::default(),
);
assert_eq!(draft.status_class, Some("local"));
assert!(!draft.retryable);
}
}
#[test]
fn upstream_transport_failure_maps_to_retryable_transport() {
let error = MattingError::upstream_transport_error(
@@ -79,7 +79,6 @@ use crate::{
resolve_editor_screen_background_color,
},
http_error::AppError,
openai_image_generation::DownloadedOpenAiImage,
platform_errors::map_oss_error,
prompt::role_asset_studio::{
build_role_asset_workflow, normalize_animation_prompt_text_by_key,
@@ -2418,7 +2417,7 @@ async fn process_and_persist_editor_character_animation_frame(
audit: &crate::external_api_audit::ExternalApiAuditContext,
) -> Result<ProcessedEditorCharacterAnimationFrame, AppError> {
// 中文注释:每一帧只要求自己的绿幕源图先落 OSS,不再等待整批源图全部上传完成。
put_character_animation_object(
let source_put = put_character_animation_object(
state,
LegacyAssetPrefix::Animations,
vec![
@@ -2432,7 +2431,7 @@ async fn process_and_persist_editor_character_animation_frame(
frame.extension
),
frame.mime_type.clone(),
frame.bytes.clone(),
frame.bytes,
build_asset_metadata(
EDITOR_CHARACTER_ANIMATION_ASSET_KIND,
owner_user_id,
@@ -2444,14 +2443,9 @@ async fn process_and_persist_editor_character_animation_frame(
)
.await?;
let image = DownloadedOpenAiImage {
bytes: frame.bytes,
mime_type: frame.mime_type,
extension: frame.extension,
};
let removed = remove_editor_generated_screen_background_with_bgfilter_with_request_timeout(
state,
&image,
source_put.object_key.as_str(),
screen_color,
EDITOR_BGFILTER_DEFAULT_SEG_MODEL,
EDITOR_BGFILTER_CROSS_CHECK_ENABLED,
@@ -6223,6 +6217,7 @@ mod tests {
"put_character_animation_object",
"green-screen-frame",
"remove_editor_generated_screen_background_with_bgfilter_with_request_timeout",
"source_put.object_key.as_str()",
"bgfilter_request_timeout_ms",
"finalize_animation_frame_payload",
"put_character_animation_object",
@@ -6244,6 +6239,13 @@ mod tests {
"finalize_animation_frame_payload",
],
);
let frame_pipeline = source
.split_once("async fn process_and_persist_editor_character_animation_frame")
.and_then(|(_, tail)| tail.split_once("async fn publish_animation_set"))
.map(|(body, _)| body)
.expect("frame pipeline function should exist");
assert!(!frame_pipeline.contains("frame.bytes.clone()"));
assert!(!frame_pipeline.contains("DownloadedOpenAiImage"));
}
#[test]
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More