Compare commits

...

70 Commits

Author SHA1 Message Date
k88936 97b0a4b9b8 更新测试用例以反映按钮名称从 "gpt-image-2" 修改为 "GPT Image 2.5"
Project CI / AI game creator shell Rust crates (pull_request) Successful in 3m0s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 3m50s
Project CI / Backend tests (pull_request) Successful in 5m25s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 9m9s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 10m17s
Project CI / Native shell tests (pull_request) Successful in 7m0s
Project CI / Frontend tests (pull_request) Failing after 3m41s
Project CI / Repository checks (pull_request) Successful in 3m16s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m51s
2026-09-21 22:09:17 +08:00
k88936 e3ba986596 修正 OpenAPI 中 model 字段与实现不一致的描述
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
- EditorImageGenerationRequest.model 说明 kind=ui-design 与 publication-material 固定使用 gpt-image-2.5 并忽略传入 model,其余 kind 未传时默认 gpt-image-2.5
- EditorUiDesignAssetExtractionRequest.model 说明未传时默认 nanobanana,传 gpt-image-2.5 时按 GPT Image 2.5 编辑任务执行
2026-09-21 22:02:44 +08:00
k88936 69af571242 修订生成带参考图的模型路由与计价文档
- ADR 改为「带参考图的生成与编辑都按编辑 concrete model 执行、审计与计价」,并补定价档位跟随 concrete model 的后果
- 里程碑文档路由表拆分 flare-c(仅无参考图生成)与 sunburst-c(编辑 + 带参考图的生成),必须成立的行为补第 7 条
- decision-log 修正 2026-09-18 条目,并新增本次「生成带参考图统一按编辑 concrete model 执行与计价」决策记录
- pitfalls、后端架构与开发运维文档同步 provider 具体模型与计价档位口径
2026-09-21 22:02:05 +08:00
k88936 166977baf0 图片素材脚本改用 concrete model 与 Tiantoken 凭据
- generate-template-samples 与 generate-anthro-cat-illustrations 的 preferredImageModel 改为 gpt-image-2.5-flare-c
- 两个脚本的 base URL / API key / 超时改为只读 TIANTOKEN_BASE_URL、TIANTOKEN_API_KEY、TIANTOKEN_IMAGE_REQUEST_TIMEOUT_MS,不再回退 VectorEngine
- 脚本内 URL 构造函数、错误文案与错误字段名从 VectorEngine 改为 provider 中性的 Tiantoken 口径
- SKILL.md 请求契约、环境变量与模型说明同步到 concrete model + Tiantoken,并说明带参考图的编辑走 gpt-image-2.5-sunburst-c
2026-09-21 22:01:44 +08:00
k88936 5c58830545 删除仅测试使用的图片编辑封装
- 删除 create_openai_image_edit 与 create_openai_image_edit_with_references 两个 dead_code 封装
- 空参考图本地校验用例改走 create_openai_image_edit_with_references_and_model,保留原覆盖
2026-09-21 22:01:21 +08:00
k88936 14c2255d54 补 provider 白名单的历史值断言
- resolve_image_provider 单测增加 gpt-image-2 → Tiantoken 断言,钉死 ADR 记录的历史可读值意图
2026-09-21 22:00:58 +08:00
k88936 f8f0479488 图片兜底 task id 前缀跟随实际 provider
- executor 新增 image_task_id_prefix,按 settings.provider 拼接前缀
- 生成、nanobanana 生成与编辑三处不再硬编码 vector-engine 前缀,Tiantoken 任务的持久化与审计不再带错误前缀
2026-09-21 22:00:40 +08:00
k88936 a06c7d7f1e raw image edit 发送前复用共享 provider 白名单校验
- platform-image 导出 ensure_provider_matches_model 供 crate 内复用
- create_raw_image_edit 在构造请求前校验 settings.provider 与固定的编辑 concrete model 一致
2026-09-21 22:00:21 +08:00
k88936 c38e4b4175 生成带参考图统一按编辑 concrete model 执行与计价
- 新增 editor_image_generation_concrete_model 作为生成类任务选择 concrete model 的唯一入口,dispatch 与计价共用同一判据
- image_generation_mud_points 增加 has_reference_images,命中编辑 concrete model 时改走 image_edit_model_mud_points
- editor_project/editor_project_icon 的 provider_model 改用共享解析函数,替换三处重复的带参考图 if 表达式
- character_visual_assets 的角色主形象生成改用共享解析函数
- 图标素材图集、UI 素材提取、图标规范带 referenceId、Agent 生成类工具带 referenceImageIds 的计价同步切到编辑档
- EditImageTool 计价从生成档改为编辑档,与直连快速编辑、raw edit 对齐
- normalize_editor_generation_options 的具体 provider key 改用常量替换字符串字面量
- 补充参考图档位路由与 Agent 工具档位路由单测
2026-09-21 22:00:08 +08:00
k88936 f3c362b1ec 整体删除已退役的 gpt-image-2-c
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m59s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 3m47s
Project CI / Backend tests (pull_request) Successful in 5m4s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 9m30s
Project CI / Native shell tests (pull_request) Successful in 6m36s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 10m48s
Project CI / Frontend tests (pull_request) Failing after 3m14s
Project CI / Repository checks (pull_request) Successful in 2m46s
Project CI / AI game creator shell web tests (pull_request) Failing after 2m24s
- 删除 platform-image 的 GPT_IMAGE_2_C_MODEL 常量、re-export、尺寸语义与审计标签
- 移除 api-server 三处任务边界对 gpt-image-2-c 的兼容解析分支
- 移除主站前端历史模型常量与恢复分支,并同步其测试
- 同步 External v1 OpenAPI 描述、ADR、CONTEXT、pitfalls 与后端/运维文档
- 数据库既有 gpt-image-2-c 审计字符串保持原样,不回写不改写
2026-09-21 20:37:43 +08:00
k88936 fc53f362e4 同步图片 provider 路由白名单与启动日志文档
ADR 补充业务名与 gpt-image-2-c 的拒绝规则

决策记录补充启动报错点名环境变量与启动日志
2026-09-21 20:37:39 +08:00
k88936 8988e91ee9 补全图片 provider 启动报错与日志信息
缺少 BASE_URL / API_KEY 时点名对应环境变量

启动成功时记录 provider、base_url 与超时配置
2026-09-21 20:37:21 +08:00
k88936 784f38220b 图片 provider 路由拒绝已退役的 gpt-image-2-c
gpt-image-2-c 仅保留历史 fallback 与审计字符串

resolve_image_provider 白名单不再接受该值

补充拒绝断言
2026-09-21 20:37:21 +08:00
k88936 44798ba380 收紧图片 provider 路由白名单拒绝业务模型名
resolve_image_provider 不再接受 gpt-image-2.5 业务名

业务名必须先在任务边界解析成 flare-c / sunburst-c 具体 key

新增白名单回归测试
2026-09-21 20:37:21 +08:00
k88936 2d679122cf 合并前端历史图片模型判定与归一
抽取 LEGACY_EDITOR_IMAGE_MODELS 作为单一来源

normalizeEditorImageModel 与 isLegacyEditorImageModel 复用该集合
2026-09-21 20:37:21 +08:00
k88936 4cbbcecaa2 减少图片工具参数归一的冗余 JSON 拷贝
normalize_legacy_image_model_args 改为按需返回 Cow

未命中归一时复用原始参数视图,避免多一次深拷贝
2026-09-21 20:37:21 +08:00
k88936 98bfa5efd0 按模型解析图片编辑缺失参考图的 provider
create_openai_image_edit_with_references_and_model 错误归属与路由一致
2026-09-21 20:37:21 +08:00
k88936 ab472b4562 删除无调用方的旧图片生成封装
移除 create_openai_image_generation 及其未再用到的导入
2026-09-21 20:37:21 +08:00
k88936 1a1f124acd 公开定价投影隐藏旧 GPT Image 2-C 别名
public_projection 同步移除 gpt-image-2-c 键
2026-09-21 20:37:21 +08:00
k88936 4dc199cf33 修正 nanobanana 端点空模型的回落值
nanobanana_generate_content_url 空模型回落到 nanobanana 模型

新增空模型路径的单元测试
2026-09-21 20:37:21 +08:00
k88936 ef34e0ed64 统一 nanobanana 执行器的 provider 标签
取消 create_nanobanana_generate_content 中硬编码的 VectorEngine

错误、重试与审计统一读取 settings.provider
2026-09-21 20:37:21 +08:00
k88936 786898f7f8 移除模板样例试运行结果的空回退字段
干运行输出只保留实际请求信息
2026-09-21 20:37:21 +08:00
k88936 e880f58723 移除猫插画试运行结果的空回退字段
干运行输出只保留实际请求信息
2026-09-21 20:37:21 +08:00
k88936 6f19e155ca 清理模板样例脚本的单模型请求循环
移除单元素模型循环和无操作异常捕获

保留原有请求与错误语义
2026-09-21 20:37:21 +08:00
k88936 6e9f991a76 清理猫插画脚本的单模型请求循环
移除单元素模型循环和无操作异常捕获

保留原有请求与错误语义
2026-09-21 20:37:21 +08:00
lhk229 a6f99c8871 Merge branch 'master' into feat/gptimage2to2.5
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m20s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m59s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 10m32s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 8m27s
Project CI / Backend tests (pull_request) Successful in 5m36s
Project CI / Native shell tests (pull_request) Successful in 8m0s
Project CI / Frontend tests (pull_request) Failing after 4m13s
Project CI / AI game creator shell web tests (pull_request) Failing after 3m55s
Project CI / Repository checks (pull_request) Successful in 4m7s
2026-09-21 19:47:37 +08:00
k88936 27e3d9bfd0 Merge remote-tracking branch 'origin/master' into feat/gptimage2to2.5
Project CI / AI game creator shell Rust crates (pull_request) Successful in 3m3s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 3m55s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Failing after 7m25s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Failing after 8m18s
Project CI / Backend tests (pull_request) Successful in 8m6s
Project CI / Native shell tests (pull_request) Successful in 9m16s
Project CI / Frontend tests (pull_request) Failing after 7m0s
Project CI / Repository checks (pull_request) Successful in 7m8s
Project CI / AI game creator shell web tests (pull_request) Failing after 5m16s
2026-09-21 14:43:20 +08:00
k88936 c6f90c5b22 Merge branch 'master' into feat/gptimage2to2.5
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m49s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 3m47s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Failing after 7m42s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Failing after 8m24s
Project CI / Backend tests (pull_request) Successful in 7m13s
Project CI / Frontend tests (pull_request) Failing after 4m19s
Project CI / Native shell tests (pull_request) Successful in 9m35s
Project CI / Repository checks (pull_request) Successful in 5m57s
Project CI / AI game creator shell web tests (pull_request) Failing after 5m48s
2026-09-21 13:39:16 +08:00
k88936 3b137ca369 合并 master 并保留 GPT Image 2.5 与外置提示词行为
Project CI / AI game creator shell Rust crates (pull_request) Successful in 3m1s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 3m54s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Failing after 6m52s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Failing after 8m1s
Project CI / Native shell tests (pull_request) Successful in 9m25s
Project CI / Backend tests (pull_request) Successful in 10m37s
Project CI / Repository checks (pull_request) Successful in 7m52s
Project CI / Frontend tests (pull_request) Failing after 10m40s
Project CI / AI game creator shell web tests (pull_request) Failing after 5m25s
保留编辑器图片工具使用 gpt-image-2.5 业务模型名

保留工具参数说明从 prompts 外置文件读取

同步外置模型与尺寸文案并完成冲突验证
2026-09-21 10:50:32 +08:00
k88936 3da0936c1f Merge remote-tracking branch 'refs/remotes/origin/master' into feat/gptimage2to2.5
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 7m17s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 7m25s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 7m34s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 7m37s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m3s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 3m23s
Project CI / Frontend tests (pull_request) Failing after 14m1s
Project CI / Repository checks (pull_request) Successful in 12m35s
Project CI / Native shell tests (pull_request) Successful in 17m2s
Project CI / Backend tests (pull_request) Successful in 17m18s
Project CI / AI game creator shell web tests (pull_request) Failing after 14m48s
2026-09-19 15:39:41 +08:00
k88936 c8311a47b0 Merge remote-tracking branch 'refs/remotes/origin/master' into feat/gptimage2to2.5
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Has been cancelled
# Conflicts:
#	docs/project-memory/shared-memory/decision-log.md
#	server-rs/crates/api-server/src/state.rs
#	src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts
2026-09-19 15:37:29 +08:00
k88936 1c1deff732 修复编辑器 LLM mock 用例无法构造 Tiantoken client 的问题
- AppConfig 新增 tiantoken_base_url / tiantoken_api_key 字段,from_env 只读 TIANTOKEN_* 环境变量,不回退 VectorEngine 凭据
- AppState 改用配置字段构造编辑器 LLM 与 Tiantoken 图片 client,删除现读环境变量的 config::tiantoken_* 访问器
- editor_background_music_prompt_assist / editor_sound_effect_prompt_assist / sound_effect_translation 三处 mock LLM 夹具改用 tiantoken_* 字段,恢复 28 条失败用例
- config 用例改为断言 from_env 的 Tiantoken 凭据来源,state 用例改名为从 Tiantoken 设置构造编辑器 LLM client
- 同步更新后端架构文档与 decision-log 记录
2026-09-19 15:33:14 +08:00
k88936 0459109897 修复 SpacetimeDB 定价 record 缺少历史模型回填的问题
- 将 backfill_legacy_gpt_image_2_5_pricing 提升为 pub(crate) 并补充文档说明其覆盖磁盘 override 与 SpacetimeDB record 两条加载路径
- editor_generation_pricing_from_record 在 validate 前先对旧 record 做受控 backfill,避免升级前只写单个 gpt-image-2 key 时被必填校验拒绝
- 新增用例覆盖旧 record 自动补齐两个 GPT Image 2.5 key,以及显式配置不被回填覆盖
- 更新兼容回填的 TODO 注释,说明移除条件还需包含 SpacetimeDB record
2026-09-19 15:18:40 +08:00
k88936 6318baf7c6 修正缺省图片模型校验用例的 fixture 模型 key
- 将 editor_generation_pricing_rejects_missing_required_image_size 的 fixture 从已退役的 gpt-image-2 换成 gpt-image-2.5-flare-c / gpt-image-2.5-sunburst-c
- 同步断言文案为 models.gpt-image-2.5-flare-c 缺少 2K 的泥点配置,使缺档校验仍覆盖真实必填模型
2026-09-19 15:17:45 +08:00
k88936 28e6e67e98 补齐后台定价保存用例的 GPT Image 2.5 档位
后台保存 payload 用 gpt-image-2.5-flare-c 与 gpt-image-2.5-sunburst-c 替换历史单 key,保存重新返回 200

