3D 生成定价迁移先落文档:两段并列结构、SpacetimeDB 权威与后台编辑

- 主规范把 3D 定价改成按端点两段并列结构,事实源改为 SpacetimeDB,补契约归一化、后台保存语义与验收项
- 新增 ADR 0005,记录权威事实源迁移、公开读模型不变的取舍与四条被否决做法
- 新增里程碑规范与实施计划,列出范围、验收标准、文件切分、验证命令与两个回滚点
- 同步 Tripo 技术方案的定价段形状、后端架构表目录的两列说明、CONTEXT.md 术语与决策记录
This commit is contained in:
2026-09-23 10:53:28 +08:00
parent caafb8baa3
commit 1327cd53b7
9 changed files with 202 additions and 26 deletions
+12
View File
@@ -80,6 +80,18 @@ _Avoid_: 将 Tripo taskId 直接当公开 operationId、把 SDK task 状态模
Tripo 任务完成后,服务端下载模型与预览、写入受控对象存储并登记资源元数据,成功后才把 Tripo operation 置为 completed;本期按完整字节写入,流式上传是后续目标。
_Avoid_: 返回临时签名 URL作为永久资源、在 API handler 中把完整模型读成 Vec<u8>
**3D 生成底价**:
一次 3D 生成在指定端点和模型版本下、按是否带贴图区分的基准泥点价,不包含任何叠加项,也不随请求的其它参数变化。
_Avoid_: 把底价与加价项合并成一个档位价、把 provider 的 credit 数值当底价
**3D 生成加价项**:
叠加在底价之上、由请求参数判定的可选泥点加价;每个端点各自持有一份加价项价目,同一加价项在不同端点可以是不同价。
_Avoid_: 全局唯一一份加价项价目、按用途而不是按请求参数判定加价
**定价配置归一化**:
读取持久化定价时把它与当前契约对齐的过程:缺的键用受控默认值补齐,契约之外的遗留键剔除并告警;它只发生在读取方向,写入方向仍要求完整合法。
_Avoid_: 把「省略即沿用」当作保存语义、把归一化当成静默改价
**模型生成落点**:
3D 生成结果要进入的平台容器,调用方必须在工程资源与素材库资产之间显式二选一;工程分支可附带画布占位框回填,素材库分支必须给出目录与标签。
_Avoid_: 两个都不给就默认落素材库、把落点混进 provider 请求参数
+1
View File
@@ -91,6 +91,7 @@
- [Tripo 生成 API 集成边界与提交时定价 ADR](./adr/【ADR】0002-Tripo生成API集成边界与提交时定价-2026-09-21.md):全新 API 不复用 Hyper3Dat-most-once submit,按提交时定价真实扣费。
- [3D 资源客户端媒体投影与格式真实性 ADR](./adr/【ADR】0003-3D资源客户端媒体投影与格式真实性-2026-09-21.md):3D 资源的客户端投影按类别分叉,模型内容类型按字节识别,格式判定只在查看器包内(声明类型 → 字节魔数 → 地址扩展名),画布不判格式。
- [3D 生成入口的价格真相与幂等身份 ADR](./adr/【ADR】0004-3D生成入口价格与幂等身份-2026-09-21.md):3D 生成入口的价格只读实时定价查询,缺段即不可提交;幂等键由前端铸造,重试必须换键。
- [3D 生成定价迁入 SpacetimeDB 与后台编辑 ADR](./adr/【ADR】0005-3D生成定价迁入SpacetimeDB与后台编辑-2026-09-23.md):3D 定价按端点拆成两段并列设置并改为 SpacetimeDB 权威、后台可编辑;公开读模型与画布 3D 入口不变。
- [BgFilter 受限资源调度方案](./technical/【后端架构】BgFilter受限资源调度方案-2026-07-21.md)
- [Issue225 登录成功 AGC 用户归属修复](./technical/【后端架构】Issue225登录成功AGC用户归属修复方案-2026-09-03.md):登录 route tracking 的真实用户归属、`daily_login` 幂等边界和实施验收。
@@ -0,0 +1,13 @@
# 【ADR】0005-3D 生成定价迁入 SpacetimeDB 与后台编辑-2026-09-23
状态:已接受
3D 生成定价(Tripo 的 `model3d` 段)的权威事实源,从「受控默认 JSON + 旧 override 文件的本地缓存」改为 SpacetimeDB `editor_generation_pricing_config` 表新增的两段强类型列;后台「模型定价」页面直接编辑这两段,保存与 `models` 段在同一事务内生效,改价不再需要改文件或重启进程。前提是记录形状先能表达 3D 计费:不在一段里用端点做判别键,而是并列 `textToModelPricing` / `imageToModelPricing` 两段,各段自带全部受支持模型版本的 `noTexture` / `texture` 底价与自己的六个加价项键(两段加价项当前数值相同,仍各留一份,便于后续按端点差异化)。
选择这个方向,是因为文件方案只解决了它诞生时的临时问题:文件是发布物,改价必须重新构建并重启进程,而价格是运营参数。更糟的是后台保存把「`model3d` 省略即沿用」当合法输入,而 admin-web 会把读取到的整份配置原样回传,于是一个陈旧的浏览器标签页保存图片价格时可以静默写回旧的 3D 段。把 3D 纳入与 `models` 同一行、同一事务、同一审计与 writer 授权之后,「谁是权威」只剩一个答案,价格编辑回到已经处理过并发与权限问题的后台路径。
代价与已知取舍:加载期必须做契约归一化。发布新增模型版本时 SpacetimeDB 行里缺键,会让整段读取失败,而失败点同时打掉后台定价页,形成「进程起不来、后台也补不了价」的死锁。因此读取方向按契约对齐——缺键用默认 JSON / 覆盖文件同段值补齐,契约之外的遗留键剔除并告警,只有归一化后仍不合法才判非法;写入方向不做宽松,后台保存要求两段都显式给出且完整合法。覆盖文件从「3D 的生效路径」降级为「表为空或 SpacetimeDB 不可达时的兜底与首次种子」。
公开读模型 `GET /api/editor/generation-pricing` 的 3D 段形状保持不变(仍是按端点判别的 `basePrices` 投影),因为它是画布 3D 入口的唯一价格真相且前端零兜底,而 nginx 静态资源与 api-server 是两条独立发布线,改形状会在错开发布窗口内让入口取不到价。保存语义同时收紧为「两段必填 + 期望版本 CAS,不一致返回 409」,旧 payload 的隐式沿用兼容不保留。
被否决的做法:**新建独立表**(同一份「模型定价」事实被拆成两行,跨行不同步与两套 writer / 审计语义,且仍要在同一逻辑保存里原子提交);**后台保存时改写 override JSON 文件**(违背「后台保存不再写文件」的口径,多进程各读各的,且与静态发布物耦合);**加载缺键即失败**(死锁,见上);**连公开读模型一起改成两段**(把发布耦合引入价格真相,收益只是形状统一)。
@@ -0,0 +1,77 @@
# 3D 生成定价后台可编辑实施计划
Version: 1.0
Status: active
Date: 2026-09-23
Milestone Spec: `docs/project-memory/plans/【里程碑】3D生成定价后台可编辑-2026-09-23.md`
## 边界
改动集中在定价配置这一条链路:SpacetimeDB 表的两个追加列与 procedure、`spacetime-client` 记录映射、api-server 的定价类型 / 记录转换 / 后台路由、admin-web 的定价页与 API 类型、受控默认 JSON。不改公开读模型形状、不动画布 3D 入口、不改 `models` 段语义、不改 provider 与 worker 扣费链路。
## 文件切分
| 文件 | 职责 |
| --- | --- |
| `server-rs/crates/spacetime-module/src/editor_project_storage.rs` | 表追加两列,新增按端点分段的 `SpacetimeType` 行类型(版本价行 / 加价项行),upsert input / snapshot 扩字段,事务内做期望版本 CAS |
| `server-rs/crates/spacetime-module/src/migration.rs` | 行形状变更的迁移登记与白名单 |
| `server-rs/crates/spacetime-client/src/module_bindings/**``src/active/mapper/editor_project.rs` | 生成绑定的记录映射:两段行、定价版本字段 |
| `server-rs/crates/api-server/src/tripo3d/pricing.rs` | 两段强类型定价、按段强校验、按端点段查价 |
| `server-rs/crates/api-server/config/editor-generation-pricing.default.json` | 两段并列的受控默认价 |
| `server-rs/crates/api-server/src/editor_generation_config.rs` | 两段解析 / 校验 / `model3d_price` / 定价版本字段与 `with_previous_model3d` 的退役 |
| `server-rs/crates/api-server/src/state.rs` | `to_records` / `from_record` 的两段序列化与契约归一化、保存路径接入 CAS |
| `server-rs/crates/api-server/src/admin.rs` | GET 返回定价版本、POST 两段必填、409 错误映射 |
| `apps/admin-web/src/api/adminApiTypes.ts``adminApiClient.ts` | 两段类型与 409 透出 |
| `apps/admin-web/src/pages/AdminEditorGenerationPricingPage.tsx` | 3D 区块、本地校验、409 / 400 提示 |
## 步骤
1. **schema 两列**
- 表末尾追加 `text_to_model_pricing` / `image_to_model_pricing`,各带显式默认值;行类型保持展平(版本价一行含两档价,加价项一行一条)。
- 同步 `migration.rs`、表目录文档与生成绑定。
- 验收:`npm run check:spacetime-schema` 通过;旧行读出来的两段为空且不影响既有 `models`
2. **定价类型与默认文件**
- `Model3dPricingConfig` 改成两段结构,端点由字段名承载而不是 map 键;强校验按段执行,错误信息带端点名。
- 默认 JSON 改成两段并列形状,换算口径与 credit 表不变。
- 验收:`tripo3d::pricing` 定向用例(按段缺键即非法、查价 = 该段底价 + 命中加价项)通过;默认价仍按 `ceil(0.8 × credit)` 反算。
3. **记录转换与归一化**
- `to_records` 写两段;`from_record` 读两段并按契约归一化(缺键用默认 JSON / 覆盖文件同段值补齐,契约外键剔除),归一化动作写日志。
- 覆盖文件降级为种子与兜底:不再参与「每次启动覆盖」。
- 验收:结构体 record 往返不丢 3D;手工从 record 删掉一个版本键后读取仍成功且补齐该键;塞入契约外键后读取成功且该键被剔除并告警。
4. **后台契约**
- GET 响应加定价版本;POST 要求两段显式给出,带期望版本做事务内 CAS,不匹配返回 409 与中文文案。
- 退役 `with_previous_model3d` 与其用例,改为「缺段即 400」。
- 验收:缺段 400、段内缺键 400、版本不匹配 409 且不落库;保存后公开路由立即返回新价。
5. **admin-web 3D 区块**
- 类型补两段;页面按端点渲染版本 × 两档价与加价项输入;提交前校验全部数值 ≥ 1;409 提示重新读取。
- 验收:组件用例覆盖渲染、校验、保存 payload 与 409 提示。
6. **文档与验收**
- 同步主规范、表目录、`CONTEXT.md`、ADR 0005 的实现状态,必要时补 `pitfalls.md`(例如「不要把省略即沿用重新引入」)。
- 手工验收:后台改 3D 价 → 不重启 → 画布 3D 面板价格随之变化;重启后仍是保存值。
## 验证命令
```bash
cargo check --locked -p api-server --manifest-path server-rs/Cargo.toml
cargo test --locked -p api-server --manifest-path server-rs/Cargo.toml pricing
cargo test --locked -p spacetime-module
npm run spacetime:generate
npm run check:spacetime-schema
npx vitest run apps/admin-web/src/pages/AdminEditorGenerationPricingPage.test.tsx
npm run check:encoding
node scripts/check-doc-index.mjs
git diff --check
```
## 风险与回滚
- **归一化补齐的数值来源**:缺键用默认 JSON / 覆盖文件同段值补齐,若现网运营价与默认值不同,升级瞬间会把表内缺失键补成默认价。上线前先核对现网两段价格;必要时把现网值写进覆盖文件作为种子来源,再执行升级。
- **覆盖文件的旧形状 3D 段**:处理策略见主规范「未决问题」,确认前不进入实现。
- **发布顺序**:必须先发 Stdb 模块再发 API;API 先发时会因记录形状不匹配而报错。Web 面板可以晚于 API 发布。
- **回滚点 1(未发布)**:仅回滚代码,两列是纯追加且带默认值,不影响旧行为。
- **回滚点 2(已发布 API)**:必须同时回滚 Stdb 与 API(旧 API 会把 3D 权威退回文件),回滚窗口内不要在后台改 3D 价;回滚后重新保存一次定价以确认两段仍在。
@@ -0,0 +1,45 @@
# 3D 生成定价后台可编辑
Version: 1.0
Status: active
Date: 2026-09-23
Parent Spec: `docs/【编辑器】模型定价配置管理方案-2026-06-22.md`
Implementation Plan: `docs/project-memory/plans/【实施计划】3D生成定价后台可编辑-2026-09-23.md`
Related: `docs/adr/【ADR】0005-3D生成定价迁入SpacetimeDB与后台编辑-2026-09-23.md``docs/technical/【技术方案】Tripo 3D生成API集成-2026-09-21.md`
## 目标
把 3D 生成定价从「受控默认 JSON + override 文件的本地缓存」迁到 SpacetimeDB `editor_generation_pricing_config`,并让后台「模型定价」页面能编辑按端点拆分的两段定价(底价与加价项);保存与 `models` 段在同一事务内生效,改价不再需要改文件或重启进程。
## 范围
- 表新增 `text_to_model_pricing` / `image_to_model_pricing` 两列,沿用 `models` 的展平风格;同步 `migration.rs`、表目录文档与生成绑定。
- 定价文件的 3D 段改成两段并列形状,受控默认 JSON 与定向用例同步。
- 读取方向契约归一化:表内缺键用默认 JSON / 覆盖文件同段值补齐,契约之外的遗留键剔除并告警。
- 后台 GET / POST:两段必填,携带读取时得到的定价版本做 CAS,不一致返回 409;保存成功后返回权威快照。
- admin-web「模型定价」页新增 3D 区块:按端点分组,模型版本 × 两档价 + 六个加价项;提交前本地校验与 400 / 409 中文提示。
- 覆盖文件降级为种子与兜底,不再是 3D 改价的生效路径。
## 不做
- 不改公开读模型 `GET /api/editor/generation-pricing` 的 3D 段形状;不改画布 3D 入口与其「零兜底」口径。
- 不改 `models` 段的字段、列、语义与名字。不新增后台 tab、权限字符串或 3D 定价总开关。
- 不允许后台增删模型版本键或加价项键,不允许整段停用。
- 不接 multiview / Splat / rig / animation / texture / convert;不改 provider 侧、worker 扣费与退款链路。
## 验收标准
1. 后台 GET 返回两段 3D 定价与定价版本;公开 GET 的 3D 段形状与本期之前逐字段一致。
2. 后台 POST 缺少任一段 → 400;段内任一键缺失或非法 → 400;版本不匹配 → 409,且两段与 `models` 均不落库。
3. 保存成功后不重启进程,`GET /api/editor/generation-pricing` 立即反映新价;重启后仍是保存后的值。
4. 表内两段缺键(模拟发布新增模型版本)时读取自动补齐并记录告警,读取不失败;契约之外的遗留键被剔除并记录告警。
5. 表为空或 SpacetimeDB 不可达时,3D 定价用默认 JSON / 覆盖文件同段值兜底;override 文件的存在不再导致「重启覆盖已保存价格」。
6. 3D 定价未配置时提交仍 fail closed503 `model3d-pricing-unavailable`),不扣费、不入队。
7. admin-web 3D 区块渲染两个端点各 5 行 × 2 档价与 6 个加价项;存在 0 或空值时禁用保存并给出中文提示;收到 409 时提示重新读取。
8. `npm run check:spacetime-schema``npm run check:encoding``cargo test --locked -p api-server` 定向用例、admin-web 定向 vitest、`node scripts/check-doc-index.mjs``git diff --check` 全部通过。
## 依赖
- 画布 3D 入口已上线,价格只读公开定价查询(ADR 0004),本期不改其契约。
- 生产 SpacetimeDB 需要先发布含两列的模块版本,发布顺序为 Stdb → API → Web。
- 生产覆盖文件若含旧形状 3D 段,按主规范「未决问题」确认后的兼容策略处理。
@@ -9364,4 +9364,12 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
- 代价与取舍:ts-rs 重新进入 AGC 构建脚本与 `spacetime-module`wasm32-unknown-unknown)依赖图,即 `a5ee45aa4` 省下的编译开销不再保留。已实测该依赖图可编译:`cargo check -p spacetime-module --target wasm32-unknown-unknown` 通过(`ts-rs` / `ts-rs-macros` 在 wasm32 上可编译)。
- 影响面:`server-rs/Cargo.toml``server-rs/crates/shared-contracts/Cargo.toml``server-rs/crates/shared-contracts/src/game_creation_app/asset_kind.rs``apps/ai-game-creator-shell/src-tauri/Cargo.toml`;以及本文件、`pitfalls.md``docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md``docs/project-memory/todos/【待办】AGC资源kind枚举化扫描清单-2026-09-15.md``docs/project-memory/plans/【实施计划】AGC资源kindRust枚举与ts-rs绑定-2026-09-15.md` 里的生成命令(去掉 `--features ts-bindings`)。
- 验证方式:`cargo metadata --locked`server-rs 与 AGC 两个 workspace)、`cargo test -p shared-contracts`119 + 5 + 5 + 2 + 2 全绿、无告警)、`npm run contracts:model3d:generate``packages/shared` 零 diff、`cargo check -p spacetime-module --target wasm32-unknown-unknown``npm run check:encoding``npm run check:rustfmt``npm run check:spacetime-schema``git diff --check`
- 合并结果还丢过依赖:`apps/ai-game-creator-shell/src-tauri/Cargo.toml` 里 master 新增的 `codex-patch-parser``chrono` 在解决同段落 `ts-rs` 冲突时被一并丢掉(vendor crate 仍是 workspace member,光看 members 不显眼),CI 编译 AGC Rust 分片时报 8 条 `cannot find module or crate codex_patch_parser`。已按 manifest 恢复这两行并同步 lock;排障口径记在 `pitfalls.md` 2026-09-22 条。
## 2026-09-23 3D 生成定价迁入 SpacetimeDB 并由后台编辑,两段并列结构取代端点判别键
- 背景:3D 生成定价此前只存在于受控默认 JSON 与旧 override 的本地缓存;`editor_generation_pricing_to_records` 只序列化 `models``from_record` 再从文件把 `model3d` 补回来,因此后台改 3D 价只在进程内存生效、重启即回滚;而 admin-web 把读取到的整份配置原样回传,陈旧的浏览器标签页保存图片价时会静默写回旧 3D 段。
- 决策:3D 定价改为 SpacetimeDB `editor_generation_pricing_config` 权威。表新增 `text_to_model_pricing` / `image_to_model_pricing` 两列(沿用 `models` 的展平风格:版本键一行含无贴图 / 带贴图两档价,加价项一行一条),不在一段里用端点做判别键;定价文件同步为两段并列结构,各段自带版本底价与自己的六个加价项键(当前数值相同)。后台「模型定价」页新增 3D 区块编辑两段,POST 两段必填并携带读取时的定价版本做事务内 CAS,不一致返回 409;读取方向做契约归一化(缺键用默认 JSON / 覆盖文件同段值补齐,契约外遗留键剔除并告警),写入方向仍要求两段完整合法;覆盖文件降级为种子与兜底。公开读模型 `GET /api/editor/generation-pricing` 的 3D 段形状与画布 3D 入口不变。
- 原因:价格是运营参数,不该绑定发布物与进程重启;把 3D 纳入与 `models` 同一行、同一事务、同一 writer 与审计语义后,「谁是权威」只剩一个答案,价格编辑回到已经处理过并发与权限问题的后台路径。
- 代价与取舍:必须做加载期归一化,否则发布新增模型版本时表内缺键会让整段读取失败,且失败点同时打掉后台定价页,形成「进程起不来、后台也补不了价」的死锁;加价项目前按端点各留一份,用少量冗余换未来按端点差异化加价的空间。公开读取保持旧投影,代价是存储形状与公开形状短期内不一致。
- 验证方式:文档先行阶段(ADR 0005、主规范、里程碑与实施计划、`CONTEXT.md` 术语);实现与验收证据见 [`【实施计划】3D生成定价后台可编辑-2026-09-23.md`](../plans/【实施计划】3D生成定价后台可编辑-2026-09-23.md)。
- 关联文档:[ADR 0005](../../adr/【ADR】0005-3D生成定价迁入SpacetimeDB与后台编辑-2026-09-23.md)、[编辑器模型定价配置管理方案](../../【编辑器】模型定价配置管理方案-2026-06-22.md)、[技术方案 Tripo 3D生成API集成](../../technical/【技术方案】Tripo 3D生成API集成-2026-09-21.md)。
@@ -92,22 +92,22 @@ ImageToModelResult = { modelArtifact, renderedPreview }
## 定价与扣费
定价是**真实扣费**,不是计量占位。Tripo 的计费形态是“底价 + 可叠加 add-on”,现有图片/视频那套“模型 → 档位 → 单价”查表表达不了,因此在同一份定价配置中新增 `model3d` 段,全部以泥点计价:
定价是**真实扣费**,不是计量占位。Tripo 的计费形态是“底价 + 可叠加加价项”,现有图片/视频那套“模型 → 档位 → 单价”查表表达不了,因此在同一份定价配置中另立 `model3d` 段,按端点拆成两段并列设置,全部以泥点计价:
```json
"model3d": {
"basePrices": {
"text-to-model": { "<modelVersion>": { "noTexture": , "texture": } },
"image-to-model": { "<modelVersion>": { "noTexture": , "texture": } }
"textToModelPricing": {
"versionPrices": { "<modelVersion>": { "noTexture": , "texture": } },
"addOnPrices": { "hdTexture": , "ultraTexture": , "hdGeometry": , "quadMesh": , "smartLowPoly": , "generateParts": }
},
"addOnPrices": {
"hdTexture": , "ultraTexture": , "hdGeometry": ,
"quadMesh": , "smartLowPoly": , "generateParts":
"imageToModelPricing": {
"versionPrices": { "<modelVersion>": { "noTexture": , "texture": } },
"addOnPrices": { "hdTexture": , "ultraTexture": , "hdGeometry": , "quadMesh": , "smartLowPoly": , "generateParts": }
}
}
```
模型版本键使用 `Model3dModelVersion` 的枚举值,不另造字符串。add-on 由请求参数判定,判定规则固定为:
模型版本键使用 `Model3dModelVersion` 的枚举值,不另造字符串。两段各自持有自己的加价项价目(当前数值相同),因此同一加价项在不同端点可以不同价。加价项由请求参数判定,判定规则固定为:
```text
hdTexture ← texture=true && textureQuality=detailed
@@ -122,19 +122,21 @@ textureQuality=fast | standard → 不给任何贴图加价
最终价格:
```text
price = basePrices[endpoint][modelVersion][texture ? "texture" : "noTexture"]
+ Σ addOnPrices[命中的 add-on]
price = <端点段>.versionPrices[modelVersion][texture ? "texture" : "noTexture"]
+ Σ <端点段>.addOnPrices[命中的加价项]
```
两条硬约束:
1. **加载即校验**`model3d` 段一旦存在,两个 endpoint × 每个受支持模型版本 × 两种贴图态与全部 add-on 键必须同时存在,缺一即配置非法、服务拒绝启动;段内不再写 `unit`3D 生成恒为按次计价。
1. **加载即校验**两段各自强校验——每段的每个受支持模型版本 × 两种贴图态与全部六个加价项键必须同时存在,缺一即该段非法、服务拒绝加载;段内不再写 `unit`3D 生成恒为按次计价。
2. **提交即拒绝**:价格计算发生在调用 provider 之前;拿不到价格就不提交、不扣费、不入队。
泥点数值已确认并写入受控默认配置 `server-rs/crates/api-server/config/editor-generation-pricing.default.json`。换算口径固定为 **泥点 = `ceil(0.8 × Tripo 官方 credit)`**credit 取自 Tripo 官方价目表的 H 系列与 P 系列:文生 3D 的 H 系列 `10 / 20`、P1 `30 / 40`、P2 `100 / 110`,图生 3D 的 H 系列 `20 / 30`、P1 `40 / 50`、P2 `100 / 110`(依次为无贴图 / 标准贴图),六个 add-on 为 `HD Texture 10``8K Ultra Texture 20``HD Geometry 20``Quad Mesh 5``Smart Low-poly 10``Generate Parts 20``editor_generation_config` 的定向用例按这张 credit 表反算默认泥点价,改价必须先改 credit 表,避免默认值脱离换算口径。
`model3d` 段缺失仍是合法形态:缺失即 3D 定价未配置,提交被拒绝(fail closed),既不扣费也不调用 provider;段存在时走上面的强校验路径。
2026-09-23 起这两段的事实源是 SpacetimeDB `editor_generation_pricing_config` 的两段强类型列,后台「模型定价」页面直接编辑,保存与 `models` 段在同一事务内生效,改价不再需要改文件或重启进程。读取方向按契约归一化:表内缺键用默认 JSON / 覆盖文件同段值补齐,契约之外的遗留键剔除并告警;写入方向要求两段完整合法。公开读模型 `GET /api/editor/generation-pricing` 的 3D 段形状保持不变,画布 3D 入口的价格真相与前端零兜底口径不变。详见 [ADR 0005](../adr/【ADR】0005-3D生成定价迁入SpacetimeDB与后台编辑-2026-09-23.md) 与 [编辑器模型定价配置管理方案](../【编辑器】模型定价配置管理方案-2026-06-22.md)。
扣费依据固定为**提交时定价**:用户在提交前就能得到确定价格,失败退款是单次冲正,不需要按实际消耗退差额。provider 返回的实际消耗只写日志用于成本对账,不进入契约、不进 API 响应、不进资源行。
价格随 job 写入 `price_mud_points`,扣费与冲正沿用现有资产操作账务(attempt 级 consume / refund ledger);素材侧继续用 `generation_cost_mud_points` 记录单次成本。同一 `Idempotency-Key` 命中已有 job 时直接返回原 operation,不新建、不重复扣费。
@@ -199,6 +201,6 @@ width/height = 预览图像素尺寸
## 未决问题
- add-on 价格目前是跨模型统一一份;若 Tripo 后续对某个模型版本差异化加价,再补“按模型版本覆盖”一层,现在不预留。
- 加价项已按端点各持一份价目(当前数值相同);若 Tripo 后续对某个模型版本差异化加价,再补“按模型版本覆盖”一层,现在不预留。
- 3D 模型的多文件 glTF 兄弟资源(`.bin`、贴图)、多视角与模型信息展示仍未安排;画布侧接入见 [`【实施计划】Tripo生成结果前端预览接入-2026-09-21.md`](../project-memory/plans/【实施计划】Tripo生成结果前端预览接入-2026-09-21.md)。
- AGC 项目画布自带两份格式判断(`resourceModelScene.ts``isResourceModelFbx``octet-stream` 当 FBX、`resourceCardPreviewModel.ts` 的资源卡预览按扩展名正则选解析器),本期不动,后续统一收敛到 `packages/model3d-viewer`;素材库行、后台 renderer、精选 read model 与公开 grant 的 3D 展示同样未进入。
@@ -813,7 +813,7 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复
- Rust 结构体:`EditorGenerationPricingConfig`
- 源码:`server-rs/crates/spacetime-module/src/editor_project_storage.rs`
- 说明:图片画布生成类模型定价全局配置表,当前使用固定 `config_id = global``models: Vec<EditorGenerationModelPricing>` 强类型保存模型、定价单位、单价或档位列表,procedure / `spacetime-client` 边界不传递不透明 JSON;模块事务会再次校验完整正式模型矩阵、必需档位、单位、正价格和重复键。`writer_identity` 只保留在 private 表内,记录首次初始化的真实 `ctx.sender()`,不进入 procedure 返回快照;后续 `upsert_editor_generation_pricing_config_and_return` 只允许同一 identity 或已授权迁移操作员修改价格,但始终保留原 writer。表为空时只有 HTTP 角色通过 `initialize_editor_generation_pricing_config_if_missing_and_return` 在单事务内仅缺失时种子入库;worker / controller 启动只调用受鉴权的 queue-stats procedure 做只读身份预检。后台保存请求携带 `AppConfig` 中的受保护 bootstrap secret,以便配置行意外缺失时原子恢复;表存在时 bootstrap secret 不能接管 writer。原始 bootstrap secret 固定为 64 位十六进制;WASM 只嵌入其 SHA-256procedure 对入参原文重新计算摘要并做常量时间比较。runtime queue / 钱包 procedure 只接受精确 writer,当前生产 API / worker / controller 因此继承同一 runtime token;迁移操作员不自动获得在线运行权限,且 operator / writer 身份必须互斥。SpacetimeDB 不可达时仅使用默认 JSON 或旧 override 缓存兜底。
- - 说明:图片画布生成类模型定价全局配置表,当前使用固定 `config_id = global``models: Vec<EditorGenerationModelPricing>` 强类型保存模型、定价单位、单价或档位列表,procedure / `spacetime-client` 边界不传递不透明 JSON;模块事务会再次校验完整正式模型矩阵、必需档位、单位、正价格和重复键。`writer_identity` 只保留在 private 表内,记录首次初始化的真实 `ctx.sender()`,不进入 procedure 返回快照;后续 `upsert_editor_generation_pricing_config_and_return` 只允许同一 identity 或已授权迁移操作员修改价格,但始终保留原 writer。表为空时只有 HTTP 角色通过 `initialize_editor_generation_pricing_config_if_missing_and_return` 在单事务内仅缺失时种子入库;worker / controller 启动只调用受鉴权的 queue-stats procedure 做只读身份预检。后台保存请求携带 `AppConfig` 中的受保护 bootstrap secret,以便配置行意外缺失时原子恢复;表存在时 bootstrap secret 不能接管 writer。原始 bootstrap secret 固定为 64 位十六进制;WASM 只嵌入其 SHA-256procedure 对入参原文重新计算摘要并做常量时间比较。runtime queue / 钱包 procedure 只接受精确 writer,当前生产 API / worker / controller 因此继承同一 runtime token;迁移操作员不自动获得在线运行权限,且 operator / writer 身份必须互斥。SpacetimeDB 不可达时仅使用默认 JSON 或旧 override 缓存兜底。 3D 生成定价按端点拆成 `text_to_model_pricing` / `image_to_model_pricing` 两列(追加在结构体末尾并带显式默认值),列内同样展平为强类型行:版本键一行含无贴图 / 带贴图两档价,加价项一行一条,不在一段里用端点做判别键;两段各自要求全版本 × 两档价与六个加价项键齐全。api-server 读取时按契约归一化——表内缺键用默认 JSON / 覆盖文件同段值补齐,契约之外的遗留键剔除并告警,只有归一化后仍不合法才判非法;后台保存要求两段完整合法并与 `models` 同事务写入。公开读模型 `GET /api/editor/generation-pricing` 的 3D 段仍是既有的按端点判别投影,不随存储形状变化。
- 索引:主键 `config_id`
### `editor_generation_operation`
@@ -7,31 +7,36 @@
## 配置来源
- 默认配置文件:`server-rs/crates/api-server/config/editor-generation-pricing.default.json`
- 运行时事实源:SpacetimeDB `editor_generation_pricing_config` 全局配置表,固定 `config_id = global`,以强类型 `models` 列保存模型、单位、单价和档位列表。
- 旧运行时覆盖文件仅作为迁移兼容种子读取;当前兼容路径为 `/var/lib/genarrative/editor-generation-pricing/editor-generation-pricing.override.json`,并在默认新路径不存在时兼容读取旧 `.app/editor-generation-pricing.override.json`。后台保存不再写文件。
- 运行时事实源:SpacetimeDB `editor_generation_pricing_config` 全局配置表,固定 `config_id = global`,以强类型 `models` 列保存模型、单位、单价和档位列表,并以 `text_to_model_pricing` / `image_to_model_pricing` 两个列分端点保存 3D 生成定价
- 旧运行时覆盖文件仅作为迁移兼容种子读取;当前兼容路径为 `/var/lib/genarrative/editor-generation-pricing/editor-generation-pricing.override.json`,并在默认新路径不存在时兼容读取旧 `.app/editor-generation-pricing.override.json`。后台保存不再写文件。图片 / 视频 / 音频与 3D 段都以 SpacetimeDB 为准,覆盖文件只承担「表为空时的种子」与「SpacetimeDB 暂不可达时的兜底」。
默认文件进入 Git,作为空表或 SpacetimeDB 暂不可达时的兜底。只有 HTTP 角色会在启动恢复阶段用当前本地缓存尝试初始化空的 `editor_generation_pricing_config`;本地缓存可来自默认 JSON,也可来自旧 override 文件。`external-generation-worker` / `external-generation-controller` 不调用定价 initializer,而是在启动时通过只读的 queue-stats procedure 验证当前 SpacetimeDB identity 是否具备队列运行权限。生产 API 发布脚本会在切换 `current` 前把旧 release 下的 `.app/editor-generation-pricing.override.json` 迁移到 `/var/lib/genarrative/editor-generation-pricing/`,避免既有后台自定义价格被默认值覆盖。
## 配置结构
### 3D 生成定价(`model3d` 段2026-09-21 增补
### 3D 生成定价(两段并列结构2026-09-23 起后台可编辑
Tripo 3D 生成的计费形态是「底价 + 可叠加 add-on」,现有「模型 → 档位 → 单价」的 `models` 记录形状表达不了,因此在同一份定价文件里新增并列的 `model3d` 段,形状与 `models` 无关
Tripo 3D 生成的计费形态是「底价 + 可叠加加价项」,现有「模型 → 档位 → 单价」的 `models` 记录形状表达不了。3D 定价按端点拆成并列的两段,各段承载该端点自己的整套设置,不在一段里用端点做判别键
```json
"model3d": {
"basePrices": {
"text-to-model": { "<modelVersion>": { "noTexture": , "texture": } },
"image-to-model": { "<modelVersion>": { "noTexture": , "texture": } }
"textToModelPricing": {
"versionPrices": { "<modelVersion>": { "noTexture": , "texture": } },
"addOnPrices": { "hdTexture": , "ultraTexture": , "hdGeometry": , "quadMesh": , "smartLowPoly": , "generateParts": }
},
"addOnPrices": { "hdTexture": , "ultraTexture": , "hdGeometry": , "quadMesh": , "smartLowPoly": , "generateParts": }
"imageToModelPricing": {
"versionPrices": { "<modelVersion>": { "noTexture": , "texture": } },
"addOnPrices": { "hdTexture": , "ultraTexture": , "hdGeometry": , "quadMesh": , "smartLowPoly": , "generateParts": }
}
}
```
- 事实源仍是同一份文件配置(默认 JSON 与旧 override 种子),但**不写入 SpacetimeDB `models` 记录**`editor_generation_pricing_to_records` 只序列化 `models`,运行时读取时由 `editor_generation_pricing_from_record` 用本地文件配置补回 `model3d`。因此后台「模型定价」页面改价**不会覆盖** 3D 段,改 3D 价格必须改文件并重启进程
- `model3d` 段存在时按强校验解析:两个端点 × 契约支持的全部模型版本都必须给出 `noTexture` / `texture` 两个底价键,缺键即启动失败;段缺失则 3D 提交一律 fail closed,不扣费也不调用 provider
- 两段各自持有全部受支持模型版本的 `noTexture` / `texture` 底价和本端点自己的六个加价项键;两段加价项当前数值相同,仍各自保留一份,便于后续按端点差异化。加载时按段独立强校验:模型版本键取自 `Model3dModelVersion` 枚举,每段必须给出全部受支持版本 × 两种贴图态与六个加价项键,缺一即该段非法;段内不写 `unit`3D 生成恒为按次计价
- 事实源是 SpacetimeDB`editor_generation_pricing_config` 表新增 `text_to_model_pricing` / `image_to_model_pricing` 两列,按 `models` 同样的展平风格存键值行(版本键一行含两档价,加价项一行一条),字符串键由 api-server 解析回枚举并强校验。后台「模型定价」页面直接编辑两段,保存与 `models` 段在同一事务内生效,改价不再需要改文件或重启进程
- 读取方向做契约归一化:表内两段缺键(例如发布新增模型版本后表里还没有该键)时,用默认 JSON / 覆盖文件同段值补齐;契约之外的遗留键(例如版本下线后表里的旧键)剔除并告警;只有归一化后仍不合法才判配置非法。**写入方向不做宽松**:后台保存要求两段都显式给出且完整合法。
- 覆盖文件在 3D 上只保留两个用途:SpacetimeDB 表为空 / 不可达时的兜底,以及表内 3D 段缺失时的首次种子。不再存在「改文件 + 重启」作为 3D 改价的生效路径。
- 两段缺失仍是合法形态:缺失即 3D 定价未配置,提交被拒绝(fail closed),既不扣费也不调用 provider。
- 默认价已确认并写入受控默认 JSON,换算口径固定为 **泥点 = `ceil(0.8 × Tripo 官方 credit)`**:文生 3D 的 H 系列 `10 / 20`、P1 `30 / 40`、P2 `100 / 110`,图生 3D 的 H 系列 `20 / 30`、P1 `40 / 50`、P2 `100 / 110`add-on 为 `HD Texture 10``8K Ultra Texture 20``HD Geometry 20``Quad Mesh 5``Smart Low-poly 10``Generate Parts 20`。泥点数值是产品定价,不是 provider 的 credit 数值;改价必须先改 credit 表并让 `editor_generation_config` 的定向用例同步,禁止写入未确认的猜测值。
- 后续把 `models` 记录形状扩展成能表达「端点 × 模型版本 × 是否有贴图 + add-on」后,再把 3D 段迁入 SpacetimeDB 并接后台编辑,届时删除本节的例外说明。
- 关联规范:[技术方案 Tripo 3D生成API集成](technical/【技术方案】Tripo 3D生成API集成-2026-09-21.md)、[后端架构 server-rs与SpacetimeDB数据契约](【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md)。
定价只按模型区分,不按用途区分。同一个模型用于多个入口时必须读取同一条模型配置:
@@ -77,7 +82,7 @@ SpacetimeDB 模块会在事务内重复执行同等强度的校验,并拒绝
- `GET /api/editor/generation-pricing`:主站读取当前模型定价。
- `GET /admin/api/editor-generation-pricing`:后台读取当前模型定价。
- `POST /admin/api/editor-generation-pricing`:后台保存完整模型定价,并写入 SpacetimeDB `editor_generation_pricing_config`;只有 procedure 入库成功后才更新进程内缓存并返回成功,不能把“仅内存生效”当作保存成功。
- `POST /admin/api/editor-generation-pricing`:后台保存完整模型定价,`models` 与 3D 两段都必须显式给出(缺段即 400,不接受「省略即沿用」),并写入 SpacetimeDB `editor_generation_pricing_config`请求必须携带读取时得到的定价版本,与服务端当前版本不一致时返回 409 并要求重新读取,不允许静默覆盖他人改动;只有 procedure 入库成功后才更新进程内缓存并返回成功,不能把“仅内存生效”当作保存成功。
后端 `AppState` 启动时加载默认配置和旧 override 作为本地缓存;接口读取优先走 SpacetimeDB。表为空时调用 `initialize_editor_generation_pricing_config_if_missing_and_return`,在单事务内仅缺失时种子入库,不能使用“先读空、再无条件 upsert”的两事务流程。首次写入把真实 `ctx.sender()` 保存为表内 `writer_identity`;procedure 对外返回的定价快照不包含该身份字段,公开主站和后台仍只经 BFF 读取价格。表已存在时 initializer 只接受同一 writerbootstrap secret 和迁移操作员都不能借该入口接管既有 writer。后续后台保存只允许同一 writer identity 或已授权迁移操作员,但即使由迁移操作员修复价格也必须保留原 writer;若运行中的配置行意外缺失,保存请求会携带 `AppConfig` 已读取的受保护 bootstrap secret 完成原子首次写入,已有配置不会消费该 secret,也不会隐式轮换 writer。后台用户 ID 只记录审计信息,不能充当数据库授权。SpacetimeDB 暂不可达时才使用本地缓存兜底。
@@ -105,6 +110,13 @@ SFX V2 上线前已经存在的 SpacetimeDB 定价快照或旧本地 override
- 定价单位:按次 / 按秒。
- 单价或档位价格;生图模型展示 `0.5K / 1K / 2K` 等尺寸档位,视频模型展示 `480p / 720p / 1080p` 等分辨率档位。
3D 生成定价在同一页面的独立区块编辑,按端点分成「文生 3D」与「图生 3D」两组:
- 每组是一张「模型版本 × {无贴图, 带贴图}」的数字表,加上该端点自己的六个加价项单值输入。
- 模型版本键与加价项键都来自接口返回,后台不硬编码版本清单,也不能增删键、不能停用整段;新增模型版本仍是「先改契约枚举与默认 JSON,再发布」的动作。
- 提交前要求全部数值 ≥ 1,否则禁用保存并给出中文提示;服务端 400 的中文错误原样透出,保存成功后用回包覆盖本地状态。
- 服务端返回 409 时提示「定价已被其他人更新,请刷新后重新编辑」,并提供重新读取入口。
## 前端展示
主站画板启动后调用 `GET /api/editor/generation-pricing`,成功后覆盖前端内置兜底价格并触发重渲染。接口失败时保留内置兜底,避免画板不可用。图片类价格计算必须传入当前模型和 `imageSize`;规范生成固定读取 `gpt-image-2``2K` 定价。
@@ -118,3 +130,9 @@ SFX V2 上线前已经存在的 SpacetimeDB 定价快照或旧本地 override
- 部署脚本 Bash 语法、生产运维静态门禁、64 位十六进制 secret 校验、manifest 摘要匹配、Build / Publish credential ID 一致性、bootstrap secret 明文日志扫描和 FILE 权限 / 服务重启检查。
- 前端价格读取、运行时覆盖、图片尺寸档位计算测试。
- 管理端模型定价页面单位展示和档位保存测试。
- 3D 定价两段的加载归一化(缺键补齐、契约外键剔除)、后台保存与重启读取、缺段 400、版本不匹配 409、公开读取仍返回 3D 段且形状不变的测试。
- 管理端 3D 区块的渲染、数值校验、保存 payload 与 409 提示测试;画布 3D 入口在后台改价后立即读到新价的定向验证。
## 未决问题
- 现网覆盖文件(`/var/lib/genarrative/editor-generation-pricing/editor-generation-pricing.override.json`)若存有旧形状的 3D 段(`basePrices` 端点判别结构),迁移后的处理方式待确认:推荐在文件读取方向做一次白名单兼容(旧形状折算成两段并打废弃告警),发布脚本迁移完成、确认现网文件不再含 3D 段后删除兼容分支与用例;不推荐「旧形状解析失败即拒绝启动」。