修复编辑器付费生成目标预检 (#151)
修复 Issue #147。 - 新增只读生成目标预检 procedure,并要求编辑器生成 Runtime 服务身份 - 图片、改图、图集、UI 提取、视频、角色动作、SFX、BGM 在入队、扣费、Provider 与 OSS 写之前失败关闭 - 预检返回 canonical projectId / assetFolderId,enqueue、worker/direct 与最新原子 persist 全链复用 - 覆盖 project、旧 folder-*、默认目录、自定义目录、空值与角色生图缺省目录语义 - 更新后端架构文档、项目排障记忆和源码顺序回归 验证: - API preflight 定向测试 4/4 - canonical target 定向测试 1/1 - SpacetimeDB module preflight 定向测试 2/2 - cargo check -p api-server -p spacetime-client -p spacetime-module --all-targets - npm run check:spacetime-schema - npm run check:encoding - npm run check:rustfmt - git diff --check 剩余边界:预检不持有跨 Provider 调用的锁;若目录在预检后并发变化,最终原子提交仍会重新校验并安全失败。 Closes #147 Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/151
This commit was merged in pull request #151.
This commit is contained in:
@@ -4391,3 +4391,10 @@
|
||||
- receipt 存在不等于引用 object 仍然可信:省略 candidate 的已登记 object 在重放时也要回读 owner/key/task/kind/媒体身份。同时先查同 operation ID job,存在 job 却漏传 completion 必须整笔回滚,否则会得到 receipt 成功而 job 仍 running 的永久分裂。resource/asset/binding 也不得仅核对 object ID/key,必须按 operation 合法 tuple 交叉验证业务元数据。
|
||||
- 验证:故障注入覆盖 resource 后 asset/binding 失败、canvas CAS 冲突、过期 lease、同 operation 异 fingerprint / 异 commit、receipt 缺失的部分既有记录、精确既有 object 复用与 object 内容漂移;成功重放必须证明记录数、时间、binding/job 事件数和 canvas revision 全部不变。
|
||||
- 关联:`docs/technical/【后端架构】编辑器生成结果原子提交与幂等重放方案-2026-08-06.md`、Issue #134。
|
||||
|
||||
## 付费生成不能把素材目录归属校验留到 provider 之后(2026-08-07)
|
||||
|
||||
- 现象:登录用户给自己的合法 `projectId` 搭配不存在或属于其他账号的 `assetFolderId`,图片、改图、图集、UI 提取、视频、角色动作或音频生成会先扣泥点并调用付费 provider / OSS,直到创建 `editor_asset` 才拒绝目录;失败退款让用户成本归零,平台侧 provider 和存储成本不可逆。
|
||||
- 原因:`normalize_generated_asset_folder_id` 只处理 `project`、旧 `folder-*` 和默认目录 ID 的兼容映射,不读取 SpacetimeDB;真正的 `require_owned_asset_folder` 位于生成结果持久化末端。把“失败会退款”误当成副作用补偿,漏掉退款不能撤销 provider 请求与 OSS PUT。
|
||||
- 处理:所有付费编辑器生成在队列 enqueue 前和 worker / inline 执行前复用只读 `preflight_editor_generation_target_and_return`,按认证 owner 校验可选项目及归一化目录;读取失败和归属不匹配一律失败关闭。helper 返回 canonical 项目与目录并覆写后续入队 / worker / 原子准备使用的 payload,不能校验 trim 后的项目却持久化原始空白值。角色图片、角色动作、图标 spritesheet 与 UI 提取省略目录时按实际默认目录预检;默认目录允许尚未创建,自定义目录必须存在且 owned。预检不替代最终 procedure 复验,也不保证跨外部调用的目录锁定。
|
||||
- 验证:源码顺序回归必须覆盖图片生成、图片修改、图标 spritesheet、UI 设计图提取、视频、角色动作、SFX 与 BGM 的 enqueue / direct 两层,证明纯本地格式和 `data:` / `blob:` 稳定引用门禁先执行,canonical target 在预检后写回 payload,远端引用解析、generation input rebuild、扣费、入队、provider 与 OSS 均留在预检之后;模块侧扫描证明预检只调用 runtime identity、项目、目录只读校验且不含 insert / update / delete,并覆盖带空白项目、`project`、旧 `folder-*`、默认目录 ID、自定义目录与 `None` 归一化。
|
||||
|
||||
@@ -240,10 +240,10 @@ npm run check:server-rs-ddd
|
||||
3. 每日免费泥点是独立于每日任务和会员周期的正式余额额度,基础发放量读取 `profile_wallet_config.daily_free_points_per_day`,不得由前端或后台任务配置改写。`profile_daily_free_points` 保存当前北京时间业务日、当日基础发放及跨日退款叠加后的总额度和剩余额度;北京时间每日 `00:00` 作为业务日边界,个人中心、充值中心、账单读取和钱包扣费入口在首次触达新业务日时原子清除昨日剩余及退款叠加量,并按当时最新配置重置今日 `granted_points`、`remaining_points`。同一业务日已初始化的用户不因后台改配置被即时追补或回收;新配置从尚未初始化当日额度的用户或下一次跨日重置起生效。首次初始化使用 `daily_free_grant` 流水,跨日重置使用 `daily_free_reset` 流水。充值中心的 `dailyFreeResetPoints` 显式来自该配置,不得用可因跨日退款增大的当日 `granted_points` 反推。惰性落库不能改变“北京时间 00:00 后读取即为新日额度”的对外语义。
|
||||
4. 每日任务奖励继续使用 `daily_task_reward` 流水并进入普通永久泥点,但主站隐藏每日任务卡片和任务中心入口,不再把每日登录任务描述为“每日免费泥点”。任务配置、进度、领取记录和后台管理能力暂时保留,除非后续需求明确删除。
|
||||
5. 编辑器画板所有会调用外部生成 provider 的入口都不从前端请求接收 `priceMudPoints`;同步请求以 SpacetimeDB `editor_generation_pricing_config` 当前全局配置计算,外部生成队列则以 `external_generation_job.price_mud_points` 保存的入队价格为准,worker 的扣费、退款、响应和资产成本不得按执行时配置重算。前端按钮泥点只作为展示。
|
||||
6. 编辑器图片生成 / 图片修改 / 图标 spritesheet / UI 设计图提取素材 / 视频 / 角色动作 / 音效 / 背景音乐必须在后端计算模型价格后使用 `execute_billable_asset_operation_with_cost` 预扣泥点;预扣失败必须 fail-closed,不得继续提交 VectorEngine、Ark、Suno 或 Vidu 上游任务。
|
||||
6. 编辑器图片生成 / 图片修改 / 图标 spritesheet / UI 设计图提取素材 / 视频 / 角色动作 / 音效 / 背景音乐必须在后端计算模型价格后使用 `execute_billable_asset_operation_with_cost` 预扣泥点;预扣失败必须 fail-closed,不得继续提交 VectorEngine、Ark、Suno 或 Vidu 上游任务。进入预扣或外部生成队列前,还必须调用只读 `preflight_editor_generation_target_and_return`,按认证 owner 校验可选 `projectId`,并校验归一化后的可选 `assetFolderId`。请求 JSON、纯本地格式、引用数量及 `data:` / `blob:` 稳定媒体门禁必须先于该数据库预检返回 4xx;目标预检随后执行,并且仍必须早于定价读取、引用 owner 解析、generation input rebuild、入队、扣费、provider 请求或 OSS 写入,不得为调整错误优先级把任何远端读取或副作用搬到预检前。`project`、任意旧 `folder-*` 与 owner 默认目录 ID 都统一指向当前 owner 的默认素材目录;角色图片、角色动作、图标 spritesheet 与 UI 提取在请求省略目录时也必须按最终真实写入的默认目录预检。尚未创建的默认目录允许通过,自定义目录必须已存在且归属当前 owner。预检 helper 必须返回同一份 canonical `projectId + assetFolderId`,调用方在入队、worker/provider 执行和原子结果准备中都复用这份值;禁止校验 trim/默认映射后的值却继续序列化或持久化原始请求。worker / inline 执行在首个 provider 或 OSS 写副作用前再次执行同一预检,不能只信任入队时结果;任一读取不可达、超时、项目或目录不匹配都失败关闭。该预检不创建锁或 reservation,最终资源 / 素材 procedure 仍必须重新校验归属,以处理预检后并发删除或转移。
|
||||
7. 队列任务按 `job_id + claim_attempt` 使用独立 consume/refund ledger。新 attempt 结算旧 attempt 时必须先写 `asset_operation_wallet_settlement`:旧 consume 已存在则原子退款,尚不存在则写取消 intent;迟到 consume 在同一 SpacetimeDB 事务内看到 intent 后必须失败关闭。重复 consume/refund 只有用户、金额、来源和配对 ledger 全部一致时才可视为幂等成功。lease 过期时只有 `attempt < max_attempts` 才能递增并重领;最终 attempt 已耗尽时,claim transaction 必须直接把 job 收口为 `failed`、清理 lease、写失败事件并结算当前 attempt,不能再把任务返回 worker 或调用 provider。
|
||||
8. 音频生成的编辑器链路虽然任务提交和结果发布分离,仍必须把提交时后端计算出的模型价格写入 `AudioAssetBindingTarget.billing_points_cost`,最终发布落资产时按该价格扣费;创作音频目标未提供该字段时才使用旧的创作音频固定成本。
|
||||
9. 编辑器进入外部生成持久队列的图片生成、图片修改、去背景、图标 spritesheet、UI 设计图提取、角色动作和视频参考图,调用方必须提交 `objectKey` / `resourceId` / `assetId` 候选引用;BFF 只做内联媒体与 payload 门禁,登记状态和归属由 worker 统一解析。任务 `request_payload_json` / `result_payload_json` 任意层级都禁止 `data:` / `blob:`,并受统一字节上限保护。无效普通字符串可以入队,但必须在签名和 provider 调用前失败;本次不增加 API 侧数据库查询或同步 owner 校验。若以后要求无效引用同步返回 400,应作为独立改造。objectKey 最终必须归属于当前账号的 `editor_project_resource`、`editor_asset` 或 `asset_object`,由 worker 在解析后、签名读取 OSS 前完成归属校验。本地红框序号标注图必须先上传并确认对象,再把 objectKey 入队;不得把既有 objectKey 下载成 Data URL 后写入任务。图标素材、图片快速编辑和 UI 素材提取的额外参考图必须真正传入 provider,不得只写入 `generationInputs` 展示快照。普通图片生成最多 5 张参考图;图片修改、图标素材和 UI 提取的额外参考图上限还必须与所选 provider 的总容量共同取最小值:GPT-image-2 总计 5 张,nanobanana2 总计 14 张。前端添加和提交、api-server 入队 / 扣费前以及 `platform-image` provider 边界都必须明确拒绝超限,禁止用 `.take(...)` 静默截断。同步且不持久化的历史兼容入口即使仍能解析 Data URL,也不能把该值转存到工程、素材、元数据、审计或任务表。
|
||||
9. 编辑器进入外部生成持久队列的图片生成、图片修改、去背景、图标 spritesheet、UI 设计图提取、角色动作和视频参考图,调用方必须提交 `objectKey` / `resourceId` / `assetId` 候选引用;BFF 对生成目标执行上一条只读 owner / project / asset folder 预检,但参考媒体的登记状态和归属仍由 worker 统一解析。任务 `request_payload_json` / `result_payload_json` 任意层级都禁止 `data:` / `blob:`,并受统一字节上限保护。无效普通参考字符串可以入队,但必须在签名和 provider 调用前失败;若以后要求无效参考引用同步返回 400,应作为独立改造。objectKey 最终必须归属于当前账号的 `editor_project_resource`、`editor_asset` 或 `asset_object`,由 worker 在解析后、签名读取 OSS 前完成归属校验。本地红框序号标注图必须先上传并确认对象,再把 objectKey 入队;不得把既有 objectKey 下载成 Data URL 后写入任务。图标素材、图片快速编辑和 UI 素材提取的额外参考图必须真正传入 provider,不得只写入 `generationInputs` 展示快照。普通图片生成最多 5 张参考图;图片修改、图标素材和 UI 提取的额外参考图上限还必须与所选 provider 的总容量共同取最小值:GPT-image-2 总计 5 张,nanobanana2 总计 14 张。前端添加和提交、api-server 入队 / 扣费前以及 `platform-image` provider 边界都必须明确拒绝超限,禁止用 `.take(...)` 静默截断。同步且不持久化的历史兼容入口即使仍能解析 Data URL,也不能把该值转存到工程、素材、元数据、审计或任务表。
|
||||
10. 已有静态图片的 `POST /api/editor/images/pixel-art-snaps` 是免费 inline 派生操作,不调用外部 provider、不创建 `external_generation_job`、不读写泥点 ledger,也不进入任务侧栏。免费不放宽 owner、稳定引用、输入上限、持久化或处理阶段零持久化门禁。
|
||||
11. 主站编辑器生成队列使用同一次前端请求稳定复用的 `x-request-id`,按 namespace + owner + job kind + request id 生成唯一 `dedupe_key`;首次请求已入队但响应丢失时,重试必须返回原任务。同一幂等键携带不同 payload 返回 `409`,不得创建第二个任务或串到旧结果。外部 v1 的 `Idempotency-Key` 使用独立 namespace,不能与主站请求标识碰撞。幂等 payload 比较只对本次已迁移 sanitizer 的图片生成、图片修改、去背景、图标图集和 UI 提取任务,兼容“升级前旧任务仍含客户端 `generationInputs.references`、当前请求已删除该字段”的单向形状;当前请求仍含 references,或 job kind 属于音频 / 视频 / 角色动作等未迁移任务时必须完整比较,其余请求字段始终完全一致。
|
||||
12. `generationInputs.references` 是最终资产的服务端权威行引用,不接受客户端自报 provenance。图片生成类请求入队、完美像素及直接创建资源 / 素材时删除客户端 references;worker 和 inline 路径按本次真实参考图、当前 owner 的项目资源 / 素材记录重建 `refType/refId` 后再持久化。仅能证明 owned objectKey、但找不到对应资源或素材行时可以参与生成,不得制造虚假行引用;`title/label` 只作为展示快照,不提升为资源身份。完美像素为兼容升级前的未知结果重放,可继续用旧版 canonical 客户端输入计算 operation fingerprint;新操作持久化元数据只能使用服务端重建值,检测到 owner 项目中已存在同一稳定 task/resource 的历史结果时则复用该服务端既存 metadata 完成精确 compare-and-return。
|
||||
|
||||
Reference in New Issue
Block a user