公开定价断言改为读取业务模型名 gpt-image-2.5 的生成档价格,并新增 admin 定价读回具体 key 的断言
2026-09-19 14:23:18 +08:00
k88936 4415723f7f 同步公开定价路由用例到 GPT Image 2.5 公开投影
公开定价断言改为读取业务模型名 gpt-image-2.5 的生成档价格

补充断言公开投影不暴露 gpt-image-2 与两个 provider 具体 key
2026-09-19 14:21:33 +08:00
k88936 fa84a364d6 修正宣发素材生成锁定的模型断言
源码文本断言从 Some(GPT_IMAGE_2_MODEL) 更新为 Some(GPT_IMAGE_2_5_BUSINESS_NAME),与当前锁定实现一致

用例改名为 publication_material_generation_is_locked_to_gpt_image_2_5
2026-09-19 14:20:29 +08:00
k88936 bd0a06e3d3 兼容图片工具的历史 gpt-image-2 模型别名
api-server 的图片类 Agent 工具在反序列化前把 gpt-image-2 / gpt-image-2-c 归一为 gpt-image-2.5,旧持久化工具调用不再被校验直接拒绝

EditImageTool 计价改用 gpt-image-2.5 业务模型常量,并新增别名归一单测、更新受影响的校验与计价断言
2026-09-19 13:55:30 +08:00
k88936 939c35a9b9 统一缺省图片模型的归一化结果
normalize_image_model 的空白模型回退改为 GPT_IMAGE_2_5_GENERATION_MODEL,与 build_image_request_body 默认值一致

补充单测断言空白模型与默认生成模型产出同一请求体
2026-09-19 13:48:11 +08:00
k88936 42fdb57efe 校验图片请求模型与 provider 绑定一致
executor 三个请求入口改用 ensure_provider_matches_model,模型不属于当前 provider 时直接返回 InvalidRequest

新增单测覆盖 provider 不一致的快速失败,并把 GPT 模型相关集成测试的 settings 调整为 tiantoken
2026-09-19 13:46:47 +08:00
k88936 0c54cca81c 修正生成规范缺省模型的计价档
spec_model_mud_points 在模型为空时显式回退 GPT Image 2.5 生成档,避免落到 nanobanana 档

补中文注释说明与 normalize_editor_generation_options 的缺省模型保持一致
2026-09-19 13:43:12 +08:00
k88936 1a2e60bf05 修复图片编辑缺少参考图时的 panic
platform-image 新增 missing_reference_images_error 并在 create_image_edit_with_references_and_model 中复用

api-server 两个多参考图编辑入口在解析 provider 客户端前先校验参考图,返回 400 而不是 panic

恢复 vector_engine_multi_reference_edit_rejects_empty_references 期望的缺少参考图错误
2026-09-19 13:41:21 +08:00
k88936 5ce267ae85 按参考图选择角色主形象的 provider 模型
角色主形象带参考图时走后端编辑端点,模型改用 gpt-image-2.5-sunburst-c

无参考图生成继续使用 gpt-image-2.5-flare-c
2026-09-19 13:37:55 +08:00
k88936 8924bf335e 修正 UI 设计素材提取的编辑模型
UI 设计图 spritesheet 提取走后端编辑端点,模型由 gpt-image-2.5-flare-c 改为 gpt-image-2.5-sunburst-c
2026-09-19 13:36:28 +08:00
k88936 ae257a6ff8 按端点选择编辑器生成的 provider_model
generate_editor_image_for_owner 在参考图存在时改用 gpt-image-2.5-sunburst-c(编辑模型)

无参考图生成继续使用 gpt-image-2.5-flare-c,nanobanana 分支沿用自身模型
2026-09-19 13:36:03 +08:00
k88936 ff4f42d2f2 修正编辑器生成模型归一化注释
注释改为与实现一致:未显式传模型时归一为 gpt-image-2.5,而非继续走 gpt-image-2
2026-09-19 13:35:05 +08:00
k88936 2e2ed47bec 补齐图标 spritesheet 请求的 provider_model
editor_project_icon 的 EditorImageProviderRequest 增加 provider_model 字段

参考图存在时按编辑模型 gpt-image-2.5-sunburst-c 提交,nanobanana 与无参考图分支分别沿用自身模型
2026-09-19 13:29:11 +08:00
k88936 dedc58cce1 补齐 api-server 的 GPT_IMAGE_2_C_MODEL 重导出
openai_image_generation 重新导出 platform_image 的 GPT_IMAGE_2_C_MODEL

修复 editor_project 兼容历史 gpt-image-2-c 时引入的未解析导入与随之产生的模式绑定错误
2026-09-19 13:27:31 +08:00
k88936 c86ac04220 恢复 image_provider 单元测试模块
image_provider/mod.rs 重新声明 #[cfg(test)] mod tests,tests.rs 去掉失效的模块包裹并补齐 helper 导入

提取器 extract_image_urls / extract_b64_images 放宽到 pub(crate),parse_reference_image_data_url 调用补 provider 参数

补回 5 个此前被静默跳过的单元测试
2026-09-19 13:26:35 +08:00
k88936 0817ea8158 补齐审计模型白名单的 GPT Image 2.5 与 nanobanana 模型
auditable_image_model 识别 gpt-image-2.5、gpt-image-2.5-flare-c、gpt-image-2.5-sunburst-c 与 nanobanana2

避免新模型请求的审计 image_model 与错误元数据丢失模型归因
2026-09-19 13:25:09 +08:00
k88936 e393a803f2 修正 executor 日志与重试错误的 provider 归属
生成 / 编辑路径的 tracing 字段与参考图数量校验错误改用 settings.provider

nanobanana 路径改用 VectorEngine provider

重试辅助函数 retry_image_send_after_delay / retry_image_upstream_status_after_delay 增加 provider 参数
2026-09-19 13:23:50 +08:00
k88936 89587c9e9e 修正 raw image edit 的 provider 归属
created_raw_image_edit 用 settings.provider 统一驱动审计、日志与错误构造

invalid_request / invalid_input / request_error 增加 provider 参数,不再写死 vector-engine
2026-09-19 13:22:20 +08:00
k88936 5cba2dbeb7 修正 libcurl 传输错误的 provider 归属
map_curl_error 接收实际 provider,审计、日志与 Request 错误不再写死 vector-engine

executor 生成 / 编辑路径传入 settings.provider,nanobanana 路径传入 VectorEngine
2026-09-19 13:20:44 +08:00
k88936 d3407e8910 修正预算与图片下载错误的 provider 归属
request_budget_exhausted_error 接收实际 provider,审计、日志与 Request 错误不再写死 vector-engine

download_remote_image / download_images_from_urls / resolve_reference_images / parse_reference_image_data_url 逐层透传 provider

executor 与 raw image edit 调用处传入各自 settings.provider,nanobanana 下载路径传入 VectorEngine
2026-09-19 13:19:08 +08:00
k88936 4888a0eb01 修正响应解析错误的 provider 归属
parse_json_payload 增加 ImageProvider 参数,ResponseParse 使用实际 provider

共享的 handle_image_response 解析失败时不再把 Tiantoken 响应记成 vector-engine
2026-09-19 13:16:25 +08:00
k88936 90b77d6bd0 修正共享响应处理的 provider 归属
handle_image_response 与 build_failure_audit 接收实际 provider,不再写死 vector-engine

executor 生成 / 编辑路径传入 settings.provider,nanobanana 路径传入 VectorEngine

budget / curl / raw edit 的审计先沿用原常量,待各自按实际 provider 收口
2026-09-19 13:16:03 +08:00
k88936 1c950df116 删除已失效的跨模型回退分支
移除 generation / edit 重试路径中恒为 false 的 should_fallback_to_gpt_image_2_c 判断与 record_image_model_fallback 调用

删除为计算该判断而额外调用 handle_image_response 的预解析,避免每次重试都白解析响应并下载图片

删除只断言该函数恒为 false 的单元测试,跨模型不切换已由集成测试覆盖
2026-09-19 13:14:23 +08:00
k88936 6c33459b90 为 ImageProviderSettings 增加脱敏 Debug 实现
手动实现 Debug 并在其中把 api_key 渲染为 <redacted>

settings() 公开返回该结构体,默认 derive 会让 {:?} 直接打印 provider 凭据
2026-09-19 13:09:58 +08:00
k88936 874d061cb9 统一 ImageProvider 模型路由使用的 nanobanana 常量
resolve_image_provider 改用 NANOBANANA_2_MODEL 常量匹配 VectorEngine 模型

避免常量更新后路由仍匹配写死的旧模型字符串
2026-09-19 13:09:28 +08:00
k88936 fa2b36654b 修正 ImageProvider 生成测试的错误命名
将 image_generation_uses_gpt_image_2_without_fallback_on_success 更名为 image_generation_uses_default_generation_model_on_success

测试实际覆盖 GPT Image 2.5 默认模型且跨模型回退已删除,旧名称会误导后续模型迁移
2026-09-19 13:09:10 +08:00
k88936 345c9c3a99 收敛 ImageProvider 集成测试的模型断言
集成测试改用 GPT_IMAGE_2_5_GENERATION_MODEL / GPT_IMAGE_2_5_EDIT_MODEL 常量拼接断言

避免常量改名后遗留过期的 wire model 字符串断言
2026-09-19 13:08:50 +08:00
k88936 99081455ee 移除 image_provider transport 冗余 path 属性
去掉 transport 模块上多余的 #[path] 声明,恢复默认模块解析

保持 client / curl 模块导出与人眼可见的目录结构一致
2026-09-19 13:07:21 +08:00
k88936 5b1b0ddb9a 补齐 GPT Image 2.5 前端回归测试
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 6m51s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 6m53s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 6m28s
Project CI / Backend tests (pull_request) Failing after 13s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 6m49s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m0s
Project CI / Repository checks (pull_request) Failing after 9s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m56s
Project CI / AI game creator shell web tests (pull_request) Failing after 4m52s
Project CI / Frontend tests (pull_request) Failing after 14m17s
Project CI / Native shell tests (pull_request) Successful in 16m11s
更新模型标签、提交 payload 与固定模型断言

覆盖 gpt-image-2-c 历史恢复及参数回退告警

保留历史 fixture 原值并验证 nanobanana 默认不变
2026-09-18 23:31:47 +08:00
k88936 f4315ffd23 更新主站 GPT Image 2.5 生成边界
将 GPT 专用新任务与锁定模型切换为 gpt-image-2.5

保留 nanobanana 普通生成默认行为

将历史 gpt-image-2 与 gpt-image-2-c 归一并触发参数回退告警
2026-09-18 23:30:48 +08:00
k88936 beb3c99240 同步 GPT Image 2.5 契约与工具文档
更新 OpenAPI 与外部编辑器模型说明

补充历史模型兼容解析与参数回退告警约定

将现役 ImageGen 工具脚本切换到 GPT Image 2.5
2026-09-18 23:28:45 +08:00
k88936 1574f206a2 整理图片 provider 模块层次并拆出 raw edit
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 5m26s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 5m18s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 4m39s
Project CI / Backend tests (pull_request) Failing after 15s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m0s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 4m37s
Project CI / Repository checks (pull_request) Failing after 10s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m28s
Project CI / AI game creator shell web tests (pull_request) Failing after 4m26s
Project CI / Frontend tests (pull_request) Failing after 5m49s
Project CI / Native shell tests (pull_request) Successful in 10m45s
将 raw image edit 提取到独立 raw_image_edit 模块

按 protocol、runtime、transport 目录收拢 image_provider 实现

同步后端架构与 raw edit 技术文档路径
2026-09-18 17:45:09 +08:00
k88936 1367e9c04e 兼容历史 GPT Image 2-c 提交模型
读取历史 gpt-image-2-c 时按当前 GPT Image 2.5 业务任务处理

保留历史字符串不改写并沿用新的生成与编辑定价路由
2026-09-18 16:50:10 +08:00
k88936 b294cbc3db 接入 GPT Image 2.5 双 provider 启动路由
启动时分别构造 VectorEngine 与 Tiantoken 图片 client

