修复旧模型定价覆盖缺少ElevenLabs音效模型导致启动恢复持续失败 支持对话框回车发送并保留Shift换行和输入法组合态 重试受理后立即清理旧失败投影并展示新Run状态 阻止Windows后台Codex探测反复弹出控制台窗口 首板试玩持久回执通过后幂等登记初始项目版本 补充定价、交互、Windows与版本登记回归测试和文档
13 KiB
编辑器模型定价配置管理方案
背景
图片画布的生成图片、生成规范、生成角色、生成素材、生成 UI、宣发素材、快速编辑、图片修改、生成视频、角色动作、UI 设计图提取素材、音效和背景音乐都需要在按钮上展示泥点消耗,但生成请求不携带 priceMudPoints。实际扣费金额由后端按运行时模型定价配置统一计算。定价不能散落在前端组件和具体 handler 中,也不能按“图片 / 规范”“视频 / 动作”等用途拆出不同价格事实源。
配置来源
- 默认配置文件:
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。后台保存不再写文件。
默认文件进入 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/,避免既有后台自定义价格被默认值覆盖。
配置结构
定价只按模型区分,不按用途区分。同一个模型用于多个入口时必须读取同一条模型配置:
gpt-image-2同时用于普通图片、规范、UI 设计等图片类入口。seedance2.0-fast同时用于生成视频和角色动作;角色动作入口仍只允许该模型。
{
"models": {
"gemini-3.1-flash-image-preview": {
"unit": "perGeneration",
"prices": { "0.5K": 8, "1K": 12, "2K": 24 }
},
"gpt-image-2": {
"unit": "perGeneration",
"prices": { "1K": 3, "2K": 5 }
},
"seedance2.0-fast": {
"unit": "perSecond",
"prices": { "480p": 10, "720p": 20, "1080p": 40 }
},
"audio1.0": { "unit": "perGeneration", "price": 5 },
"eleven_text_to_sound_v2": { "unit": "perGeneration", "price": 5 },
"chirp-v5": { "unit": "perGeneration", "price": 12 }
}
}
字段规则:
unit:定价单位,perGeneration表示按次,perSecond表示按秒。price:单一价格,适合音效、背景音乐等单次生成模型。prices:档位价格,图片模型按尺寸档位配置,视频模型按分辨率配置。- 生图模型必须补齐支持尺寸:
gemini-3.1-flash-image-preview配0.5K / 1K / 2K,gpt-image-2配1K / 2K。 - 新编辑器 SFX 只读取
eleven_text_to_sound_v2;audio1.0继续保留为历史 Vidu 配置兼容键,两者均按次独立配置。
后端保存前校验当前正式模型、必要尺寸和必要分辨率都存在且大于 0。
SpacetimeDB 模块会在事务内重复执行同等强度的校验,并拒绝重复模型、重复档位和单位不匹配。procedure 与 spacetime-client 之间传递强类型模型 / 档位列表,不传递 pricing_json 字符串。
后端契约
GET /api/editor/generation-pricing:主站读取当前模型定价。GET /admin/api/editor-generation-pricing:后台读取当前模型定价。POST /admin/api/editor-generation-pricing:后台保存完整模型定价,并写入 SpacetimeDBeditor_generation_pricing_config;只有 procedure 入库成功后才更新进程内缓存并返回成功,不能把“仅内存生效”当作保存成功。
后端 AppState 启动时加载默认配置和旧 override 作为本地缓存;接口读取优先走 SpacetimeDB。表为空时调用 initialize_editor_generation_pricing_config_if_missing_and_return,在单事务内仅缺失时种子入库,不能使用“先读空、再无条件 upsert”的两事务流程。首次写入把真实 ctx.sender() 保存为表内 writer_identity;procedure 对外返回的定价快照不包含该身份字段,公开主站和后台仍只经 BFF 读取价格。表已存在时 initializer 只接受同一 writer,bootstrap secret 和迁移操作员都不能借该入口接管既有 writer。后续后台保存只允许同一 writer identity 或已授权迁移操作员,但即使由迁移操作员修复价格也必须保留原 writer;若运行中的配置行意外缺失,保存请求会携带 AppConfig 已读取的受保护 bootstrap secret 完成原子首次写入,已有配置不会消费该 secret,也不会隐式轮换 writer。后台用户 ID 只记录审计信息,不能充当数据库授权。SpacetimeDB 暂不可达时才使用本地缓存兜底。
所有会调用外部生成 provider 的编辑器生成请求都必须由后端计算价格,前端请求不提交价格字段;同步执行按当前运行时配置进入 execute_billable_asset_operation_with_cost 预扣泥点,预扣失败不得继续调用上游。外部生成队列在入队时把价格写入 external_generation_job.price_mud_points,worker 必须用该冻结价格完成扣费、退款、响应和资产成本持久化,配置更新不得改变已入队任务金额。普通图片、规范、角色、UI 设计、宣发素材、快速编辑 / 图片修改、图标 spritesheet、UI 设计图提取素材、视频、角色动作、音效和背景音乐均遵循该规则。背景色决策(gpt-5-mini)本身也是一次上游调用,同样必须在预扣泥点之后发起:预扣前只做颜色无关的算价 / 校验(动画用默认色占位算价),决策放进 billable 闭包,余额不足则决策不跑、决策失败走失败退款。需要向前端展示实际扣费时,由后端在响应中返回 priceMudPoints。
SFX V2 上线前已经存在的 SpacetimeDB 定价快照或旧本地 override 可能只有 audio1.0。读取这类历史快照时,api-server 只允许从当前受控默认配置补入缺失的 eleven_text_to_sound_v2 条目,再执行完整配置校验,使旧快照可继续读取;其它必需模型缺失仍失败。该兼容不修改 schema,也不在读取时写数据库或 override;下一次后台保存完整定价矩阵时自然持久化新键。发布前仍应确认运行时配置中的新键和价格已经批准。
运行时身份首次授权
模型定价 writer、外部生成队列和钱包调用都以真实 SpacetimeDB ctx.sender() 校验运行时服务 identity。原始 bootstrap secret 固定为 64 位十六进制;首次授权使用与当前 spacetime_module.wasm 构建时注入 SHA-256 摘要对应的原始值,模块收到原始值后重新计算 SHA-256 并做常量时间比较,WASM 只嵌入摘要、不嵌入原文。bootstrap secret 只能在配置表为空时建立首个受信身份,表存在后不能重复使用。queue 和钱包 runtime guard 只接受精确 writer_identity,迁移操作员身份不自动获得在线生成或钱包权限;因此当前生产 API、worker 和 controller 必须继承同一份 runtime token。非 HTTP 角色只做 queue procedure 鉴权预检,不具备 seed 或轮换身份的职责。migration operator 与 runtime writer 必须互斥:任何已登记 operator 都不能成为 writer,当前 writer 也不能被授权为 operator;一旦已有 operator,bootstrap secret 不得再新增或接管 operator。
运行时 token 轮换必须由已授权迁移操作员显式调用 rotate_editor_generation_runtime_service_identity_and_return,传入新 identity、操作员用户 ID 和原因。该 procedure 会拒绝把新 writer 设成当前 writer 或任一已登记 migration operator,只修改 writer,不覆盖模型价格,并向 editor_generation_runtime_identity_rotation 写入旧 writer、新 writer、迁移操作员 identity、操作人、原因和服务端时间。生产使用 scripts/deploy/production-runtime-writer-identity-rotate.mjs:要求当前 SpacetimeDB CLI 登录 identity 与 --operator-identity 一致,并通过 --next-writer-identity / --confirm-next-writer-identity 双录确认;执行成功后必须核对审计行,再切换 API token。不得把 API 启动、普通定价 upsert 或 bootstrap secret 复用成隐式轮换流程。
- 本地
npm run dev持久复用专用 API identity token 和 runtime bootstrap secret,分别写入<spacetimeDataDir>/dev-api-identities/<serverSha256>.json与<spacetimeDataDir>/dev-runtime-service-bootstrap-secrets/<scopeSha256>.json;两个 gitignored 文件都必须是普通文件且权限为0600。token 和 secret 只进入 api-server,不得输出明文,也不得继续传给 Web / Vite 子进程。 - 生产环境只配置
GENARRATIVE_SPACETIME_RUNTIME_SERVICE_BOOTSTRAP_SECRET_FILE=/var/lib/genarrative/spacetime/runtime-service-bootstrap-secret.txt,不得把GENARRATIVE_SPACETIME_RUNTIME_SERVICE_BOOTSTRAP_SECRET明文写入 env 文件。直传值优先于 FILE,因此生产 env 中残留直传值必须视为配置错误。 Genarrative-Stdb-Module-Build仅通过受保护 Jenkins Secret File 读取并校验 64 位十六进制原始 secret,构建 shell 计算 SHA-256 后只把GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET_SHA256交给 Rust;WASM、Jenkins artifact 和copyArtifacts过滤器都不包含原文,Stdbrelease-manifest.json只记录非敏感的migration_bootstrap_secret_sha256。Genarrative-Stdb-Module-Publish在发布阶段重新挂载同一个 Secret File,重算摘要并与 manifest 强制匹配后才允许发布;Build / Publish 的MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID必须完全相同,并把文件路径传给production-stdb-publish.sh。module 发布成功后安装为root:genarrative 0440,目录为root:genarrative 0750,目标路径和文件都不能是符号链接。Full Build 中 Stdb 先于 API 发布,因此同一凭据 ID 必须同时透传给 build / publish,Stdb publish 还必须向 API / worker env 原子补齐固定 FILE 路径,覆盖首次 rollout 的旧环境文件。- 替换 FILE 后必须在退出维护模式前重启已运行的 API、外部生成 controller 和 worker。
AppConfig只在进程启动时读取 secret,单纯覆盖文件不会刷新已运行进程。 - 人工运行
build-production-release.sh且未显式提供 secret / SHA-256 时,脚本把自动生成的 64 位十六进制原始 secret 仅写入 gitignored 的server-rs/.spacetimedb/build-secrets/<version>.txt,目录权限0700、文件权限0600;发布包只含带摘要的 WASM,并在 manifest 记录摘要,不含该文件。Jenkins 日志只能记录来源、长度、目标路径和是否启用,不得cat或插值打印原文。
管理端
后台“模型定价”页面按模型统一展示和编辑,不再拆“图片模型 / 规范模型 / 视频模型 / 角色动作”等用途分组。每个模型卡片显示:
- 模型名。
- 定价单位:按次 / 按秒。
- 单价或档位价格;生图模型展示
0.5K / 1K / 2K等尺寸档位,视频模型展示480p / 720p / 1080p等分辨率档位。
前端展示
主站画板启动后调用 GET /api/editor/generation-pricing,成功后覆盖前端内置兜底价格并触发重渲染。接口失败时保留内置兜底,避免画板不可用。图片类价格计算必须传入当前模型和 imageSize;规范生成固定读取 gpt-image-2 的 2K 定价。
后续新增模型时必须先补默认 JSON、后端校验、前端兜底和测试,再暴露到模型选择框。
验证
- 后端配置解析、模块强校验与 writer 授权、SpacetimeDB 保存 / 重启读取、仅 HTTP 角色 seed、非 HTTP 角色 queue-stats 鉴权预检、路由保存与公开读取不返回
writer_identity的测试。 - 外部生成任务在配置变更前后仍按入队价格扣费并落资产的测试;旧 attempt 退款先于迟到 consume 时,持久化 settlement intent 必须阻止迟到扣费;最终 attempt lease 过期后必须直接失败结算且不再返回 provider executor。
- 部署脚本 Bash 语法、生产运维静态门禁、64 位十六进制 secret 校验、manifest 摘要匹配、Build / Publish credential ID 一致性、bootstrap secret 明文日志扫描和 FILE 权限 / 服务重启检查。
- 前端价格读取、运行时覆盖、图片尺寸档位计算测试。
- 管理端模型定价页面单位展示和档位保存测试。