合并最新 master 并解决资源依赖图冲突
Project CI / Repository checks (pull_request) Failing after 7s
Project CI / Frontend tests (pull_request) Failing after 2m19s
Project CI / Backend tests (pull_request) Failing after 8s
Project CI / Native shell tests (pull_request) Successful in 12m34s

同步 mentor 已合入 master 的最新工程改动。

保留资源依赖图决策记录并整合 master 的新增决策。

# Conflicts:
#	docs/project-memory/shared-memory/decision-log.md
This commit is contained in:
2026-07-31 15:54:40 +08:00
200 changed files with 19452 additions and 3560 deletions
@@ -1761,7 +1761,8 @@
"type": "object",
"required": [
"viewport",
"layers"
"layers",
"expectedRevision"
],
"properties": {
"viewport": {
@@ -1778,7 +1779,7 @@
"expectedRevision": {
"type": "integer",
"minimum": 0,
"description": "可选的画布 revision CAS;不匹配时返回 409。"
"description": "必填的画布 revision CAS;不匹配时返回 409。"
}
},
"additionalProperties": false
@@ -2561,6 +2562,21 @@
],
"description": "纯色抠像背景色。可传画布支持的纯色背景 hex(如 #CFEFFF)指定;传 \"auto\"、null 或省略则由服务端自动决策。"
},
"style": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"examples": [
"none",
"pixelArt"
],
"description": "可选生成后处理风格,当前识别 none 与 pixelArt。省略、null、空字符串或 none 按无风格处理;pixelArt 仅支持普通图片(kind 省略)和 character。未知字符串或不支持该风格的 kind 按 none 继续生成并返回 unsupported-image-style 告警;非字符串值返回 400。"
},
"size": {
"type": "string",
"description": "兼容旧 size 入参;未传 aspectRatio/imageSize 时生效。",
@@ -2585,7 +2601,7 @@
"ui-design",
"publication-material"
],
"default": "spec"
"description": "省略时生成普通图片;其它值选择对应的专用生成流程。"
},
"model": {
"type": "string",
@@ -2950,6 +2966,7 @@
},
"iconDescriptions": {
"type": "array",
"description": "图标生成需求文本数组,供 prompt 组装使用;数组长度不控制自动切片数量。画布前端把完整用户提示词作为唯一数组元素提交;其它调用方可继续提交 1 到 100 条非空文本。",
"minItems": 1,
"maxItems": 100,
"items": {
@@ -2963,6 +2980,21 @@
],
"description": "纯色抠像背景色。可传画布支持的纯色背景 hex(如 #CFEFFF)指定;传 \"auto\"、null 或省略则由服务端自动决策。"
},
"style": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"examples": [
"none",
"pixelArt"
],
"description": "可选生成后处理风格,当前识别 none 与 pixelArt。省略、null、空字符串或 none 按无风格处理;pixelArt 启用图标图集像素规整。未知字符串按 none 继续生成并返回 unsupported-image-style 告警;非字符串值返回 400。"
},
"model": {
"type": "string",
"default": "gemini-3.1-flash-image-preview"
@@ -3158,7 +3190,7 @@
"properties": {
"code": {
"type": "string",
"description": "自动拆分未完成的稳定原因码。"
"description": "自动拆分未完成的稳定原因码,包括原始连通域超限、局部候选拥挤、输出切片超限、处理超时、未识别到素材或切片持久化失败。"
},
"reason": {
"type": "string",
@@ -3176,8 +3208,13 @@
"properties": {
"code": {
"type": "string",
"const": "postprocess-failed-source-preserved",
"description": "透明背景处理最终失败并保留 provider 原图时的稳定原因码。"
"enum": [
"postprocess-failed-source-preserved",
"dimension-restore-fallback",
"unsupported-image-style",
"multiple-generation-warnings"
],
"description": "生成成功但后处理发生非阻断降级时的稳定原因码。"
},
"reason": {
"type": "string",
@@ -3212,7 +3249,7 @@
},
"iconImageSrcs": {
"type": "array",
"description": "按图集 alpha 连通域拆分并持久化的独立素材列表。",
"description": "识别图集中全部有效 alpha 连通域并持久化的独立素材列表,按视觉阅读顺序命名为“素材 N”;数量由图集内容决定,不由 iconDescriptions 数量决定。自动生成与手动拆分图集使用相同识别规则。",
"items": {
"$ref": "#/components/schemas/EditorIconSpritesheetIconResult"
}
@@ -3226,7 +3263,7 @@
"type": "null"
}
],
"description": "图集已成功持久化,但自动拆分未完成时返回;此时 iconImageSrcs 为空,调用方仍应使用整张图集。与通用 warning 互斥。"
"description": "可信透明图集已成功持久化,但全连通域自动拆分未完成时返回;此时 iconImageSrcs 为空,调用方仍应使用整张图集。原始连通域、输出数量或 CPU 预算超限不会产生切片 PUT、资源或画布切片。透明处理、Alpha/尺寸恢复、provider 原图修复性回读或透明图完整解码失败时走 provider 原图 source-only,sliceWarning 为 null。"
},
"prompt": {
"type": "string"
@@ -3290,14 +3327,8 @@
"type": "null"
}
],
"description": "透明背景处理最终失败、provider 原图作为主结果时返回的非阻断告警。与 sliceWarning 互斥。"
"description": "生成成功但风格归一化、尺寸恢复、透明背景处理、Alpha 回贴、provider 原图修复性回读、透明图完整解码或像素规整发生非阻断降级时返回。source-only 降级只返回 provider 原图且不会进入拆分;其它通用告警可以与 sliceWarning 并存。"
}
},
"not": {
"required": [
"warning",
"sliceWarning"
]
}
},
"EditorCharacterAnimationGenerationRequest": {
@@ -25,6 +25,82 @@
- 验证方式:图模型与 SVG 定向测试覆盖去重、无效 ID、完整任务环、可见资源自引用闭环、任务流聚合、搜索过滤、选择高亮和拖动几何;AppSurface 覆盖两种边、type 模式卸载和项目切换销毁,并运行 shell typecheck、编码检查与 `git diff --check`。
- 关联文档:`docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md`、`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。
## 2026-07-31 每日免费泥点基础额度纳入后台钱包配置
- 背景:每日免费泥点已是独立余额桶,但基础发放量仍在运行时固定为 `20`,后台“账号配置”只能维护注册初始泥点,运营调整需要改代码。
- 决策:在 `profile_wallet_config` 尾部追加带默认值 `20` 的 `daily_free_points_per_day`,与 `initial_mud_points` 共用 `/admin/api/profile/wallet-config` 和后台账号配置页一次读写。尚未初始化当日额度的用户立即使用最新值;已初始化用户的当日余额不追补、不回收,下一北京时间业务日首次触达时按最新配置重置。跨日退款可继续使当日 `granted_points` 高于基础额度,因此充值中心 `dailyFreeResetPoints` 必须显式投影配置值,不用当日已发放总额反推。
- 迁移与边界:旧 SpacetimeDB 表行和旧迁移 JSON 均缺少新字段,自动迁移与 `migration.rs` 导入归一统一补 `20`;新字段只允许正整数。每日任务奖励、扣费桶顺序、退款归因和北京时间日切边界不变。
- 影响范围:`module-runtime`、`spacetime-module`、`spacetime-client`、`shared-contracts`、`api-server`、`apps/admin-web`、SpacetimeDB 迁移与生成绑定。
- 验证方式:后台页面与 API 定向测试、每日免费日切与迁移定向 Rust 测试、`npm run spacetime:generate -- --rust-only`、`npm run check:spacetime-schema`、`npm run admin-web:typecheck`、`npm run check:encoding`、`git diff --check`。
- 关联文档:`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。
---
## 2026-07-31 发布前延期冷备份由独立 systemd 上传并补偿扫描
- 背景:Jenkins Stdb Publish 的 async 备份先生成 `uploadStatus=deferred` 的本地 tar.gz,再从 EXIT trap 用 `nohup` 启动上传。后台进程仍继承 Jenkins Cookie,作业结束时可被清理;旧 deferred manifest 也没有后续补偿扫描,导致 dev 的本地冷备份持续占满根盘。
- 决策:`production-stdb-publish.sh` 只能用具名、`Type=exec`、`--collect` 的 `systemd-run` transient service 启动异步上传,禁止回退 `nohup`。独立服务执行 `database-backup-to-oss.mjs --upload-deferred-dir <backup-dir>`,在同一备份锁内按文件名串行补传同库 `deferred/pending` 归档;目录外路径或 manifest/归档不匹配时失败关闭,缺失归档的历史 manifest 只报告不删除。
- 清理边界:只有 archive 上传与 HEAD 验真、manifest sidecar 上传验真、baseline state 写入全部成功后,才按 `GENARRATIVE_DATABASE_BACKUP_KEEP_LOCAL` 删除精确的 archive 与 manifest。transient unit 未启动或上传失败时保留归档,由后续 publish 继续补偿;`files-history` timer 仍不负责清理这些 tar.gz。
- 影响范围:`scripts/deploy/production-stdb-publish.sh`、`scripts/database-backup-to-oss.mjs`、生产运维门禁和本文档。
- 验证方式:`npm run check:database-backup`、`npm run check:production-ops`、`npm run check:encoding`、`git diff --check`;dev 现场还必须确认 transient unit 不在 Jenkins session scope,旧 deferred 归档逐份变为 OSS 已验真对象后被删除,备份锁清空,核心服务与公开接口健康。
- 关联文档:`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。
---
## 2026-07-29 图集切片必须受前置容量和有界 CPU 保护
- 背景:图标与 UI 图集的 alpha 连通域识别会在 async handler 上同步执行;原始连通域合并采用全量两两比较,`64` 个输出限制又晚于排序、裁剪和 PNG 编码。碎块或噪点图会放大 CPU 与内存成本,手动拆分、图标自动拆分和 UI 提取都受影响。另一方面,图标与 UI 的 Alpha 尺寸恢复、provider 原图回读或透明图解码失败此前只记日志,仍会把不可信透明图持久化并拆分。
- 决策:`platform-image` 在每次 flood-fill 后累计所有原始连通域(包括随后过滤的噪点)并以 `4096` 为硬上限;合并只通过 `64px` 空间网格查询 `48px` 最大邻域内且满足辅助部件尺寸条件的候选,单网格最多登记 `256` 个组件、单 source 最多保留 `512` 个候选,拥挤时明确返回资源限制错误;调用方把固定 `maxOutputSlices=64` 传入 platform slicer,并在排序、裁剪和 PNG 编码前拒绝超限。三条入口统一走 2 路 semaphore、30 秒本地上界与请求绝对 deadline 共同保护的 `spawn_blocking`,permit 必须由 blocking 闭包持有。自动图标 / UI 超限以空切片和稳定 `sliceWarning` 完成,手动拆分返回 `422`,两者都不得产生任何切片 PUT、资源或画布切片;自动路径已成功的整张图集仍按既有契约保留。
- source-only 收口:角色、图标和 UI 共用同一个 provider 原图收口 helper。BgFilter 最终失败、Alpha 比例漂移超过 `5%`、provider 原图修复性回读失败、Alpha 回贴失败或透明图完整解码失败时,只用已保存 provider 原图完成占位,返回 `completed + warning`;图标 / UI 固定 `iconImageSrcs=[]`、`sliceWarning=null`,不写透明图、不拆分。provider 原图本身无法解码时在首次持久化前失败,不再伪造 `512×512` 元数据。
- 影响范围:`server-rs/crates/platform-image/src/generated_asset_sheets/`、`server-rs/crates/api-server/src/editor_project.rs`、图片画布图标与 UI 素材生成 / 手动拆分链路;不修改请求 DTO、扣费退款、SpacetimeDB schema 或成功路径多产物布局。
- 验证方式:platform-image 覆盖大量独立 `4×4` 块、单像素噪点和 65 个有效输出;api-server 覆盖比例漂移、原图回读失败、截断透明 PNG、共享 source-only helper 无持久化副作用,以及三入口统一 bounded slicer。运行 `cargo test -p platform-image generated_asset_sheets --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server editor_project::tests --manifest-path server-rs/Cargo.toml`、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`、`npm run check:encoding` 和 `git diff --check`。
- 关联文档:`docs/【编辑器】画板图标素材生成入口设计-2026-06-15.md`、`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`。
---
## 2026-07-29 像素规整降级必须复用交付尺寸守卫
- 背景:像素模式接入「角色带背景原图与透明图统一交付尺寸」后,删除了原先像素路径末尾的后置尺寸恢复。但像素规整的 best-effort 降级分支(预算耗尽、回读 provider 原图失败或超时、CPU permit 获取失败、worker 内 deadline、join 异常、worker 超时)都直接返回 BgFilter 原始输出并把尺寸错误置为 `None`,跳过了非像素路径已有的尺寸比对与 alpha 回贴。BgFilter 回图尺寸漂移是已知现象,叠加并发上限 2 导致的 permit 超时后,角色会绕过「改用已保存的同尺寸原图完成画布」的安全降级,角色和图标都可能持久化尺寸漂移的低分辨率透明图。
- 决策:像素路径的每一条降级都必须经 `degrade_editor_pixel_art_to_postprocessed_with_dimension_guard` 收口,该守卫复用非像素路径的 `apply_editor_postprocessed_alpha_from_persisted_provider_source_or_original`:先做纯内存尺寸比对,与交付尺寸一致就原样返回且不产生额外 OSS GET;漂移才回读原图重贴 alpha;修复失败如实返回尺寸错误,由调用方按各自既有语义处理。由 provider 原图逐像素合成的 `rgba_source` fallback 尺寸天然正确,不再经守卫。像素路径函数因此需要显式接收交付宽高。
- 生效范围(由同日后续决策补齐):像素路径继续保证不把 BgFilter 原始输出连同 `None` 尺寸错误交回调用方;角色、图标和 UI 拿到尺寸 / Alpha 错误后现已统一走 provider 原图 source-only 收口,不再持久化或拆分尺寸异常、比例异常或不可解码的透明图。
- 影响范围:`server-rs/crates/api-server/src/editor_project.rs` 的角色与图标像素规整降级路径;不改变成功路径、OSS PUT 次数、资源类型、画布项或前端契约,OSS GET 仍只在尺寸漂移时发生。
- 验证方式:`pixel_art_degrade_paths_guard_postprocessed_delivery_dimensions` 结构断言固定"降级分支不得返回 `(postprocessed, None, …)`"与守卫的委托实现;运行 `cargo test -p api-server editor_project --manifest-path server-rs/Cargo.toml`、`npm run check:rustfmt`、`npm run check:encoding` 和 `git diff --check`。
- 关联文档:本文件「2026-07-29 角色带背景原图与透明图统一交付尺寸」与「2026-07-28 图片生成风格使用可扩展字段并以纯内存像素规整首发」。
- 补充(同日):守卫的回读必须分两类处理。已取得 provider 原图的四条降级分支(permit 获取失败、worker 内 deadline、join 异常、worker 超时)改走纯内存守卫 `degrade_editor_pixel_art_with_provider_source`,零额外 GET;尚未取得原图的三条分支(进函数即预算耗尽、第一次回读失败、第一次回读超时)才走会回读的守卫。计数断言固定「回读守卫 3 处、内存守卫 4 处」,防止后续新增分支时误用回读版本。
- OSS 回读口径(修正此前「最多增加一次 OSS GET」的措辞):约束是**不重复读取已经成功取得的对象**,而不是"整个请求至多一次 GET"。仅在尺寸漂移且尚未持有原图时才发起最多一次修复性回读,失败后不再重试;因此第一次回读失败或被像素预算掐断时,允许存在第二次、也是最后一次尝试——第一次超时往往并非 OSS 异常,而是被 30 秒像素预算切断,此时对象通常可正常读取,放弃修复反而会让角色更频繁地退化为原图单产物。
- 回读上界:修复性回读必须始终有绝对 deadline。优先取外层 `request_deadline`,但它只在队列 worker 路径上有值——inline HTTP 请求的 `RequestContext` 默认 `external_call_deadline = None`,此时守卫自行以 `Instant::now() + EDITOR_PIXEL_ART_MAX_PROCESSING_DURATION` 重新计时派生上界,不得退化为无界 `download.await`。`apply_editor_postprocessed_alpha_from_persisted_provider_source_or_original` 的可选 `download_deadline` 只对像素守卫传值,非像素路径继续传 `None` 保持既有语义不变。结构断言固定守卫内必须同时出现 `request_deadline.unwrap_or_else(` 与 `EDITOR_PIXEL_ART_MAX_PROCESSING_DURATION`,防止兜底上界被移除后静默退回无界。
---
## 2026-07-29 角色带背景原图与透明图统一交付尺寸
- 背景:图片画布已将模型原生回图归一到统一业务像素矩阵,但角色分支为了保留 provider 原生分辨率,先持久化带背景原图,只在扣背后归一透明主图。因此同一个 1K 角色任务会同时给出模型原生大图和长边 `1024` 的透明图。
- 决策:角色分支必须在持久化带纯色背景原图和调用 BgFilter 之前,先按统一业务像素矩阵执行一次尺寸归一;该原图和透明派生图始终使用同一实际像素尺寸,1K 的长边为 `1024`。若 provider 回图任意一边小于业务目标或比例偏差过大,仍禁止放大或大幅裁切;此时两张图一同保留 provider 实际尺寸并返回通用 `warning`,不允许只改透明图。BgFilter 回图尺寸漂移时只允许在宽高比偏差不超过 `5%` 时重采样 alpha 蒙版并回贴到该原图;蒙版比例超限、回贴失败或尺寸验证失败时必须改用原图单产物降级,不持久化尺寸或比例不一致的透明图。若尺寸降级和后处理降级同时发生,同一条 `warning.reason` 必须同时保留两个原因。
- 影响范围:`server-rs/crates/api-server/src/editor_project.rs` 的角色生成、原图持久化、BgFilter 输入、项目资源尺寸与画布图层 Resolution;不改变前端请求 DTO、扣费、素材类型或多产物布局。
- 验证方式:后端定向测试覆盖角色全尺寸矩阵:`nanobanana2` 的 `0.5K / 1K / 2K` 和 `gpt-image-2` 的 `1K / 2K`,每档均覆盖 `1:1 / 4:3 / 3:2 / 2:3 / 9:16 / 16:9`,30 个组合全部构造大于目标尺寸的真实 PNG provider 回图并执行像素恢复,不只校验字符串映射;另覆盖欠尺寸禁止放大、比例超限、BgFilter 错比例 alpha 蒙版拒绝和组合告警。同时从函数调用顺序上固定“尺寸归一 → 持久化带背景原图 → BgFilter”。运行 `cargo test -p api-server editor_project --manifest-path server-rs/Cargo.toml`、`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`。
---
## 2026-07-29 图标图集 BgFilter 开启 cross-check
- 背景:图标 spritesheet 的透明化需要提高主体内部孔洞、轮廓和相邻小图标边缘的交叉校验质量。
- 决策:生成图标素材的 BgFilter `background_mode=flat` 请求固定显式传 `cross_check=on`,与角色形象和角色动作逐帧去背一致;UI 设计图素材提取及手动 complex 去背景继续传 `off`。该参数仍属于后端内部供应商策略,不进入前端 DTO 或外部 OpenAPI。
- 边界:不修改 BgFilter fallback、Alpha 回贴、默认关闭 despill、图标切片、OSS / 资源 / 画布持久化和任务告警语义。
- 验证方式:运行 `cargo test -p api-server editor_canvas_screen_background_generation_uses_bgfilter_postprocess --manifest-path server-rs/Cargo.toml`、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`、`npm run check:rustfmt`、`npm run check:encoding` 和 `git diff --check`。
- 关联文档:`docs/【编辑器】画板图标素材生成入口设计-2026-06-15.md`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`。
---
## 2026-07-23 画布 Agent 工具生命周期统一经 object-safe trait 分派
- 背景:画布 Agent 八类工具的参数规范化、确认展示、计价与 worker payload、完成结果格式化和媒体投影分别在 `tool_args.rs`、`display_args.rs`、`api.rs`、`reconcile.rs` 重复按工具名分派;新增或调整工具时容易漏改其中一处。
- 决策:api-server 以 object-safe `EditorAgentTool: ToolDyn` 取代仅承载计价的 `EditorAgentPricedTool`。trait 的所有动态方法统一接收 `serde_json::Value`;每个具体工具实现自行反序列化为真实 Args / 结果,`validate_args` 与 `format_execute_message` 显式转发到 `platform-editor-agent` 已有强类型实现,再把规范 Args、展示投影、job payload、完成文本或媒体引用擦除回公共类型。`editor_agent_tool(toolName, context)` 绑定当前 `EditorToolContext` 并作为唯一八分支工具名分派;规划、确认和回填不得再维护平行 switch。LLM builder 的工具注册列表保持独立显式维护。
- 边界:不改变工具名、LLM schema、OSS 消息文档、`displayArgs`、模型定价、job kind / payload、dedupe key、worker、计费、完成消息或图片 / 视频 / 音频引用契约,不涉及前端、SpacetimeDB schema 或迁移。
- 影响范围:`server-rs/crates/api-server/src/editor_agent` 的工具 trait、参数规范化、确认入队与终态回填,以及画布 Agent 专题文档。
- 验证方式:覆盖八类 factory 与 dyn validation / pricing / display / job / formatter / media projection 的 api-server 定向测试,运行 `cargo test -p api-server --manifest-path server-rs/Cargo.toml editor_agent`、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`、`npm run check:rustfmt`、`npm run check:encoding` 和 `git diff --check`。
---
## 2026-07-28 AI 游戏创作资源画布布局使用本地双模式 CAS sidecar
- 背景:项目开发工作台当前只在 React 会话内保存同分类资源的一维拖拽顺序,项目切换或客户端重启后重建默认排列;工作台 PRD 虽已给出二维位置字段,但缺少落盘路径、坐标系、Tauri API、CAS、异常与安全边界,仍不足以直接编码。
@@ -312,8 +388,8 @@
## 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` 的旧消息降级路径。
- 背景:画布 Agent 已在实际生成前进入 `pending_confirmation`,但 `EditorAgentToolCall.args` 只保存工具私有的规范参数 JSON,其中图片参数是保护真实 data key 的 SHA-256 opaque ID。前端直接解析 `args` 只能显示内部哈希或图片数量,无法向用户准确展示即将使用的目标图、参考图和完整参数;若直接把图片 URL 或对象塞回 `args`,又会破坏确认执行反序列化和 LLM 不可见真实 data key 的安全边界。
- 决策:LLM 返回的原始工具参数只作为 api-server 本次处理的瞬时输入;后端按已注册 ToolArgs 反序列化、补齐默认值、删除未知 / 退役字段、完成工具参数校验并重新序列化后,才把结果写入 `EditorAgentToolCall.args`。校验失败的调用不得持久化为待确认消息。该规范 `args` 是确认执行唯一真相,不允许前端改写或回传替代参数;新增必填 `displayArgs` 只读展示投影,内含 `stringArgs`、`imageArgs` 和 `extras.priceMudPoints`。`stringArgs` 承载提示词与规格等用户可见字段,`imageArgs.refs` 承载规范 `args` 中的 `imageId` 及后端解析出的 `objectKey`、`imageSrc`、可选缩略图、标签和尺寸;`extras.priceMudPoints` 由 api-server 在创建待确认消息时使用后端运行时模型定价快照计算,前端只显示“预计消耗 N泥点”,不自行计算或回传价格。api-server 必须按已注册 tool 白名单,从规范 `args` 与 OSS 会话文档的附件 / 历史生成结果构建该投影;前端只渲染投影,以 `ResolvedAssetImage` 换签显示图片,不解析 tool 私有 schema、不展示 SHA-256 ID。展示价格不参与确认执行或实际扣费,确认后仍由既有生成 BFF 按后端运行时定价预扣费。删除只重复 `args` 且没有稳定语义的 `EditorAgentToolCall.summary`。模块尚未上线,不保留缺少 `displayArgs` 时读取 `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`。
@@ -438,7 +514,7 @@
## 2026-07-14 后台账号采用 owner 引导账号与一级 Tab 实时授权
- 背景:后台此前只支持一组环境变量管理员,所有 `/admin/api/*` 共用统一 admin 门禁,无法给运营、审核等人员分配独立账号和页面范围。
- 决策:现有 `GENARRATIVE_ADMIN_USERNAME/PASSWORD` 账号固定作为不可编辑 owner;新增 member 独立保存到私有 `admin_account` 表,密码使用 Argon2id 摘要。登录凭据快照与普通账号快照在类型层分离,普通列表、按 ID 查询和写入响应不包含 `password_hash`。Argon2id 在 blocking 任务中运行并由 api-server 有界限流;未知、停用和 owner 错密账号使用 dummy hash 抹平耗时。member 权限粒度固定为后台 18 个一级 Tab,“账号管理”只允许 owner 且不可授予 member。member 每次请求重新读取当前账号并校验启停、`token_version` 和 Tab 权限;权限、密码或启停变化递增版本并立即淘汰旧 JWT。账号不存在返回 `401`,SpacetimeDB 故障保留 `502/503` 而不清理有效 token。前端导航过滤和页面挂载门禁只负责体验,正式授权由 api-server 的 API-to-Tab 矩阵执行,未登记的新后台路由对 member 默认拒绝。后台面向运营展示管理员身份时统一使用 `displayName`;持久审计仍保存稳定 subject,由 api-server 解析显示名称,前端不得暴露账号 ID 或用登录用户名代替。写接口必须在主事务前加载显示名目录,或在主事务后降级解析,不能把已提交写入伪装为失败。
- 决策:现有 `GENARRATIVE_ADMIN_USERNAME/PASSWORD` 账号固定作为不可编辑 owner;新增 member 独立保存到私有 `admin_account` 表,密码使用 Argon2id 摘要。登录凭据快照与普通账号快照在类型层分离,普通列表、按 ID 查询和写入响应不包含 `password_hash`。Argon2id 在 blocking 任务中运行并由 api-server 有界限流;未知、停用和 owner 错密账号使用 dummy hash 抹平耗时。member 常规权限粒度固定为后台 15 个一级 Tab,“账号管理”只允许 owner 且不可授予 member;2026-07-24 起,历史花费手动对账作为独立高风险操作权限 `profile-wallet-consumption-reconcile`,不随任意 Tab 自动授予。member 每次请求重新读取当前账号并校验启停、`token_version`、Tab 权限和独立操作权限;任一权限、密码或启停变化递增版本并立即淘汰旧 JWT。账号不存在返回 `401`,SpacetimeDB 故障保留 `502/503` 而不清理有效 token。前端导航和操作按钮过滤只负责体验,正式授权由 api-server 路由权限矩阵执行,未登记的新后台路由对 member 默认拒绝。后台面向运营展示管理员身份时统一使用 `displayName`;持久审计仍保存稳定 subject,由 api-server 解析显示名称,前端不得暴露账号 ID 或用登录用户名代替。写接口必须在主事务前加载显示名目录,或在主事务后降级解析,不能把已提交写入伪装为失败。
- 影响范围:`admin_account`、SpacetimeDB typed procedures / client facade、后台 JWT 与 session DTO、`/admin/api/accounts*`、后台路由权限中间件、admin-web 导航和账号管理页。
- 验证方式:SpacetimeDB schema / client / API 定向测试、`npm run check:admin-account-procedures` 隔离 procedure smoke、完整路由矩阵测试、admin-web 权限路由与账号 API 测试、owner/member 浏览器 smoke、`npm run check:spacetime-schema`、编码与 diff 门禁。
- 关联文档:`docs/technical/【后台管理】多账号与Tab访问权限方案-2026-07-14.md`。
@@ -447,7 +523,7 @@
- 背景:图片画布的普通图片、规范、角色、图标图集、UI 设计、宣发素材、视频和音频默认使用“类型 + 数字”命名,用户只能在生成后单独重命名素材,画布图层、项目资源和素材库名称容易不一致。
- 决策:主生成状态继续使用可选 `assetLabel`,名称最多 80 个字符并在提交时去除首尾空格;当前生成面板不展示“资源名称”标签和输入框,默认沿用现有自动编号名称,历史状态或内部调用若携带非空名称,仍必须让同一个名称贯穿 `assetLabel`、`canvasCompletion.title`、本地结果图层标题、项目资源和账号素材库,不允许各链路自行生成不同名称。移除名称输入后,角色、图标图集、UI 设计和角色动作等提示词输入恢复统一可见边框。
- 派生产物:图标和角色动作后端契约补齐 `assetLabel`。带背景原图、角色动作绿幕预览等具有独立复用价值的中间产物基于主名称追加“(原图)”等后缀;普通图片和图片修改的纯尺寸变换在内存完成后只上传一次,不生成“原始输出”副本。图标切片继续按用户填写的图标描述命名,不继承图集名称覆盖独立素材语义。
- 派生产物:图标和角色动作后端契约补齐 `assetLabel`。带背景原图、角色动作绿幕预览等具有独立复用价值的中间产物基于主名称追加“(原图)”等后缀;普通图片和图片修改的纯尺寸变换在内存完成后只上传一次,不生成“原始输出”副本。2026-07-29 起,图标切片不再按用户提示词命名,统一按全连通域视觉顺序命名为 `素材 N`。
- 影响范围:图片画布生成状态与面板、提交模型、图标和角色动作请求契约、项目资源 / 素材库持久化和相关编辑器文档。
- 验证方式:覆盖生成面板不渲染资源名称输入、提示词边框、空白回退、内部自定义名与长度限制,以及图片 / 图标 / 视频 / 音频 / 角色动作的请求名称、完成快照标题和素材名称一致性;运行前端定向测试、Rust 契约与 API 定向测试、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。
@@ -867,7 +943,9 @@
## 2026-06-24 图片画布项目封面使用静态快照资源
- 背景:项目页和创作主页最近项目曾在卡片中根据项目 `layers + viewport + resources` 临时重建一份迷你画布,视觉上像封面,但它不是持久快照,也会把列表页变成画布布局解释器。
- 决策:项目封面图改为画布当前视口栅格化后的静态资源。前端在项目加载后和防抖保存 layout 时生成 320x240 PNG,走私有 OSS / asset object 上传,再创建 `editor_project_resource`,其中 `assetKind="project-cover-snapshot"`、`sourceType="uploaded"`;项目列表和创作主页最近项目只读取最新封面快照资源渲染,没有快照时显示项目占位,不再回退为实时画布组合。
- 决策:项目封面图改为画布当前视口栅格化后的静态资源。前端在项目加载后和防抖保存 layout 时生成 320x240 WebP,走私有 OSS / asset object 上传,再创建 `editor_project_resource`,其中 `assetKind="project-cover-snapshot"`、`sourceType="uploaded"`;项目列表和创作主页最近项目只读取最新封面快照资源渲染,没有快照时显示项目占位,不再回退为实时画布组合。
- 2026-07-24 补充:封面取景以当前画布工作区的实际尺寸和渲染态 viewport 为准,先绘制工作区背景色,再从视口中心等比放大并裁成 4:3;持久化显示倍率不得直接用于封面渲染。
- 2026-07-29 补充:常规编辑仍沿用防抖保存;用户从画布返回项目页时必须取消待执行 timer,以最新权威 revision 立即保存 layout,并等待同一视口封面写入本地缓存和正式项目资源后再导航。当前视口存在图层但全部位于取景外时仍生成纯背景封面,不沿用旧缩略图。
- 影响范围:`src/components/image-editor/useImageCanvasProjectPersistence.ts`、`src/components/image-editor/ImageCanvasProjectCoverSnapshotModel.ts`、`src/components/project/ProjectCanvasCover.tsx`、`src/components/project/ProjectGalleryView.tsx`、`src/components/creation-home/CreationLandingView.tsx` 和图片画布数据契约文档。
- 验证方式:运行项目页、封面快照模型、图片画布项目持久化和媒体上传相关前端测试,执行 `npm run typecheck`、`npm run check:encoding` 和 `git diff --check`。
- 关联文档:`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`。
@@ -915,7 +993,7 @@
## 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"` 图层放到画布,再放 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"` 单图标隐藏并拒绝快速编辑;工具栏、右键菜单、打开流程和提交兜底必须共用同一判定。本条新决策取代“图标素材生成只保留图集”的旧口径。
- 决策:`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` 收口,不创建透明图集,也不继续拆分。2026-07-29 起,图标生成的自动拆分与手动拆分共同识别全图集有效连通域,限制单边 `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`。
@@ -4725,8 +4803,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 的 `toolName` / `resourceId` / `objectKey` 等轻量元数据。用户用「这张」「刚才那个」「上一张」「把衣服换成……」等方式指代上一张图或继续编辑时,规划默认调用 `edit_image` 并引用该结果;不能因为本轮没有手动附件而退回 `generate_image`。
- 决策补充:画布 Agent 侧边栏的“规范图 / 视觉规范图 / 风格规范图 / 素材规范展板”是 Agent 规划 prompt 和 function-calling 工具选择约束,不是侧边栏 UI 说明文案。此类请求默认走 `generate_image`,prompt 必须要求规范展板包含统一视角、线条粗细、色卡、材质、阴影、圆角、状态层级、尺寸标注等视觉规范元素;角色规范图若是规范展板也走 `generate_image`,只有实际角色立绘才走 `generate_character`,多个图标素材 / 图集才走 `generate_icon_spritesheet`。
- 决策补充:画布 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`。
@@ -4851,7 +4928,7 @@
## 2026-07-13 临时维护公告改为 release 外运行态覆盖
- 背景:一次性停服公告曾直接提交到 `public/maintenance.html`,后续 Web Build 将它持续打入 `web.tar.gz`,每次 Web Deploy 或再次进入维护都会重新显示已经过期的公告。
- 决策:`public/maintenance.html` 永久作为无日期、无具体时段的默认维护页,并使用 `public/branding/taonier-maintenance-page.png` 作为品牌视觉;生产 Web 打包必须对最终 `web/maintenance.html` 执行临时文案门禁。临时公告通过 `maintenance-on.sh --page-file <公告HTML>` 原子安装到 `/var/lib/genarrative/maintenance/page.html`,Nginx 与 Pingora 优先读取该运行态文件,缺失时回退 Web 制品默认页。
- 决策:`public/maintenance.html` 永久作为无日期、无具体时段的默认维护页,并使用 `public/branding/taonier-maintenance-page.png` 作为品牌视觉;生产 Web 打包必须对最终 `web/maintenance.html` 执行临时文案门禁。临时公告通过 `maintenance-on.sh --page-file <公告HTML>` 原子安装到 `/var/lib/genarrative/maintenance/page.html`,Nginx 与 Pingora 优先读取该运行态文件,缺失时回退 Web 制品默认页。维护期间只精确放行 `/branding/taonier-maintenance-page.png` 与 `/branding/taonier-product-ip.png`,不得扩大到整个品牌或静态资源目录;网关 smoke 必须验证这两个路径仍返回 PNG,同时其它公网页面、API 与后台静态资源继续命中维护门禁。
- 生命周期:新维护窗口未提供 `--page-file` 时清理 marker 外残留公告;同一窗口内 Stdb / API 发布重复调用 `maintenance-on.sh` 时保留已安装公告;`maintenance-off.sh` 同时清理 marker 和公告页。Web Deploy 不再拥有临时公告事实源。
- 影响范围:默认维护页、维护开关脚本、Nginx snippet、Pingora 配置与 smoke、生产 Web 发布包门禁和生产运维文档。
- 验证方式:`npm run check:maintenance-page`、`npm run check:nginx-spa-routes`、`cargo test -p pingora-gateway --manifest-path server-rs/Cargo.toml`、`npm run check:pingora-gateway-smoke`、`npm run check:production-ops`、`npm run check:encoding`、`git diff --check`。
@@ -5654,6 +5731,40 @@
- 微信边界:小程序客户端仍只上传 `wechatPhoneCode`;`platform-auth` 必须要求微信成功响应中的 `phoneNumber`、`countryCode` 与 `purePhoneNumber` 均存在且非空,但只使用后两项执行国家码校验和 E.164 构造。腾讯官方仅说明境外 `phoneNumber` 会带区号,并未承诺 E.164 格式,中国号码示例中它与纯号码相同,因此不得校验 `phoneNumber == +{countryCode}{purePhoneNumber}`。微信字段缺失时失败关闭,不能使用普通请求的 `86` 默认值。
- 数据边界:认证投影与 SpacetimeDB 的 `phone_number_e164` 保持不变,不新增国家码或纯号码列,也不需要 schema 迁移或 bindings 生成。
## 2026-07-24 后台用户详情展示历史花费泥点
- 口径:`historicalConsumedPoints` 表示用户历史总消费,只累计 `profile_wallet_ledger.source_type = asset_operation_consume` 且 `amount_delta < 0` 的绝对值;`asset_operation_refund` 不冲减,充值退款追回、余额重置、赠送和退款 hold 均不计入。
- 投影边界:新增 `profile_wallet_consumption_total`,已有投影时消费流水成功落账在同一 SpacetimeDB 事务内按主键 O(1) 原子累加;退款不回减。首次上线在停止业务写入的维护窗口由 owner 调用 `POST /admin/api/profile/users/initialize-consumption-projections`,一次扫描全部权威钱包流水,为每个已有钱包流水的用户建立存量投影,成功后才能恢复流量。维护遗漏或新用户缺行时,首次消费和 runtime service identity 受限的 `admin_get_profile_wallet_detail_and_return` 都可按用户索引兜底重建一次;消费事务重建已包含当前流水,不重复加本次金额。不得用最近 50 条流水列表近似,也不得把全量流水扫描塞进充值订单每行复用的通用钱包快照。
- 对账边界:保留管理员显式手动对账。owner 始终可用;member 必须单独持有 `profile-wallet-consumption-reconcile` 独立操作权限,任意 Tab 都不隐式授予。`POST /admin/api/profile/users/reconcile-consumption` 经二次确认后调用 runtime service identity 受限 procedure,扫描该用户全部权威流水、比较并校准投影,记录管理员与对账时间。
- 展示边界:现有共享“用户详情”弹窗的钱包区增加“历史花费”,前端只展示 BFF 顶层字段,不自行汇总账单;只有 BFF 返回 `canReconcileConsumption=true` 时展示手动对账按钮。
- 验证方式:SpacetimeDB 钱包聚合测试、api-server / admin-web 定向测试、`npm run spacetime:generate`、`npm run check:spacetime-schema`、`npm run check:spacetime-runtime-access`、`npm run admin-web:typecheck`、`npm run check:encoding`、`git diff --check`。
## 2026-07-28 图片生成风格使用可扩展字段并以纯内存像素规整首发
- 契约:普通图片 / 角色共用的图片生成请求和图标图集生成请求增加可选字符串 `style`,当前公开合法值为 `none / pixelArt`。省略、`null`、空字符串和 `none` 归一为内部 `None` 且不告警;未知字符串、或在 `spec / quick-edit / ui-design / publication-material` 等不支持的图片 `kind` 上请求 `pixelArt` 时,按 `None` 继续原管线并返回 `unsupported-image-style` 通用告警;非字符串 JSON 返回 `400`。旧队列 payload 缺少字段时兼容为 `None`。
- UI 边界:只有普通 `生成图片`、`生成角色形象` 和 `生成图标素材` 显示 `像素艺术` 勾选项;当前选择可进入已有生成器快照和请求 / 队列 payload,但不写入 `generationInputs`、素材元数据或新表。画布 Agent 和其它生成 / 编辑入口不开放该选项。
- 处理边界:`PixelArt` 由 `platform-image` 的纯同步、纯内存 Rust 模块执行,不运行 Python、不访问 OSS / 数据库 / 画布。普通图片直接使用 provider 图;角色和图标必须等 BgFilter 成功并把 Alpha 回贴到 provider 原尺寸后,以 provider 平底原图分析网格、以透明 RGBA 图采样。固定参数为分析色数 16、Alpha 覆盖阈值 0.375、像素尺寸自动、相邻边缘峰间距使用线性插值 P30 估算步长、无固定色板、K-means 最大采样 262144;单格 RGB 按 Alpha 加权,输出 Alpha 只为 0 / 255,逻辑低分辨率结果用 nearest 恢复交付尺寸并跳过 Lanczos。2026-07-29 合并「角色带背景原图与透明图统一交付尺寸」后本条修订:像素模式不再豁免提前归一,网格分析源是已按业务像素矩阵 `resize_to_fill`(Lanczos 重采样 + 居中裁切)后的交付尺寸平底图,不再是 provider 原生分辨率图;像素规整在交付尺寸上完成、由 snapper 自行还原回输入尺寸,因此不再执行后置的 nearest 二次恢复。
- 执行边界:像素规整 CPU 工作使用进程级最大并发 2;取得并发许可的排队时间与实际处理时间共享最多 30 秒预算,同时不得晚于当前请求 deadline,最终取更早者。输入图片任一边上限为 10000 像素、总像素上限为 8294400;超限、排队超时或处理超时均按 best-effort 非致命降级,不持久化部分结果。
- 去背边界:不修改 BgFilter `flat` 参数、`cross_check`、fallback、Alpha 回贴和默认关闭 despill 的现有行为。BgFilter 最终失败时不运行像素规整;像素规整失败按 best-effort 非致命降级,保留进入该步骤前的图片并通过既有通用 `warning` 完成任务,不退款。
- 持久化边界:逻辑低分辨率图、像素化前后对比图、预览、诊断和报告一律不持久化;像素模式只替换原本即将上传的最终图片字节。普通图片、角色、图标的 OSS PUT、asset / project resource 和画布 item 数量必须与 `None` 模式完全一致;角色 / 图标最多因复用失败增加一次对已有 provider 对象的 OSS GET,不得增加 PUT、资源类型、画布项、队列类型或 schema 字段。
- 关联文档:`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`、`docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md`、`docs/【编辑器】画板图标素材生成入口设计-2026-06-15.md`、`docs/openapi/genarrative-external-v1.openapi.json`。
## 2026-07-28 画布 Agent 的通用 function-calling harness 与画布 prompt 分层
- 背景:画布 Agent 的 JSON 输出协议、tool schema 注入、memory / hook、轮次保护和“全部工具待确认即结束回合”原先位于 `platform-editor-agent/src/framework`,与规范展板、已有图编辑路由、模型超时和画布工具混在同一 crate;八类工具还重复携带待确认控制话术。旧 `platform-agent` 已随 Creative Agent 退役,不能作为新公共层复活。
- 决策:新增无旧玩法依赖的现役 `platform-agent-harness`,只承载业务中立的 function-calling 执行协议;`platform-editor-agent` 通过兼容 re-export 复用该 crate,并继续承载画布 LLM profile、角色 prompt、公共美术工具路由策略、图片上下文和工具实现。无工具场景同样注入 JSON 响应格式;prompt 不再宣称工具并发执行;request 级 system prompt 必须真实进入本轮请求。待确认卡片的对话路由必须使用正向、条件化语义:只在当前意图匹配一条现存 pending 调用时引导用户点击该卡片,该确认 / 取消意图不产生新 tool call;不在 prompt 中写“不得重新发起相同工具调用”一类全局否定句,因为实测证明模型会将其过度泛化为拒绝后续明确的新生成、修改或重做请求。cancelled 调用不再确认,pending 调用不阻塞无关新任务。
- 执行与失败决策:prompt 每轮通过 `AgentMemory::begin_staged` 使用与调用方 memory 行为等价、写入隔离的 `StagedAgentMemory` 事务;成功或已有工具活动时显式 `commit()`,直接 drop 表示回滚。无工具活动失败时回滚本轮 staged 增量,已发生工具活动后失败时提交已发生工具事实并追加 terminal error closure。外部 future drop / abort 若发生在工具完成后,提交工具结果与取消闭环;若发生在工具执行中,提交“已启动、结果未知”与取消闭环,后续先 reconcile,不能假装副作用未发生。harness 通过 `PromptRunError { error, partial_outputs }` 显式返回终态错误和失败前输出;结构化工具失败还必须向调用方保留 `ToolFailure.kind/retryable/fatal` 与原始 `output`,不在 harness 内压成单一字符串。api-server 的 18 分钟总 deadline 以 runtime future 下沉到 runner:completion 可被 deadline 终止,工具在开始前检查、开始后等待返回、返回后携带结果收口;禁止外层 timeout drop prompt 或中途取消 effectful tool 后伪造空 partial。
- 保留边界:会话幂等、OSS 消息、120 秒前端软提示、20 分钟 transport、18 分钟 handler 总 deadline、1024 tokens、8 分钟 provider attempt、泥点计费、确认入队和 external job 懒回填均不进入公共 harness。SpacetimeDB schema、前端 wire DTO 和侧边栏 UI 不变。
- 验证方式:`cargo test -p platform-agent-harness`、`cargo test -p platform-editor-agent`、`cargo test -p api-server editor_agent`、`cargo check -p api-server --locked`、DDD 边界检查、Rustfmt、编码检查和 `git diff --check`。
## 2026-07-29 图标图集拆分数量只由有效连通域决定
- 背景:图标素材生成前端曾把单个提示词按换行、逗号、顿号等分隔符解析成描述数组,后端再用数组长度作为期望切片数。这会把“各种敌人头像:骷髅 哥布林 强盗 龙 蝙蝠等”一类自然语言错误地解释为固定数量,并在图集中存在更多有效素材时截断结果。
- 决策:画布前端不再从提示词解析素材数量,完整提示词作为 `iconDescriptions` 的唯一数组元素提交以兼容现有请求契约;后端仍允许其它调用方提交多条文本,但数组长度只参与 prompt 组装,绝不作为切片数量或切片命名依据。生成后的自动拆分与手动 `拆分图集` 复用同一套全连通域识别、视觉阅读顺序和 `素材 N` 命名,识别多少个有效素材就拆多少个;手动按钮与 `/api/editor/icon-spritesheets/slices` 路由继续保留。
- 失败与限制:两条图标拆分路径共同限制单边 `4096`、总像素 `2048×2048`、最多 `64` 个切片,并在持久化前完成校验。自动拆分仍是 best-effort,失败后保留整张透明图集并返回 `sliceWarning`;手动拆分失败返回接口错误。UI 设计图素材提取继续使用全连通域识别,不受提示词数量影响。
- 验证方式:调整既有前端提交、Prompt、连通域切片、上限和响应契约测试,不新增仅用于证明旧解析函数已删除的测试;运行前后端定向测试、类型与 Rust 检查、编码检查和 `git diff --check`。
- 关联文档:`docs/【编辑器】画板图标素材生成入口设计-2026-06-15.md`、`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`、`docs/openapi/genarrative-external-v1.openapi.json`。
## 2026-07-27 Anthropic 与流式统一使用 Provider 原生工具
- 背景:`platform-llm` 的 Anthropic 分支从未实现工具——请求体没有 `tools` / `tool_choice` 字段,`validate()` 还会以「Anthropic api_kind 暂不支持 function tools」本地拒绝,响应解析只取 `text` block 并硬编码 `tool_calls: Vec::new()`。App 侧因此在 `provider_request_builders.rs` 与 `interaction.rs` 用 `api_kind != Anthropic` 绕开原生工具,改用长提示词描述工具并要求模型输出单个 JSON object,等于让 Anthropic 退回 V1.26 之前的状态。三种协议的流式路径同样恒返回空工具调用,靠「无文本 → EmptyResponse → 非流式重打」兜底;模型若在工具调用前先输出解说文本,该兜底不触发,工具调用会被静默丢弃并把解说当成最终回复。
@@ -490,6 +490,7 @@ npm run check:native-shells
```
该命令会覆盖 H5 HostBridge 关键测试、微信 / Expo / Tauri 三端桥接层文件结构门禁、完整相对路径文档反查、微信 capability 到真实 WebView / 支付 / 分享页面流程和测试清单的映射门禁、H5 HostBridge 事件订阅双能力门控反查、H5 `navigation.canGoBack` 消费 hook 与直达二级页返回锚点测试、移动端和桌面端单端源码清单门禁、Expo 壳 typecheck / test / EAS build config smoke / config smoke / Metro export smoke、Tauri 壳 typecheck / cargo test、桌面壳 release `--no-bundle` 构建烟测,以及可分发壳与 H5 HostBridge 真实调用链的临时替身词扫描,确认 Expo managed config、移动端 EAS 原生包构建 profile、移动端 iOS / Android production bundle、打包 H5 资产、Tauri release 入口、H5 页面内导航保留完整原生宿主上下文和 H5 HostBridge 真实调用链没有漂移;扫描范围包含微信小程序壳生产 `.js`、Tauri `Info.plist`、共享 HostBridge 契约、H5 native transport,并自动覆盖已接入真实宿主能力 facade 的 H5 生产调用链文件,但不扫描 Expo export、Tauri `target/`、Cargo / Metro 缓存或 release 构建产物。移动壳配置检查必须反查 EAS 生产 profile、文本 / 文档 / 图片 / 音频导入边界都来自共享 HostBridge 契约。登录与支付外链跳转必须保持在该调用链扫描内,`src/services/authService.ts` 和 `src/services/payment/paymentRedirect.ts` 是必扫文件;`AuthGate` 的登录成功、退出登录、身份边界刷新和登录状态异常重试都必须通过 `app.reloadWebView` 优先路径,并由 `src/components/auth/AuthGate.test.tsx` 进入该门禁。壳源码和配置继续严格禁止 mock / fake / placeholder / stub / TODO / FIXME / 占位 / 模拟 / 伪造 / 未实现 / 临时;H5 业务调用链允许正常表单 `placeholder` 属性、业务占位图文案和真实兼容 / 故障语义中的“未实现”“临时”表述,但仍禁止 mock / fake / stub / TODO / FIXME / 模拟 / 伪造等替身痕迹。
根仓 Vitest 加载独立 AI 游戏客户端源码时,不得为了模块解析把 `@tauri-apps/api` 或 `@tauri-apps/plugin-*` 加入根 H5 依赖;根测试只通过 `vitest.config.ts` 的精确别名使用无副作用测试替身,独立客户端的正式 Tauri guest 依赖继续只由 `apps/ai-game-creator-shell/package.json` 与其 lock 管理。隔离 worktree 验收前需分别执行根 `npm ci` 和 `npm ci --prefix apps/ai-game-creator-shell`。
反馈页上传凭证在原生壳声明 `file.importImage` 时必须优先走宿主图片导入;移动壳声明 `file.captureImage` 时才显示拍摄凭证入口,并把拍摄图片同样转为 `File` 后复用反馈页原有数量、大小、MIME、data URL 预览和提交 payload 校验。
Expo / Tauri 声明 `navigation.openNativePage` 时,只用于现役同源 H5 路由的受控导航和宿主上下文续接;微信小程序不再声明该能力。旧儿童动作 Demo、模板工作台、生成页、结果页和运行态不得作为 HostBridge 导航验收入口。
H5 支付链接跳转在原生壳声明 `app.openExternalUrl` 时必须优先走宿主系统浏览器;原生壳未接真实支付 SDK 前不得声明 `payment.request`,也不得把外部 H5 支付跳转伪装成原生支付成功。
+63 -5
View File
@@ -14,6 +14,38 @@
- 关联:相关文件、文档、提交或 Issue
```
## Jenkins 异步备份不能用 nohup 脱离作业
- 现象:Stdb Publish 成功,上传日志只留下“已获取进程锁 / 上传已有备份 / 目标对象”,没有成功或可捕获错误;本地 tar.gz 和 `uploadStatus=deferred` manifest 每次发布后继续增长。
- 原因:`nohup` 只忽略终端 HUP,不会移除 Jenkins/Hudson 进程 Cookie;Job 收尾可清理后台 uploader。原链路只上传当次归档,旧 deferred manifest 没有扫描重试,而 `files-history` timer 只处理 `/stdb` 历史文件。
- 处理:发布退出时用独立 `systemd-run --collect --service-type=exec` transient unit 执行 `--upload-deferred-dir`,串行处理同库 deferred/pending 归档。启动前拒绝符号链接和非绝对路径;unit 启动失败必须保留 status、archive 和 manifest。补偿扫描不删除上传未验真的文件,也不扫描目录外路径。
- 验证:门禁必须禁止 `nohup`,要求命名 transient unit、`--collect`、`Type=exec` 与失败后保留 status;备份测试覆盖稳定顺序、同库过滤、已上传但未清理的归档收敛、归档缺失报告与路径逃逸拒绝。现场最终核对 backup lock、manifest、transient unit/result、根盘、SpacetimeDB/API/worker/controller/Nginx 和公开端点。
- 关联:`scripts/deploy/production-stdb-publish.sh`、`scripts/database-backup-to-oss.mjs`、`scripts/check-production-ops-guardrails.mjs`、`scripts/check-database-backup-to-oss.mjs`。
## 图集切片上限必须早于合并、裁剪和编码
- 现象:透明图集含大量独立碎块或噪点时,接口长时间占用 async worker;最终即使报“超过 64 个切片”,此前仍已完成全量两两合并、裁剪和 PNG 编码。
- 原因:原始连通域无上限,辅助部件合并全量扫描所有 pair,输出限制只在 platform slicer 返回后由 api-server 检查;UI 提取还绕过了该 wrapper。
- 处理:platform slicer 对全部 flood-fill 连通域设置 `4096` 硬上限,用空间网格只查 `48px` 邻域候选;单网格最多 `256` 个组件、单 source 最多 `512` 个候选,避免拥挤网格重新退化为全量 pair。`maxOutputSlices` 与 padding crop 总像素预算在首片 PNG 编码前拒绝。图标自动、手动和 UI 三入口统一在 2 路 CPU semaphore 与 30 秒 / 请求 deadline 保护下 prepare 出共享 RGBA + bounds 计划,不再一次返回最多 64 份 PNG。api-server 只按需编码并用容量 2 的有界管线上传,OSS 连接 / 单请求超时固定为 `10s / 60s`;手动入口在下载最大 32 MiB 来源对象前取得独立内存 admission,同一 admission 覆盖下载、计划与上传生命周期,并在最后一次 HEAD 完成后、数据库调用前释放,排队请求、慢 OSS 或慢数据库都不能绕过内存边界。全部 `PUT + HEAD` 成功后,单个 SpacetimeDB procedure 在一个事务中批量确认对象、创建项目资源 / 账号素材并完成 cohort;resource / asset ID 按 owner + task + 序号稳定派生,重放只复用内容一致的素材,来源资源必须存在且同 owner / project;不在上传失败后留下部分数据库批次,也不在不确定结果重放后复制整批素材。
- 验证:覆盖大量独立 `4×4` 块、超过上限的单像素噪点、65 个有效输出和既有高光 / 阴影合并样本;手动超限必须发生在首次持久化前,自动超限不得产生切片 PUT、资源或画布切片。
- 关联:`server-rs/crates/platform-image/src/generated_asset_sheets/sheet.rs`、`server-rs/crates/api-server/src/editor_project.rs`。
## Alpha 恢复失败后不能继续持久化原始后处理图
- 现象:BgFilter 返回比例漂移、损坏或低分辨率图片,provider 原图修复性回读又失败时,图标 / UI 仍可能落库透明图与切片,尺寸元数据甚至回退为 `512×512`。
- 原因:Alpha helper 会同时返回原后处理字节和错误;角色调用方会 source-only 早退,图标 / UI 却只写日志后继续。相同尺寸快路径还只读图片 header,没有完整解码。
- 处理:角色、图标、UI 共用 provider 原图 source-only helper;比例漂移超过 `5%`、原图回读、Alpha 回贴或透明图完整解码任一失败都立即返回原图、通用 warning、空切片和空 `sliceWarning`,禁止透明图 PUT、派生资源、拆分和透明 / 切片画布层。provider 原图尺寸必须完整解码取得,不得伪造兜底值。
- 验证:覆盖错比例 Alpha、缺失 provider 原图、合法 PNG header 但截断正文;结构断言 source-only helper 不含任何透明持久化、切片或多图层完成调用。
- 关联:`server-rs/crates/api-server/src/editor_project.rs`、`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`。
## 工具 JSON Schema 的条件约束必须覆盖运行时默认值
- 现象:LLM 按工具 schema 生成的参数可以通过结构约束,但参数补默认值后被运行时校验拒绝,白白消耗一次工具修复轮次。例如固定 `gpt-image-2` 的 UI 工具仍暴露 `0.5K`,或视频调用省略 `model` 时 schema 允许 `1080p`,运行时却默认成 `seedance2.0-fast` 后拒绝。
- 原因:通用枚举 schema 被固定模型工具直接复用;JSON Schema 的 `if` 又用 `required: ["model"]` 排除了字段缺失场景,而 Serde 默认值只在 schema 校验之后生效。description 只能提示 LLM,不能替代 `enum` / `if` / `then` 的结构约束。
- 处理:固定模型工具使用与该模型能力一致的专用枚举;可切换模型的图片工具在对象层复用共享 `model + image_size` 条件约束。条件字段有运行时默认值时,省略字段必须落入默认模型对应的 schema 分支:默认 nanobanana2 的图片工具只在显式选择 `gpt-image-2` 时收紧尺寸,所以条件保留 `required: ["model"]`;默认 fast 的视频工具则利用字段缺失时 `properties.model.const` 条件成立的语义,不额外要求 `model` 存在。运行时校验仍保留为最终防线。
- 验证:锁定 `generate-ui-design.image_size = ["1K", "2K"]`,三个可切换图片模型的工具都接入共享 `gpt-image-2 -> image_size = ["1K", "2K"]` 条件,以及视频 fast 条件没有内层 `required`、其 `then.resolution = ["480p", "720p"]`;同时保留运行时拒绝 `gpt-image-2 + 0.5K` 与 `seedance2.0-fast + 1080p` 的测试。
- 关联:`server-rs/crates/platform-editor-agent/src/agent/tools/image_generation_options.rs`、`server-rs/crates/platform-editor-agent/src/agent/tools/generate_ui_design.rs`、`server-rs/crates/platform-editor-agent/src/agent/tools/generate_video.rs`、`docs/【编辑器】画布Agent对话面板-2026-07-03.md`。
## 重复成功的 agent.message 不能被当成新的 Runtime 进展
- 现象:专业 Agent 已把一条定向消息写入目标 Session,却在后续 planning 中反复发送相同正文;目标会话看起来没有重复消息,但 Provider 请求持续增长,run 可能长期不返回自身终态回执。
@@ -413,7 +445,7 @@
- 现象:`/api/editor/projects*`、素材库、项目资源或 layout payload 里出现数 MB 的 `data:image/*`、`data:video/*`、`data:audio/*`,刷新恢复变慢,发布入口可能 OOM / 413,素材库缩略图还可能只显示文件名。
- 原因:生成、规范图、角色图、图标 / UI spritesheet、音视频或动画帧如果直接把 Data URL / signed URL 写入 `editor_project_resource`、`editor_asset` 或 `editor_canvas.layers_json`,就把媒体本体塞进了项目快照;signed URL 还会过期,素材库也无法稳定换签。
- 处理:登录态媒体必须先上传 OSS / asset object,持久化只写 `imageSrc: "/<objectKey>"`、`objectKey`、`assetObjectId`;素材库和图层缩略图都通过 `PlatformMediaFrame -> ResolvedAssetImage` 传 `objectKey` 并调用 `/api/assets/read-url`。layout 序列化和后端保存要递归拒绝 `data:*` / `blob:`;旧行有 `objectKey` 时读出归一成 `/<objectKey>`,没有 `objectKey` 的旧 Data URL 必须走修复上传后回写轻量引用。刷新恢复可先用 session 轻量缓存显示,但缓存不得含内联媒体,也不能在后端快照回来前自动保存。生成扣费、失败退款或 queue 终态后,右上角泥点余额通过 `/profile/dashboard` 回读,不做本地乐观扣减。
- 处理:登录态媒体必须先上传 OSS / asset object,持久化只写 `imageSrc: "/<objectKey>"`、`objectKey`、`assetObjectId`;素材库和图层缩略图都通过 `PlatformMediaFrame -> ResolvedAssetImage` 传 `objectKey` 并调用 `/api/assets/read-url`。layout 序列化和后端保存要递归拒绝 `data:*` / `blob:`;旧行有 `objectKey` 时读出归一成 `/<objectKey>`,没有 `objectKey` 的旧 Data URL 必须走修复上传后回写轻量引用。刷新恢复可先用 session 轻量缓存显示,但缓存不得含内联媒体,必须按用户隔离,而且不能在后端快照回来前自动保存。认证状态变化重跑加载 effect 时,要同步用 ref 关闭写门禁并清除 revision、pending save 和 timer;不能只等 `isProjectReady=false` 的下一次 render,否则旧 effect 会先消费 skip 标记,再把公司浏览器的旧缓存无版本 PATCH 到服务端,覆盖另一台设备的新画布布局。现役 Web 与 External layout PATCH 的 `expectedRevision` 都必填,三层门禁分别放在 autosave effect、queue 和真正发送前;session cache 即使带 revision 也只有显示权。异步 project resource 创建必须把未发请求队列按用户 / 项目隔离,并记录发起时已接受的权威快照序号;若资源响应前发生认证重载、409 恢复或生成完成快照替换,只把新资源对应图层合并进当前权威布局,禁止用历史 `snapshotLayers` 整体覆盖。生成扣费、失败退款或 queue 终态后,右上角泥点余额通过 `/profile/dashboard` 回读,不做本地乐观扣减。
- 验证:Network 中 `/api/editor/projects*`、`PATCH /api/editor/projects/{id}`、素材库接口不应出现 `data:image` / `data:video` / `data:audio`;素材库和图层面板缩略图都能换签显示;`npm run test -- src/components/image-editor/ImageCanvasEditorModel.test.ts src/components/image-editor/useImageCanvasProjectPersistence.test.tsx src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/common/PlatformMediaFrame.test.tsx src/services/assetReadUrlService.test.ts src/services/image-editor/editorProjectClient.test.ts`,后端跑 `cargo test -p api-server editor_project --manifest-path server-rs/Cargo.toml`。
- 关联:`server-rs/crates/api-server/src/editor_project.rs`、`src/components/image-editor/ImageCanvasEditorModel.ts`、`src/components/image-editor/useImageCanvasProjectPersistence.ts`、`src/components/common/PlatformMediaFrame.tsx`、`src/services/assetReadUrlService.ts`。
@@ -430,7 +462,9 @@
- 现象:画布项目已反复打开、保存或操作,但 `/project` 列表卡片仍只显示“项目”占位,没有封面图。
- 原因:项目封面快照需要先在浏览器生成 Blob,再上传 OSS 并创建 `assetKind: "project-cover-snapshot"` 项目资源;本地 dev 或 OSS CORS 异常时,Blob 生成成功但上传失败,服务端不会产生正式封面资源。
- 处理:服务端 `project-cover-snapshot` 仍是跨设备正式封面;前端在生成封面 Blob 后立即把 Blob 以项目 ID 写入 IndexedDB,仅作为当前浏览器展示兜底。项目列表读取时优先使用服务端封面资源,其次使用本地 IndexedDB 封面,最后才退回可见画布图层或占位。IndexedDB 兜底不得写入项目快照、不得进入 `editor_project_resource`,也不得替代 OSS / asset object 正式持久化。
- 验证:`npm run test -- src/components/project/ProjectCanvasCover.test.ts src/components/project/ProjectGalleryView.test.tsx src/components/image-editor/useImageCanvasProjectPersistence.test.tsx` 覆盖服务端封面优先、本地缓存兜底、上传失败仍保留本地封面缓存;浏览器 smoke 可在 `/project` 对没有服务端封面的项目写入 `genarrative-editor-project-covers` IndexedDB 记录,刷新后应显示 `blob:` 封面图。
- 封面是展示派生物,不是 layout 真相。常规编辑只在项目加载和原有 layout 保存触发点采样当前 `canvasSize`,不监听 ResizeObserver 尺寸变化单独增加保存频率;但用户主动返回项目页时必须先 flush 最新权威 layout,并等待同一视口封面写入 IndexedDB 和正式项目资源后再导航。为避免移动端、窄窗口或首次尺寸尚未稳定时取景过小,以当前视口中心为锚点把取景宽高至少扩大到 `1280x960`;实际值更大时保留更大值。画布存在 drawable 图层但当前取景全部离屏时,要保存纯背景封面,不能因相交列表为空而保留旧缩略图。
- 封面生成不要为同一 OSS 对象发起另一套换签缓存维度:图片、序列帧和 poster 分别复用主画布预览的 refresh key,保证封面取得相同 signed URL,由浏览器合并 in-flight 请求或命中 HTTP 缓存。通用素材上传里的 `bypassCache: true` 只用于上传后立即预览;项目封面不消费该 `src`,应在 confirm 后直接使用 object-only 结果创建项目资源。
- 验证:`npm run test -- src/components/project/ProjectCanvasCover.test.ts src/components/project/ProjectGalleryView.test.tsx src/components/image-editor/ImageCanvasProjectCoverSnapshotModel.test.ts src/components/image-editor/useImageCanvasProjectPersistence.test.tsx` 覆盖服务端封面优先、本地缓存兜底、上传失败仍保留本地封面缓存、小视口居中扩大到 `1280x960`以及大视口不缩小;浏览器 smoke 可在 `/project` 对没有服务端封面的项目写入 `genarrative-editor-project-covers` IndexedDB 记录,刷新后应显示 `blob:` 封面图。
- 关联:`src/services/image-editor/editorProjectCoverCache.ts`、`src/components/project/ProjectGalleryView.tsx`、`src/components/project/ProjectCanvasCover.tsx`、`src/components/image-editor/useImageCanvasProjectPersistence.ts`。
## 图片画布框选预览要复用源图换签缓存
@@ -3776,8 +3810,25 @@
- 现象:画布 Agent 已生成有效工具规划,却最终只保存 `ERROR max turns reached: 3`,助手文本和待确认工具卡都消失。
- 原因:八类画布工具的 `call()` 只返回待用户确认的规划结果,但 function-calling runner 在成功工具后仍继续请求 LLM,只靠 prompt 要求模型不再重试;模型连续返回工具调用直到上限后,错误结果又丢弃此前累积的输出。
- 处理:工具通过框架契约显式声明 `requires_user_confirmation`;当本批全部工具都成功且等待确认时,runner 在处理完整批次后立即返回已有助手文本和工具结果。未知工具、参数错误、hook skip、普通连续工具和不可解析响应仍继续受 `max_turns` 门禁保护。不要用单纯提高轮次上限掩盖终止条件缺失。
- 验证:runner 回归测试必须同时覆盖“待确认工具只调用一次 LLM 并成功结束”和“普通连续工具仍会触发 max-turn 门禁”。
- 关联:`server-rs/crates/platform-editor-agent/src/framework/run.rs`、`server-rs/crates/platform-editor-agent/src/framework/tool.rs`、`server-rs/crates/platform-editor-agent/src/agent/tools/`。
- 验证:runner 回归测试必须同时覆盖“待确认工具只调用一次 LLM 并成功结束”“普通连续工具仍会触发 max-turn 门禁”“多工具按数组顺序执行”“request 级 system prompt 真实进入请求”;公共 prompt 在无工具时仍必须包含 runner 所需的 JSON 响应格式,且不得宣称并发执行。
- 关联:`server-rs/crates/platform-agent-harness/src/run.rs`、`server-rs/crates/platform-agent-harness/src/tool.rs`、`server-rs/crates/platform-editor-agent/src/agent/tools/`。
## 待确认工具的 prompt 不能使用全局禁止重发话术
- 现象:为防止用户在对话中说“确认 / 可以 / 取消”时重复生成待确认卡片,prompt 加入“不得重新发起相同工具调用”后,模型在用户随后明确提出新生成、修改或重做请求时也拒绝调用工具。
- 原因:LLM 容易把面向“当前确认 / 取消意图 + 特定 pending 卡片”的限制过度泛化为跨回合、跨意图的全局禁止;单看工具名或参数相似度不能区分“重复确认旧卡片”和“用户明确发起新任务”。
- 处理:prompt 只用正向条件句描述当前回合:确认或取消意图确实匹配某条现存 pending 卡片时,引导用户点击该卡片按钮,本条意图不生成新 tool call。不添加全局的“禁止重发相同工具”规则。cancelled 卡片不再处理;用户要求修改、重做或新任务时正常发起新调用,pending 卡片不阻塞无关请求。
- 验证:业务 prompt 契约测试要同时锁定“匹配 pending 时引导确认 / 取消按钮”“cancelled 后可发起新调用”和“pending 不阻塞无关新请求”;模型实测必须另外覆盖同工具名的后续新任务,确认不会因过度泛化而拒绝。
- 关联:`server-rs/crates/platform-editor-agent/src/agent/prompt.rs`、`docs/【编辑器】画布Agent对话面板-2026-07-03.md`。
## Agent 终态失败不能吞掉已发生的工具事实
- 现象:同一轮 prompt 中前面工具已经成功生成待确认结果,但后续工具、hook、completion 或 `max_turns` 失败后,API 只保存最后一条 `ERROR `,已执行工具和用户本轮语义从会话历史中消失。
- 原因:runner 只返回单一 `PromptError`,或者直接向 committed memory 逐步写入,无法区分“尚未发生外部工具事实,整轮可回滚”与“已发生工具事实,只能提交并闭合错误”。工具失败若被压成字符串,调用方还会丢失 `kind`、`retryable`、`fatal` 和原始 `output`。
- 处理:用 `PromptRunError { error, partial_outputs }` 保留失败前输出,并将本轮 memory 先写入 staged buffer。无工具活动失败时整体回滚 staged 增量;有成功或失败工具活动时提交已发生事实,并追加 terminal error closure。api-server 按 `partial_outputs` 顺序先持久化成功工具的 `not_completed` 待确认消息,再追加 `ERROR ` 终态消息;`ToolFailed` 保留给调用方做诊断和流程决策,不伪装成成功确认卡。
- 取消边界:不能在 prompt future 内对 `agent.memory.take()` 后跨 await 持有,也不能用统一 `VecMemory` staging 绕过自定义 memory 的限长、摘要或脱敏规则。`AgentMemory::begin_staged` 必须产生行为等价、写入隔离的 `StagedAgentMemory`,成功或已有工具活动时显式 `commit()`,直接 drop 才表示回滚。外部 drop 若发生在工具完成后,guard 必须提交结果与取消闭环;若工具仍在执行,至少提交“已启动、结果未知”事实,供后续 reconcile。正式总 deadline 应作为 runner 内部 future 终止 completion;工具开始前检查 deadline,一旦开始则不能中途 drop,必须等待结果后再携带 partial outputs 收口。外层 timeout 只适合作为进程级最后保险,不能承担业务收口。
- 验证:至少覆盖“无工具 completion 失败回滚 staged 用户消息”“非 fatal 工具失败对调用方暴露 `kind/retryable/fatal/output`”“成功工具后终态失败保留 partial tool output”“有工具活动时 committed memory 末尾存在 error closure”以及“API 增量中待确认工具位于 terminal `ERROR ` 之前”。
- 关联:`server-rs/crates/platform-agent-harness/src/run.rs`、`server-rs/crates/platform-agent-harness/src/tool.rs`、`server-rs/crates/platform-editor-agent/src/agent/prompt.rs`、`server-rs/crates/api-server/src/editor_agent/api.rs`。
## 画布 Agent 的规划请求不能关闭瞬时失败重试
@@ -3785,7 +3836,7 @@
- 原因:规划请求虽然有 Agent 专用单次 timeout,但 `editor_agent_llm_client` 把 `max_retries` 硬编码为 0;VectorEngine `gpt-5.4-mini` 的偶发长尾、连接超时或可重试上游状态会在第一次失败后直接持久化成 system error。framework 的英文 `completion error` 前缀也被原样暴露给用户。
- 处理:120 秒改为前端软提示阈值:POST 仍 pending 时显示不入库的“仍在处理中,请耐心等待”;provider 明确断开/失败才写正式错误。专用 provider 单 attempt 使用 8 分钟 hard timeout,请求发起阶段读取 `GENARRATIVE_LLM_MAX_RETRIES`,但画布 Agent 最多重试 1 次且重试退避最多 60 秒。不要只计算单次 complete 的最坏时间:runner 还可因非法 JSON/工具校验失败进入后续轮次,必须从 handler 入口开始计算 18 分钟总 deadline,进入 `agent.prompt(...)` 时扣除会话锁/上下文准备已用时间,为持久化和前端 20 分钟 timeout 留出余量。响应头后的体读取/解析错误按明确失败收口,必须使用真实 attempt 计数;规划、配置和定价错误对用户统一为中文,原始诊断只记后端日志。重试发生在任何生成工具执行前,不会重复提交生成任务或扣费,不要通过提高前端 timeout 或 runner `max_turns` 掩盖 provider 重试缺失。
- 验证:`platform-editor-agent` 测试锁定 8 分钟 hard timeout 与中文错误;前端 fake timer 用例锁定 120 秒前只显示思考动画、到点后显示耐心等待、成功/失败后移除;`platform-llm` 回归用例锁定第二次 attempt 成功响应头后的 body timeout 仍报累计 2 次;`api-server` 测试锁定专用 client retry、18 分钟整体 deadline 与中文直达错误。运行态排障按同一 request id 对齐 `platform_llm` failure stage 与 `/messages` 总耗时,并确认仍 pending 的请求不再在 120 秒形成错误气泡。
- 关联:`server-rs/crates/platform-editor-agent/src/agent/agent.rs`、`server-rs/crates/platform-editor-agent/src/framework/error.rs`、`server-rs/crates/api-server/src/state.rs`、`src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.ts`、`src/components/image-editor/EditorAgentConversation/MessageBubble.tsx`、`src/services/image-editor/editorAgentClient.ts`。
- 关联:`server-rs/crates/platform-editor-agent/src/agent/agent.rs`、`server-rs/crates/platform-agent-harness/src/error.rs`、`server-rs/crates/api-server/src/state.rs`、`src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.ts`、`src/components/image-editor/EditorAgentConversation/MessageBubble.tsx`、`src/services/image-editor/editorAgentClient.ts`。
## 前端退役目录不能只靠扫描和 ignore 隔离
@@ -3858,6 +3909,13 @@
- 处理:灰度页只能以 `/admin/api/feature-gates` 为数据源,固定目标列表只登记现役功能;新增或退役业务 target 只修改固定目标注册,不得让通用页面依赖业务列表接口。旧 `creation-entry:*` 目标、接口和页面保持退役。
- 验证:`adminRoutes` 必须包含 `gray-release`,admin-web TypeScript/ESLint/Vitest 不得排除灰度页;页面测试必须断言只请求 feature-gates,并继续覆盖现役固定 target、直接 Gate Key 保存与新 target 状态重置。
- 关联:`apps/admin-web/src/pages/AdminGrayReleaseConfigPage.tsx`、`apps/admin-web/src/app/adminRoutes.ts`、`server-rs/crates/api-server/src/modules/admin.rs`、`docs/technical/【架构下线】旧创作模板业务退役方案-2026-07-17.md`。
## 历史钱包消费不能从最近流水或通用订单快照推算
- 现象:后台用户详情要展示累计花费时,直接复用只返回最近 50 条的 `list_profile_wallet_ledger`,或在充值订单每行使用的通用钱包快照里扫描该用户全部流水。
- 原因:最近流水会低估历史总额;通用钱包快照又会被订单列表反复构造,把一次按用户聚合放大为 `订单数 × 流水数` 的重复扫描。
- 处理:历史花费只累计 `asset_operation_consume` 负向流水绝对值,退款不冲减;通过 `profile_wallet_consumption_total` 在已有投影时按主键 O(1) 累加。首次上线必须在停写维护窗口由 owner 执行全量初始化,为每个已有钱包流水的用户建立投影,不能让所有存量用户的首次正常消费各自扫描历史;维护遗漏或新用户缺行时才在首次消费或详情读取中按用户索引兜底重建一次。手动对账扫描是独立高风险操作,member 必须单独持有 `profile-wallet-consumption-reconcile`,不能因为能打开共享用户详情就自动获得。
- 验证:构造消费、退款、充值退款追回和赠送混合流水,断言只累计消费;维护初始化后正常消费只按主键累加;重复详情读取不得重复扫描或重复累计;任意 Tab 权限不能调用手动对账,同时确认充值订单列表的通用钱包快照没有新增历史流水扫描。
## AI 游戏 game-chat 自动预览不能在调用前消费授权(2026-07-29)
- 症状:`code-prototype` 首次完成后 `.agent/logs/command.log` 已出现 `permission.confirm preview.start`,但客户端没有 iframe,`.agent/logs/preview.log` 也没有新的 running 记录;后续即使父 run 完成也不再启动。
File diff suppressed because one or more lines are too long
@@ -1,12 +1,12 @@
# 后台管理多账号与 Tab 访问权限方案
更新时间:`2026-07-23`
更新时间:`2026-07-24`
## 1. 文档定位
本文定义陶泥儿后台从单一环境变量管理员扩展为“1 个 owner 引导账号 + 多个 member 持久账号”的编码契约,并为每个一级 Tab 建立前后端一致的访问权限。
本次只增加后台管理员账号与整页访问权限,不引入页面内按钮级、字段级或只读权限。正式实现必须同时完成前端导航过滤和后端 API 鉴权;前端过滤只改善体验,不能作为安全边界。
后台权限默认仍以整页 Tab 为粒度;只有会触发权威钱包全量扫描的“手动对账用户历史花费”作为明确例外,使用独立操作权限,不随任何 Tab 自动授予。正式实现必须同时完成前端按钮过滤和后端 API 鉴权;前端过滤只改善体验,不能作为安全边界。
## 2. 当前基线与目标
@@ -17,8 +17,8 @@
1. 现有环境变量账号升级为 `owner`,仍由部署环境提供,不迁移、不复制到 SpacetimeDB。
2. owner 始终拥有全部 15 个业务 Tab 权限,并独占“账号管理”Tab 和账号管理 API。
3. owner 可以创建、修改、启停 member;member 保存在 SpacetimeDB 私有表 `admin_account`。
4. member 按一级 Tab 分配权限;获得一个 Tab 权限即获得该页面内全部读写能力,页面内部二级 Tab、弹窗和操作区继承一级权限。
5. member JWT 每次请求都重新读取当前账号并校验 `enabled`、`token_version` 和实时权限,权限、密码或启停变更应立即让旧 JWT 失效。
4. member 按一级 Tab 分配常规权限;获得一个 Tab 权限即获得该页面内常规读写能力。历史花费手动对账必须另行授予 `profile-wallet-consumption-reconcile`,任何 Tab 都不隐式包含。
5. member JWT 每次请求都重新读取当前账号并校验 `enabled`、`token_version`、实时 Tab 权限和独立操作权限,权限、密码或启停变更应立即让旧 JWT 失效。
## 3. 角色与不可变规则
@@ -26,18 +26,18 @@
- owner 用户名和密码继续读取 `GENARRATIVE_ADMIN_USERNAME`、`GENARRATIVE_ADMIN_PASSWORD`。
- owner 是环境变量构造的虚拟账号,不写入 `admin_account`,不允许通过后台改名、改密、禁用或删除。
- owner 始终拥有本文列出的全部 15 个可分配权限,不能在前端取消,也不从数据库加载权限。
- “账号管理”是 owner-only 能力。它可以作为新增一级路由 `accounts` / `#accounts` 展示,但 `accounts` 不进入 `ADMIN_TAB_PERMISSIONS`,不能写入 member 的 `permissions_json`。
- owner 始终拥有本文列出的全部 15 个 Tab 权限和全部独立操作权限,不能在前端取消,也不从数据库加载权限。
- “账号管理”是 owner-only 能力。它可以作为新增一级路由 `accounts` / `#accounts` 展示,但 `accounts` 不进入 `ADMIN_TAB_PERMISSIONS` 或 `ADMIN_ACTION_PERMISSIONS`,不能写入 member 的权限 JSON。
- owner 会话返回 `accountRole = "owner"`、`roles = ["admin", "owner"]`;账号管理权限必须根据服务端确认的 `accountRole` 判断,不能只相信前端角色字符串。
- owner 配置缺失时,后台整体保持未启用状态;不能依赖数据库中的 member 绕过 owner 引导配置启动后台。
### 3.2 member
- member 只来自 `admin_account`,不新增第二套环境变量账号。
- member 会话返回 `accountRole = "member"`、`roles = ["admin", "member"]` 和当前实时 `tabPermissions`。
- member 会话返回 `accountRole = "member"`、`roles = ["admin", "member"]`、当前实时 `tabPermissions` 和 `actionPermissions`。
- member 永远不能访问账号管理页面或账号管理 API,也不能给自己或他人分配 `accounts`。
- member 的一个一级 Tab 权限覆盖该页面的查询、创建、修改、启停、退款等全部现有操作,不拆成 `read` / `write`。
- 页面内二级 Tab、筛选视图、抽屉、弹窗和共享详情弹窗继承触发它的一级 Tab 权限,不另设 permission id。
- member 的一个一级 Tab 权限覆盖该页面的常规查询、创建、修改、启停、退款等操作,不拆成通用 `read` / `write`。
- 页面内二级 Tab、筛选视图、抽屉、弹窗和共享详情弹窗默认继承触发它的一级 Tab 权限;历史花费手动对账是唯一独立高风险操作例外,无权限时共享用户详情不展示按钮,直接请求仍由后端返回 403。
## 4. 权限标识
@@ -61,9 +61,15 @@
| `editor-showcase` | 精选审核 | `#editor-showcase` |
| `editor-assets` | 素材查询 | `#editor-assets` |
权限数组必须去重并按上表顺序规范化后保存。保存时拒绝未知值和 `accounts`;读取旧数据时遇到未知值应忽略并记录告警,绝不能将未知值解释为全权限。空数组合法,表示 member 可以登录但没有业务页面权限。
Tab 权限数组必须去重并按上表顺序规范化后保存。保存时拒绝未知值和 `accounts`;读取旧数据时遇到未知值应忽略并记录告警,绝不能将未知值解释为全权限。空数组合法,表示 member 可以登录但没有业务页面权限。
后续新增一级 Tab 时,必须在同一次改动中更新:
`ADMIN_ACTION_PERMISSIONS` 是独立操作权限闭合集合,当前只有:
| permission id | 操作 | 授权边界 |
| --- | --- | --- |
| `profile-wallet-consumption-reconcile` | 手动对账用户历史花费 | owner 默认拥有;member 必须在账号管理中单独勾选,不要求同时持有特定 Tab |
独立操作权限保存在 `action_permissions_json`,响应为 `actionPermissions`;未知值必须拒绝。后续新增一级 Tab 时,必须在同一次改动中更新:
- shared-contracts 的 `ADMIN_TAB_PERMISSIONS`。
- admin-web 的路由定义、权限标签和第一可访问项顺序。
@@ -80,13 +86,14 @@
| `username` | `String` | `unique`;登录名,创建后不可修改;按 `trim + ASCII lowercase` 规范化 |
| `display_name` | `String` | 展示名,去除首尾空白后 1 至 64 字符 |
| `password_hash` | `String` | Argon2id PHC 字符串;只在内部登录查询中返回给 api-server,永不进入 HTTP DTO、日志或前端状态 |
| `permissions_json` | `String` | 规范化后的 Tab permission JSON;只允许第 4 节 15 个值,空数组为 `[]` |
| `tab_permissions_json` | `String` | 规范化后的 Tab permission JSON;只允许第 4 节 15 个值,空数组为 `[]` |
| `enabled` | `bool` | 是否允许登录和继续使用现有 JWT |
| `token_version` | `u64` | 初始为 `1`;权限、密码或启停状态发生有效变化时加 `1` |
| `created_by` | `String` | 创建者后台 subject;当前只能是 owner subject |
| `updated_by` | `String` | 最近更新者后台 subject;当前只能是 owner subject |
| `created_at` | `Timestamp` | 创建时间,使用 `ctx.timestamp` |
| `updated_at` | `Timestamp` | 最近更新时间,使用 `ctx.timestamp` |
| `action_permissions_json` | `Option<String>` | 既有表末尾追加;旧行默认 `None` 并按 `[]` 读取,只允许第 4 节独立操作权限 |
账号规则:
@@ -94,7 +101,7 @@
- owner 用户名属于保留名称。创建 member 时必须同时与当前规范化后的 owner 用户名比较并拒绝冲突,不能只依赖 `admin_account.username` 唯一索引。
- 密码明文只存在于登录、创建和改密请求生命周期内;限制为 6 至 128 个字符,并复用 `platform-auth` 的 Argon2id 哈希与校验能力。Argon2id 必须在 blocking 任务中执行,api-server 通过有界信号量限制同时 hash / verify 数量,不得占用 Tokio worker 或无界堆积高成本任务。
- 不提供物理删除 API。离职或停用通过 `enabled = false` 完成,以保留 `created_by`、`updated_by` 和账号标识。
- `display_name` 单独变化只更新 `updated_by`、`updated_at`,不要求递增 `token_version`;权限、密码、`enabled` 任一有效变化必须在同一事务中递增版本。
- `display_name` 单独变化只更新 `updated_by`、`updated_at`,不要求递增 `token_version`;Tab 权限、独立操作权限、密码、`enabled` 任一有效变化必须在同一事务中递增版本。
- `u64` 版本到达上限时更新失败关闭,不能回绕。
## 6. SpacetimeDB 与 facade 边界
@@ -158,9 +165,10 @@ owner 优先既保持原账号行为,也防止数据库同名记录遮蔽或
```text
accountRole: "owner" | "member"
tabPermissions: string[]
actionPermissions: string[]
```
owner 返回全部 15 个 permission id;member 返回数据库中的实时规范化数组。`GET /admin/api/me` 同样执行逐请求校验并返回实时权限,供刷新页面后恢复导航。
owner 返回全部 15 个 Tab permission id 和全部独立操作权限;member 返回数据库中的两组实时规范化数组。`GET /admin/api/me` 同样执行逐请求校验并返回实时权限,供刷新页面后恢复导航和操作按钮。
后台所有面向运营展示的管理员身份统一使用 `displayName`。审计表继续保存稳定 subject,例如 owner subject 或 `admin-account-<uuid>`;api-server 在返回兑换码、邀请码等操作记录时,按 owner 运行态和 `admin_account` 批量解析显示名称,同时兼容历史用户名记录。已无法解析的历史主体统一展示“已停用管理员”,前端不得直接渲染 `operatorUserId`、账号 ID 或登录用户名代替显示名称。对写接口,显示名目录必须在主事务前加载,或在主事务成功后降级为占位文案;不得因二次读取失败把已提交写入伪装成失败。
@@ -168,14 +176,15 @@ owner 返回全部 15 个 permission id;member 返回数据库中的实时规
在统一 `require_admin_auth` 之后增加可复用的权限守卫,支持:
- `require_admin_permission(permission)`:owner 自动通过;member 必须包含该 permission。
- `require_any_admin_permission([permission...])`:owner 自动通过;member 至少包含一个,用于共享 API。
- `require_admin_tab_permission(permission)`:owner 自动通过;member 必须包含该 Tab permission。
- `require_any_admin_tab_permission([permission...])`:owner 自动通过;member 至少包含一个,用于共享读取 API。
- `require_admin_action_permission(permission)`:owner 自动通过;member 必须包含该独立操作 permission。
- `require_admin_owner`:只接受服务端确认的 owner。
返回语义统一如下:
- `401 Unauthorized`:token 缺失、无效、过期,member 不存在、被停用或 `token_version` 过期。
- `403 Forbidden`:会话有效但缺少目标 Tab 权限,或 member 请求 owner-only API。
- `403 Forbidden`:会话有效但缺少目标 Tab / 独立操作权限,或 member 请求 owner-only API。
- 前端收到 `401` 清除本地 token 并回到登录页;收到 `403` 不应伪装成掉线,应刷新 `/me` 权限并跳转到第一可访问项或零权限空态。
## 9. API-to-Tab 权限矩阵
@@ -223,6 +232,8 @@ owner 返回全部 15 个 permission id;member 返回数据库中的实时规
| `POST` | `/admin/api/profile/recharge-refunds/register` | `recharge-orders` |
| `POST` | `/admin/api/profile/recharge-refunds/manual-review/resolve` | `recharge-orders` |
| `GET` | `/admin/api/profile/users/detail` | `tables OR tracking OR recharge-orders OR editor-showcase OR editor-assets` |
| `POST` | `/admin/api/profile/users/reconcile-consumption` | 独立操作权限 `profile-wallet-consumption-reconcile` |
| `POST` | `/admin/api/profile/users/initialize-consumption-projections` | owner-only 维护窗口操作 |
| `POST` | `/admin/api/profile/wallet-restriction` | `recharge-orders` |
| `GET` | `/admin/api/accounts` | owner-only |
| `POST` | `/admin/api/accounts` | owner-only |
@@ -249,6 +260,7 @@ accounts: Array<{
username,
displayName,
tabPermissions,
actionPermissions,
enabled,
tokenVersion,
createdBy,
@@ -270,6 +282,7 @@ accounts: Array<{
displayName: string,
password: string,
tabPermissions: string[],
actionPermissions: string[],
enabled?: boolean
}
```
@@ -285,11 +298,12 @@ accounts: Array<{
displayName: string,
password?: string,
tabPermissions: string[],
actionPermissions: string[],
enabled: boolean
}
```
`username` 和 `account_id` 不可修改。更新请求完整提交显示名称、Tab 权限和启停状态;密码省略表示不修改,空字符串密码作为非法参数拒绝。api-server 只在提供新密码时生成新 hash。procedure 比较有效变化,在权限、密码或启停任一变化时只递增一次 `token_version`,并在同一事务写入账号字段、`updated_by`、`updated_at`。响应仍不返回密码或 hash。
`username` 和 `account_id` 不可修改。更新请求完整提交显示名称、Tab 权限、独立操作权限和启停状态;密码省略表示不修改,空字符串密码作为非法参数拒绝。api-server 只在提供新密码时生成新 hash。procedure 比较有效变化,在权限、密码或启停任一变化时只递增一次 `token_version`,并在同一事务写入账号字段、`updated_by`、`updated_at`。响应仍不返回密码或 hash。
## 11. admin-web 行为
@@ -313,7 +327,7 @@ accounts: Array<{
### 11.3 账号管理页
- 权限编辑器展示 15 个明确的 checkbox,每项使用现有 Tab 中文名称;不能展示或提交 `accounts`。
- 权限编辑器分为 15 个 Tab checkbox 和独立操作权限区;当前独立区只显示“手动对账用户历史花费”。不能展示或提交 `accounts`。
- 创建和编辑使用独立弹窗或抽屉,不在列表下方追加表单。
- 编辑时密码字段默认空,空表示请求中省略 `password`;页面永不展示现有密码或 hash。
- 停用使用开关并二次确认。保存成功后以 API 返回 account snapshot 更新列表。
@@ -323,17 +337,17 @@ accounts: Array<{
建议按以下边界落地,避免在前端或 `api-server` 重新发明持久化规则:
- `shared-contracts`:`ADMIN_TAB_PERMISSIONS`、扩展后的 `AdminSessionPayload`、账号管理 request/response DTO。
- `shared-contracts`:`ADMIN_TAB_PERMISSIONS`、`ADMIN_ACTION_PERMISSIONS`、扩展后的 `AdminSessionPayload`、账号管理 request/response DTO。
- `spacetime-module`:私有表、输入类型、typed procedures、唯一性与版本递增事务。
- `spacetime-client`:生成绑定、row mapper、登录查询与账号管理 facade。
- `api-server`:owner/member 登录编排、Argon2id、逐请求账号解析、权限 middleware、账号管理 handlers。
- `apps/admin-web`:权限感知路由、hash 回落、零权限空态、owner-only 账号管理页。
不能将 `permissions_json` 的解析与授权只放在前端;不能让 admin-web 直连 SpacetimeDB;不能用进程内 member 列表替代 `admin_account`。
不能将 Tab / 独立操作权限 JSON 的解析与授权只放在前端;不能让 admin-web 直连 SpacetimeDB;不能用进程内 member 列表替代 `admin_account`。
## 13. 迁移、绑定与发布顺序
`admin_account` 是新增私有表,没有旧数据回填。原环境变量 owner 不入表,因此迁移不创建 owner 行。
`admin_account` 已是私有表;本次只在表结构体最后追加带 `None` 默认值的 `action_permissions_json`,旧 member 自动按空独立权限读取。原环境变量 owner 不入表,并始终由 api-server 合成全部权限。
实现 schema 后必须:
@@ -380,16 +394,17 @@ spacetime publish <database> \
- member 私表不能被普通 SpacetimeDB identity 查询或调用 procedure;只有 runtime service identity 可读写。
- 创建重复规范化用户名、owner 保留用户名、未知权限或 `accounts` 权限均失败。
- GET/POST/PUT 账号 API 任何响应和日志都不包含明文密码或 `password_hash`。
- 权限、密码、启停更新各自会递增 `token_version`;同一次请求修改多项只递增一次;仅改展示名不递增。
- Tab 权限、独立操作权限、密码、启停更新各自会递增 `token_version`;同一次请求修改多项只递增一次;仅改展示名不递增。
- member 被停用、改密或改权限后,旧 JWT 下一次请求返回 401;重新登录后获得实时权限。
- API-to-Tab 矩阵逐路由覆盖 `modules/admin.rs`,每条路由至少测试 owner 成功、具备权限的 member 成功、缺权限 member 返回 403。
- 两个共享读取接口分别覆盖每个允许 permission 的成功用例,以及无关 permission 的 403 用例。
- owner-only 账号 API 对任意 member 都返回 403,即使其 `permissions_json` 被污染为包含 `accounts`。
- 历史花费手动对账对仅持有任意 Tab 的 member 返回 403;只持有独立操作权限时允许调用;全量投影初始化始终 owner-only。
- owner-only 账号 API 对任意 member 都返回 403,即使其 Tab 或独立权限 JSON 被污染为包含 `accounts`。
### 14.2 前端
- owner 看到 15 个业务 Tab 和账号管理;member 只看到被分配的业务 Tab。
- 每个一级 Tab 内的二级 Tab、弹窗和写操作继承一级权限并正常使用,不出现“页面可见但内部 API 403”的错误映射。
- 常规二级 Tab、弹窗和写操作继承一级权限;历史花费对账按钮只在用户详情返回 `canReconcileConsumption=true` 时显示。
- 直接输入无权限 hash 自动替换为第一可访问项,不短暂挂载无权限页面。
- 当前 Tab 权限被 owner 收回后,下一请求触发重新登录;新会话恢复后落到第一可访问项。
- 零权限 member 登录后显示空态,不回落 Dashboard、不发送 Dashboard 或其它业务请求,并可正常退出。
@@ -207,7 +207,7 @@ controller 配置:
透明背景处理正常成功时,角色形象、图标 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 缓存下的跨版本字符串兼容。
inline 与 external v1 成功响应继续使用结构化 `warning.code/reason`;图标 / UI 的透明图已经成功、只有自动拆分失败时,继续返回结构化 `sliceWarning.code/reason`,其中 `sliceWarning.reason` 保留原始诊断。queue worker 把两类告警归一为有界的 `result_payload_json.warning`:只有一条时原样保留完整 `reason`;两条并存时按“通用在前、拆分在后”拼接,`code` 收敛为 `multiple-generation-warnings`(两条 `code` 相同则沿用原 `code`),任何一条都不得被丢弃。`sliceWarning.reason` 无论是否与通用告警并存都由 worker 添加“图集已生成,但自动拆分未完成:”前缀,拼接结果最后统一做长度上界收敛。任务摘要将该展示就绪的 `reason` 原样提取到 `warning_message`,单 job 状态和刷新后的任务列表 BFF 再以 `warning: string` 返回;Web 必须直接展示,不再补前缀或按 code 推断类型。历史任务保留写入时的 `reason` 快照,摘要 backfill 不按当前格式重新解释或补写前缀。该字符串语义是 worker / BFF / Web 的内部同版本契约,三者必须协调发布,不承诺滚动混部或旧 Web 缓存下的跨版本字符串兼容。
## 验收
@@ -59,7 +59,7 @@
- SpacetimeDB schema guard 比较当前工作树与基线提交时,两侧都必须分别读取各自 `Cargo.toml` 的 `lib.path`,再沿 `mod` / `#[path]` 只扫描该快照 crate root 可达的 schema;不得递归扫描整个 `src/`,否则原位保留的旧源码会与现役历史数据壳产生假 accessor 重复。
- `module-runtime` 仍是账号、钱包、公共设置、追踪和 feature gate 的现役领域 crate;其混合源码中的 `CreationEntry*`、旧公开作品、旧存档 / 浏览历史 / 游玩统计 DTO、command、mapper 和规则必须以编译条件退出,且不再依赖只为旧创作契约存在的 `shared-contracts`。历史 schema 只继续编译 `RuntimeBrowseHistoryThemeMode` 六个变体和完整保序的 `RuntimeProfileWalletLedgerSourceType` 等持久化 ABI,不保留围绕这些类型的旧业务实现。
- 纯模板 crate 和专属运行态 crate 不属于 workspace members、default members 或任何在运 crate 的依赖图;源码目录保持原样。
- `platform-agent` 及其专属 `langchainrust` 依赖同样退出 workspace 与 `api-server` 依赖图;现役编辑器 Agent 仅需的模型常量收口到 `platform-llm`,不再通过旧拼图 Phase 1 / Creative Agent 执行器 crate 复用。
- `platform-agent` 及其专属 `langchainrust` 依赖同样退出 workspace 与 `api-server` 依赖图;现役编辑器 Agent 仅需的模型常量收口到 `platform-llm`,不再通过旧拼图 Phase 1 / Creative Agent 执行器 crate 复用。后续抽出的 `platform-agent-harness` 是无旧玩法依赖的通用 JSON function-calling 底座,不得依赖、复用或重新挂回本条退役 crate。
- `platform-auth` 不再编译 runtime guest token;`platform-wechat` 不再编译旧生成结果订阅服务,只保留现役认证和支付协议。
## 验收
File diff suppressed because one or more lines are too long
@@ -42,7 +42,14 @@ v1 只开放以下能力:
- `POST /api/external/v1/editor/audios/background-music/generations`:生成编辑器背景音乐素材。
- `GET /api/external/v1/openapi.json`:导出本版本 OpenAPI 3.1 JSON。
角色图生成、图标 spritesheet 和 UI 素材提取的 2xx 成功响应可携带可选结构化 `warning { code, reason }`,当前稳定 `code` 为 `postprocess-failed-source-preserved`。provider 原图已保存但透明背景处理最终失败时,接口返回原图,不返回不存在的透明处理图,图标和 UI 也不继续拆分;有 `projectId + canvasCompletion` 时由原图完成画布写回,无画布上下文时只返回原图及实际存在的资源 / 素材快照。调用方应展示 warning,但不得把任务改判为失败。该降级只覆盖透明背景处理的最终失败,phase 上报、原图或透明处理图持久化、画布写回失败仍返回错误。图标 / UI 已成功生成透明图、只有自动拆分失败时继续使用既有 `sliceWarning`;服务端保证通用 `warning` 与 `sliceWarning` 互斥,防御性客户端若收到异常双字段响应仍以通用 `warning` 为准。
图片生成、图标 spritesheet 和 UI 素材提取的 2xx 成功响应可携带可选结构化 `warning { code, reason }`。外部 OpenAPI 当前公开四个稳定 `code`:
- `postprocess-failed-source-preserved`:生成成功,但透明处理、像素规整等后处理未完成,接口保留仍可使用的原图或进入该步骤前的结果。
- `dimension-restore-fallback`:provider 回图无法安全收口到目标交付尺寸,接口保留实际回图尺寸。
- `unsupported-image-style`:请求的图片后处理风格未知或不适用于当前生成类型,接口按无风格继续生成。
- `multiple-generation-warnings`:同一成功响应合并了不同 `code` 的多条非阻断告警,具体原因按顺序拼接在 `reason`。
provider 原图已保存但透明背景处理最终失败时,接口返回原图,不返回不存在的透明处理图,图标和 UI 也不继续拆分;有 `projectId + canvasCompletion` 时由原图完成画布写回,无画布上下文时只返回原图及实际存在的资源 / 素材快照。调用方应展示 `warning.reason`,但不得把任务改判为失败。该降级只覆盖透明背景处理的最终失败,phase 上报、原图或透明处理图持久化、画布写回失败仍返回错误。图标 / UI 已成功生成透明图、只有自动拆分失败时继续使用既有 `sliceWarning`。通用 `warning` 与 `sliceWarning` 只在「透明背景最终失败」这一条上互斥(该情况不会进入拆分);风格归一化或像素规整产生的通用 `warning` 可以与 `sliceWarning` 并存,调用方必须同时展示两者,不得只取其一。
管理 API Key 的登录态接口保留在站内个人中心链路,但不写入外部 OpenAPI JSON:
@@ -92,6 +92,8 @@ BgFilter 对已经落入私有 OSS 的生成原图、动作抽取帧和手动去
Full Job 通过 `EXIT_MAINTENANCE_MODE_AFTER_COMPLETION` 明确选择完整发布成功后是否退出维护,默认勾选以保持历史行为。Full 对 Stdb Publish 和 API Deploy 两个下游阶段都固定传 `KEEP_MAINTENANCE_MODE=true`,让 maintenance marker 持续覆盖 Stdb → API → Web 整段发布;Web Deploy 成功后才进入独立 `Exit Maintenance` 阶段。该阶段只能通过 `agent none` 和显式 `node(...)` 分配目标机,直接执行 `/opt/genarrative/current/scripts/deploy/maintenance-off.sh`;目标机不得 checkout Git、挂载 Git SSH 凭据或依赖 Jenkins workspace 源码。取消勾选时跳过最终退出阶段,便于内网验收完成后人工恢复公网。`Genarrative-Api-Deploy` 也单独暴露 `KEEP_MAINTENANCE_MODE` 参数,并转换为随发布包脚本的 `--keep-maintenance-mode`;失败路径仍按既有 current 切换边界保留或退出维护,不受成功态选项覆盖。外部生成 queue 的 `warning` 由 API/worker 固化为可直接展示的完整文案,Web 不再补前缀,因此 API/worker 与 Web 必须在同一维护窗口按同一版本协调发布;分开运行 Job 时先保持维护态完成 API/worker,再发布 Web,二者完成后才能恢复公网,不得在公网可用期间只滚动其中一侧。
维护门禁启用时,Nginx 与 Pingora 只精确放行默认维护页依赖的 `/branding/taonier-maintenance-page.png` 和 `/branding/taonier-product-ip.png`;不得放开整个 `/branding/`、`/assets/` 或后台静态目录。公网验收除主站、后台和 API 继续返回维护响应外,还必须确认这两个品牌图片返回 `200 image/png`,避免维护页 HTML 正常但背景图请求被再次改写成 `503`。
需要验证“更新 API 不停 worker”和“worker 是否持续消费队列”时,优先使用隔离容器 smoke:`npm run container:worker-smoke -- smoke`。该脚本生成 gitignored 的 `deploy/container/worker-smoke/api-server.env`,启动独立 compose project 与独立 SpacetimeDB,发布当前 `spacetime-module` 后写入 `source_module = editor-canvas`、`job_kind = worker_smoke_unsupported` 的测试 job;预期 worker claim 后执行 unsupported 失败分支,再执行 API-only recreate 并确认 worker 容器 ID 不变,最后再次入队验证 API 更新后队列仍可消费。`external_generation_job` 是 private table,脚本通过 worker 日志确认 job_id 被消费,不用 CLI SQL 查询私表。该 smoke 不读取 `.env.local`,也不依赖真实 VectorEngine / OSS 密钥;真实生图链路联调再在本地私有 env 中补齐 provider 配置。worker-smoke 默认把本机 `spacetime` CLI 打成轻量 SpacetimeDB 镜像,避免本机首次 smoke 依赖官方大镜像下载。若容器内 Cargo 拉取 crates.io 依赖不稳定,可用 `npm run container:worker-smoke -- smoke --local-binary` 让容器内 Cargo 复用本机 Cargo 缓存构建当前二进制,再打入 Debian bookworm smoke runtime 临时镜像;可用 `GENARRATIVE_WORKER_SMOKE_LOCAL_BASE_IMAGE` 覆盖运行时基础镜像;若隔离端口或库数据需要重建,追加 `--force`。完成 queue 链路验证时,用队列概览 BFF、单 job 状态接口和 worker 日志确认任务从 queued/running 收敛到预期失败态。
本地只做账号/UI smoke 且需要短信登录时,`SMS_AUTH_PROVIDER` 应显式设为 `mock`,并把 `SMS_AUTH_MOCK_VERIFY_CODE` 设为固定值(当前常用 `123456`),再重启 `npm run dev` 或 `npm run dev:api-server`。如果 `.env.local` 还保留 `SMS_AUTH_PROVIDER=aliyun`,`POST /api/auth/phone/login` 用 mock 验证码会稳定报“验证码错误”,不是前端表单问题。真实短信联调再切回 `aliyun` 并重启。
@@ -143,7 +145,17 @@ spacetime sql <database> "SELECT * FROM profile_recharge_refund_bill_checkpoint"
核对规则:部分退款时原订单保持 `paid`;累计退款等于订单金额后才为 `refunded`,但 `paid_at` 继续保留,因此不会恢复首充资格。泥点退款只回收普通永久泥点;每日免费泥点和会员周期泥点不动。永久泥点不足时 `recovery_status=shortfall`、`unrecovered_points>0`、`wallet_frozen=true`,正式钱包消费在欠款清零前 fail-closed;会员订单统一为 `manual_review`,不得自动缩短有效期或扣周期泥点。
后台充值订单退款必须通过管理员鉴权接口执行,不得从数据库页面直接改表:列表 `GET /admin/api/profile/recharge-orders`、用户详情 `GET /admin/api/profile/users/detail`、预检 `POST /admin/api/profile/recharge-refunds/preview`、执行 `POST /admin/api/profile/recharge-refunds/execute`、应急退款号登记 `POST /admin/api/profile/recharge-refunds/register`、人工冻结/解冻 `POST /admin/api/profile/wallet-restriction`。预检返回微信支付状态、本地累计退款、剩余可退金额、预计追回泥点、钱包总额、可消费余额、活动占用和退款欠账;只有预检允许且二次确认后才提交退款。提交使用稳定 `requestId`,接口超时后重试必须复用同一值。若返回“退款处理中”,先查同一 `out_refund_no`,不要换号再次发起。商户平台应急退款完成后,在后台登记原 `out_refund_no` 触发验签查单;微信已退款但缺少退款号时等待 T+1 账单,不得凭截图或支付订单 `REFUND` 状态直接手写退款事实。
后台充值订单退款必须通过管理员鉴权接口执行,不得从数据库页面直接改表:列表 `GET /admin/api/profile/recharge-orders`、用户详情 `GET /admin/api/profile/users/detail`、历史花费手动对账 `POST /admin/api/profile/users/reconcile-consumption`、存量消费投影初始化 `POST /admin/api/profile/users/initialize-consumption-projections`、预检 `POST /admin/api/profile/recharge-refunds/preview`、执行 `POST /admin/api/profile/recharge-refunds/execute`、应急退款号登记 `POST /admin/api/profile/recharge-refunds/register`、人工冻结/解冻 `POST /admin/api/profile/wallet-restriction`。用户详情返回的 `historicalConsumedPoints` 来自 `profile_wallet_consumption_total`,表示退款不冲减的历史总消费;已有投影的正常消费只做按主键 O(1) 原子累加,缺行时按该用户钱包流水索引兜底重建一次,不得用只返回最近 50 条的钱包流水列表在 BFF 或前端重算。手动对账是独立操作权限:owner 始终拥有,member 必须在账号管理中单独勾选“手动对账用户历史花费”,任意 Tab 权限都不隐式授予;接口经二次确认后才扫描该用户全部权威流水、校准投影并记录管理员和时间。投影初始化接口只允许 owner,必须在首次上线的停写维护窗口执行并成功后再恢复业务流量;它扫描全部权威钱包流水并初始化或校准全部消费投影。维护遗漏的存量缺行仍由用户详情首次读取按用户索引兜底回填。预检返回微信支付状态、本地累计退款、剩余可退金额、预计追回泥点、钱包总额、可消费余额、活动占用和退款欠账;只有预检允许且二次确认后才提交退款。提交使用稳定 `requestId`,接口超时后重试必须复用同一值。若返回“退款处理中”,先查同一 `out_refund_no`,不要换号再次发起。商户平台应急退款完成后,在后台登记原 `out_refund_no` 触发验签查单;微信已退款但缺少退款号时等待 T+1 账单,不得凭截图或支付订单 `REFUND` 状态直接手写退款事实。
首次发布消费投影时,先停止业务写入并确认 api-server 与新 SpacetimeDB module 已就绪,再使用当前 owner 登录获得的短期 token 执行:
```bash
curl -fsS -X POST \
-H "Authorization: Bearer ${ADMIN_BEARER_TOKEN}" \
"https://<API 域名>/admin/api/profile/users/initialize-consumption-projections"
```
响应中的 `scannedLedgerCount` 是扫描流水数,`projectedUserCount` 是已存在钱包流水并完成投影的用户数;只有请求成功且返回 `ok=true` 后才恢复业务流量。该接口允许幂等重跑,但每次都会全表扫描,只能在维护窗口由 owner 执行,不得加入普通定时任务或页面自动请求。
正式落账上线前已经被旧 debug handler 返回成功的退款回调不会因部署新版本自动重放。已知 `out_refund_no` 的历史退款应由具备真实商户凭据的受控服务端操作先调用单笔退款查询,验签后写入同一 observation 事务;未知的商户平台退款等待次日交易账单发现。自动账单按分片补扫微信 API 可查询的近 90 天,超出窗口的历史退款需从商户平台导出核对后逐笔受控查单补录,不能直接把商户平台截图或 CSV 行当作退款终态,也不能开放匿名或普通用户补录 / 退款入口。
@@ -362,7 +374,7 @@ UI 相关修改要重点验证:
npm run database:backup:oss -- --data-dir /stdb --stop-service spacetimedb.service --restart-service-after genarrative-api.service --restart-service-after genarrative-external-generation-worker@1.service --restart-service-after genarrative-external-generation-controller.service
```
脚本会将数据目录打包成 `tar.gz`,上传到 `oss://<bucket>/<prefix>/<database>/<database>-<UTC时间>.tar.gz`。生产建议做冷备份:传入 `--stop-service spacetimedb.service`,脚本会在打包前停止服务、打包后恢复服务,再上传 OSS;因 `genarrative-api.service`、`genarrative-external-generation-worker@*.service` 和 `genarrative-external-generation-controller.service` 都依赖 `spacetimedb.service`,生产定时冷备份还必须传入对应的 `--restart-service-after`,确保备份后 API、保底 worker 和 controller 随数据库一起恢复。`2026-06-10` release 故障就是现场 unit 漏掉 API 重启参数,`03:20` 冷备份停止 SpacetimeDB 后 API 被依赖关系一并停止,备份脚本只恢复了 SpacetimeDB,API 直到人工重启前都不可用;`2026-06-24` release 又出现同类依赖停机后只恢复 API、未恢复外部生成 worker/controller,导致图片画布生成任务长期停留在队列中。后续现场变更、provision 模板和 Jenkins 归档都必须通过 `npm run check:production-ops` 防止回退。由于 OSS 上传可能受服务器带宽限制,`Genarrative-Stdb-Module-Publish` 默认使用 `DATABASE_BACKUP_MODE=async`:先在 publish 前用 `--defer-upload` 生成本地冷备份和 `.manifest.json`,随后继续执行 publish;发布脚本退出前会用后台 `node -- ... --upload-archive <tar.gz>` 上传同一份发布前备份,不等待上传完成。`Genarrative-Full-Build-And-Deploy` 必须显式暴露并透传同一个 `DATABASE_BACKUP_MODE`,不得静默使用下游 `async`;release 已有验真冷备且明确禁止再上传时,Full 必须选择 `skip`。发布脚本在校验 wasm 后、执行 `spacetime publish` 前会等待显式 `SPACETIME_SERVER_URL` 的 `/v1/ping` 就绪,默认最多等待 `60` 秒;如生产机器冷备份恢复 `spacetimedb.service` 较慢,可临时设置 `GENARRATIVE_STDB_PUBLISH_READY_TIMEOUT_SECONDS` 调整等待时间。需要强一致发布闸门时改用 `DATABASE_BACKUP_MODE=sync`(等价脚本参数 `--backup-mode sync`),备份会在 publish 前同步打包并上传,失败会阻断 publish;确认已有其他备份窗口时才使用 `DATABASE_BACKUP_MODE=skip`(兼容脚本参数 `--skip-backup`)。若业务不能接受停机窗口,应先规划 SpacetimeDB 原生快照或主备策略,不要直接在写入中的数据目录上做热拷贝并当作强一致备份。
脚本会将数据目录打包成 `tar.gz`,上传到 `oss://<bucket>/<prefix>/<database>/<database>-<UTC时间>.tar.gz`。生产建议做冷备份:传入 `--stop-service spacetimedb.service`,脚本会在打包前停止服务、打包后恢复服务,再上传 OSS;因 `genarrative-api.service`、`genarrative-external-generation-worker@*.service` 和 `genarrative-external-generation-controller.service` 都依赖 `spacetimedb.service`,生产定时冷备份还必须传入对应的 `--restart-service-after`,确保备份后 API、保底 worker 和 controller 随数据库一起恢复。`2026-06-10` release 故障就是现场 unit 漏掉 API 重启参数,`03:20` 冷备份停止 SpacetimeDB 后 API 被依赖关系一并停止,备份脚本只恢复了 SpacetimeDB,API 直到人工重启前都不可用;`2026-06-24` release 又出现同类依赖停机后只恢复 API、未恢复外部生成 worker/controller,导致图片画布生成任务长期停留在队列中。后续现场变更、provision 模板和 Jenkins 归档都必须通过 `npm run check:production-ops` 防止回退。由于 OSS 上传可能受服务器带宽限制,`Genarrative-Stdb-Module-Publish` 默认使用 `DATABASE_BACKUP_MODE=async`:先在 publish 前用 `--defer-upload` 生成本地冷备份和 `.manifest.json`,随后继续执行 publish;发布脚本退出前会用独立 `systemd-run` transient service 执行 `--upload-deferred-dir <backup-dir>`,串行补传该目录内同库的 `deferred/pending` 归档,不依赖 Jenkins 作业进程树存活。任一归档只有在 OSS archive、manifest 和 baseline state 全部上传并验真后,才按 `keep-local` 规则删除;失败归档保留原 manifest,由下次 publish 重试。`Genarrative-Full-Build-And-Deploy` 必须显式暴露并透传同一个 `DATABASE_BACKUP_MODE`,不得静默使用下游 `async`;release 已有验真冷备且明确禁止再上传时,Full 必须选择 `skip`。发布脚本在校验 wasm 后、执行 `spacetime publish` 前会等待显式 `SPACETIME_SERVER_URL` 的 `/v1/ping` 就绪,默认最多等待 `60` 秒;如生产机器冷备份恢复 `spacetimedb.service` 较慢,可临时设置 `GENARRATIVE_STDB_PUBLISH_READY_TIMEOUT_SECONDS` 调整等待时间。需要强一致发布闸门时改用 `DATABASE_BACKUP_MODE=sync`(等价脚本参数 `--backup-mode sync`),备份会在 publish 前同步打包并上传,失败会阻断 publish;确认已有其他备份窗口时才使用 `DATABASE_BACKUP_MODE=skip`(兼容脚本参数 `--skip-backup`)。若业务不能接受停机窗口,应先规划 SpacetimeDB 原生快照或主备策略,不要直接在写入中的数据目录上做热拷贝并当作强一致备份。
生产环境变量模板在 `deploy/env/api-server.env.example`:
@@ -755,6 +767,8 @@ cargo test -p platform-auth --manifest-path server-rs/Cargo.toml aliyun_send_sms
- `profile_wallet_ledger`
- `profile_wallet_config`
后台“账号配置”通过 `GET/POST /admin/api/profile/wallet-config` 一次读写账号初始泥点数和每日免费泥点数。修改每日免费额度不重算已初始化的当日余额;尚未初始化的当日额度或下一次北京时间跨日重置使用最新配置。
个人任务首版 scope 仅支持 `user`。每日登录任务按北京时间自然日 0 点重置;用户已登录并停留在“我的”页跨日时,前端需要先非阻断调用 refresh session 以写入新业务日 `daily_login`,再请求 `/api/profile/tasks` 刷新任务中心。认证成功后的 `daily_login` 必须通过 `SpacetimeClient::record_daily_login_tracking_event(...)` 调用 SpacetimeDB 专用 `record_daily_login_tracking_event_and_return` procedure,由数据库事务时间生成当日幂等事件并推进任务进度;不要改回普通 `record_tracking_event_after_success`、tracking outbox 或旧 `profile.login.daily` 事件键。后台、RPG、大鱼吃小鱼、Visual Novel、Story、Combat 等特定链路按 tracking 中间件排除规则处理;作品游玩统一使用 `work_play_start`。
外部 API 失败审计复用 `tracking_event`,不新增表。普通 API / external-generation 调用的失败事件优先写入本机 tracking outbox,再由后台 worker 批量落库;如果 outbox 因权限、磁盘或保护阈值不可写,仍回退同步直写 SpacetimeDB。BgFilter worker 是受限资源例外:provider 失败审计在 spawn 前受进程级 `1024` 硬上限保护,获准任务写入 `GENARRATIVE_TRACKING_OUTBOX_DIR/bgfilter-worker/` 独立目录;任务满载、outbox 缺失、达到保护阈值或写盘失败时直接丢弃并记录指标,不同步直写。`metadata_json` 包含 endpoint、operation、failureStage、statusCode、statusClass、timeout、retryable、errorMessage、errorSource、latencyMs、promptChars、referenceImageCount、imageModel、rawExcerpt、userId、profileId 和 requestId;其中 `userId` 是触发生成的用户,`profileId` 是调用方传入的草稿 / 作品 / 场景作用域,`requestId` 用于回查同一次 HTTP 请求日志,入口拿不到上下文时允许为空。常用查询:
@@ -115,7 +115,7 @@ V3 退款入口在正式落账的同时保留可用于真实联调的安全诊
4. 已经发生的外部退款没有 hold 时沿用现有结算:回收当前未被其他 hold 占用的永久泥点,余额不足部分继续以 `profile_recharge_order_refund_settlement.unrecovered_points` 作为唯一退款欠账真相,状态为 `shortfall` 并限制消费。后续永久泥点到账后在同一钱包事务内按最早退款单自动继续追回;每日免费和会员周期泥点不参与。不得再建一张平行 debt 表重复累计欠账。
5. 人工钱包冻结单独使用 `profile_wallet_manual_restriction`,保存当前是否冻结、原因、操作管理员和操作时间。普通消费同时检查人工冻结、退款欠账和活动 hold;解除人工冻结不能解除仍存在的退款欠账。
6. 后台 API 统一位于管理员鉴权下:充值订单列表与详情、用户详情、退款预检、退款执行、应急 `out_refund_no` 登记、钱包人工冻结/解冻。任何接口都不得返回原始手机号、商户私钥、APIv3 Key、微信签名、回调密文或账单下载 URL。
7. 通用用户详情通过内部 `user_id` 或陶泥号解析同一认证用户,展示头像、昵称、陶泥号、内部 ID、脱敏手机号、登录/微信绑定状态、钱包总额、可消费余额、活动占用、退款欠账、冻结原因和最近充值订单。后台语义明确的用户 ID 或陶泥号旁统一使用图标按钮打开同一个弹窗,不复制页面级用户查询逻辑。
7. 通用用户详情通过内部 `user_id` 或陶泥号解析同一认证用户,展示头像、昵称、陶泥号、内部 ID、脱敏手机号、登录/微信绑定状态、钱包总额、可消费余额、活动占用、退款欠账、历史花费、冻结原因和最近充值订单。历史花费读取 `profile_wallet_consumption_total`:首次上线在停写维护窗口由 owner 全量初始化存量投影,已有投影的消费落账按主键 O(1) 原子累加,退款不冲减;维护遗漏或新用户缺行时,在首次消费或详情读取中按用户全部流水兜底回填一次。不得用最近 50 条账单列表近似。手动对账不继承任意 Tab,member 必须单独持有 `profile-wallet-consumption-reconcile` 操作权限;无权限时用户详情不展示对账按钮。后台语义明确的用户 ID 或陶泥号旁统一使用图标按钮打开同一个弹窗,不复制页面级用户查询逻辑。
真实联调时显式开启该模块的 debug 日志:
@@ -45,7 +45,7 @@ layer 只表达“某个资源怎样放在画布上”。`src / prompt / actualP
所有用户布局写入必须携带读取快照时获得的 `expectedRevision`。procedure 在事务中校验 canvas 当前 revision;不一致返回 `409`,前端应重载后端最新快照,不能只换上新 revision 就原样重放冲突前的整包布局。
前端保存队列只对无 HTTP 响应的传输失败以及 `408 / 425 / 429 / 502 / 503 / 504` 做有界退避重试,`400 / 403 / 404 / 413` 等确定性错误不重复提交。若旧请求执行期间已有更新布局排队,旧请求失败后必须继续发送最新布局;`409` 后权威快照暂时加载失败时保留 pending save 并定时重新进入冲突恢复,不能等待用户再次拖动画布才恢复保存。
前端保存队列只对无 HTTP 响应的传输失败以及 `408 / 425 / 429 / 502 / 503 / 504` 做有界退避重试,`400 / 403 / 404 / 413` 等确定性错误不重复提交。若旧请求执行期间已有更新布局排队,旧请求失败后必须继续发送最新布局;`409` 后冲突布局立即作废,权威快照暂时加载失败时保留冲突恢复状态并定时只重试 GET,不能把旧布局换上新 revision 后重放,也不能等待用户再次拖动画布才恢复。
本次结构化 V1 先保留旧 `{ viewport, layers }` PATCH 作为兼容输入。legacy canvas 即使携带 `expectedRevision` 也只做 CAS legacy 保存,不允许用户写入绕过 migration operator 直接激活 structured;只有已完成 backfill / activate、且 active 迁移记录的 revision / hash / 数量 / 资源引用校验均通过时,后端才在单个事务内把兼容输入拆成 layer / dialog 行并递增一次 revision。旧无 CAS procedure 不得写 structured canvas。V1 快照从 typed 列重组,`item_json / dialog_json` 只保留最大 512 KiB 的未结构化扩展字段。自包含本地图片序列在 active canvas 中只能继续保存已回填且 `layerId / resourceId / sourceType / item_json` 语义完全一致的原行;允许修改几何、层级、分组、显隐等 typed 布局字段。前端序列化按正常资源真相边界省略 `assetKind / generationInputs` 时,后端只从既有结构化行恢复这两个冻结字段再校验;显式修改仍拒绝。active 路径不再经过 legacy 元数据清洗,拒绝新增缺资源序列或改写既有帧、预览、prompt 和生成扩展。后续将新增、移动、缩放、删除、重排和分组收窄为有界 batch mutation;在此之前 2 MiB 仍是兼容整包入口的上限。
@@ -97,7 +97,7 @@
- 生成中的占位图允许通过键盘 `Delete` / `Backspace` 删除;不额外增加画布上的可见删除按钮。用户删除后,后续异步成功或失败回写不得重新创建该生成对象。
- 待生成占位的空白样式按生成类型区分:视频使用视频图标和视频角标,角色形象使用角色图标和角色角标,角色动作使用角色动作图标和动作角标,音效使用音效图标和音效角标,背景音乐使用音乐图标和背景音乐角标。
- 待生成占位的图标语义必须与底部入口或触发入口保持一致:生成图片用图片图标,生成规范用规范图标,生成角色形象用角色图标,生成图标素材用图标网格,生成 UI 设计图用应用窗口,宣发素材用宣发图标,快速编辑用闪光图标,不能默认全部回退为图片图标。
- 图片类待生成占位尺寸必须与面板当前比例和尺寸同步:普通图片、角色形象、图标素材、UI 设计图按当前 `aspectRatio + imageSize` 计算像素尺寸;生成规范固定为 `16:9·2K`,占位为 `2048 x 1152`;宣发素材按 workflow 输出尺寸创建占位。
- 图片类待生成占位尺寸必须与面板当前比例和尺寸同步:普通图片、角色形象、图标素材、UI 设计图按当前 `aspectRatio + imageSize` 的统一业务像素矩阵计算;所有共用规格都不因模型不同改变画布占地,例如 `nanobanana2` 与 `gpt-image-2` 的 `16:9·2K` 占位和普通最终结果都为 `2048 x 1152`。provider 请求尺寸可因接口合法值不同,但不得泄漏为正常完成的画布尺寸;回图更大时只允许缩小和轻微裁切,回图低于目标时保留实际像素并告警,禁止放大伪造所选档位;生成规范固定为 `16:9·2K`,占位为 `2048 x 1152`;宣发素材按 workflow 输出尺寸创建占位。
- 视频待生成占位必须与面板当前比例和清晰度同步:默认 `16:9 · 480p` 为 `854 x 480`,切换比例、`720p` 或 `1080p` 后按比例和清晰度重算偶数宽度;调整参数时保持占位中心点不变。
- 面板中用户修改比例、尺寸或清晰度后,已有空白待生成占位立即同步更新 `width / height / originalWidth / originalHeight`,且保持中心点不跳动。
- 快速编辑点击修改后不创建独立 `Quick Edit Generator` 画布生成占位;当前快速编辑面板显示修改中,生成成功后结果直接覆盖源图,失败时保留当前面板并显示错误。用户选定的比例和尺寸是新的业务目标分辨率,覆盖旧的“始终保持源图精确分辨率”约束;替换时更新图层原始分辨率,并保持图层中心位置不跳动。需要新建占位的是生成图片、生成视频、重绘、去背景和角色动作等会产出新图层的入口。
@@ -123,7 +123,8 @@
- 生成成功后仍保留生成器快照;画布渲染优先用 `generatedLayerId` 锚定到成品图层,不再重复显示灰色占位框。
- 一次生成任务产生多个可复用产物时,已实际生成的产物都必须由后端登记为项目资源并随同一次完成快照加入画布,不能由前端临时追加。角色形象、图标 spritesheet 和 UI 素材提取在透明背景处理正常成功时同时回填纯色背景原图与透明后处理结果,UI 素材提取继续一并回填拆分成功的素材;`generatedLayerId` 锚定透明后处理主结果,附属产物从主结果右侧开始错开放置。透明背景处理最终失败、但 provider 原图已经持久化时,任务以 `completed + warning` 收口,只把 provider 原图作为唯一主图放入画布,`generatedLayerId` 指向原图,不创建不存在的透明处理图,图标和 UI 也不继续拆分;角色重绘遵循同一规则。该收口只捕获透明背景处理本身的最终失败;phase 上报、provider 原图持久化、透明处理图持久化和 `canvasCompletion` 写回错误仍正常传播,不能被原图降级吞掉。
- 多产物任务的可恢复中间产物还必须进入账号素材库,未传 `assetFolderId` 时落默认“项目”文件夹,并在抠图、尺寸恢复、抽帧或拆分前完成登记。图片修改保存模型对齐尺寸的原始输出;角色动作把绿幕预览视频保存为一个素材,逐帧绿幕源图只保留在同一任务 OSS 路径,避免素材库一次新增 32 至 48 张帧图。普通图片、去背景和音频等没有独立上游中间产物的任务不重复复制最终结果。
- 图标和 UI 图集自动拆分只在透明图集成功后执行,属于非阻断附加动作;识别或切片持久化失败时整张透明图集仍完成并回填,前端通过 `sliceWarning` toast 提示用户可手动重试。透明背景最终失败使用通用 `warning.code/reason`,与 `sliceWarning` 互斥;`sliceWarning` 只表示透明图集成功但自动拆分失败,其 `reason` 原始契约保持不变。inline 响应、worker 队列终态和刷新后的任务列表必须使用同一 warning 语义,不能把已完成或降级完成的任务标记为失败。
- 图标和 UI 图集自动拆分只在透明图集成功后执行,属于非阻断附加动作;识别或切片持久化失败时整张透明图集仍完成并回填,前端通过 `sliceWarning` toast 提示用户可手动重试。透明背景最终失败使用通用 `warning.code/reason`,该情况不会进入拆分,因此与 `sliceWarning` 互斥;风格归一化或像素规整产生的通用 `warning` 则可与 `sliceWarning` 并存,inline 与队列两条链路都必须把两者拼成同一条提示展示,不得只取通用告警。`sliceWarning` 只表示透明图集成功但自动拆分失败,其 `reason` 原始契约保持不变。inline 响应、worker 队列终态和刷新后的任务列表必须使用同一 warning 语义,不能把已完成或降级完成的任务标记为失败。
- 画布顶部的生成 / 参考图选择 warning toast 保留手动关闭按钮,并在每次 warning 事件进入显示态后 `3` 秒自动消失,避免一次错误提示持续遮挡画布。同样文案在未消失时再次触发也必须重新计时,不能沿用上一次事件的剩余时间。
- 普通图片、图片修改、规范、角色、图标、UI 设计、宣发素材、视频、音效、背景音乐和角色动作生成面板不展示“资源名称”输入,默认继续使用现有“类型 + 编号”名称;提示词输入保持统一可见边框。状态与请求契约仍兼容可选 `assetLabel`,内部调用或历史状态携带名称时最多 80 个字符并在提交时 trim,最终解析出的同一个名称必须同时写入画布图层、`editor_project_resource`、`editor_asset` 和 `canvasCompletion.title`。中间原图在主名称后追加“(原图)/(原始输出)”,拆分图标仍使用各自素材描述。
- 图片、视频和音频生成结果都要写入账号级素材库;视频 / 音频结果由后端持久化到 OSS 并回传 `objectKey` / `assetObjectId`,前端保存素材库时一并记录,后续预览和再次加入画布走统一换签链路。
- 刷新项目后,画布需要同时恢复图层、生成器快照和生成输入框跟随关系。
@@ -1,6 +1,6 @@
# 画布Agent对话面板
日期:`2026-07-20`
日期:`2026-07-23`
## 定位与边界
@@ -28,6 +28,7 @@
- 用户要求“规范图 / 视觉规范图 / 风格规范图 / 素材规范展板”时,规划默认选择 `generate_image`,并在 prompt 中明确要求生成规范展板,包含统一视角、线条粗细、色卡、材质、阴影、圆角、状态层级、尺寸标注等可落地的视觉规范元素。
- 用户要求“角色规范图”且语义是角色的规范展板、风格展板或设定板时,仍走 `generate_image`,不要误分流到 `generate_character`;只有实际生成角色立绘、角色主形象或角色视觉资产时才走 `generate_character`。用户要求多个图标素材、图集或 spritesheet 时才走 `generate_icon_spritesheet`。
- 所有生成必须走 `execute_billable_asset_operation_with_cost` 与模型定价配置,禁止绕过定价收口。
- function-calling 的 JSON Schema 必须与参数默认值和运行时校验保持一致,不能只在 description 中提示会被运行时拒绝的组合。`generate-ui-design` 固定 `gpt-image-2`,因此 `image_size` 只暴露 `1K / 2K`;其它可切换图片模型的工具通过共享条件 schema 在显式选择 `gpt-image-2` 时同样把 `image_size` 限制为 `1K / 2K`,省略模型时仍按默认 nanobanana2 允许 `0.5K`。`generate-video` 省略 `model` 时按默认 `seedance2.0-fast` 约束 `resolution` 为 `480p / 720p`,显式选择其它模型时仍使用其现有分辨率范围。运行时强类型校验继续作为最终防线。
- 图层操作及其他未注册的画板功能第一期不进入对话工具面,仍走现有面板。
## 当前分支落地状态
@@ -59,8 +60,14 @@
- 对话框与既有任务侧栏(`ImageCanvasTaskSidebarView`)**互斥展开**:展开一个自动收起另一个;各自收起后保留入口按钮。
- 对话框与左侧素材 / 图层侧栏**不互斥**,允许同时展开,便于在对话中选取和核对画布素材;左侧栏切换不改变 Agent 面板开关状态。
- 桌面端对话框固定宽约 360–400px;移动端抽屉式全宽覆盖;收起态为胶囊/圆形入口按钮。
- 底部消息输入框随输入内容从单行高度自动增长,最大高度为
128px;输入框及其 Enter 提交、原生自适应和兼容降级统一封装在独立 `EditorAgentDraftTextarea` 组件中。支持 `field-sizing: content` 的浏览器使用原生内容尺寸自适应,不支持该属性的旧 Safari / iOS WebView 使用前端测量降级,并在宽度变化时重新计算换行高度。内容超过最大高度后停止增长并启用内部纵向滚动,内容缩短或清空后同步收缩。内部滚动条使用浅灰窄滑块和透明轨道,上下留白不得溢出输入框圆角边界;输入框在窄屏下允许收缩且不产生横向滚动。
- Enter 发送必须同时排除 `isComposing` 和旧 Safari / WebKit 候选词确认事件的 `keyCode === 229`,避免输入法选词时误发送。
- 用户消息必须包含去除首尾空白后的非空文本;附件只能随文本消息发送,前端发送门禁与后端 `module-editor-agent` 领域校验必须同时拒绝纯附件消息。
- 会话管理入口在对话框头部:当前会话标题 + 历史会话下拉(按更新时间倒序)+ 新建对话按钮,全部包在对话框内。
- 快速切换会话或会话轮询刷新产生并发详情请求时,前端只允许最后发起的请求更新当前会话、消息、错误和加载态;旧响应不得覆盖用户最新选择。
- 当前会话没有任何已发送消息时,新建对话按钮置灰且不可点击;输入框草稿和未发送附件不算会话内容。当前会话已有消息时可新建,新建成功后只切换到返回的空白会话,输入文字、附件及附件选择状态与切换历史会话时一样原样保留,旧会话继续保留在历史会话下拉中;创建失败同样不修改草稿。
- 新会话创建请求 pending 时禁用历史会话下拉和发送动作,但输入框与附件仍可编辑;会话列表或历史消息加载期间同样禁用发送。表单提交处理器必须复用相同门禁,不能先清空草稿再由 hook 静默跳过发送。
- 快速切换会话或会话轮询刷新产生并发详情请求时,每个请求必须获得唯一且单调递增的请求序号;前端只允许最后发起且有权生效的请求更新当前会话、消息、错误和加载态。被正在进行的会话切换压制的旧会话 refresh 不得提前结束新切换的加载态,旧响应也不得覆盖用户最新选择。
- 普通 JSON 消息请求的回包必须绑定发送时的会话:用户在等待期间切换到其他会话后,只更新原会话的列表摘要,不得把原会话的 `deltaMessages` 、错误或画布刷新副作用应用到当前面板。
- 收起对话框只是隐藏面板,不卸载当前会话 hook;普通 JSON 消息请求的等待态和外部生成任务状态必须在收起 / 重新打开之间保持一致。
@@ -74,7 +81,9 @@
- 附件选择弹窗使用 `PlatformToolModalShell` 承接 portal 主题变量和不透明 panel 背景;不能直接把未注入 `platform-theme` 的 `UnifiedModal` portal 到 `document.body`,否则 `--platform-modal-fill` 失效后面板会变透明。
- 应用后附件以胶囊 chip 挂在输入框上方;发出的消息内附件渲染为纯文本胶囊 chip(名称 + 小图标),**默认无缩略图,鼠标悬浮才浮出缩略图预览**。
- 附件领域形状:统一为画布资源 / 素材库对象引用(`resourceId` / `assetId` + 可选 `objectKey`),不存在只属于对话的第三种图;单条消息上限 9 张(前后端共同校验)。前端可携带展示用 `imageSrc` / `thumbnailSrc`,后端必须按当前工程和当前账号重新归一、校验归属与 `objectKey`。
- 输入区附件临时状态统一收口到 `useConversationAttachments`,选择弹窗由独立的 `AttachmentPicker` 负责纯展示;选择、引用、粘贴上传完成、移除、发送清空和失败恢复都必须经同一最新状态更新入口。引用历史消息附件时先保留消息中的展示快照,最终发送前再按 `source + referenceId` 从当前画布和素材库选项刷新,避免提前刷新后又被失败恢复的旧快照覆盖。发送失败时,已发送附件必须与等待期间新增的附件去重合并,不得因输入区已非空而丢弃;同一 `source + referenceId` 冲突时保留等待期间的当前草稿快照,失败请求快照只补充缺失 identity。合并后超过 9 张时优先保留等待期间的最新附件,不恢复失败请求的附件,并立即显示上限错误。异步粘贴完成时基于当时的最新附件去重并重新校验 9 张上限,不能用上传开始时捕获的旧列表覆盖期间新增的引用。
- 附件 `label` 是人类可读的展示元数据,统一限制为最多 24 个 Unicode 码点。归一化时先去掉首尾空白,删除控制字符以及除 `-`、`_`、`.` 之外的 ASCII 标点,把连续空白折叠为一个半角空格,再按 24 码点截断;只含被过滤字符的 label 视为缺失。中文等非 ASCII 标点不属于本轮过滤范围。
- 前端在创建画布、素材库和粘贴上传附件引用,以及把历史消息附件重新引用到输入区时,先执行上述归一化;原 label 无有效内容时依次归一化并使用调用方提供的 fallback(默认 `referenceId`)和固定文案「图片」。后端不能信任前端结果:校验当前工程 / 当前账号归属和 `objectKey` 后,必须用同一套字符规则、同一 24 码点上限再次归一化并重建权威附件;素材库附件的提交 label 缺失或过滤为空时,才回退到同样归一化后的素材库 label。前后端常量和规则必须保持同步。
- 输入区附件临时状态统一收口到 `useConversationAttachments`,选择弹窗由独立的 `AttachmentPicker` 负责纯展示;选择、引用、粘贴上传完成、移除、发送清空和失败恢复都必须经同一最新状态更新入口。引用历史消息附件时,原消息继续保留存量展示快照;进入输入区的新引用先归一化 label 并保留其他展示快照字段,最终发送前再按 `source + referenceId` 从当前画布和素材库选项刷新,避免提前刷新后又被失败恢复的旧快照覆盖。发送失败时,已发送附件必须与等待期间新增的附件去重合并,不得因输入区已非空而丢弃;同一 `source + referenceId` 冲突时保留等待期间的当前草稿快照,失败请求快照只补充缺失 identity。合并后超过 9 张时优先保留等待期间的最新附件,不恢复失败请求的附件,并立即显示上限错误。异步粘贴完成时基于当时的最新附件去重并重新校验 9 张上限,不能用上传开始时捕获的旧列表覆盖期间新增的引用。
## 工具调用确认展示契约
@@ -83,12 +92,13 @@
- 确认接口必须先把工具参数转换为既有编辑器 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.args` 的正式持久化契约是**校验后的规范参数 JSON**,不是 LLM 返回的原始 JSON。api-server 收到工具调用后,必须先按已注册的 ToolArgs 反序列化、补齐字段默认值、删除未进入 ToolArgs 的未知 / 退役字段、执行工具参数校验,再重新序列化并写入 `args`;校验失败的调用不得持久化为待确认消息。所有有明确默认值的工具标量参数在强类型 ToolArgs 中必须使用非 `Option` 字段:调用方省略字段或把顶层字段显式传为 `null` 时,统一在 ToolArgs 反序列化前视为未提供,由 Serde 补齐默认值,并把具体默认值写入规范 `args`;没有默认值的必填字段显式传为 `null` 时同样按缺失处理.(for compatibility) 后续计价、确认展示和 job payload 不得再次使用 `unwrap_or` 补同一默认值。LLM 原始参数只作为本次规范化的瞬时输入,不作为执行或审计真相;确认、取消、任务回填与后续上下文统一读取同一条消息中的规范 `args`。图片参数继续只保存由真实 data key 计算出的 opaque SHA-256 `imageId`;不得为了前端预览把 `args` 中的图片 ID 改写成 `objectKey`、URL 或展示对象,也不得由前端重组或回传一份新的执行参数。
- api-server 内画布 Agent 工具统一实现 object-safe `EditorAgentTool: ToolDyn`。`validate_args`、计价、确认展示、worker job 构建、`format_execute_message` 和结果媒体投影都使用统一 JSON 边界;每个具体工具实现负责把 JSON 反序列化为自己的强类型 Args / 结果,并把校验与完成消息格式化转发到 `platform-editor-agent` 中既有的 typed `validate_args` / `format_execute_message`,不得在调用方复制工具规则。`editor_agent_tool(toolName, context)` 是唯一按工具名分派的位置,规划、确认和任务回填只调用返回的 dyn tool;新增工具必须补齐同一个 trait 实现和该工厂分支。LLM builder 的 `.tool(...)` 注册列表仍是独立显式清单,不属于本次动态分派。framework runner 必须在 `ToolCallOutput` 中保留工具返回的结构化 output;runner 写入 LLM memory 与 api-server 使用规范参数持久化 system text 时统一调用公开的 `format_tool_call_message`,不得丢弃 `TOOL_CALL_PENDING_MESSAGE` 后自行拼另一套“等待确认”输出。
- `EditorAgentToolCall.displayArgs` 是必填、只读的用户确认展示投影,与 `args` 分离:
- `stringArgs` 保存提示词、比例、清晰度、模型、时长等可展示参数的稳定名称、用户可见标题和值;
- `imageArgs` 按“目标图片 / 参考图片”等参数分组,每个 `refs` 项包含与原始参数对应的 `imageId`,以及后端从已校验会话上下文解析出的 `objectKey`、`imageSrc`、可选 `thumbnailSrc` / `label` / `width` / `height`。
- `stringArgs` 保存提示词、比例、清晰度、模型、时长等可展示参数的稳定名称、用户可见标题和值;前端渲染模型字段时复用图片编辑器公共展示名映射,`gemini-3.1-flash-image-preview` 显示为 `nanobanana2`、`audio1.0` 显示为 `Vidu`、`chirp-v5` 显示为 `Suno`,视频模型显示现有产品标签,不得改写后端参数真相;
- `imageArgs` 按“目标图片 / 参考图片”等参数分组,每个 `refs` 项包含与规范参数对应的 `imageId`,以及后端从已校验会话上下文解析出的 `objectKey`、`imageSrc`、可选 `thumbnailSrc` / `label` / `width` / `height`。
- `extras.priceMudPoints` 保存创建待确认消息时按后端运行时模型定价快照计算的预计泥点消耗;前端统一展示为“预计消耗 N泥点”,不自行计算价格。
- `displayArgs` 只能由 api-server 按已注册 tool 白名单,基于已经通过 ToolArgs 校验的 `args` 和当前 OSS 会话文档中的附件 / 历史生成结果构建;不能信任 LLM 自报的展示地址、标题或素材元数据。展示投影不参与确认执行,确认接口仍只读取同一条持久化 tool call 的 `args`,避免“看到的素材”和“实际执行的素材”分叉。
- `displayArgs` 只能由 api-server 按已注册 tool 白名单,基于已经通过 ToolArgs 校验的 `args` 和当前请求开始时从 OSS 会话文档一次性构建的 `EditorToolContext` 生成;该 context 必须按 opaque `ImageId` 同时保存执行所需的 `dataKey` 与展示所需的图片地址、Object Key、缩略图、label、宽高,参数校验、确认展示和 job payload 统一查同一份 context。不能信任 LLM 自报的展示地址、标题或素材元数据。展示投影不参与确认执行,确认接口仍只读取同一条持久化 tool call 的 `args`,避免“看到的素材”和“实际执行的素材”分叉。
- `extras.priceMudPoints` 同样只属于展示投影,不作为扣费输入;确认后仍由既有生成 BFF 按后端运行时定价执行预扣费,因此该字段表达用户确认时看到的价格快照,而不是前端可提交或覆盖的计费真相。
- `EditorAgentToolCall.summary` 只是 `args` 的重复字符串且没有稳定语义,当前契约删除该字段,不再作为展示或执行输入。
- 前端待确认卡只消费必填 `displayArgs`,不解析各 tool 私有的 snake_case / camelCase schema,也不把 `sha256:*` ID 当标题或图片地址。图片统一通过 `ResolvedAssetImage` 使用 `objectKey` 换签后显示,签名 URL 不进入消息文档。模块尚未上线,不保留缺少 `displayArgs` 时读取 raw `args` 的旧消息降级路径。
@@ -96,13 +106,16 @@
## LLM 与计费
- 编排复用 `creative_agent_gpt5_client` 的 LLM 接入配置(同 provider/env,独立用途标识),画布 Agent 规划请求固定使用 VectorEngine `gpt-5.4-mini` Chat Completions;function-calling 注册八类工具。
- 每个用户回合必须由 LLM 返回结构化计划;LLM 未配置、连接已经断开、请求明确失败、达到最终安全上限或返回格式不可解析时,后端写入正文为 `ERROR <错误内容>` 的 system 消息,不使用本地关键词或“收到:...”回显兜底。面向用户的规划错误使用中文语义,不暴露 `completion error` 等 framework 内部前缀或原始配置/定价诊断;原始错误只记录在后端日志。该错误消息与其它 system 消息一样进入后续 LLM memory,使 Agent 能看到上一轮失败上下文。普通 JSON POST 尚未结束只表示 provider request future 仍在等待,不能伪装成已持久化失败。
- 规划 prompt 必须自动带入上一条已完成生成结果的 `latestGeneratedImage` 引用,内容只包含上一轮 generation 的 `toolName` / `resourceId` / `objectKey` / `assetObjectId` 等轻量元数据,不把私有签名 URL 或大图内容塞进 prompt。
- 每个用户回合必须由 LLM 返回结构化计划;单次 completion 不是有效 JSON 时,runner 先把无效原文作为 assistant message 追加到当前 staged turn,再追加 system 纠正消息,明确要求下一轮只按既定 JSON Response Format 重试;下一次 completion 必须同时看到该无效原文和纠正指令。无效原文只是重试上下文,不进入对外 `PromptOutput` 或用户可见的会话增量;后续规划成功时随 staged turn 一并提交,无工具活动且最终失败时按下文事务规则整体回滚。LLM 未配置、连接已经断开、请求明确失败、达到最终安全上限或多轮重试后仍不可解析时,后端写入正文为 `ERROR <错误内容>` 的 system 消息,不使用本地关键词或“收到:...”回显兜底。面向用户的规划错误使用中文语义,不暴露 `completion error` 等 framework 内部前缀或原始配置/定价诊断;原始错误只记录在后端日志。该错误消息与其它 system 消息一样进入后续 LLM memory,使 Agent 能看到上一轮失败上下文。普通 JSON POST 尚未结束只表示 provider request future 仍在等待,不能伪装成已持久化失败。
- 工具参数中的图片 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`。发送后 120 秒是前端软提示阈值,不是 provider 失败 deadline:若普通 JSON POST 仍 pending,消息流临时显示“仍在处理中,请耐心等待”并继续等待,提示不写入 OSS 消息历史;连接或请求明确失败则立即按正式错误收口。provider 单 attempt 保留 8 分钟 hard timeout;请求发起阶段的 timeout、连接失败、`408`、`429` 与 `5xx` 读取 `GENARRATIVE_LLM_MAX_RETRIES`,但画布 Agent 最多重试 1 次,专用重试退避最多 60 秒。消息规划生命周期从 handler 入口开始计入 18 分钟总 deadline,进入 `agent.prompt(...)` 时使用扣除会话锁和上下文准备后的剩余预算;该 deadline 覆盖非法 JSON/工具校验失败触发的后续规划轮,并为错误持久化和 HTTP 返回保留约 2 分钟,不再让前端 20 分钟 transport timeout 先触发。已收到成功响应头后的响应体读取或解析失败直接按明确失败收口,错误计数/日志使用该响应所属的真实 attempt。规划重试发生在任何生成工具执行之前,不会重复提交生成任务或扣费;生成图片/编辑图片仍走对应生成工具和模型计费。
- 规划 prompt 必须显式区分“规范展板”和“实际素材产出”:规范图、视觉规范图、风格规范图、素材规范展板、角色规范图等规范展板请求走 `generate-image`,并补齐统一视角、线条粗细、色卡、材质、阴影、圆角、状态层级、尺寸标注等要求;实际角色立绘才走 `generate-character`,多个图标素材 / 图集才走 `generate-icon-spritesheet`。
- 用户的当前消息确实在确认或取消一条已存在且仍为 pending 的工具调用时,画布 Agent 只引导使用该卡片的确认 / 取消按钮,本条确认 / 取消意图不产生新 tool call。这条边界必须使用“匹配 pending 调用时如何处理”的正向、条件化描述,不得改写成“不得重新发起相同工具调用”一类全局否定话术:实测中模型会把这类否定句过度泛化为拒绝后续新请求。已 cancelled 的卡片不再处理;用户明确要求修改、重做或发起新任务时必须允许新 tool call,pending 卡片也不阻塞无关的新请求。
- 画布 Agent 规划请求使用 Chat Completions 和 1024 `max_tokens`。发送后 120 秒是前端软提示阈值,不是 provider 失败 deadline:若普通 JSON POST 仍 pending,消息流临时显示“仍在处理中,请耐心等待”并继续等待,提示不写入 OSS 消息历史;连接或请求明确失败则立即按正式错误收口。provider 单 attempt 保留 8 分钟 hard timeout;请求发起阶段的 timeout、连接失败、`408`、`429` 与 `5xx` 读取 `GENARRATIVE_LLM_MAX_RETRIES`,但画布 Agent 最多重试 1 次,专用重试退避最多 60 秒。消息规划生命周期从 handler 入口开始计入 18 分钟总 deadline,进入 `agent.prompt(...)` 时使用扣除会话锁和上下文准备后的剩余预算;该 deadline 必须作为 runner 内部 deadline future 参与 completion await,并在每个 tool 开始前、返回后检查,不能用外层 `tokio::timeout` 丢弃整个 prompt future,也不能中途 drop 已开始的工具。工具一旦开始就等待其返回,再按 deadline 携带结果收口;当前八类画布工具只做同步参数校验并返回待确认,因此不会延长正式生成链。deadline 命中时仍按 `PromptRunError` 返回已经完成的工具结果、提交对应 staged memory 并追加终态错误。该 deadline 覆盖非法 JSON/工具校验失败触发的后续规划轮,并为错误持久化和 HTTP 返回保留约 2 分钟,不再让前端 20 分钟 transport timeout 先触发。已收到成功响应头后的响应体读取或解析失败直接按明确失败收口,错误计数/日志使用该响应所属的真实 attempt。规划重试发生在任何生成工具执行之前,不会重复提交生成任务或扣费;生成图片/编辑图片仍走对应生成工具和模型计费。
- function-calling runner 必须把“等待用户确认”作为显式工具语义:当本批所有工具都校验成功并进入待确认状态时,立即以成功结果结束当前规划回合并持久化助手文本与待确认卡,不得继续依赖 LLM 自行停止;未知工具、参数错误、普通连续工具和不可解析响应仍受 `max_turns` 保护。
- runner 失败必须返回显式的 `PromptRunError { error, partial_outputs }`,不得只返回终态错误而丢弃本轮已产生的文本或工具事实。prompt 执行使用 `AgentMemory::begin_staged` 创建行为等价且写入隔离的 `StagedAgentMemory` 事务,限长、摘要、脱敏等 append 规则必须在本轮 completion 前生效;成功或已发生工具活动时必须显式调用 `commit()`,直接 drop staged transaction 表示回滚,不得统一复制成 `VecMemory` 或仅替换 box 冒充持久化提交。本轮无工具活动失败时回滚 staged 用户消息、助手文本和不可解析响应;已有工具活动时在末尾追加 terminal error closure 后提交。外部 drop / abort 若尚无工具活动则回滚并保持原 committed memory;若工具已完成则提交结果与取消闭环,若工具仍在执行则提交“已启动、结果未知”事实与取消闭环,后续必须先 reconcile 再决定是否重试。
- `ToolFailure` 必须以结构化工具失败输出暴露给 harness 调用方:调用方能读取 `kind`、`retryable`、`fatal` 和工具返回的原始 `output`;不得把它们压成单一错误字符串。这些字段只提供流程决策与诊断事实,是否重试、如何展示或持久化仍由业务调用方决定。
- api-server 收到带 `partial_outputs` 的终态失败时,必须先按原顺序把其中已成功工具转成 `status=not_completed` 待确认消息并写入同一会话增量,再追加 `ERROR <错误内容>` 终态 system 消息;不得因后续轮次、其它工具或 `max_turns` 失败而吞掉已经执行并返回的工具结果。
- 通用 JSON function-calling 协议、工具 schema 注入、memory / hook、`max_turns` 和“全部工具待确认即结束回合”统一由现役 `platform-agent-harness` 承载;无工具时也必须输出同一 JSON 响应格式。画布角色 prompt、规范展板 / 已有图路由、模型与超时 profile、八类工具、计费、OSS 会话和 external job 编排继续留在 `platform-editor-agent` / `api-server`,不得回流已退役的旧 `platform-agent`。
- **对话回合免费**(聊天、分析回复不扣泥点),仅 Agent 实际触发生成工具时按对应模型定价扣泥点。
- 工具调用前后端校验泥点余额;不足时该次生成失败并在对话中以明确错误气泡告知,对话本身可继续。
@@ -143,7 +156,7 @@
- `GET/POST /api/editor/projects/{projectId}/agent-conversations`(列表/新建);
- `GET/DELETE /api/editor/agent-conversations/{conversationId}`(详情/软删);
- `POST /api/editor/agent-conversations/{conversationId}/messages`(JSON);
- Agent 编排(function-calling 循环、工具内部调既有生成执行链路)放 api-server 编排层,独立文件,不复用 `creative_agent.rs` 内存会话。
- 通用 function-calling harness 放 `platform-agent-harness`;画布 Agent profile 与工具放 `platform-editor-agent`;会话、计费、OSS 和工具内部生成执行链路由 api-server 编排层承接,不复用 `creative_agent.rs` 内存会话。
- `shared-contracts` + `packages/shared`:`editorAgent` 会话、消息、工具确认展示与轻量媒体结果 DTO;消息响应返回 `conversation`、`deltaMessages` 和可选 `errorMessage`。
## 实施顺序
@@ -2,9 +2,11 @@
日期:`2026-06-15`
更新时间:`2026-07-29`
## 背景
图片画布编辑器已有普通图片生成、生成规范、生成角色形象和角色动画入口。本次新增 `生成图标素材`,用于一次输入多条图标素材描述,生成一张纯色背景 spritesheet;后端去背景正常成功后,再尝试自动拆分为可独立编辑的素材。
图片画布编辑器已有普通图片生成、生成规范、生成角色形象和角色动画入口。本次新增 `生成图标素材`,用于通过一段完整需求生成一张纯色背景 spritesheet;后端去背景正常成功后,再尝试自动拆分为可独立编辑的素材。
## 入口与画布表现
@@ -14,6 +16,7 @@
- 图标素材面板锚定在占位图下方,和现有生成输入框同一层级展示。
- 透明背景处理正常成功后删除占位态:透明 spritesheet 作为主图(`assetKind: "icon-spritesheet"`,`generatedLayerId` 锚点)放入画布,provider 带背景原图作为第二个同类型图层放在透明主图右侧,按 alpha 连通域成功拆出的 `assetKind: "icon"` 素材从原图右侧继续铺放;透明背景处理最终失败时,后端完成快照只用 provider 原图替换占位态。
- 选中 `assetKind: "icon-spritesheet"` 图层时,图片浮动工具栏显示 `拆分图集`;手动拆分只追加独立素材,不复制原图集。
- 用户把现有图层手动标记为“图集”时,必须先持久化一条 `assetKind: "icon-spritesheet"` 的项目资源并把返回的 `resourceId` 写回图层;项目资源只能在媒体来源和 `assetKind` 都相同时复用,不得因同源图片而返回旧类型资源。持久化完成前必须禁用“拆分图集”,持久化失败时回滚到上一个已确认的素材标签和资源引用,并失效该轮未确认的标签撤销记录。
- 图标规范图写入 `assetKind: "icon-spec"`,用于刷新后保留标签和限制点选来源。
## 面板结构
@@ -26,7 +29,7 @@
2. 第二模块为素材描述文本框。
- UI 复用角色形象生成面板同款单个文本输入框,让用户直接叙述多个素材。
- 默认按换行填入:`返回按钮`、`设置按钮`、`下一关按钮`、`提示按钮`、`原图按钮`、`冻结按钮`。
- 生成时按换行、逗号、顿号、分号、斜杠或竖线切分,过滤空文本后最多保留 `100` 个素材描述,并按文本顺序作为 prompt 的素材清单。
- 生成时只去除整段文本首尾空白,不按换行、逗号、顿号、分号、斜杠、竖线或语义枚举解析素材数量;文本框内容作为一段完整用户需求进入 prompt。
## 面板外观
@@ -39,10 +42,11 @@
- 前端提交到 `POST /api/editor/icon-spritesheets/generations`。
- 请求字段:
- `referenceImageSrc`:图标规范的稳定引用(当前账号的 `objectKey`、项目资源 ID 或素材 ID);本地临时图必须先上传 OSS,禁止 Data URL / Blob URL。
- `iconDescriptions`:过滤空文本后的图标描述数组,`1..100`。
- `iconDescriptions`:兼容现有接口的图标需求数组,`1..100`;当前画布前端固定把完整文本作为唯一数组元素提交。数组长度只表达请求文本,不作为自动拆分数量。
- `model`:支持 `gemini-3.1-flash-image-preview`(UI 显示 `nanobanana2`)和 `gpt-image-2`,默认 `nanobanana2`。
- `aspectRatio`:按 `x:y` 展示,选项跟随模型。
- `imageSize`:按 `0.5K / 1K / 2K` 展示,选项跟随模型。
- `style`:可选生成后处理风格;未勾选像素艺术时传 `"none"`,勾选时传 `"pixelArt"`。
- `priceMudPoints`:按当前模型和尺寸从编辑器生成计费配置计算;`nanobanana2 1K` 为 `12`,`gpt-image-2 1K` 为 `3`、`gpt-image-2 2K` 为 `5`。前端只提交配置函数计算值,后端用 `editor_generation_config` 校验,不允许素材生成面板自行写死价格。
- 模型与尺寸选项:
- `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"`。
@@ -52,18 +56,29 @@
- Prompt 固定为:
```text
参考图1的图标规范,背景必须是自动决策出的单一纯色抠图背景,且平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便扣除背景;素材自身不要出现与背景色相同或相近的描边、底板、投影或反光;禁止出现文字,保证每个图标素材的所有内容区域是完全连通的。按照以下的素材的顺序从上到下从左到右依次生成并整理成一张spritesheet:
参考图1的图标规范,背景必须是自动决策出的单一纯色抠图背景,且平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便扣除背景;素材自身不要出现与背景色相同或相近的描边、底板、投影或反光;禁止出现文字。根据以下用户需求生成图标素材并整理成一张 spritesheet;不同图标素材之间必须彼此分离并保留清晰间距,避免描边、底板、投影或装饰元素连接相邻图标:
<素材描述按中文顿号拼接>
<完整用户需求>
```
## 像素风格后处理
- 图标素材面板增加紧凑的 `像素艺术` 勾选项。选择保存于现有生成器快照,并可随现有请求和队列 payload 传递;不写入用户可见 `generationInputs`、素材元数据或新建的持久化记录。
- `style` 省略、为 `null`、空字符串或 `"none"` 时按内部 `None` 处理且不告警;`"pixelArt"` 启用像素规整。未知字符串按 `None` 继续生成,并通过既有通用 `warning` 返回 `unsupported-image-style`;非字符串 JSON 仍返回 `400`。
- 图标链路以已持久化的带纯色背景 provider 原图实际尺寸为基准;BgFilter 正常成功后,把 Alpha 蒙版回贴到该同尺寸平底原图,再执行像素规整。网格分析源使用平底 provider 原图,RGBA 采样源使用 Alpha 已回贴的透明图;规整结果不再经过独立的最终尺寸处理,直接上传透明 spritesheet,成功后才进入原有连通域自动拆分。
- 首版固定参数为分析色数 `16`、Alpha 覆盖阈值 `0.375`、像素格尺寸自动检测、固定色板关闭、K-means 最大采样 `262144`。单格颜色按 `Σ(A × RGB) / ΣA` 进行 Alpha 加权;覆盖率 `Σ(A / 255) / N >= 0.375` 且 `ΣA > 0` 时输出硬 Alpha `255`,否则输出严格 `[0,0,0,0]`。分析色数不限制最终输出色数。
- 像素规整 CPU 工作使用进程级最大并发 `2`;取得并发许可的排队时间与实际处理时间共享最多 `30` 秒预算,同时不得晚于当前请求 deadline,最终以两者中更早者为准。输入图片任一边不得超过 `10000` 像素,总像素不得超过 `8294400`;超限、排队超时或处理超时均保留 Alpha 已回贴的透明图并走非致命降级,随后仍可进入原有自动拆分。
- 逻辑低分辨率图只存在内存;snapper 在规整内部使用 nearest 恢复到当前 RGBA 输入尺寸,即前述平底 provider 原图的实际尺寸。图标链路不执行角色链路的前置 Lanczos 交付尺寸归一,nearest 也不是规整后的独立交付尺寸恢复。实现应复用 Alpha 回贴阶段读取的平底 provider 原图;必要时最多增加一次读取已有 provider 对象的 OSS GET,不得增加 OSS PUT。
- 开启或关闭像素风格都保持现有 provider 原图、透明图集和实际成功切片的持久化与画布数量不变。禁止上传逻辑低分辨率图、像素化前后双份图集、预览或诊断图,也不新增 asset kind、项目资源、画布 item、任务类型或数据库字段。
- 图标图集的 BgFilter `flat` 调用固定使用 `cross_check=on`。BgFilter 最终失败、Alpha 比例漂移超过 `5%`、provider 原图修复性回读失败、Alpha 回贴失败或透明图完整解码失败时,都统一只保留 provider 原图且不拆分,像素规整不运行;像素规整自身失败但透明图仍通过完整解码和尺寸守卫时,才保留该透明图并继续上传和拆分,通过通用 `warning` 非致命提示,不退款。`sliceWarning` 继续只表达可信透明图成功后的自动拆分失败。
## 去背与保存
- 父流程收到 spritesheet 后先把带解析后纯色背景的源图写入私有 OSS,并在上传完成后释放原图缓冲;随后只持 object key,并仅向同机唯一 loopback `bgfilter-worker` 发起一次内部 HTTP RPC,请求中的源图只以 object key 传递,并附带 BgFilter 参数、排队预算 `maxQueueWaitMs`、调用预算 `callBudgetMs` 和有界审计关联,父流程不签发 BgFilter URL、不直连 provider,也不重试已被 worker 接收的内部 RPC(连接从未建立时按调度方案 §5.1 有界重连)。子 worker 在 `Q` admission 和 `Semaphore(N)` 约束下执行这次逻辑调用;排队只消耗 `maxQueueWaitMs`,取得 provider permit 后才启动 `callBudgetMs`。每次 provider attempt 前重新签发 600 秒 GET URL,multipart 固定传 `image_url`、`screen_color=<screenColor>`、`seg_model=<segModel>`、`background_mode=flat` 和 `cross_check=off`,不包含 `file`,并在调用预算内最多执行两次顺序 attempt。前端用户路径固定提交 `screenColor=auto` 与默认 `segModel=birefnet`,后端仍识别内部保留的 `anime-seg`,但这些内部参数不对用户可见。成功时,子 worker 通过内部 HTTP 二进制 body 把经过校验的图片字节直接返回父流程,不持久化中间结果;BgFilter 最终失败且父业务预算仍有效时,由父流程进入“阿里云通用抠图(按签名 URL 单独下载)→ 本地键色(再按 object key 独立下载一次原图并在产出后释放)”降级链。
- 透明背景处理正常成功时,父流程把带背景原图和去背后的透明 spritesheet 写入 OSS、项目资源和账号素材库,再按 alpha 连通域和素材描述顺序执行附加拆分;若 BgFilter 返回较小图集,只把 alpha 蒙版重采样到 provider 原图尺寸并应用回原始高分辨率 RGB,不放大低分辨率后处理成品。画布完成快照同时写入透明主图与右侧 provider 原图(二者均已登记为 project resource / 账号素材),`generatedLayerId` 仍锚定透明主图;成功拆出的切片从 provider 原图右侧继续排列。调用方未指定素材文件夹时统一落默认“项目”文件夹。每个成功切片单独写入 OSS、项目资源和账号素材库,`sourceResourceId` 指向透明图集资源。BgFilter 与父侧 fallback 最终均失败、但 provider 原图已经持久化时,任务以 `completed + warning` 收口,只把 provider 原图作为唯一主图放入画布,`generatedLayerId` 指向原图,不创建透明图集,也不继续拆分,`iconImageSrcs=[]`。该收口只捕获透明背景处理本身的最终失败;phase 上报、provider 原图持久化、透明处理图持久化和 `canvasCompletion` 写回错误仍正常传播,不能被原图降级吞掉。最终透明结果及切片的 OSS / 资源 / 画布持久化仍全部由父流程负责。
- 自动拆分只在透明图集成功后执行,属于 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 后先把带解析后纯色背景的源图写入私有 OSS,并在上传完成后释放原图缓冲;随后只持 object key,并仅向同机唯一 loopback `bgfilter-worker` 发起一次内部 HTTP RPC,请求中的源图只以 object key 传递,并附带 BgFilter 参数、排队预算 `maxQueueWaitMs`、调用预算 `callBudgetMs` 和有界审计关联,父流程不签发 BgFilter URL、不直连 provider,也不重试已被 worker 接收的内部 RPC(连接从未建立时按调度方案 §5.1 有界重连)。子 worker 在 `Q` admission 和 `Semaphore(N)` 约束下执行这次逻辑调用;排队只消耗 `maxQueueWaitMs`,取得 provider permit 后才启动 `callBudgetMs`。每次 provider attempt 前重新签发 600 秒 GET URL,multipart 固定传 `image_url`、`screen_color=<screenColor>`、`seg_model=<segModel>`、`background_mode=flat` 和 `cross_check=on`,不包含 `file`,并在调用预算内最多执行两次顺序 attempt。前端用户路径固定提交 `screenColor=auto` 与默认 `segModel=birefnet`,后端仍识别内部保留的 `anime-seg`,但这些内部参数不对用户可见。成功时,子 worker 通过内部 HTTP 二进制 body 把经过校验的图片字节直接返回父流程,不持久化中间结果;BgFilter 最终失败且父业务预算仍有效时,由父流程进入“阿里云通用抠图(按签名 URL 单独下载)→ 本地键色(再按 object key 独立下载一次原图并在产出后释放)”降级链。
- 透明背景处理正常成功时,父流程把带背景原图和经完整解码 / 尺寸守卫验证的透明 spritesheet 写入 OSS、项目资源和账号素材库,再识别 alpha 连通域并执行附加拆分。BgFilter 最终失败或后续 Alpha / 尺寸恢复、原图回读、透明图完整解码失败、但 provider 原图已经持久化时,任务以 `completed + warning` 收口,只把 provider 原图作为唯一主图放入画布,不创建透明图集,也不继续拆分,`iconImageSrcs=[]`、`sliceWarning=null`。该收口不捕获 phase 上报、provider 原图持久化或 `canvasCompletion` 写回错误;provider 原图本身解码失败时在首次持久化前失败,不允许用 `512×512` 伪造元数据。
- 自动拆分只在透明图集成功后执行,属于 best-effort 附加动作,不参与图集生成的成功判定。连通域识别或切片持久化失败时,接口仍返回并回填整张透明图集,`iconImageSrcs=[]`,并通过 `sliceWarning.code/reason` 暴露非阻断原因;`sliceWarning` 与透明背景最终失败使用的通用 `warning` 互斥,因为透明背景失败时不会进入拆分,但可与风格归一化或像素规整产生的通用 `warning` 并存。前者只表示透明图集成功但自动拆分失败,`sliceWarning.reason` 原始契约保持不变。前端在 inline、worker 队列完成和刷新恢复三条路径统一显示对应 warning toast,用户可在图集工具栏手动重试。
- 响应通过 `iconImageSrcs` 返回成功切片素材。图标自动拆分、手动 `拆分图集` 和 UI 提取复用同一个 bounded CPU helper 和 platform 实现:全部原始连通域(包括随后过滤的噪点)最多 `4096` 个,辅助部件通过 `64px` 空间网格只检查最大 `48px` 邻域候选;有效输出按视觉阅读顺序命名为 `素材 N`。
- 三条拆分路径共同限制单边最多 `4096` 像素、总像素最多 `2048×2048`、最多 `64` 个输出;输出限制在排序、裁剪和 PNG 编码前检查。整段图片 CPU 工作在 2 路 semaphore、30 秒本地上限与请求 deadline 共同保护的 `spawn_blocking` 中执行,permit 由 blocking 闭包持有。自动拆分超限以稳定 `sliceWarning` 非阻断降级且不产生切片 PUT、资源或画布切片;手动拆分超限在首次持久化前返回 `422`。
## 前端铺放规则
@@ -75,10 +90,12 @@
- 点击 `生成图标素材` 后出现一叠空白图标占位和图标素材面板。
- `图标规范 -> 从画布中选择` 只能选择图标规范图,点击普通图片或角色规范图不会绑定。
- 默认 6 个素材描述会进入 prompt;用户在单个文本框中继续输入时最多解析 100 个素材描述。
- 默认提示文本会完整进入 prompt;用户输入不再被解析为素材数量。例如“各种敌人头像:骷髅 哥布林 强盗 龙 蝙蝠等”只是一段完整需求,不代表必须生成或拆出 `6` 个素材。
- 默认打开图标素材面板时选中 `nanobanana2 / 1:1 / 1K`;模型切换后,角色和图标素材面板之间沿用上次选择的模型。
- 图标素材生成请求必须带 `model`、`aspectRatio` 和 `imageSize`;`nanobanana2` 请求体必须包含 `generationConfig.imageConfig.aspectRatio/imageSize`,`gpt-image-2` 请求必须包含文档映射后的 `size`。
- 图标素材面板可选择 `style: "none" | "pixelArt"`;`none` 完整保持原处理路径,`pixelArt` 在 Alpha 回贴后、自动拆分前执行内存像素规整,最终 OSS PUT、项目资源、图集画布项和切片画布项数量不得因此增加。
- 图标素材生成可以上传普通参考图;提交时图标规范图仍走 `referenceImageSrc`,普通参考图走 `referenceImageSrcs`,二者都必须是稳定引用(`objectKey` / 项目资源 ID / 素材 ID),禁止 Data URL / Blob URL,并写入 `generationInputs.references`。
- 透明背景处理和自动拆分都成功后,画布同时出现透明 spritesheet 主图、其右侧的 provider 原图,以及从原图右侧铺开的按描述命名的独立图标图层;透明图集成功但拆分失败时仍出现透明主图与右侧原图,透明背景处理最终失败时只出现 provider 原图。
- 透明背景处理和自动拆分都成功后,画布同时出现透明 spritesheet 主图、其右侧的 provider 原图,以及从原图右侧铺开的全部有效连通域图标图层,图标依次命名为 `素材 N`;透明图集成功但拆分失败时仍出现透明主图与右侧原图,透明背景处理最终失败时只出现 provider 原图。
- 选中透明图集图层时显示 `拆分图集`;点击后源图集显示扫描蒙层与 `拆图中` 状态,工具栏按钮同步切换为旋转图标和 `拆图中` 并禁用重复提交。完成后恢复工具栏,不新增第二张图集,只在 provider 原图右侧追加自动识别的独立素材,并同步写入素材库。
- 把同源派生图层从其它标签改为“图集”时,在项目资源返回新 `resourceId` 前“拆分图集”保持禁用;持久化成功后拆分请求必须指向 `assetKind: "icon-spritesheet"` 的新资源,失败时标签回滚且不发起拆分请求。
- 生成图标素材提交体包含按模型和尺寸计算的 `priceMudPoints`;`nanobanana2 1K` 应为 `12`,`gpt-image-2 1K` 应为 `3`,`gpt-image-2 2K` 应为 `5`。若前端传入与后端计费配置不一致的值,后端返回 `priceMudPoints` 校验错误,不继续调用上游生成。
@@ -2,7 +2,7 @@
日期:`2026-06-15`
更新时间:`2026-07-21`
更新时间:`2026-07-28`
## 背景
@@ -60,6 +60,18 @@
- `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` 走带多参考图的图片编辑链路。没有参考图时按所选模型走纯文本生成链路。
## 风格与像素规整
- 角色面板增加紧凑的 `像素艺术` 勾选项,请求使用可选字符串字段 `style`:未勾选传 `"none"`,勾选传 `"pixelArt"`。该选择可以随现有生成器快照和队列 payload 保存,但不写入用户可见 `generationInputs`、素材元数据或新建的持久化记录。
- `style` 省略、为 `null`、空字符串或 `"none"` 时按内部 `None` 处理且不告警;`"pixelArt"` 在 `kind="character"` 时启用像素规整。未知字符串按 `None` 继续生成,并通过既有通用 `warning` 返回 `unsupported-image-style`;非字符串 JSON 仍返回 `400`。同一图片生成请求 DTO 被其它 `kind` 复用时,只有普通图片和 `character` 支持 `"pixelArt"`,其它 `kind` 收到该值也按不支持风格降级。
- 角色 provider 回图先按统一业务像素矩阵执行交付尺寸归一:允许无放大恢复时使用 Lanczos 重采样并居中裁切,无法安全恢复时保留 provider 实际尺寸并返回非阻断告警。归一后的带纯色背景图先持久化并作为 BgFilter 输入;BgFilter 正常成功后,把 Alpha 蒙版回贴到这张同尺寸平底原图,再执行像素规整并上传透明主图。网格分析源使用已收口到实际交付尺寸的平底原图,RGBA 采样源使用 Alpha 已回贴的透明图;软 Alpha 只参与单格覆盖率和 Alpha 加权 RGB 计算,输出 Alpha 硬化为 `0 / 255`。
- 首版参数固定为分析色数 `16`、Alpha 覆盖阈值 `0.375`、像素格尺寸自动检测、固定色板关闭、K-means 最大采样 `262144`。单格覆盖率 `Σ(A / 255) / N >= 0.375` 且 `ΣA > 0` 时输出 `A=255`,颜色按 `Σ(A × RGB) / ΣA` 计算;否则输出 `[0,0,0,0]`。分析色数不限制最终输出色数。
- 像素规整 CPU 工作使用进程级最大并发 `2`;取得并发许可的排队时间与实际处理时间共享最多 `30` 秒预算,同时不得晚于当前请求 deadline,最终以两者中更早者为准。输入图片任一边不得超过 `10000` 像素,总像素不得超过 `8294400`;超限、排队超时或处理超时均保留 Alpha 已回贴的透明图并走非致命降级。
- 逻辑低分辨率图只存在内存;snapper 在规整内部使用 nearest 恢复到当前 RGBA 输入尺寸,该输入已经是前述 Lanczos 归一后的交付尺寸,或尺寸归一无法安全执行时保留的 provider 实际尺寸。nearest 不是新的交付尺寸归一,规整完成后也不再执行第二次 Lanczos 或其它尺寸恢复。实现应复用 Alpha 回贴阶段读取的已持久化平底原图;必要时最多增加一次读取已有 provider 对象的 OSS GET,不得增加 OSS PUT。
- 开启或关闭像素风格都保持现有 provider 原图与透明主图两份产物、项目资源和画布图层数量不变。禁止上传逻辑低分辨率图、像素化前后双份主图、预览或诊断图,也不新增 asset kind、画布 item 或任务类型。
- 本功能不修改 BgFilter `flat` 调用、`cross_check=on`、fallback、Alpha 回贴或默认关闭 despill 的现状。BgFilter 最终失败时沿用只保留 provider 原图的既有收口且不运行像素规整;像素规整自身失败时保留已成功的透明图并继续原有持久化,通过通用 `warning` 非致命提示,不退款。
- `kind = "character"` 时,后端不直接把前端文本当完整生图提示词,而是把文本作为 `角色设定` 填入固定提示词骨架:
```text
@@ -106,6 +118,7 @@
- `从画布中选择` 后点击已有画布图片可绑定为角色规范,`Esc` 可退出点选状态。
- 上传常规参考图后缩略图右下角显示序号。
- 输入角色设定并生成时,请求包含 `kind: "character"`、角色设定 prompt、参考图数组、`model`、`screenColor`、`aspectRatio` 和 `imageSize`。
- 角色面板可选择 `style: "none" | "pixelArt"`;`none` 的处理路径和产物保持不变,`pixelArt` 在 Alpha 回贴后执行内存像素规整,最终 OSS PUT、项目资源和画布图层数量不得增加。
- 默认打开角色生成面板时选中 `nanobanana2 / 1:1 / 1K`;切换到 `gpt-image-2` 后再次打开角色或图标素材面板应沿用该模型。
- 生成成功后在占位图位置创建 `assetKind: "character"` 图层,右上角显示 `角色` 标签,布局保存包含该字段。