按生成、编辑和 nanobanana 模型选择具体 provider

迁移 api-server、编辑器 Agent 与 raw edit 调用方

拆分 GPT Image 2.5 生成与编辑定价并隐藏 provider 具体值
2026-09-18 16:28:45 +08:00
k88936 c34d24c2c8 重构图片 provider 中立执行层
将 platform-image 图片协议从 vector_engine 目录迁移到 image_provider

引入 ImageProviderClient 与 VectorEngine/Tiantoken provider 类型

按 concrete model 白名单拒绝未知模型并移除跨模型回退
2026-09-18 16:03:29 +08:00
k88936 0dbc34d13c 明确 GPT Image 2.5 provider 边界与路由计划
更新业务模型、具体模型与 provider client 领域术语

记录 Tiantoken/VectorEngine 双 client 启动约束

同步开发运维文档中的图片路由与环境变量要求
2026-09-18 16:01:32 +08:00
102 changed files with 2715 additions and 1781 deletions
@@ -102,7 +102,7 @@ Use OpenAPI as the final authority; these common values are a routing aid:
- Image `kind`: `spec`, `character`, `quick-edit`, `ui-design`, `publication-material`; ordinary image generation may omit it. - Image `kind`: `spec`, `character`, `quick-edit`, `ui-design`, `publication-material`; ordinary image generation may omit it.
- External v1 currently has no structured game-scene generation operation. Do not send `kind: "scene"` or `assetKind: "scene"` through generic image generation; the server rejects both before queueing. - External v1 currently has no structured game-scene generation operation. Do not send `kind: "scene"` or `assetKind: "scene"` through generic image generation; the server rejects both before queueing.
- Image `model`: `gpt-image-2`, `gemini-3.1-flash-image-preview`, `nanobanana2`, `nano-banana`. - Image `model`: `gpt-image-2.5`, `gemini-3.1-flash-image-preview`, `nanobanana2`, `nano-banana`. Persisted `gpt-image-2` is a legacy value resolved only when submitting a new task; the retired `gpt-image-2-c` is no longer accepted and is handled as an unsupported value.
- Image `aspectRatio`: `1:1`, `2:3`, `3:2`, `9:16`, `16:9`. - Image `aspectRatio`: `1:1`, `2:3`, `3:2`, `9:16`, `16:9`.
- Image `imageSize`: `0.5K`, `1K`, `2K`. - Image `imageSize`: `0.5K`, `1K`, `2K`.
- Video `model`: `seedance2.0`, `seedance2.0-fast`, `kling3.0`, `kling3.0-omni`, `veo3.1`, `veo3.1-fast`. - Video `model`: `seedance2.0`, `seedance2.0-fast`, `kling3.0`, `kling3.0-omni`, `veo3.1`, `veo3.1-fast`.
+22 -14
View File
@@ -1,11 +1,17 @@
--- ---
name: gpt-image-2-apimart name: gpt-image-2-apimart
description: Generate or inspect project image assets through this repository's VectorEngine gpt-image-2 workflow with gpt-image-2-c fallback. Use when Codex needs to create puzzle template sample images, reproduce the server-rs image request body, dry-run image prompts, batch-generate local project thumbnails, or debug VECTOR_ENGINE_BASE_URL / VECTOR_ENGINE_API_KEY image-generation configuration without exposing secrets. The directory name is historical. description: Generate or inspect project image assets through this repository's Tiantoken GPT Image 2.5 workflow. Use when Codex needs to create puzzle template sample images, reproduce the server-rs image request body, dry-run image prompts, batch-generate local project thumbnails, or debug TIANTOKEN_BASE_URL / TIANTOKEN_API_KEY image-generation configuration without exposing secrets. The directory name is historical.
--- ---
# gpt-image-2 VectorEngine # GPT Image 2.5 project image workflow
Use this skill for project-local image asset generation that must match the repository's `server-rs` VectorEngine image path. Keep the product/price model identifier and primary provider request as `gpt-image-2`, then fall back once to `gpt-image-2-c` for eligible provider failures. The folder still contains `apimart` in its name for compatibility with existing local plugin references. Use this skill for project-local image asset generation that must match the repository's image request contract. Provider routing is owned by `server-rs`; when this skill talks to the provider directly it must send the concrete provider model and the matching provider credentials, because the provider side only accepts concrete models:
- generation (no reference images): `gpt-image-2.5-flare-c` through Tiantoken
- edits (any reference image): `gpt-image-2.5-sunburst-c` through Tiantoken
- nanobanana (`gemini-3.1-flash-image-preview`) stays on VectorEngine
The business model name `gpt-image-2.5` is resolved to a concrete key by `server-rs` at the task boundary and is never sent to a provider directly. This client must not perform a cross-model fallback. The folder still contains `apimart` in its name for compatibility with existing local plugin references.
## Workflow ## Workflow
@@ -24,15 +30,15 @@ Use this skill for project-local image asset generation that must match the repo
``` ```
5. Save final project assets under `public/` or another explicitly requested workspace path. 5. Save final project assets under `public/` or another explicitly requested workspace path.
6. Never print `VECTOR_ENGINE_API_KEY`. Report only whether configuration exists. 6. Never print `TIANTOKEN_API_KEY`. Report only whether configuration exists.
## Request Contract ## Request Contract
The repository image path uses: The repository image path uses:
```text ```text
POST {VECTOR_ENGINE_BASE_URL}/v1/images/generations POST {TIANTOKEN_BASE_URL}/v1/images/generations
Authorization: Bearer {VECTOR_ENGINE_API_KEY} Authorization: Bearer {TIANTOKEN_API_KEY}
Content-Type: application/json Content-Type: application/json
``` ```
@@ -40,7 +46,7 @@ Default body:
```json ```json
{ {
"model": "gpt-image-2", "model": "gpt-image-2.5-flare-c",
"prompt": "<prompt>", "prompt": "<prompt>",
"n": 1, "n": 1,
"size": "1024x1024" "size": "1024x1024"
@@ -50,22 +56,22 @@ Default body:
For visual references, use the edit endpoint instead of the create endpoint: For visual references, use the edit endpoint instead of the create endpoint:
```text ```text
POST {VECTOR_ENGINE_BASE_URL}/v1/images/edits POST {TIANTOKEN_BASE_URL}/v1/images/edits
Authorization: Bearer {VECTOR_ENGINE_API_KEY} Authorization: Bearer {TIANTOKEN_API_KEY}
Content-Type: multipart/form-data Content-Type: multipart/form-data
``` ```
Multipart fields: Multipart fields:
```text ```text
model=gpt-image-2 model=gpt-image-2.5-sunburst-c
prompt=<prompt> prompt=<prompt>
n=1 n=1
size=1024x1024 size=1024x1024
image=@reference.png image=@reference.png
``` ```
In this repository, calls with no reference images use `POST /v1/images/generations`; calls with any reference image use `POST /v1/images/edits` and pass references as one or more `image` form parts. Both paths prefer `gpt-image-2`; on an eligible upstream/model failure they retry with `gpt-image-2-c`. Do not fall back for authentication, local validation, request-budget exhaustion, uncertain send/connection failure, content-safety rejection, or a generated image URL download failure. Match3D container UI generation embeds `public/match3d-background-references/pot-fused-reference.png` into the edit request as an `image` part. In this repository, calls with no reference images use `POST /v1/images/generations` with `gpt-image-2.5-flare-c`; calls with any reference image use `POST /v1/images/edits` with `gpt-image-2.5-sunburst-c` and pass references as one or more `image` form parts. Server-side calls still submit the business model `gpt-image-2.5` and let `server-rs` resolve the concrete key; provider routing and retry policy remain server-owned, and pricing follows the same reference-image rule (generation with a reference image is charged at the edit tier). Match3D container UI generation embeds `public/match3d-background-references/pot-fused-reference.png` into the edit request as an `image` part.
Accept image output from `data[].url`, `data[].b64_json`, or direct nested `url` fields. VectorEngine image generation currently returns synchronously; do not poll APIMart task endpoints. Accept image output from `data[].url`, `data[].b64_json`, or direct nested `url` fields. VectorEngine image generation currently returns synchronously; do not poll APIMart task endpoints.
@@ -75,12 +81,14 @@ Load environment values from process env first, then `.env.secrets.local`, `.env
Required for live generation: Required for live generation:
- `VECTOR_ENGINE_BASE_URL` - `TIANTOKEN_BASE_URL`
- `VECTOR_ENGINE_API_KEY` - `TIANTOKEN_API_KEY`
Optional: Optional:
- `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS` - `TIANTOKEN_IMAGE_REQUEST_TIMEOUT_MS`
`VECTOR_ENGINE_*` values no longer serve GPT Image 2.5: those credentials belong to VectorEngine, which only serves nanobanana. Do not fall back from `TIANTOKEN_*` to `VECTOR_ENGINE_*`.
If the key or base URL is missing, stop after dry-run or explain the missing configuration. Do not ask the user to paste the key in chat. If the key or base URL is missing, stop after dry-run or explain the missing configuration. Do not ask the user to paste the key in chat.
@@ -9,8 +9,9 @@ const skillRoot = path.resolve(__dirname, '..');
const repoRoot = path.resolve(skillRoot, '..', '..', '..'); const repoRoot = path.resolve(skillRoot, '..', '..', '..');
const defaultOutDir = path.join(repoRoot, 'public', 'anthro-cat-illustrations'); const defaultOutDir = path.join(repoRoot, 'public', 'anthro-cat-illustrations');
const defaultTimeoutMs = 1000000; const defaultTimeoutMs = 1000000;
const preferredImageModel = 'gpt-image-2'; // GPT Image 2.5 生成任务只接受 concrete provider model;业务模型名 `gpt-image-2.5`
const fallbackImageModel = 'gpt-image-2-c'; // 由 server-rs 在任务边界解析,脚本直连 provider 时必须自己给出 concrete key。
const preferredImageModel = 'gpt-image-2.5-flare-c';
const prompts = [ const prompts = [
{ {
@@ -101,18 +102,18 @@ function resolveEnv() {
...process.env, ...process.env,
}; };
return { return {
baseUrl: String(loaded.VECTOR_ENGINE_BASE_URL || '') baseUrl: String(loaded.TIANTOKEN_BASE_URL || '')
.trim() .trim()
.replace(/\/+$/u, ''), .replace(/\/+$/u, ''),
apiKey: String(loaded.VECTOR_ENGINE_API_KEY || '').trim(), apiKey: String(loaded.TIANTOKEN_API_KEY || '').trim(),
timeoutMs: Number.parseInt( timeoutMs: Number.parseInt(
String(loaded.VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS || defaultTimeoutMs), String(loaded.TIANTOKEN_IMAGE_REQUEST_TIMEOUT_MS || defaultTimeoutMs),
10, 10,
), ),
}; };
} }
function buildVectorEngineImagesGenerationUrl(baseUrl) { function buildTiantokenImagesGenerationUrl(baseUrl) {
return baseUrl.endsWith('/v1') return baseUrl.endsWith('/v1')
? `${baseUrl}/images/generations` ? `${baseUrl}/images/generations`
: `${baseUrl}/v1/images/generations`; : `${baseUrl}/v1/images/generations`;
@@ -233,22 +234,22 @@ async function fetchJson(url, options, timeoutMs) {
const text = await response.text(); const text = await response.text();
if (!response.ok) { if (!response.ok) {
const error = new Error( const error = new Error(
`VectorEngine ${response.status}: ${text.slice(0, 600)}`, `Tiantoken ${response.status}: ${text.slice(0, 600)}`,
); );
error.vectorEngineStatus = response.status; error.providerStatus = response.status;
error.vectorEngineBody = text; error.providerBody = text;
throw error; throw error;
} }
try { try {
return JSON.parse(text); return JSON.parse(text);
} catch (error) { } catch (error) {
error.vectorEngineResponseParse = true; error.providerResponseParse = true;
error.vectorEngineBody = text; error.providerBody = text;
throw error; throw error;
} }
} catch (error) { } catch (error) {
if (error?.name === 'AbortError') { if (error?.name === 'AbortError') {
throw new Error(`VectorEngine request timed out after ${timeoutMs}ms`); throw new Error(`Tiantoken request timed out after ${timeoutMs}ms`);
} }
throw error; throw error;
} finally { } finally {
@@ -256,81 +257,35 @@ async function fetchJson(url, options, timeoutMs) {
} }
} }
function shouldFallbackImageModel(error) {
const raw =
`${error?.message || ''}\n${error?.vectorEngineBody || ''}`.toLowerCase();
if (error?.vectorEngineResponseParse) {
return !containsContentRejection(raw);
}
const status = Number(error?.vectorEngineStatus || 0);
if (status === 408 || status >= 500) {
return true;
}
if (status === 429) {
return !containsContentRejection(raw);
}
const mentionsImageModel =
raw.includes('model') ||
raw.includes('模型') ||
raw.includes(preferredImageModel) ||
raw.includes(fallbackImageModel);
return (
[400, 404, 422].includes(status) &&
mentionsImageModel &&
/(not found|not supported|unsupported|unavailable|does not exist|invalid model|unknown model|不存在|不支持|不可用|未开通)/u.test(
raw,
)
);
}
function containsContentRejection(raw) {
return /(invalid_prompt|safety|content[_ ]policy|moderation|prompt rejected|content rejected|prompt refusal|content refusal|rejected by safety|rejected by moderation|敏感|违规|安全策略|内容审核|提示词拒绝|内容拒绝)/u.test(
raw,
);
}
async function requestImagePayload(env, entry) { async function requestImagePayload(env, entry) {
for (const model of [preferredImageModel, fallbackImageModel]) { const model = preferredImageModel;
const requestBody = { const requestBody = {
model, model,
prompt: buildPrompt(entry), prompt: buildPrompt(entry),
n: 1, n: 1,
size: '1024x1024', size: '1024x1024',
}; };
try { const payload = await fetchJson(
const payload = await fetchJson( buildTiantokenImagesGenerationUrl(env.baseUrl),
buildVectorEngineImagesGenerationUrl(env.baseUrl), {
{ method: 'POST',
method: 'POST', headers: {
headers: { Authorization: `Bearer ${env.apiKey}`,
Authorization: `Bearer ${env.apiKey}`, Accept: 'application/json',
Accept: 'application/json', 'Content-Type': 'application/json',
'Content-Type': 'application/json', },
}, body: JSON.stringify(requestBody),
body: JSON.stringify(requestBody), },
}, env.timeoutMs,
env.timeoutMs, );
); const base64Image = decodeStrictBase64Image(extractBase64Images(payload)[0]);
const base64Image = decodeStrictBase64Image( if (extractImageUrls(payload)[0] || base64Image) {
extractBase64Images(payload)[0], return payload;
);
if (extractImageUrls(payload)[0] || base64Image) {
return payload;
}
const error = new Error(`VectorEngine returned no image for ${entry.id}`);
error.vectorEngineResponseParse = true;
error.vectorEngineBody = JSON.stringify(payload).slice(0, 600);
throw error;
} catch (error) {
if (model !== preferredImageModel || !shouldFallbackImageModel(error)) {
throw error;
}
console.warn(
`VectorEngine ${preferredImageModel} failed, retrying with ${fallbackImageModel}: ${error.message}`,
);
}
} }
throw new Error(`VectorEngine returned no image for ${entry.id}`); const error = new Error(`Tiantoken returned no image for ${entry.id}`);
error.providerResponseParse = true;
error.providerBody = JSON.stringify(payload).slice(0, 600);
throw error;
} }
async function downloadUrl(url, timeoutMs) { async function downloadUrl(url, timeoutMs) {
@@ -373,7 +328,7 @@ async function generateOne(env, entry, outDir) {
const bytes = decodeStrictBase64Image(b64Images[0]); const bytes = decodeStrictBase64Image(b64Images[0]);
if (!bytes) { if (!bytes) {
throw new Error( throw new Error(
`VectorEngine returned invalid base64 image for ${entry.id}`, `Tiantoken returned invalid base64 image for ${entry.id}`,
); );
} }
image = { image = {
@@ -381,7 +336,7 @@ async function generateOne(env, entry, outDir) {
extension: inferExtensionFromBytes(bytes), extension: inferExtensionFromBytes(bytes),
}; };
} else { } else {
throw new Error(`VectorEngine returned no image for ${entry.id}`); throw new Error(`Tiantoken returned no image for ${entry.id}`);
} }
mkdirSync(outDir, { recursive: true }); mkdirSync(outDir, { recursive: true });
@@ -408,7 +363,6 @@ if (dryRun) {
requests: selectedPrompts.map((entry) => ({ requests: selectedPrompts.map((entry) => ({
id: entry.id, id: entry.id,
title: entry.title, title: entry.title,
fallbackModel: fallbackImageModel,
body: { body: {
model: preferredImageModel, model: preferredImageModel,
prompt: buildPrompt(entry), prompt: buildPrompt(entry),
@@ -429,7 +383,7 @@ if (!env.baseUrl || !env.apiKey) {
console.error( console.error(
JSON.stringify({ JSON.stringify({
ok: false, ok: false,
error: 'Missing VECTOR_ENGINE_BASE_URL or VECTOR_ENGINE_API_KEY', error: 'Missing TIANTOKEN_BASE_URL or TIANTOKEN_API_KEY',
hasBaseUrl: Boolean(env.baseUrl), hasBaseUrl: Boolean(env.baseUrl),
hasApiKey: Boolean(env.apiKey), hasApiKey: Boolean(env.apiKey),
}), }),
@@ -18,8 +18,9 @@ const defaultOutDir = path.join(
'puzzle-creation-templates', 'puzzle-creation-templates',
); );
const defaultTimeoutMs = 1000000; const defaultTimeoutMs = 1000000;
const preferredImageModel = 'gpt-image-2'; // GPT Image 2.5 生成任务只接受 concrete provider model;业务模型名 `gpt-image-2.5`
const fallbackImageModel = 'gpt-image-2-c'; // 由 server-rs 在任务边界解析,脚本直连 provider 时必须自己给出 concrete key。
const preferredImageModel = 'gpt-image-2.5-flare-c';
const args = new Map(); const args = new Map();
for (let index = 2; index < process.argv.length; index += 1) { for (let index = 2; index < process.argv.length; index += 1) {
@@ -71,18 +72,18 @@ function resolveEnv() {
...process.env, ...process.env,
}; };
return { return {
baseUrl: String(loaded.VECTOR_ENGINE_BASE_URL || '') baseUrl: String(loaded.TIANTOKEN_BASE_URL || '')
.trim() .trim()
.replace(/\/+$/u, ''), .replace(/\/+$/u, ''),
apiKey: String(loaded.VECTOR_ENGINE_API_KEY || '').trim(), apiKey: String(loaded.TIANTOKEN_API_KEY || '').trim(),
timeoutMs: Number.parseInt( timeoutMs: Number.parseInt(
String(loaded.VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS || defaultTimeoutMs), String(loaded.TIANTOKEN_IMAGE_REQUEST_TIMEOUT_MS || defaultTimeoutMs),
10, 10,
), ),
}; };
} }
function buildVectorEngineImagesGenerationUrl(baseUrl) { function buildTiantokenImagesGenerationUrl(baseUrl) {
return baseUrl.endsWith('/v1') return baseUrl.endsWith('/v1')
? `${baseUrl}/images/generations` ? `${baseUrl}/images/generations`
: `${baseUrl}/v1/images/generations`; : `${baseUrl}/v1/images/generations`;
@@ -203,22 +204,22 @@ async function fetchJson(url, options, timeoutMs) {
const text = await response.text(); const text = await response.text();
if (!response.ok) { if (!response.ok) {
const error = new Error( const error = new Error(
`VectorEngine ${response.status}: ${text.slice(0, 600)}`, `Tiantoken ${response.status}: ${text.slice(0, 600)}`,
); );
error.vectorEngineStatus = response.status; error.providerStatus = response.status;
error.vectorEngineBody = text; error.providerBody = text;
throw error; throw error;
} }
try { try {
return JSON.parse(text); return JSON.parse(text);
} catch (error) { } catch (error) {
error.vectorEngineResponseParse = true; error.providerResponseParse = true;
error.vectorEngineBody = text; error.providerBody = text;
throw error; throw error;
} }
} catch (error) { } catch (error) {
if (error?.name === 'AbortError') { if (error?.name === 'AbortError') {
throw new Error(`VectorEngine request timed out after ${timeoutMs}ms`); throw new Error(`Tiantoken request timed out after ${timeoutMs}ms`);
} }
throw error; throw error;
} finally { } finally {
@@ -226,83 +227,35 @@ async function fetchJson(url, options, timeoutMs) {
} }
} }
function shouldFallbackImageModel(error) {
const raw =
`${error?.message || ''}\n${error?.vectorEngineBody || ''}`.toLowerCase();
if (error?.vectorEngineResponseParse) {
return !containsContentRejection(raw);
}
const status = Number(error?.vectorEngineStatus || 0);
if (status === 408 || status >= 500) {
return true;
}
if (status === 429) {
return !containsContentRejection(raw);
}
const mentionsImageModel =
raw.includes('model') ||
raw.includes('模型') ||
raw.includes(preferredImageModel) ||
raw.includes(fallbackImageModel);
return (
[400, 404, 422].includes(status) &&
mentionsImageModel &&
/(not found|not supported|unsupported|unavailable|does not exist|invalid model|unknown model|不存在|不支持|不可用|未开通)/u.test(
raw,
)
);
}
function containsContentRejection(raw) {
return /(invalid_prompt|safety|content[_ ]policy|moderation|prompt rejected|content rejected|prompt refusal|content refusal|rejected by safety|rejected by moderation|敏感|违规|安全策略|内容审核|提示词拒绝|内容拒绝)/u.test(
raw,
);
}
async function requestImagePayload(env, template) { async function requestImagePayload(env, template) {
for (const model of [preferredImageModel, fallbackImageModel]) { const model = preferredImageModel;
const requestBody = { const requestBody = {
model, model,
prompt: buildPrompt(template), prompt: buildPrompt(template),
n: 1, n: 1,
size: '1024x1024', size: '1024x1024',
}; };
try { const payload = await fetchJson(
const payload = await fetchJson( buildTiantokenImagesGenerationUrl(env.baseUrl),
buildVectorEngineImagesGenerationUrl(env.baseUrl), {
{ method: 'POST',
method: 'POST', headers: {
headers: { Authorization: `Bearer ${env.apiKey}`,
Authorization: `Bearer ${env.apiKey}`, Accept: 'application/json',
Accept: 'application/json', 'Content-Type': 'application/json',
'Content-Type': 'application/json', },
}, body: JSON.stringify(requestBody),
body: JSON.stringify(requestBody), },
}, env.timeoutMs,
env.timeoutMs, );
); const base64Image = decodeStrictBase64Image(extractBase64Images(payload)[0]);
const base64Image = decodeStrictBase64Image( if (extractImageUrls(payload)[0] || base64Image) {
extractBase64Images(payload)[0], return payload;
);
if (extractImageUrls(payload)[0] || base64Image) {
return payload;
}
const error = new Error(
`VectorEngine returned no image for ${template.id}`,
);
error.vectorEngineResponseParse = true;
error.vectorEngineBody = JSON.stringify(payload).slice(0, 600);
throw error;
} catch (error) {
if (model !== preferredImageModel || !shouldFallbackImageModel(error)) {
throw error;
}
console.warn(
`VectorEngine ${preferredImageModel} failed, retrying with ${fallbackImageModel}: ${error.message}`,
);
}
} }
throw new Error(`VectorEngine returned no image for ${template.id}`); const error = new Error(`Tiantoken returned no image for ${template.id}`);
error.providerResponseParse = true;
error.providerBody = JSON.stringify(payload).slice(0, 600);
throw error;
} }
async function downloadUrl(url, timeoutMs) { async function downloadUrl(url, timeoutMs) {
@@ -345,7 +298,7 @@ async function generateOne(env, template, outDir) {
const bytes = decodeStrictBase64Image(b64Images[0]); const bytes = decodeStrictBase64Image(b64Images[0]);
if (!bytes) { if (!bytes) {
throw new Error( throw new Error(
`VectorEngine returned invalid base64 image for ${template.id}`, `Tiantoken returned invalid base64 image for ${template.id}`,
); );
} }
image = { image = {
@@ -353,7 +306,7 @@ async function generateOne(env, template, outDir) {
extension: inferExtensionFromBytes(bytes), extension: inferExtensionFromBytes(bytes),
}; };
} else { } else {
throw new Error(`VectorEngine returned no image for ${template.id}`); throw new Error(`Tiantoken returned no image for ${template.id}`);
} }
mkdirSync(outDir, { recursive: true }); mkdirSync(outDir, { recursive: true });
@@ -384,7 +337,6 @@ if (dryRun) {
requests: selectedTemplates.map((template) => ({ requests: selectedTemplates.map((template) => ({
id: template.id, id: template.id,
title: template.title, title: template.title,
fallbackModel: fallbackImageModel,
body: { body: {
model: preferredImageModel, model: preferredImageModel,
prompt: buildPrompt(template), prompt: buildPrompt(template),
@@ -405,7 +357,7 @@ if (!env.baseUrl || !env.apiKey) {
console.error( console.error(
JSON.stringify({ JSON.stringify({
ok: false, ok: false,
error: 'Missing VECTOR_ENGINE_BASE_URL or VECTOR_ENGINE_API_KEY', error: 'Missing TIANTOKEN_BASE_URL or TIANTOKEN_API_KEY',
hasBaseUrl: Boolean(env.baseUrl), hasBaseUrl: Boolean(env.baseUrl),
hasApiKey: Boolean(env.apiKey), hasApiKey: Boolean(env.apiKey),
}), }),
+29
View File
@@ -44,6 +44,35 @@ _Avoid_: 把同一资源的全局元数据和某一次摆放坐标混在同一
由图片生成或图片修改流程产生的画布资源,必须记录来源资源、提示词、实际提示词、模型、provider、任务 ID 和生成时间;本期 `/editor` 的生成修改先允许 mock 生成资源,但仍按生成资源元数据形状保存。 由图片生成或图片修改流程产生的画布资源,必须记录来源资源、提示词、实际提示词、模型、provider、任务 ID 和生成时间;本期 `/editor` 的生成修改先允许 mock 生成资源,但仍按生成资源元数据形状保存。
_Avoid_: 无来源的静态素材、只显示在 UI 但不落工程资源记录的生成结果 _Avoid_: 无来源的静态素材、只显示在 UI 但不落工程资源记录的生成结果
**图片模型历史值与使用端解析**:
图片资源中已持久化的 `gpt-image-2` 是历史业务事实,读回时保持原值;新任务使用业务模型值 `gpt-image-2.5`。当用户基于历史资源再次发起生成或编辑任务时,服务端只在新任务的使用端把历史值解析为当前业务模型,不改写历史资源。provider route 属于服务端执行与审计边界,前端不接收、不持久化、不展示,也不据此分支。
_Avoid_: 读取数据库时改写历史模型值、把 provider route 暴露为前端模型选项或公开 DTO
**图片 provider 显式路由**:
api-server 在任务入口按业务语义显式选择具体 provider model name(生成或编辑),并把同一具体名传给图片平台适配器和后台定价解析;图片平台适配器不从参考图数量或前端字段猜测任务。具体 provider model name 只存在于服务端调用、定价配置和审计边界。
后台管理 Web/API 是明确例外,可以查看和编辑两个具体定价 key;主站普通前端与公开定价 API 不接收这些 key。
_Avoid_: 让图片适配器隐式猜路由、让主站前端携带 provider model name
**业务模型**:
面向任务与产品契约的稳定模型值;当前 GPT 图片新任务的业务模型是 `gpt-image-2.5`。业务模型不等同于 provider 的具体计费/请求 model,也不暴露 provider 凭证或 endpoint。
_Avoid_: 把 provider concrete model 当作前端业务选项、用业务模型值直接推断 provider 凭证
**具体模型**:
服务端发送请求和定价使用的 concrete model name。GPT Image 2.5 生成与编辑分别是 `gpt-image-2.5-flare-c``gpt-image-2.5-sunburst-c`nanobanana 仍使用 `gemini-3.1-flash-image-preview`。具体模型只在服务端执行、定价和审计边界出现。
_Avoid_: 把具体模型写入普通前端 DTO、让未知字符串自动选择 provider
**provider client**:
按具体模型选出的外部图片 provider 连接配置,包含 provider identity、base URL 和 API keyVectorEngine 与 Tiantoken client 共享图片协议执行器,不复制请求/响应业务逻辑。两套 required client 在 api-server 启动时构造。
_Avoid_: 在首次请求时才创建 client、在 provider client 中复制尺寸/重试/审计逻辑、跨 provider credential fallback
**历史模型值**:
已持久化的 `gpt-image-2` 字符串,只作为历史事实原样读取和审计;基于历史资源提交新任务时,在使用端解析为当前 GPT Image 2.5 业务任务,不回写历史记录,也不把旧值作为现役 provider route。已退役的 `gpt-image-2-c` 已从代码整体删除,只有数据库里的历史审计字符串原样保留,任何入口传入该值都按不支持的值处理。
_Avoid_: 数据库批量改写历史值、把历史值重新路由到 VectorEngine、把兼容解析扩散到普通前端、为已删除的 `gpt-image-2-c` 重新加回常量或解析分支
**GPT Image 2.5 新生成展示名**:
`GPT Image 2.5` 是新生成任务的产品展示名;历史资源与既有编辑上下文不因新模型上线而改写展示语义。
_Avoid_: 把新生成展示名扩散到历史记录、历史生成器或旧编辑上下文
**系列素材图集生成**: **系列素材图集生成**:
一组同类素材的统一批量生成方式,采用批量规划、sheet 生图、后端切图、透明化、OSS 持久化和局部重生成的通用流水线。 一组同类素材的统一批量生成方式,采用批量规划、sheet 生图、后端切图、透明化、OSS 持久化和局部重生成的通用流水线。
_Avoid_: 为每个玩法单独发明素材流水线、把系列素材建模成任一玩法专属 DTO _Avoid_: 为每个玩法单独发明素材流水线、把系列素材建模成任一玩法专属 DTO
@@ -185,7 +185,12 @@ test('nextVersion 只在 patch 位递增', () => {
test('ossutil 参数默认使用 v1 签名,并可按需带 region 与 v4', () => { test('ossutil 参数默认使用 v1 签名,并可按需带 region 与 v4', () => {
const base = { const base = {
args: ['cp', '--force', '/tmp/a.json', 'oss://agc-dev/agc/global-version.json'], args: [
'cp',
'--force',
'/tmp/a.json',
'oss://agc-dev/agc/global-version.json',
],
endpoint: 'oss-rg-china-mainland.aliyuncs.com', endpoint: 'oss-rg-china-mainland.aliyuncs.com',
accessKeyId: 'id', accessKeyId: 'id',
accessKeySecret: 'secret', accessKeySecret: 'secret',
@@ -1192,7 +1192,7 @@ async function main() {
function isDirectModuleExecution() { function isDirectModuleExecution() {
return Boolean( return Boolean(
process.argv[1] && process.argv[1] &&
resolve(process.argv[1]) === fileURLToPath(import.meta.url), resolve(process.argv[1]) === fileURLToPath(import.meta.url),
); );
} }
@@ -166,10 +166,10 @@ export function ActiveProjectRunsPanel({
const elapsed = formatActiveTurnElapsed(turn.startedAt, now); const elapsed = formatActiveTurnElapsed(turn.startedAt, now);
const isCurrent = Boolean( const isCurrent = Boolean(
currentProjectPath && currentProjectPath &&
projectPathsMatchForInvalidation( projectPathsMatchForInvalidation(
turn.projectPath, turn.projectPath,
currentProjectPath, currentProjectPath,
), ),
); );
return ( return (
<li key={`${turn.projectPath}:${turn.turnId}`}> <li key={`${turn.projectPath}:${turn.turnId}`}>
@@ -215,10 +215,10 @@ export function ActiveProjectRunsPanel({
const elapsed = formatActiveTurnElapsed(turn.startedAt, now); const elapsed = formatActiveTurnElapsed(turn.startedAt, now);
const isCurrent = Boolean( const isCurrent = Boolean(
currentProjectPath && currentProjectPath &&
projectPathsMatchForInvalidation( projectPathsMatchForInvalidation(
turn.projectPath, turn.projectPath,
currentProjectPath, currentProjectPath,
), ),
); );
return ( return (
<li key={`${turn.projectPath}:${turn.turnId}`}> <li key={`${turn.projectPath}:${turn.turnId}`}>
@@ -4,4 +4,6 @@
* 审批 / 提问请求与解决:本轮只透传,不并入聊天状态。 * 审批 / 提问请求与解决:本轮只透传,不并入聊天状态。
*/ */
export type DirectThreadRequestKind = export type DirectThreadRequestKind =
'approval.requested' | 'ask.requested' | 'request.resolved'; | 'approval.requested'
| 'ask.requested'
| 'request.resolved';
@@ -40,7 +40,9 @@ export type ResourceCanvasGenerationPlaceholderPanel =
* - `failed`:这次生成失败;占位留在画布上,点它可以用同一份输入与引用重试。 * - `failed`:这次生成失败;占位留在画布上,点它可以用同一份输入与引用重试。
*/ */
export type ResourceCanvasGenerationPlaceholderStatus = export type ResourceCanvasGenerationPlaceholderStatus =
'draft' | 'submitted' | 'failed'; | 'draft'
| 'submitted'
| 'failed';
/** /**
* 工具点击后先在当前栏目创建的临时占位卡。 * 工具点击后先在当前栏目创建的临时占位卡。
@@ -61,8 +61,9 @@ export function ResourceModelViewer({
void (async () => { void (async () => {
try { try {
const THREE = await import('three'); const THREE = await import('three');
const { OrbitControls } = const { OrbitControls } = await import(
await import('three/examples/jsm/controls/OrbitControls.js'); 'three/examples/jsm/controls/OrbitControls.js'
);
if (disposed) { if (disposed) {
return; return;
} }
@@ -22,7 +22,8 @@ export type ResourceBatchTagTarget = {
}; };
export type ResourceBatchTagTargetResolution = export type ResourceBatchTagTargetResolution =
{ ok: true; assetIds: string[] } | { ok: false; reason: string }; | { ok: true; assetIds: string[] }
| { ok: false; reason: string };
export function resolveResourceBatchTagTargets( export function resolveResourceBatchTagTargets(
resources: readonly { resources: readonly {
@@ -127,10 +127,10 @@ function validResourceCanvasCardSize(
): size is ResourceCanvasCardSize { ): size is ResourceCanvasCardSize {
return Boolean( return Boolean(
size && size &&
Number.isFinite(size.width) && Number.isFinite(size.width) &&
Number.isFinite(size.height) && Number.isFinite(size.height) &&
size.width > 0 && size.width > 0 &&
size.height > 0, size.height > 0,
); );
} }
@@ -32,12 +32,14 @@ export async function loadResourceModelObject(
source: ResourceModelSource, source: ResourceModelSource,
): Promise<ThreeTypes.Object3D> { ): Promise<ThreeTypes.Object3D> {
if (isResourceModelFbx(source)) { if (isResourceModelFbx(source)) {
const { FBXLoader } = const { FBXLoader } = await import(
await import('three/examples/jsm/loaders/FBXLoader.js'); 'three/examples/jsm/loaders/FBXLoader.js'
);
return new FBXLoader().loadAsync(source.sourceUrl); return new FBXLoader().loadAsync(source.sourceUrl);
} }
const { GLTFLoader } = const { GLTFLoader } = await import(
await import('three/examples/jsm/loaders/GLTFLoader.js'); 'three/examples/jsm/loaders/GLTFLoader.js'
);
const gltf = await new GLTFLoader().loadAsync(source.sourceUrl); const gltf = await new GLTFLoader().loadAsync(source.sourceUrl);
if (!gltf.scene) { if (!gltf.scene) {
throw new Error('模型内容为空'); throw new Error('模型内容为空');
@@ -104,7 +106,8 @@ export function disposeResourceModelObject(object: ThreeTypes.Object3D) {
entry as unknown as Record<string, unknown>, entry as unknown as Record<string, unknown>,
)) { )) {
const texture = value as const texture = value as
{ isTexture?: boolean; dispose?: () => void } | undefined; | { isTexture?: boolean; dispose?: () => void }
| undefined;
if (texture?.isTexture && typeof texture.dispose === 'function') { if (texture?.isTexture && typeof texture.dispose === 'function') {
texture.dispose(); texture.dispose();
} }
@@ -439,7 +439,9 @@ function reconcileLayout(
* 做,磁盘上仍是旧栏目取值的坐标不会因为 `section` 字面量不同而漏出整理范围。 * 做,磁盘上仍是旧栏目取值的坐标不会因为 `section` 字面量不同而漏出整理范围。
*/ */
organizeSections: organizeSections:
readonly ProjectResourceCanvasCategory[] | null | undefined = undefined, | readonly ProjectResourceCanvasCategory[]
| null
| undefined = undefined,
) { ) {
// `undefined` 不是整理;`null` = 整理全部栏目;否则按集合判定。 // `undefined` 不是整理;`null` = 整理全部栏目;否则按集合判定。
const organizedSections = const organizedSections =
@@ -43,8 +43,9 @@ describe('AGC 客户端更新', () => {
it('开发态开关关闭时不请求清单', async () => { it('开发态开关关闭时不请求清单', async () => {
vi.stubEnv('VITE_AGC_ENABLE_APP_UPDATE_CHECK', '0'); vi.stubEnv('VITE_AGC_ENABLE_APP_UPDATE_CHECK', '0');
vi.resetModules(); vi.resetModules();
const { checkForAppUpdate, resetAppUpdateCheckForTests } = const { checkForAppUpdate, resetAppUpdateCheckForTests } = await import(
await import('../src/services/appUpdate'); '../src/services/appUpdate'
);
await expect(checkForAppUpdate()).resolves.toBeNull(); await expect(checkForAppUpdate()).resolves.toBeNull();
expect(checkMock).not.toHaveBeenCalled(); expect(checkMock).not.toHaveBeenCalled();
@@ -55,8 +56,9 @@ describe('AGC 客户端更新', () => {
vi.stubEnv('VITE_AGC_ENABLE_APP_UPDATE_CHECK', '1'); vi.stubEnv('VITE_AGC_ENABLE_APP_UPDATE_CHECK', '1');
vi.resetModules(); vi.resetModules();
checkMock.mockResolvedValue(fakeUpdate() as never); checkMock.mockResolvedValue(fakeUpdate() as never);
const { checkForAppUpdate, resetAppUpdateCheckForTests } = const { checkForAppUpdate, resetAppUpdateCheckForTests } = await import(
await import('../src/services/appUpdate'); '../src/services/appUpdate'
);
await expect(checkForAppUpdate()).resolves.toEqual({ await expect(checkForAppUpdate()).resolves.toEqual({
version: '99.0.0', version: '99.0.0',
@@ -72,8 +74,9 @@ describe('AGC 客户端更新', () => {
vi.stubEnv('VITE_AGC_ENABLE_APP_UPDATE_CHECK', '1'); vi.stubEnv('VITE_AGC_ENABLE_APP_UPDATE_CHECK', '1');
vi.resetModules(); vi.resetModules();
checkMock.mockRejectedValue(new Error('updater: manifest 404')); checkMock.mockRejectedValue(new Error('updater: manifest 404'));
const { checkForAppUpdate, resetAppUpdateCheckForTests } = const { checkForAppUpdate, resetAppUpdateCheckForTests } = await import(
await import('../src/services/appUpdate'); '../src/services/appUpdate'
);
await expect(checkForAppUpdate()).resolves.toBeNull(); await expect(checkForAppUpdate()).resolves.toBeNull();
resetAppUpdateCheckForTests(); resetAppUpdateCheckForTests();
@@ -107,8 +110,9 @@ describe('AGC 客户端更新', () => {
it('没有待安装更新时安装请求失败关闭', async () => { it('没有待安装更新时安装请求失败关闭', async () => {
vi.stubEnv('VITE_AGC_ENABLE_APP_UPDATE_CHECK', '1'); vi.stubEnv('VITE_AGC_ENABLE_APP_UPDATE_CHECK', '1');
vi.resetModules(); vi.resetModules();
const { installAppUpdate, resetAppUpdateCheckForTests } = const { installAppUpdate, resetAppUpdateCheckForTests } = await import(
await import('../src/services/appUpdate'); '../src/services/appUpdate'
);
await expect(installAppUpdate()).rejects.toThrow('没有可安装的更新'); await expect(installAppUpdate()).rejects.toThrow('没有可安装的更新');
resetAppUpdateCheckForTests(); resetAppUpdateCheckForTests();
@@ -703,11 +703,11 @@ describe('快速编辑浮层的下拉弹层', () => {
fireEvent.click( fireEvent.click(
screen.getByRole('button', { name: '快速编辑模型 nanobanana2' }), screen.getByRole('button', { name: '快速编辑模型 nanobanana2' }),
); );
expect(screen.getByRole('button', { name: 'gpt-image-2' })).toBeTruthy(); expect(screen.getByRole('button', { name: 'GPT Image 2.5' })).toBeTruthy();
// 点面板内、弹层外的提示词区域:只收弹层,不动面板本身。 // 点面板内、弹层外的提示词区域:只收弹层,不动面板本身。
fireEvent.click(screen.getByLabelText('快速编辑提示词')); fireEvent.click(screen.getByLabelText('快速编辑提示词'));
expect(screen.queryByRole('button', { name: 'gpt-image-2' })).toBeNull(); expect(screen.queryByRole('button', { name: 'GPT Image 2.5' })).toBeNull();
expect( expect(
screen.getByRole('button', { name: '快速编辑模型 nanobanana2' }), screen.getByRole('button', { name: '快速编辑模型 nanobanana2' }),
).toBeTruthy(); ).toBeTruthy();
@@ -739,10 +739,10 @@ describe('快速编辑浮层的下拉弹层', () => {
fireEvent.click( fireEvent.click(
screen.getByRole('button', { name: '快速编辑模型 nanobanana2' }), screen.getByRole('button', { name: '快速编辑模型 nanobanana2' }),
); );
expect(screen.getByRole('button', { name: 'gpt-image-2' })).toBeTruthy(); expect(screen.getByRole('button', { name: 'GPT Image 2.5' })).toBeTruthy();
fireEvent.keyDown(document, { key: 'Escape' }); fireEvent.keyDown(document, { key: 'Escape' });
expect(screen.queryByRole('button', { name: 'gpt-image-2' })).toBeNull(); expect(screen.queryByRole('button', { name: 'GPT Image 2.5' })).toBeNull();
}); });
}); });
@@ -21,7 +21,8 @@ import {
const homeProjectOverride = vi.hoisted(() => ({ const homeProjectOverride = vi.hoisted(() => ({
openProject: null as openProject: null as
null | ((path: string, mode: 'open' | 'create') => Promise<void>), | null
| ((path: string, mode: 'open' | 'create') => Promise<void>),
})); }));
vi.mock('../src/features/app-shell/useHomeProjectCreation', async () => { vi.mock('../src/features/app-shell/useHomeProjectCreation', async () => {
+1
View File
@@ -79,6 +79,7 @@
- [画板音乐生成入口](./【编辑器】画板音乐生成入口设计-2026-06-18.md):BGM/SFX 共享视图、独立业务规则和当前发布门禁。 - [画板音乐生成入口](./【编辑器】画板音乐生成入口设计-2026-06-18.md):BGM/SFX 共享视图、独立业务规则和当前发布门禁。
- [画布 Agent 对话面板](./【编辑器】画布Agent对话面板-2026-07-03.md) - [画布 Agent 对话面板](./【编辑器】画布Agent对话面板-2026-07-03.md)
- [画布 Agent 会话消息存 OSS](./adr/【ADR】画布Agent会话消息存OSS-2026-07-03.md) - [画布 Agent 会话消息存 OSS](./adr/【ADR】画布Agent会话消息存OSS-2026-07-03.md)
- [GPT Image 2.5 模型路由与历史值兼容](./adr/【ADR】GPT Image 2.5模型路由与历史值兼容-2026-09-18.md)
- [编辑器模型定价配置](./【编辑器】模型定价配置管理方案-2026-06-22.md) - [编辑器模型定价配置](./【编辑器】模型定价配置管理方案-2026-06-22.md)
## 后端、运维与测试 ## 后端、运维与测试
@@ -0,0 +1,25 @@
# GPT Image 2.5 模型路由与历史值兼容
状态:accepted
新任务使用业务模型值 `gpt-image-2.5`api-server 按任务显式选择具体 model:无参考图的生成使用 `gpt-image-2.5-flare-c`,编辑以及带参考图的生成使用 `gpt-image-2.5-sunburst-c`(参考图会让执行端必须走 edits multipart,因此执行与计价都按编辑 concrete model)。这两个 GPT Image 2.5 model 必须通过启动时构造的 Tiantoken client 发送;Tiantoken client 只读取显式配置的 `TIANTOKEN_BASE_URL`(部署值由环境设置为 `https://api.tiantoken.com`)和独立 `TIANTOKEN_API_KEY`,缺失即阻止 api-server 启动,不得回退到 VectorEngine 或其 API key。
图片协议执行逻辑保持 provider-neutral:请求 body、multipart、尺寸约束、重试、响应解码和审计由共享 image executor 承担;VectorEngine 与 Tiantoken client 只提供相同协议所需的 base URL、API key 和 provider identity。platform-image 根据 concrete model 做严格白名单路由:`gpt-image-2.5-flare-c``gpt-image-2.5-sunburst-c` 走 Tiantoken`gemini-3.1-flash-image-preview`nanobanana)走 VectorEngine;未知 model 直接拒绝。已持久化的 `gpt-image-2` 只在新任务提交边界按兼容规则解析为当前 GPT Image 2.5 任务,不改写历史资源,也不进入旧 VectorEngine 图片路由。
provider 白名单同时拒绝非 provider model:业务模型名 `gpt-image-2.5` 必须先由 api-server 在任务边界解析成 `gpt-image-2.5-flare-c` / `gpt-image-2.5-sunburst-c``gpt-image-2` 继续作为历史可读值接受。两者都不进入 provider 路由。
`gpt-image-2-c` 已整体删除:它自兜底移除后只剩历史审计字符串,从未进入业务模型、持久化模型字段或前端契约,因此不再保留任何常量、解析分支、尺寸语义或 provider 路由。数据库里既有的审计字符串保持原样,不参与新任务。
普通主站前端只接触业务模型和新生成展示名 `GPT Image 2.5`admin Web/API 可以查看和编辑两个具体定价 key;生成类任务带参考图时后端必须走 edits multipart,因此 dispatch、审计与计价统一按编辑 concrete model `gpt-image-2.5-sunburst-c`(包含图标素材图集的固定参考图与 UI 素材提取的来源图),不存在「生成档计价 + 编辑档执行」的分裂。新代码不再跨模型或跨 provider fallback,已退役的 `gpt-image-2-c` 审计记录原样保留在数据库,代码不再引用该值。
主站前端只把原本明确使用 GPT Image 2 的专用新任务改为业务模型值 `gpt-image-2.5`;普通图片、角色、场景、图标等原有 nanobanana 默认行为保持不变。画布参数或编辑布局读到 `gpt-image-2` 时,在使用端解析为 `gpt-image-2.5` 并触发参数迁移警告,原始资源、审计、metadata 和历史 fixture 不回写。AGC 现有资源生成界面与请求默认保持不变,不因本决策新增模型字段、选择器或尺寸行为。
## Consequences
- 定价配置的活动 key 是两个具体 provider model;旧单 key 配置只允许受控 backfill,并留下兼容 TODO。
- 定价档位跟随任务最终 concrete model:带参考图的生成任务与编辑任务同价,admin 单独调整 `gpt-image-2.5-sunburst-c` 时生成带参考图的任务必须同步生效。
- 新任务的同模型重试固定使用 api-server dispatch 的具体 model,不切换到另一个 model。
- 两套 provider client 在 api-server 启动阶段同时构造;任一 required provider 配置缺失,启动失败而不是延迟到首次图片请求。
- 两套 provider client 的缺失配置报错点名对应环境变量(`VECTOR_ENGINE_BASE_URL` / `VECTOR_ENGINE_API_KEY``TIANTOKEN_BASE_URL` / `TIANTOKEN_API_KEY`),构造成功后用启动日志记录已启用的 provider。
- provider routing 只依据 concrete model 的白名单,业务名与已退役的 `gpt-image-2-c` 一律拒绝(`gpt-image-2-c` 已从代码中整体删除,传入即按未知 model 处理);provider client 不复制共享协议执行逻辑。
- 公开资源、`generationInputs` 和普通前端契约不包含具体 provider key;admin 定价管理是明确例外。
@@ -3017,7 +3017,7 @@
}, },
"model": { "model": {
"type": "string", "type": "string",
"description": "支持 gpt-image-2、gemini-3.1-flash-image-preview、nanobanana2、nano-banana。未传时沿用编辑器默认。" "description": "支持 gpt-image-2.5、gemini-3.1-flash-image-preview、nanobanana2、nano-bananagpt-image-2 仅作为历史值在新任务提交边界兼容解析;已退役的 gpt-image-2-c 不再接受,命中按不支持的值处理。kind=ui-design 与 kind=publication-material 固定使用 gpt-image-2.5,传入的 model 会被忽略;其余 kind 未传时默认 gpt-image-2.5。"
}, },
"aspectRatio": { "aspectRatio": {
"type": "string", "type": "string",
@@ -3033,7 +3033,7 @@
"type": "array", "type": "array",
"items": { "items": {
"type": "string", "type": "string",
"description": "当前账号的 objectKey、项目资源 ID 或素材 ID;本地临时图必须先上传 OSS 再提交。禁止 Data URL / Blob URL。普通生成最多 5 张;kind=quick-edit 时 gpt-image-2 最多 5 张、nanobanana2 最多 9 张。超限返回 400,不会静默截断。" "description": "当前账号的 objectKey、项目资源 ID 或素材 ID;本地临时图必须先上传 OSS 再提交。禁止 Data URL / Blob URL。普通生成最多 5 张;kind=quick-edit 时 GPT Image 2.5 最多 5 张、nanobanana2 最多 9 张。超限返回 400,不会静默截断。"
}, },
"maxItems": 9 "maxItems": 9
}, },
@@ -3170,7 +3170,7 @@
}, },
"model": { "model": {
"type": "string", "type": "string",
"description": "支持 gpt-image-2、gemini-3.1-flash-image-preview、nanobanana2、nano-banana。" "description": "支持 gpt-image-2.5、gemini-3.1-flash-image-preview、nanobanana2、nano-bananagpt-image-2 仅作为历史值兼容解析;已退役的 gpt-image-2-c 不再接受,命中按不支持的值处理。"
}, },
"aspectRatio": { "aspectRatio": {
"type": "string", "type": "string",
@@ -3184,7 +3184,7 @@
"type": "array", "type": "array",
"items": { "items": {
"type": "string", "type": "string",
"description": "当前账号的 objectKey、项目资源 ID 或素材 ID;本地临时图必须先上传 OSS。禁止 Data URL / Blob URL。sourceReferenceId 对应的主来源原图占用 1 张 provider 容量,因此 gpt-image-2 最多再提交 4 张、nanobanana2 最多再提交 8 张;超限返回 400,不会静默截断。" "description": "当前账号的 objectKey、项目资源 ID 或素材 ID;本地临时图必须先上传 OSS。禁止 Data URL / Blob URL。sourceReferenceId 对应的主来源原图占用 1 张 provider 容量,因此 GPT Image 2.5 最多再提交 4 张、nanobanana2 最多再提交 8 张;超限返回 400,不会静默截断。"
}, },
"maxItems": 8 "maxItems": 8
}, },
@@ -3373,7 +3373,7 @@
"type": "array", "type": "array",
"items": { "items": {
"type": "string", "type": "string",
"description": "额外图标素材参考图的稳定引用:objectKey、项目资源 ID 或素材 ID;本地临时图必须先上传 OSS。禁止 Data URL / Blob URL。referenceId 占用 1 张 provider 容量,因此 gpt-image-2 最多再提交 4 张、nanobanana2 最多再提交 8 张;超限返回 400,不会静默截断。" "description": "额外图标素材参考图的稳定引用:objectKey、项目资源 ID 或素材 ID;本地临时图必须先上传 OSS。禁止 Data URL / Blob URL。referenceId 占用 1 张 provider 容量,因此 GPT Image 2.5 最多再提交 4 张、nanobanana2 最多再提交 8 张;超限返回 400,不会静默截断。"
}, },
"maxItems": 8 "maxItems": 8
}, },
@@ -3488,13 +3488,13 @@
"model": { "model": {
"type": "string", "type": "string",
"default": "gemini-3.1-flash-image-preview", "default": "gemini-3.1-flash-image-preview",
"description": "支持 gpt-image-2、gemini-3.1-flash-image-preview、nanobanana2、nano-banana。未传时默认使用 nanobanana。" "description": "支持 gpt-image-2.5、gemini-3.1-flash-image-preview、nanobanana2、nano-bananagpt-image-2 仅作为历史值兼容解析;已退役的 gpt-image-2-c 不再接受,命中按不支持的值处理。未传时默认使用 nanobananagemini-3.1-flash-image-preview);传 gpt-image-2.5 时按 GPT Image 2.5 编辑任务执行。"
}, },
"referenceImageSrcs": { "referenceImageSrcs": {
"type": "array", "type": "array",
"items": { "items": {
"type": "string", "type": "string",
"description": "额外 UI 素材参考图的稳定引用:objectKey、项目资源 ID 或素材 ID;本地临时图必须先上传 OSS。禁止 Data URL / Blob URL。sourceImageSrc 占用 1 张 provider 容量,因此 gpt-image-2 最多再提交 4 张、nanobanana2 最多再提交 5 张;超限返回 400,不会静默截断。" "description": "额外 UI 素材参考图的稳定引用:objectKey、项目资源 ID 或素材 ID;本地临时图必须先上传 OSS。禁止 Data URL / Blob URL。sourceImageSrc 占用 1 张 provider 容量,因此 GPT Image 2.5 最多再提交 4 张、nanobanana2 最多再提交 5 张;超限返回 400,不会静默截断。"
}, },
"maxItems": 5 "maxItems": 5
}, },
@@ -0,0 +1,50 @@
# GPT Image 2.5 provider 边界重构实施计划
- Version: 3
- Status: active
- Date: 2026-09-18
- Parent Milestone: `docs/project-memory/plans/【里程碑】GPT Image 2.5 provider边界重构-2026-09-18.md`
## 实施边界
1. 先抽象 provider-neutral settings/client 与共享图片执行器接口;保留一套 body、multipart、尺寸、retry、响应和 audit 逻辑。
2. 在 api-server 配置/state 初始化阶段分别构造 VectorEngine 与 Tiantoken client;删除 Tiantoken 对 VectorEngine URL/key 的任何 fallback。
3. 在 platform-image 建立 concrete model 白名单路由:GPT Image 2.5 → Tiantokennanobanana → VectorEnginelegacy/unknown 按合同处理。
4. 重命名 provider-specific client/build/transport 符号,避免共享逻辑继续伪装成 `vector_engine_*`;仅保留确有 VectorEngine 语义的名称。
5. 迁移 api-server、Agent、raw edit、角色/图标/UI 入口和测试;核对 pricing/admin/public DTO 可见性。
6. 删除跨模型/跨 provider fallback 分支,保留同 concrete model retry。
## 前端同步边界(已确认)
1. 主站 `src/components/image-editor/` 中,凡是原本明确写死 `gpt-image-2` 的 GPT 专用新任务(快速编辑、UI 设计、宣发、规范及对应提交/锁定模型)统一改为业务模型值 `gpt-image-2.5`;普通图片、角色、场景、图标等原有 nanobanana 默认行为不变。
2. `normalizeEditorImageModel` 在编辑面板、画布生成参数和历史布局恢复的使用端把 `gpt-image-2``gpt-image-2-c` 解析为 `gpt-image-2.5`。该兼容命中必须触发既有参数回退警告;未知模型仍按原有无效参数处理。原始资源、审计、metadata 和历史 fixture 不回写。
3. 主站新请求的 `model` 字段只发送业务模型值或 nanobanana 业务值,不发送 `gpt-image-2.5-flare-c` / `gpt-image-2.5-sunburst-c` 等 concrete provider model。
4. AGC`apps/ai-game-creator-shell/src` 及其 Tauri 本地资源生成链路)本里程碑不增加模型字段、选择器或尺寸行为;继续使用现有请求默认值。AGC 代码只在确属历史兼容读取/测试证明的位置保留旧值,不借本次同步引入新功能。
5. OpenAPI、External editor skill reference、现役脚本和当前技术方案中的“新任务使用 GPT Image 2 / 2-c fallback”改为 GPT Image 2.5 口径;静态已生成 manifest、历史审计和兼容 fixture 保持旧值并补充历史语义断言。
## 前端实现顺序
1. 先在 `ImageCanvasGenerationModel` 集中定义历史别名解析与迁移警告结果,保持 nanobanana 默认值、尺寸矩阵和可选模型顺序不变。
2. 迁移 `ImageCanvasGenerationDialogModel``ImageCanvasGenerationSubmissionModel`、生成工作流、快速编辑弹窗、规范/宣发面板的 GPT 专用默认、锁定值和提交 payload。
3.`generationInputs` / 画布布局 / 资源历史恢复补齐旧值迁移警告测试;确认新请求字段为 `gpt-image-2.5`,而历史原文仍未被改写。
4. 更新 OpenAPI 与现役工具/专题文档,区分业务模型、concrete provider model、历史值和产品展示名;不得把 AGC 默认行为改成前端显式模型选择。
## 验证命令
- `cargo fmt --all --manifest-path server-rs/Cargo.toml -- --check`
- `cargo test -p platform-image`
- `cargo test -p platform-editor-agent`
- api-server 定向测试/`cargo check -p api-server`
- `npm run typecheck`
- `npm run check:doc-index`
- `npm run check:encoding`
- `git diff --check`
- 主站图片编辑定向测试:模型选项/展示名、GPT 专用提交值、历史 `gpt-image-2``gpt-image-2-c` 恢复及警告、nanobanana 默认回归。
- AGC 定向测试:确认未新增 `model` IPC 入参、未改变原有尺寸/UI 行为,现有资源生成请求继续依赖服务端默认。
- OpenAPI/External editor 契约测试:模型说明、参考图容量说明和新业务模型值一致,provider concrete model 不进入普通前端契约。
## 风险与回滚
- 风险:启动阶段依赖变化、历史任务兼容解析遗漏、nanobanana 被误路由到 Tiantoken、provider key 泄露到公开 DTO。
- 风险:主站把 nanobanana 的默认路径误改成 GPT Image 2.5,或把历史兼容值静默吞掉导致用户无法识别参数迁移。
- 回滚:以 provider-neutral seam、启动配置、model route、主站前端同步四个局部提交边界回滚;不执行数据库历史迁移,也不回滚历史资源值。
@@ -0,0 +1,53 @@
# GPT Image 2.5 provider 边界重构
- Version: 2
- Status: active
- Date: 2026-09-18
- Parent Spec: `docs/adr/【ADR】GPT Image 2.5模型路由与历史值兼容-2026-09-18.md`
## 目标
在保持图片协议执行逻辑共享的前提下,建立明确的 provider client 边界:GPT Image 2.5 通过 Tiantokennanobanana 通过 VectorEngine;路由依据 concrete model 严格白名单决定;两个 client 在 api-server 启动阶段构造。
## 范围
- `platform-image`provider-neutral 图片执行器、provider client 注入 seam、concrete model 路由和错误/审计 provider 标识。
- `api-server`:启动时构造 VectorEngine/Tiantoken 两个 client,分别读取各自环境变量;任务提交边界的历史模型兼容解析。
- 共享请求、multipart、尺寸、retry、响应和 audit 逻辑保持单一实现。
- Agent 与其它 server-side 图片调用方迁移到业务模型/concrete model 合同。
- 定价、公开 DTO、admin DTO 与 provider model 可见性保持既定 ADR 约束。
## 现役路由
| Concrete model | Provider client | 业务用途 |
| --- | --- | --- |
| `gpt-image-2.5-flare-c` | Tiantoken | Generate,仅限不带参考图的生成任务 |
| `gpt-image-2.5-sunburst-c` | Tiantoken | Edit,包括快速编辑、原位修改、raw edit,以及带参考图的生成任务(图标素材图集的固定参考图、UI 素材提取的来源图同在内) |
| `gemini-3.1-flash-image-preview` | VectorEngine | nanobanana 生成/编辑能力 |
`gpt-image-2``gpt-image-2-c` 只保留为历史持久化/审计字符串;新任务不得 dispatch 到旧 GPT Image 2 路由。未知 model 直接拒绝。
## 必须成立的行为
1. api-server 启动时同时构造两个 required provider client;任一对应环境变量缺失,启动失败。
2. Tiantoken 只读取 `TIANTOKEN_BASE_URL` / `TIANTOKEN_API_KEY`VectorEngine 只读取 `VECTOR_ENGINE_BASE_URL` / `VECTOR_ENGINE_API_KEY`,互不回退。
3. platform-image 根据 concrete model 选择已注入 client;共享执行器不复制 provider 协议逻辑。
4. 同 concrete model 可以 retry,但永不跨 concrete model 或跨 provider fallback。
5. 历史值读取不改写;新任务提交边界将旧值兼容为 GPT Image 2.5 业务任务。
6. 普通前端不接收 concrete provider modeladmin 定价界面可查看和编辑两个具体 pricing key。
7. 生成任务的 concrete model 由参考图决定:同一判据同时用于 dispatch、审计与计价,带参考图的生成任务按编辑档计价。
## 非目标
- 不复制两套完整图片 client。
- 不新增 GPT Image 2 现役 VectorEngine 路由。
- 不修改历史数据库记录或旧审计字符串。
- 不把 provider client 选择下沉给普通前端。
## 验收证据
- provider routing 单元测试覆盖 flare/sunburst/nanobanana/legacy/unknown。
- 启动配置测试证明两套 client 独立读取环境变量,缺失任一配置即失败且无 VectorEngine/Tiantoken 回退。
- 请求审计测试证明 provider 与 concrete model 正确记录。
- platform-image 与 Agent 定向测试通过。
- api-server 类型/编译检查、前端类型检查、编码/文档/diff 门禁通过。
@@ -1,5 +1,28 @@
# 决策记录 # 决策记录
## 2026-09-21 生成带参考图统一按编辑 concrete model 执行与计价
- 背景:GPT Image 2.5 迁移后,生成端在带参考图时会改走 `/v1/images/edits` 并提交 `gpt-image-2.5-sunburst-c`,但计价仍按 `gpt-image-2.5-flare-c` 生成档,dispatch 判断也分散在三个入口各自的 `if 带参考图` 表达式里;结果是「生成档扣费 + 编辑档执行/审计」分裂,admin 单独调价任一档位都会错价,ADR 里「普通生成即使因参考图使用 edits multipart,仍按生成 concrete model」与代码事实不符。
- 决策:保留「按是否带参考图决定 concrete model」这一现有行为,把三处文档改到与代码一致;同时新增 `editor_image_generation_concrete_model(model, has_reference_images)` 作为生成类任务选择 concrete model 的唯一入口,dispatch 与计价共用同一判据。`image_generation_mud_points` 增加 `has_reference_images`,命中编辑 concrete model 时改走编辑档 `image_edit_model_mud_points`;图标素材图集(固定参考图)、UI 素材提取(来源图)、图标规范带 `referenceId`、Agent 生成类工具带 `referenceImageIds` 全部同规则。`EditImageTool` 计价从生成档改为编辑档,与直连快速编辑、raw edit 对齐。
- 边界:nanobanana 生成与编辑共用一个 concrete model,参考图不改变档位;不引入跨模型 fallback;历史持久化值与审计字符串不改写。
- 同批修复:`raw_image_edit` 发送前复用共享 `ensure_provider_matches_model` 白名单校验;`platform-image` 兜底 task id 前缀改为跟随实际 provider`tiantoken-edit` / `tiantoken-nanobanana` 不再带 `vector-engine` 前缀);`resolve_image_provider` 增加 `gpt-image-2 → Tiantoken` 断言把历史值意图钉死;删除 `openai_image_generation.rs` 中仅测试使用的 `create_openai_image_edit` / `create_openai_image_edit_with_references`(测试改用 `..._and_model` 变体,保留空参考图本地校验覆盖);`normalize_editor_generation_options` 的具体 provider key 改用常量;External v1 OpenAPI 的 `model` 描述改为与实现一致(`kind=ui-design` / `publication-material` 固定 `gpt-image-2.5`,UI 素材提取未传默认 nanobanana)。
- 影响范围:`api-server``editor_generation_config``editor_project``editor_project_icon``character_visual_assets``editor_agent/tool``openai_image_generation`)、`platform-image``raw_image_edit``runtime/executor`、集成测试)、ADR、里程碑文档、External v1 OpenAPI 描述、`.codex/skills/gpt-image-2-apimart` 脚本与 SKILL.md(改用 `TIANTOKEN_*` 凭据 + concrete model)。
- 验证:`cargo test -p api-server editor_generation_config``cargo test -p api-server editor_agent::tool``cargo test -p api-server`1123 passed / 1 failed,失败项为 mock LLM 连接超时,单测隔离重跑通过)。
- 关联文档:[`docs/adr/【ADR】GPT Image 2.5模型路由与历史值兼容-2026-09-18.md`](../../adr/【ADR】GPT%20Image%202.5模型路由与历史值兼容-2026-09-18.md)、[`docs/project-memory/plans/【里程碑】GPT Image 2.5 provider边界重构-2026-09-18.md`](../plans/【里程碑】GPT%20Image%202.5%20provider边界重构-2026-09-18.md)。
## 2026-09-21 图片 provider 白名单收紧,并整体删除 gpt-image-2-c
- 背景:GPT Image 2.5 迁移后,`resolve_image_provider` 仍把业务模型名 `gpt-image-2.5``gpt-image-2-c` 一起判为 Tiantoken,等于把「非 provider model」留在 provider 边界白名单里。`gpt-image-2-c` 自 2026-07-21 起只是首选模型失败时的兜底 provider model(兜底移除后仅剩历史审计字符串),从未进入业务模型、持久化 `model` 字段或前端契约——api-server 持久化的模型一律取自 `generation_options.model`(业务值),前端在 2.5 迁移前只写 `gpt-image-2`
- 决策:`resolve_image_provider` 白名单只接受现役具体 provider model,业务名 `gpt-image-2.5` 命中即拒绝,必须先在任务边界解析成具体 key;`gpt-image-2` 继续作为历史可读值接受。`gpt-image-2-c` 不再只做「拒绝」,而是整体删除:常量、re-export、`is_gpt_image_2_family_model` 尺寸语义、`auditable_image_model` 审计标签、api-server 的三处提交边界解析、前端常量与历史恢复分支全部移除;数据库既有审计字符串原样保留,代码不再引用该值。
- 影响范围:`platform-image`constants / lib / image_provider mod / protocol request / runtime executor / 集成测试)、`api-server``openai_image_generation``editor_project``editor_generation_config``editor_agent/tool`)、主站前端 `ImageCanvasGenerationModel` 与其测试、External v1 OpenAPI 描述、ADR 与本文档。前端与 External v1 传入 `gpt-image-2-c` 现在按「不支持的值」处理(走既有未知值分支),不再解析为 GPT Image 2.5。
- 验证:`cargo test -p platform-image` 全绿(含新增 `resolve_image_provider_rejects_business_model_and_accepts_concrete_models`);`cargo test -p api-server` 1115 项;`npx vitest run src/components/image-editor/ImageCanvasGenerationModel.test.ts` 34 项通过;`check:encoding``check:doc-index` 通过。
## 2026-09-21 图片 provider 启动期缺配置的报错点名环境变量
- 背景:两个图片 provider client 都在 `AppState::new` 构造,缺 base URL / API key 即阻止 api-server 启动;旧报错只写「tiantoken 图片 provider 缺少 BASE_URL 配置」,没有点名变量名,而 `TIANTOKEN_*` 已不再从 `VECTOR_ENGINE_*` 回退,运维迁移时不易定位。
- 决策:保持「不回退、缺失即启动失败」,但报错文案补上具体变量名(`VECTOR_ENGINE_BASE_URL` / `VECTOR_ENGINE_API_KEY``TIANTOKEN_BASE_URL` / `TIANTOKEN_API_KEY`);client 构造成功后用 `info` 记录 provider、base_url、超时与对应 env 变量名,便于确认实际生效的 provider 配置。
- 验证:`cargo check -p api-server` 通过;`cargo test -p api-server` 1115 项通过(0 失败)。
## 2026-09-21 渠道进安装身份:不同渠道的 AGC 包体在同一台设备并存 ## 2026-09-21 渠道进安装身份:不同渠道的 AGC 包体在同一台设备并存
- 背景:渠道此前只决定更新端点(`plugins.updater.endpoints`)与渲染层平台 origin`productName` / `identifier` 与渠道无关,于是所有渠道共用 `%LOCALAPPDATA%\陶泥儿` 安装目录、同一个卸载项(`HKCU\Software\Microsoft\Windows\CurrentVersion\Uninstall\陶泥儿`,另有 `HKCU\Software\genarrative\陶泥儿`)以及同一份 `%APPDATA%\world.genarrative.ai-game-creator` 数据目录。本机 0.1.48 安装实测:主程序二进制里只有 1 处 `agc/dev-win/latest.json`、0 处 release 端点,说明渠道在产物里只体现为端点。后果是后装的渠道静默顶掉先装的渠道,并接管更新端点、平台服务器与本地登录态/项目数据。 - 背景:渠道此前只决定更新端点(`plugins.updater.endpoints`)与渲染层平台 origin`productName` / `identifier` 与渠道无关,于是所有渠道共用 `%LOCALAPPDATA%\陶泥儿` 安装目录、同一个卸载项(`HKCU\Software\Microsoft\Windows\CurrentVersion\Uninstall\陶泥儿`,另有 `HKCU\Software\genarrative\陶泥儿`)以及同一份 `%APPDATA%\world.genarrative.ai-game-creator` 数据目录。本机 0.1.48 安装实测:主程序二进制里只有 1 处 `agc/dev-win/latest.json`、0 处 release 端点,说明渠道在产物里只体现为端点。后果是后装的渠道静默顶掉先装的渠道,并接管更新端点、平台服务器与本地登录态/项目数据。
@@ -9146,6 +9169,31 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
- 验证:限速后 `Genarrative-Full-Build-And-Deploy` #289 / #290 SUCCESS;采样期 Jenkins 峰值 10.2~10.5 核、限流不足 2s(可忽略),runner 峰值 12.07 核且持续出现 throttling,整机回落到 2.6%~19.8%。 - 验证:限速后 `Genarrative-Full-Build-And-Deploy` #289 / #290 SUCCESS;采样期 Jenkins 峰值 10.2~10.5 核、限流不足 2s(可忽略),runner 峰值 12.07 核且持续出现 throttling,整机回落到 2.6%~19.8%。
- 关联文档:[开发运维](../../【开发运维】本地开发验证与生产运维-2026-05-15.md)。 - 关联文档:[开发运维](../../【开发运维】本地开发验证与生产运维-2026-05-15.md)。
## 2026-09-18 GPT Image 2.5 业务模型与具体 provider 定价路由
- **决策**:新任务使用业务模型值 `gpt-image-2.5`api-server 按任务显式 dispatch 具体模型 `gpt-image-2.5-flare-c`(无参考图的生成)或 `gpt-image-2.5-sunburst-c`(编辑,以及带参考图的生成),并把同一具体 key 交给 `platform-image` 与后台定价解析。带参考图的生成任务因为必须走 edits multipartdispatch、审计与计价统一按编辑 concrete model;同模型重试不跨模型 fallback。
- **历史兼容**:已持久化 `gpt-image-2` 读回原值不改写;基于旧资源发起新任务时,在提交边界解析为 `gpt-image-2.5`,新任务/新产物按新业务值和当前 task price 处理。旧 `gpt-image-2-c` 仅保留历史审计,不再作为 fallback 或业务模型。
- **可见性**:普通主站前端和公开定价 API 不接收具体 provider key;新生成 UI label 为 `GPT Image 2.5`,历史资源/旧编辑上下文不扩散该 label。admin Web/API 是明确例外,可查看和编辑两个具体定价 key。旧单 key 定价配置允许受控 backfill,并加 compatibility TODO。
- **关联 ADR**[`docs/adr/【ADR】GPT Image 2.5模型路由与历史值兼容-2026-09-18.md`](../../adr/【ADR】GPT%20Image%202.5模型路由与历史值兼容-2026-09-18.md)。
- **补充**GPT Image 2.5 的两个具体模型通过显式 `TIANTOKEN_BASE_URL` 与独立 `TIANTOKEN_API_KEY` 发送;环境变量缺失时必须失败,禁止使用 VectorEngine 配置或 API key 回退。
## 2026-09-18 GPT Image 2.5 provider-neutral 执行器与双 client 启动边界
- **决策**:图片协议执行逻辑保持单一共享实现;只抽出 provider client 的 identity、base URL、API key 和 client 构造,通过依赖注入复用请求 body、multipart、尺寸、retry、响应和 audit。
- **路由**`gpt-image-2.5-flare-c` / `gpt-image-2.5-sunburst-c` 走 Tiantoken`gemini-3.1-flash-image-preview`nanobanana)走 VectorEngine`gpt-image-2` / `gpt-image-2-c` 仅是历史字符串,新任务不再进入旧 GPT Image 2 路由;未知 model 拒绝。
- **启动**api-server 启动时同时构造 VectorEngine 与 Tiantoken 两个 required client;各自只读取自己的环境变量,任一配置缺失即启动失败,不延迟到首次请求。
- **重试**:只在同一个 concrete model 内 retry,禁止跨 model、跨 provider fallback。
- **关联文档**[`docs/adr/【ADR】GPT Image 2.5模型路由与历史值兼容-2026-09-18.md`](../../adr/【ADR】GPT%20Image%202.5模型路由与历史值兼容-2026-09-18.md)、[`docs/project-memory/plans/【里程碑】GPT Image 2.5 provider边界重构-2026-09-18.md`](../plans/【里程碑】GPT%20Image%202.5%20provider边界重构-2026-09-18.md)。
## 2026-09-19 Tiantoken 凭据落到 AppConfig 字段并修复编辑器 LLM 测试夹具
- **决策**Tiantoken 凭据仍然只从 `TIANTOKEN_BASE_URL` / `TIANTOKEN_API_KEY` 读取,不回退 VectorEngine;读取点从 `AppState::new` 前移到 `AppConfig::from_env()`,落到 `AppConfig.tiantoken_base_url` / `AppConfig.tiantoken_api_key` 两个字段,与其它 provider 的配置形态一致,`AppState` 构造与测试构造复用同一条路径;删除 `config::tiantoken_base_url()` / `config::tiantoken_api_key()` 两个现读环境变量的访问器。
- **原因**2026-09-18 的「双 provider 启动边界」改成现读环境变量后,`editor_background_music_prompt_assist``editor_sound_effect_prompt_assist``vector_engine_audio_generation/sound_effect_translation` 三处 mock LLM 夹具仍用 `AppConfig.vector_engine_*` 构造状态,28 条用例拿到 503 `editor_llm_unavailable`。改用进程环境变量做夹具会让并行用例互相踩 `TIANTOKEN_*`,因此把凭据落到 config 字段而不是在测试里 set_var。
- **边界**ADR 的「不得回退 VectorEngine 或其 API key」仍然成立,`from_env` 只读 `TIANTOKEN_*`,并有用例固定该行为。
- **验证**`cargo test -p api-server` 1060 passed / 0 failed / 6 ignored`cargo fmt --all --check``npm run check:encoding``git diff --check` 通过。
- **关联文档**[`docs/adr/【ADR】GPT Image 2.5模型路由与历史值兼容-2026-09-18.md`](../../adr/【ADR】GPT%20Image%202.5模型路由与历史值兼容-2026-09-18.md)、[`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`](../../【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md)。
## 2026-09-17 AGC 抠图接入本地资源编辑恢复闭环 ## 2026-09-17 AGC 抠图接入本地资源编辑恢复闭环
- 背景:`agc_remove_background` 原先只提交 `/api/editor/images/background-removals` 并返回 `queued`,没有轮询远端任务、下载完成媒体或写入本地 manifestBgFilter 已成功处理但 Agent 因此永远只能看到受理回执。 - 背景:`agc_remove_background` 原先只提交 `/api/editor/images/background-removals` 并返回 `queued`,没有轮询远端任务、下载完成媒体或写入本地 manifestBgFilter 已成功处理但 Agent 因此永远只能看到受理回执。
@@ -2503,7 +2503,7 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
- 现象:配置了 `APIMART_BASE_URL` / `APIMART_API_KEY` 后,RPG、拼图或方洞的 GPT-image-2 生图仍返回缺配置,或请求体里还出现 `official_fallback` / `image_urls` - 现象:配置了 `APIMART_BASE_URL` / `APIMART_API_KEY` 后,RPG、拼图或方洞的 GPT-image-2 生图仍返回缺配置,或请求体里还出现 `official_fallback` / `image_urls`
- 原因:2026-05-21 后 GPT-image-2 图片生成按 VectorEngine 创建/编辑接口分流;2026-07-05 后创意 Agent 文本链路也改为 VectorEngine Chat Completions `gpt-5.4-mini`,APIMart 不再作为当前创意 Agent 来源。 - 原因:2026-05-21 后 GPT-image-2 图片生成按 VectorEngine 创建/编辑接口分流;2026-07-05 后创意 Agent 文本链路也改为 VectorEngine Chat Completions `gpt-5.4-mini`,APIMart 不再作为当前创意 Agent 来源。
- 处理:为图片生成配置 `VECTOR_ENGINE_BASE_URL=https://api.vectorengine.ai``VECTOR_ENGINE_API_KEY``VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`;排查请求体时确认无参考图路径为 `/v1/images/generations`、有参考图路径为 `/v1/images/edits`业务 / 计费与 provider 首发模型`gpt-image-2`,仅在符合条件的 provider 失败后切到兜底模型 `gpt-image-2-c` - 处理:为图片生成配置 `VECTOR_ENGINE_BASE_URL=https://api.vectorengine.ai``VECTOR_ENGINE_API_KEY``VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`;排查请求体时确认无参考图路径为 `/v1/images/generations`、有参考图路径为 `/v1/images/edits`新任务业务模型为 `gpt-image-2.5`provider 具体模型为 `gpt-image-2.5-flare-c`(不带参考图的生成)/ `gpt-image-2.5-sunburst-c`(编辑与带参考图的生成),带参考图的生成按编辑档计价,只在同一具体模型内重试。历史兜底模型 `gpt-image-2-c` 已从代码整体删除,不再存在任何 fallback 路径,请求体里出现该值即视为异常
- 验证:运行 `cargo test -p api-server openai_image --manifest-path server-rs/Cargo.toml` 和相关玩法图片生成测试;真实联调只在本地私密环境放置 VectorEngine key。 - 验证:运行 `cargo test -p api-server openai_image --manifest-path server-rs/Cargo.toml` 和相关玩法图片生成测试;真实联调只在本地私密环境放置 VectorEngine key。
- 关联:`docs/technical/VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md``server-rs/crates/api-server/src/openai_image_generation.rs` - 关联:`docs/technical/VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md``server-rs/crates/api-server/src/openai_image_generation.rs`

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