修复编辑器定价持久化与资源预览
将模型定价改为 SpacetimeDB 强类型持久化并兼容旧配置种子迁移 修复快速编辑支付弹窗期间框选显示和交互冻结 补齐后台图片放大以及视频音频资源预览 收紧主站和 External 资源换签授权并记录管理员跨用户审计 固化外部生成入队价格、attempt 钱包结算和最终 lease 失败收口 加固运行时身份轮换、bootstrap secret 与生产构建发布门禁 同步生成绑定、定向测试、运维脚本和项目文档
This commit is contained in:
+1
-1
@@ -35,7 +35,7 @@ Expo React Native 移动壳和 Tauri 桌面壳的工程结构、同源 WebView
|
||||
|
||||
`/editor/canvas` 右侧画布 Agent 对话面板、会话持久化、SSE 事件、附件与生成落画板例外见 [【编辑器】画布Agent对话面板-2026-07-03.md](./【编辑器】画布Agent对话面板-2026-07-03.md);消息正文存 OSS、元数据进 SpacetimeDB 的取舍见 [【ADR】画布Agent会话消息存OSS-2026-07-03.md](./adr/【ADR】画布Agent会话消息存OSS-2026-07-03.md)。
|
||||
|
||||
图片画布生成类面板的模型泥点默认 JSON、运行时 override、后台“模型定价”页面和主站动态下发口径见 [【编辑器】模型定价配置管理方案-2026-06-22.md](./%E3%80%90%E7%BC%96%E8%BE%91%E5%99%A8%E3%80%91%E6%A8%A1%E5%9E%8B%E5%AE%9A%E4%BB%B7%E9%85%8D%E7%BD%AE%E7%AE%A1%E7%90%86%E6%96%B9%E6%A1%88-2026-06-22.md)。
|
||||
图片画布生成类面板的模型泥点默认 JSON、SpacetimeDB 运行时事实源、后台“模型定价”页面和主站动态下发口径见 [【编辑器】模型定价配置管理方案-2026-06-22.md](./%E3%80%90%E7%BC%96%E8%BE%91%E5%99%A8%E3%80%91%E6%A8%A1%E5%9E%8B%E5%AE%9A%E4%BB%B7%E9%85%8D%E7%BD%AE%E7%AE%A1%E7%90%86%E6%96%B9%E6%A1%88-2026-06-22.md)。
|
||||
|
||||
React 组件测试的用户行为、稳定契约、hook / model 分层断言口径,以及避免内部 DOM 探针、图标 class 和完整对象快照式断言的规则见 [【前端测试】React组件测试准则-2026-06-26.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%B5%8B%E8%AF%95%E3%80%91React%E7%BB%84%E4%BB%B6%E6%B5%8B%E8%AF%95%E5%87%86%E5%88%99-2026-06-26.md)。
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -78,6 +78,14 @@
|
||||
- 验证:`cargo test -p api-server --manifest-path server-rs/Cargo.toml editor_reference`,并用前端 workflow 测试覆盖 `referenceImageSrcs` 进入图标生成请求;若快速编辑重开额外参考图,再补对应请求覆盖。
|
||||
- 关联:`server-rs/crates/api-server/src/editor_project.rs`、`server-rs/crates/spacetime-client/src/assets.rs`、`src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts`。
|
||||
|
||||
## 资产换签不能把 generated 前缀当成 objectKey 授权
|
||||
|
||||
- 现象:主站或 External API 只要拿到另一个账号的 generated `objectKey` 就能换签,已登记的私有对象因为 key 同时命中 legacy 前缀而被匿名读取,或者 `/api/assets/read-url` 已拒绝但 `/api/assets/read-bytes` 仍能读出原始字节;后台资源预览为解决跨账号读取又误把主站入口整体放开。
|
||||
- 原因:`legacyPublicPath` 与 `objectKey` 代表两种不同信任边界。前者仅用于未登记历史公开作品兼容,后者是正式对象引用;只检查 generated 前缀、或在查询 `asset_object` metadata 前直接接受 legacy 白名单,都不能证明对象公开或属于调用方。签名 URL 和 bytes proxy 如果各写一套判断也容易漂移。
|
||||
- 处理:`read-url` 与 `read-bytes` 必须共用 `authorize_asset_read_target`,先按配置 bucket / 精确 key 查询 `asset_object`;metadata 一旦存在,即使 key 命中 legacy 前缀,也严格执行 `PublicRead` / owner ACL。只有 metadata 不存在且显式 `legacyPublicPath` 命中 `platform_oss::LEGACY_PUBLIC_PREFIXES` 时才允许匿名兼容;任意 `objectKey` 必须登记。External read-url 使用 API Key owner。主站和 External 的 object confirm owner 必须来自认证主体,同 bucket / key 已登记后不能改变 owner,不能让请求体 owner 接管对象。后台跨 owner 换签只用于管理员资源预览,成功后以 `admin_asset_read_url` 持久化管理员 subject、请求对象和有效期,且不得记录 signed URL;不能为此把 admin 能力下沉到主站入口。无权访问统一返回不存在,避免泄露对象是否存在。
|
||||
- 验证:覆盖未登记 curated legacy public path、命中 legacy 前缀但已有私有 metadata、未登记 objectKey、公开对象、本人私有对象、跨 owner、匿名私有、External owner、confirm owner 不可变和 admin-only endpoint;对 `read-url` 与 `read-bytes` 使用同一组授权矩阵,并断言 Admin 成功换签会生成不含 signed URL 的管理员主体审计事件。
|
||||
- 关联:`server-rs/crates/api-server/src/assets.rs`、`server-rs/crates/api-server/src/external_assets_api.rs`、`server-rs/crates/api-server/src/admin.rs`、`server-rs/crates/api-server/src/modules/admin.rs`。
|
||||
|
||||
## 编辑器生成按钮显示泥点后仍要查真实钱包预扣
|
||||
|
||||
- 现象:画板生成按钮显示 `N泥点`,后端也能按模型配置计算出价格,但用户点击后钱包余额不变。
|
||||
@@ -426,13 +434,13 @@
|
||||
- 顺序补充:正式 runbook 还必须在 `rollback apply` 前预置 Pingora shadow env。启用前和 `--dry-run-cutover` 要求 `80/443` 空闲;启用后 `--require-direct` 复核不再要求端口空闲,因为端口应由 Pingora 占用。回退时要先用 current release 随包 `node -- /opt/genarrative/current/scripts/deploy/pingora-gateway-env-shadow-switch.mjs --apply --env-file /etc/genarrative/pingora-gateway.env` 恢复 `GENARRATIVE_PINGORA_GATEWAY_LISTEN=127.0.0.1:18081` 并清空 TLS / HTTP redirect 低端口监听,再移除 direct-entry drop-in 和重启 Pingora。
|
||||
- 关联:`deploy/systemd/genarrative-pingora-gateway-direct-entry.conf`、`deploy/env/health-patrol.env.example`、`deploy/env/pingora-direct-live.env.example`、`deploy/env/pingora-canary-live.env.example`、`scripts/deploy/pingora-direct-enable.sh`、`scripts/deploy/pingora-direct-rollback.sh`、`scripts/deploy/pingora-tls-cert-sync.mjs`、`scripts/check-pingora-direct-preflight.mjs`、`scripts/check-pingora-direct-live.mjs`、`scripts/ops/pingora-cutover-command-evidence.mjs`、`scripts/ops/pingora-cutover-evidence-verify.mjs`、`scripts/ops/pingora-cutover-evidence-audit.mjs`、`scripts/jenkins-server-provision.sh`、`scripts/build-production-release.sh`、`scripts/deploy/production-api-deploy.sh`、`docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md`。
|
||||
|
||||
## 外部生成 worker 业务失败重试会撞上钱包扣退费幂等
|
||||
## 外部生成 worker 重领必须按 claim attempt 隔离并持久结算
|
||||
|
||||
- 现象:同一个外部生成 job 如果第一次业务失败后退款,再用同一个业务资源 ID 自动重试并成功,钱包 `consume` ledger 可能因为同 ID 已存在而跳过,最终出现“失败已退、成功不再扣”的余额漂移。
|
||||
- 原因:资产操作扣费和退款都用稳定 ledger id 做幂等;这能保护 lease 过期后的崩溃重领不重复扣费,但不适合“已明确失败且已退款”的自动业务重试。
|
||||
- 处理:拼图 `puzzle_compile_draft` 首期设置 `max_attempts=1`,业务失败直接 failed,只保留 running lease 过期后的崩溃重领。后续若要恢复自动 retry,必须先引入 attempt-aware billing 或可配对撤销的账本接口。
|
||||
- 验证:检查 `external_generation_job.max_attempts`、worker 失败回写和钱包 ledger;失败后草稿进入 failed,重试应由用户重新触发新任务,而不是旧 job 自动 pending。
|
||||
- 关联:`server-rs/crates/api-server/src/puzzle/handlers.rs`、`server-rs/crates/api-server/src/asset_billing.rs`、`server-rs/crates/spacetime-module/src/runtime/profile.rs`、`docs/technical/【后端架构】外部生成Worker化方案-2026-06-03.md`。
|
||||
- 现象:同一个外部生成 job 在 worker 崩溃或 lease 过期后重领,可能出现旧 attempt 和新 attempt 都扣费,或者旧 attempt 已退款后新 attempt 因稳定 ledger 被当成幂等而免费执行。
|
||||
- 原因:只按业务资源 ID 或 job ID 生成稳定 ledger 无法区分 claim;仅在新 attempt 开始时“先查旧 consume、存在则退款”仍有竞态,旧 consume RPC 可能在检查之后才提交。
|
||||
- 处理:扣退费 ledger 固定包含 `job_id + claim_attempt`,每次重领先结算所有旧 attempt,再扣当前 attempt。结算必须在 SpacetimeDB `asset_operation_wallet_settlement` 持久化:旧 consume 已存在时原子退款;尚不存在时写取消 intent。任何迟到 consume 在同一事务内看到 intent 后失败关闭。重复 ledger 必须核对用户、金额和来源,不能只按 ID 存在就返回成功。claim 处理 lease 已过期的 `running` job 时还必须先比较 `attempt` 与 `max_attempts`:未耗尽才递增并返回 worker;最终 attempt 已耗尽时在同一事务内把 job 置为 `failed`、清空 lease、写完成时间和失败事件,并按当前 attempt 退款或写取消 intent,绝不能再次返回 provider executor。
|
||||
- 验证:`cargo test -p spacetime-module asset_operation` 覆盖缺 consume 时写 intent、冲突结算拒绝和退款配对;`cargo test -p spacetime-module external_generation::tests::` 覆盖未耗尽 lease 可重领、最终 attempt 只终态收口且不再递增;`cargo test -p spacetime-module wallet_idempotent_replay` 覆盖冲突重放;`cargo test -p api-server asset_billing` 覆盖崩溃重领、重复结算和当前 attempt 扣费。
|
||||
- 关联:`server-rs/crates/api-server/src/asset_billing.rs`、`server-rs/crates/spacetime-module/src/runtime/profile.rs`、`docs/technical/【后端架构】外部生成Worker化方案-2026-06-03.md`。
|
||||
|
||||
## 外部生成队列不再由 HTTP 进程兜底执行
|
||||
|
||||
@@ -2230,6 +2238,15 @@
|
||||
- 验证:重新跑 `Genarrative-Database-Import` 或 `Genarrative-Database-Export`,日志应先打印 `jenkins-toolchain` 的 `node=...` 解析结果,而不是在迁移中途报 `node: command not found`。
|
||||
- 关联:`scripts/jenkins-prepare-toolchain-env.sh`、`jenkins/Jenkinsfile.production-database-import`、`jenkins/Jenkinsfile.production-database-export`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。
|
||||
|
||||
## Runtime bootstrap secret 原文不能进入 WASM 或发布归档
|
||||
|
||||
- 现象:下载 Jenkins Stdb artifact 或检查 `spacetime_module.wasm` 能找到原始 bootstrap secret;Stdb Build / Publish 配了不同 credential,或 Publish 使用的 Secret File 摘要与 release manifest 不一致却仍继续发布;或者 module 已发布、`/var/lib/genarrative/spacetime/runtime-service-bootstrap-secret.txt` 也已更新,但模型定价首次初始化、队列 claim 或钱包调用仍报 identity 未授权。
|
||||
- 原因:把原文作为 Rust 编译环境变量会进入可下载 WASM;把 `migration-bootstrap-secret.txt` 归档会把构建凭据变成长生命周期 artifact。只把摘要编进 WASM、却不在 release manifest 绑定摘要并让 Publish 重算核对,仍可能把另一份 secret 配给已构建 module。另一方面,`AppConfig` 只在 api-server / worker 进程启动时读取直传值或 FILE,覆盖文件不会更新已运行进程;Full Build 又先发 Stdb、后发 API,不能等待后续 API deploy 才补旧 env。
|
||||
- 处理:原始 bootstrap secret 固定为 64 位十六进制。WASM 编译只接受 `GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET_SHA256`,模块对 procedure 入参原文重新计算 SHA-256 并做常量时间比较。生产 Jenkins Build / Publish 必须使用完全相同的 Secret File credential ID:Build 只计算摘要,WASM、artifact 和 `copyArtifacts` 不含原文,Stdb release manifest 记录 `migration_bootstrap_secret_sha256`;Publish 重新读取同一 Secret File、校验 64 位十六进制并重算摘要,与 manifest 强制匹配后才把临时文件路径交给 `production-stdb-publish.sh`。module 发布后安装固定 runtime 文件为 `root:genarrative 0440`、目录 `root:genarrative 0750`,补齐 API / worker env,再在维护模式内重启发布前 active 的 API、controller 和 worker,并执行 API `/healthz` 门禁。人工构建自动生成的原文只放 gitignored `server-rs/.spacetimedb/build-secrets/<version>.txt`,目录 `0700`、文件 `0600`;本地 dev 的 API token 和按 server/database 作用域 secret 也分别持久化为 `0600` 文件。日志不得 `cat` 或插值打印明文。
|
||||
- 身份轮换:bootstrap secret 只允许空表首次授权,不能重复接管既有 writer。migration operator 与 runtime writer 必须互斥:operator 不能成为 writer,当前 writer 不能被授权为 operator;已有任一 operator 后,bootstrap secret 不得新增或接管 operator。生产 token 确需轮换时,使用 `scripts/deploy/production-runtime-writer-identity-rotate.mjs`,由当前已授权 migration operator 登录态双录新 writer identity、填写操作人和原因;procedure 必须拒绝把新 writer 设为当前 writer 或任一 migration operator,并写 `editor_generation_runtime_identity_rotation` 审计。成功后先核对审计,再切换 token,不得靠重启 API 隐式改 writer。
|
||||
- 验证:运行相关部署脚本 `bash -n`、`node --check scripts/dev.mjs scripts/check-production-ops-guardrails.mjs scripts/deploy/production-runtime-writer-identity-rotate.mjs`、`npm run check:production-ops`,扫描 artifact 清单和 diff,确认 Build / Publish credential ID 一致、manifest 摘要与 Publish Secret File 匹配,并确认不存在 `migration-bootstrap-secret.txt`、原文编译环境变量、`cat` 或生产 env 明文键,同时验证 64 位十六进制规则及手工 secret / 本地 token 文件权限。
|
||||
- 关联:`server-rs/crates/spacetime-module/src/migration.rs`、`scripts/dev.mjs`、`scripts/build-production-release.sh`、`scripts/deploy/production-stdb-publish.sh`、`scripts/deploy/production-runtime-writer-identity-rotate.mjs`、`jenkins/Jenkinsfile.production-stdb-module-build`、`jenkins/Jenkinsfile.production-stdb-module-publish`。
|
||||
|
||||
## Windows Jenkins `powershell` step 在 Stdb module 构建里曾触发 CreateProcess error=5
|
||||
|
||||
- 当前状态:已废弃。`Genarrative-Stdb-Module-Build` 已切到 Linux agent,不再执行 Windows PowerShell 流程。
|
||||
@@ -2447,7 +2464,7 @@
|
||||
|
||||
- 现象:`npm run dev` 启动到 api-server 恢复认证投影时,日志出现 `Failed to initiate WebSocket connection ... /v1/database/<db>/subscribe?compression=Brotli: HTTP error: 401 Unauthorized`。
|
||||
- 原因:SpacetimeDB SDK 订阅需要 Web API identity token;本地 `.env.local` 常把 `GENARRATIVE_SPACETIME_TOKEN` 留空,只靠 CLI 登录态 publish 成功并不能让 api-server 的 WebSocket subscribe 获得权限。
|
||||
- 处理:`scripts/dev.mjs` 在 SpacetimeDB 就绪后调用 `/v1/identity` 创建当前进程专用 Web API identity token,并只注入本次 `api-server` 环境;不要把临时 token 写进 `.env.local` 或日志。若仍报 401,先确认是否使用了项目脚本启动、日志是否出现 `已创建本地 Web identity`,以及 `GENARRATIVE_SPACETIME_SERVER_URL` / 数据库名是否指向本次启动的实例。
|
||||
- 处理:`scripts/dev.mjs` 在 SpacetimeDB 就绪后优先读取 `<spacetimeDataDir>/dev-api-identities/<serverSha256>.json`;缺失或不可用时才调用 `/v1/identity` 创建专用 Web API identity token,并以普通 `0600` 文件持久化。token 只注入 `api-server`,不写 `.env.local`、不传 Web / Vite、也不进日志。若仍报 401,先确认是否使用项目脚本启动、记录文件是否因 server 或权限不匹配被重建,以及 `GENARRATIVE_SPACETIME_SERVER_URL` / 数据库名是否指向本次启动的实例。
|
||||
- 验证:`npm run test -- scripts/dev.test.ts`;重新运行 `npm run dev` 后 api-server 启动日志不再出现上述 subscribe 401,`/healthz` 返回 200。
|
||||
- 关联:`scripts/dev.mjs`、`scripts/dev.test.ts`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。
|
||||
|
||||
|
||||
@@ -58,11 +58,11 @@ npm run check:server-rs-ddd
|
||||
- 认证与账号:`/api/auth/*`、`/api/profile/me`,包括短信、密码、微信、refresh session、多端会话和登出。
|
||||
- 个人中心:`/api/profile/*`,包括钱包流水、任务、领奖、充值、反馈、邀请和兑换等账号侧能力。
|
||||
- 平台基础能力:`/api/llm/*`、`/api/speech/volcengine/*`,只保留通用 LLM 和语音代理。
|
||||
- 资产基础能力:`/api/assets/direct-upload-tickets`、`/api/assets/sts-upload-credentials`、`/api/assets/objects/*`、`/api/assets/read-*`,负责直传、确认、绑定和读取。
|
||||
- 外部 OpenAPI:`/api/external/v1/openapi.json`、`/api/external/v1/assets/direct-upload-tickets`、`/api/external/v1/assets/objects/confirm`、`/api/external/v1/assets/read-url`、`/api/external/v1/editor/*`,使用 Bearer API Key 鉴权;API Key 管理仍在登录态 `/api/profile/api-keys`,不进入外部 OpenAPI JSON。
|
||||
- 资产基础能力:`/api/assets/direct-upload-tickets`、`/api/assets/sts-upload-credentials`、`/api/assets/objects/*`、`/api/assets/read-url`、`/api/assets/read-bytes`,负责直传、确认、绑定和读取。两个读取入口共用同一授权函数,并先按配置 bucket 与精确 key 查询 `asset_object`:一旦存在 metadata,即使 key 命中 legacy 前缀,也必须按 `PublicRead` 或当前登录 owner 授权;只有同 bucket / key 未登记 metadata 的历史对象,才允许显式 `legacyPublicPath` 命中 `platform_oss::LEGACY_PUBLIC_PREFIXES` curated 白名单后匿名兼容。任意未登记 `objectKey`、跨 owner 和匿名私有读取统一返回不存在,`read-bytes` 不得成为绕过 `read-url` 授权的同源代理。
|
||||
- 外部 OpenAPI:`/api/external/v1/openapi.json`、`/api/external/v1/assets/direct-upload-tickets`、`/api/external/v1/assets/objects/confirm`、`/api/external/v1/assets/read-url`、`/api/external/v1/editor/*`,使用 Bearer API Key 鉴权;API Key 管理仍在登录态 `/api/profile/api-keys`,不进入外部 OpenAPI JSON。主站和 External 的 asset object confirm 都必须从已认证主体派生 owner,不能信任请求体 owner;同 bucket / key 已登记后不得改变 owner。
|
||||
- 创作 / 游玩支撑能力:`/api/creation-entry/config`、`/api/ai/tasks*`、`/api/runtime/frontend-config`、`/api/runtime/chat/*`、`/api/runtime/settings`、`/api/runtime/save/snapshot`、`/api/profile/browse-history`、`/api/profile/save-archives*`、`/api/profile/play-stats`、`/api/assets/history`、`/api/assets/character-visual/*`、`/api/assets/character-animation/*`、`/api/assets/character-workflow-cache*`、`/api/assets/hyper3d/*`、`/api/runtime/custom-world/asset-studio/*`、`/api/editor/projects*`、`/api/editor/projects/{projectId}/agent-conversations`、`/api/editor/agent-conversations/{conversationId}*`。`/api/runtime/frontend-config` 由 `api-server` 从运行时环境变量下发非敏感 UI 开关;画板右侧 Agent 入口由 `GENARRATIVE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR` 控制,默认关闭,前端不再读取 `VITE_*` 构建期变量决定生产显示。`/api/runtime/custom-world/asset-studio/*` 解析默认角色形象 / 动作提示词时可以在 OSS 缓存不可用或未配置时按无缓存返回默认提示;保存 workflow 缓存和真实素材读写仍必须要求 OSS 正常可用。
|
||||
- 后台入口配置:`/admin/api/creation-entry/config`、`/admin/api/creation-entry/config/banners` 和 `/admin/api/creation-entry/config/interactions`。
|
||||
- 后台素材查询:`GET /admin/api/editor-assets` 通过 `admin_list_editor_assets_and_return` 后台只读 procedure 读取私有账号级 `editor_asset` 中 `source_type = 'generated'` 的素材,支持 `ownerUserId`、`keyword`、`createdAfter`、`createdBefore`、`cursor` 和 `limit`;返回缩略图 / Object Key、作者展示名、陶泥号、提示词、生成输入和生成成本,只用于查询,不提供分类筛选,也不执行精选审核、返还或展示状态修改,不通过后台 SQL 直查私有表。
|
||||
- 后台素材查询:`GET /admin/api/editor-assets` 通过 `admin_list_editor_assets_and_return` 后台只读 procedure 读取私有账号级 `editor_asset` 中 `source_type = 'generated'` 的素材,支持 `ownerUserId`、`keyword`、`createdAfter`、`createdBefore`、`cursor` 和 `limit`;返回缩略图 / Object Key、作者展示名、陶泥号、提示词、生成输入和生成成本,只用于查询,不提供分类筛选,也不执行精选审核、返还或展示状态修改,不通过后台 SQL 直查私有表。图片放大、视频和音频预览统一调用仅后台管理员会话可访问的 `GET /admin/api/assets/read-url`;该入口仅为预览允许后台跨 owner 签名,不改变主站 `/api/assets/read-*` 或 External API 的 owner 边界。成功换签后必须写入 `event_key = admin_asset_read_url` 的 `tracking_event`,记录管理员 subject、Object Key / legacy path 和有效期,不记录 signed URL。
|
||||
- 自定义世界 / RPG:`/api/runtime/custom-world*`、`/api/story/*`、`/api/runtime/chat/*`。
|
||||
- 拼图:`/api/runtime/puzzle/*`。
|
||||
- 抓大鹅 Match3D:`/api/creation/match3d/*`、`/api/runtime/match3d/*`。
|
||||
@@ -217,10 +217,11 @@ npm run check:server-rs-ddd
|
||||
1. 新用户账号完成注册并成功同步正式认证表后,注册赠送金额读取 `profile_wallet_config.initial_mud_points`;后台通过 `/admin/api/profile/wallet-config` 维护“账号初始泥点数”。未写入配置时默认仍为 `100` 泥点。流水原因仍使用 `new_user_registration_reward`,流水 ID 继续保持幂等,重复发放请求不得叠加余额。
|
||||
2. 用户钱包余额对外仍暴露为一个总余额,但后端扣费时优先消耗 `profile_membership.cycle_remaining_points` 中的会员周期限时泥点,再消耗普通永久泥点;扣费流水 `metadata_json` 会记录 `membershipPeriodPointsDelta`、`permanentPointsDelta` 和限时泥点所属 `cycleResetsAtMicros`,退款会按原消费流水优先恢复同一周期的限时泥点,前端不得自行决定扣费桶。
|
||||
3. 每日免费泥点当前由每日任务体系发放,流水来源为 `daily_task_reward`,任务进度和可领取状态按北京时间每日刷新;它不参与会员 `cycle_resets_at`,也不由前端合并进会员周期泥点。
|
||||
4. 编辑器画板所有会调用外部生成 provider 的入口都不从前端请求接收 `priceMudPoints`;实际扣费真相以后端运行时模型定价配置为准,前端按钮泥点只作为展示。
|
||||
4. 编辑器画板所有会调用外部生成 provider 的入口都不从前端请求接收 `priceMudPoints`;同步请求以 SpacetimeDB `editor_generation_pricing_config` 当前全局配置计算,外部生成队列则以 `external_generation_job.price_mud_points` 保存的入队价格为准,worker 的扣费、退款、响应和资产成本不得按执行时配置重算。前端按钮泥点只作为展示。
|
||||
5. 编辑器图片生成 / 图片修改 / 图标 spritesheet / UI 设计图提取素材 / 视频 / 角色动作 / 音效 / 背景音乐必须在后端计算模型价格后使用 `execute_billable_asset_operation_with_cost` 预扣泥点;预扣失败必须 fail-closed,不得继续提交 VectorEngine、Ark、Suno 或 Vidu 上游任务。
|
||||
6. 音频生成的编辑器链路虽然任务提交和结果发布分离,仍必须把提交时后端计算出的模型价格写入 `AudioAssetBindingTarget.billing_points_cost`,最终发布落资产时按该价格扣费;创作音频目标未提供该字段时才使用旧的创作音频固定成本。
|
||||
7. 编辑器图片生成、图片修改、图标 spritesheet 和 UI 设计图提取素材的参考图可以提交 Data URL 或已登记的 generated objectKey;objectKey 必须归属于当前账号的 `editor_project_resource`、`editor_asset` 或 `asset_object`,后端通过归属校验后才签名读取 OSS。图标素材和 UI 素材提取的额外参考图必须真正传入 provider,不得只写入 `generationInputs` 展示快照;图片快速编辑当前不开放额外参考图,只提交原图或红框序号标注图作为 `sourceImageSrc`。UI 素材提取额外参考图上限为 5 张,普通图片生成上限 5 张,图标素材上限 8 张额外参考图。
|
||||
6. 队列任务按 `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。
|
||||
7. 音频生成的编辑器链路虽然任务提交和结果发布分离,仍必须把提交时后端计算出的模型价格写入 `AudioAssetBindingTarget.billing_points_cost`,最终发布落资产时按该价格扣费;创作音频目标未提供该字段时才使用旧的创作音频固定成本。
|
||||
8. 编辑器图片生成、图片修改、图标 spritesheet 和 UI 设计图提取素材的参考图可以提交 Data URL 或已登记的 generated objectKey;objectKey 必须归属于当前账号的 `editor_project_resource`、`editor_asset` 或 `asset_object`,后端通过归属校验后才签名读取 OSS。图标素材和 UI 素材提取的额外参考图必须真正传入 provider,不得只写入 `generationInputs` 展示快照;图片快速编辑当前不开放额外参考图,只提交原图或红框序号标注图作为 `sourceImageSrc`。UI 素材提取额外参考图上限为 5 张,普通图片生成上限 5 张,图标素材上限 8 张额外参考图。
|
||||
|
||||
## 外部服务与资产
|
||||
|
||||
@@ -233,7 +234,7 @@ npm run check:server-rs-ddd
|
||||
- 敲木鱼敲击物和背景环境图:VectorEngine `/v1/images/edits`,模型固定 `gpt-image-2`。敲击物支持 multipart 多参考图,第一张固定为后端内嵌默认木鱼图,用户上传图只作为新主题参考;prompt 必须要求 `1:1` 单一纯绿色 `#00FF00 / RGB(0,255,0)` 绿幕背景主体图,并禁止黑底、白底、棋盘格和任何实底背景。当前敲击物和返回按钮上传 OSS 前只做服务端绿幕去背后处理,避免泛抠图误伤玉米等主体像素。背景环境图只使用第一步抠图完成后的透明敲击物图作为参考,prompt 必须要求中央主体预留区保持干净,中央 40% 区域禁止出现主题主体、主体局部特写、轮廓影子或重复元素,主题元素只能作为外围氛围,且必须显式声明不继承任何绿色底色、绿幕底色或纯绿色画布。
|
||||
- Hyper3D / Rodin:只保留后端安全代理和旧数据兼容;Rodin 提交、状态、下载和响应解析归属 `platform-hyper3d`,`api-server/src/hyper3d_generation.rs` 只做路由、配置和错误 envelope 映射;新 Match3D 草稿和批量新增不再生成 GLB。
|
||||
- 音频:视觉小说专用音频路由保留;VectorEngine Suno/Vidu provider 协议、任务提交/查询、音频 URL 提取、下载、MIME/extension 归一和 OSS put 请求准备归属 `platform-audio`。`api-server/src/vector_engine_audio_generation.rs` 只做路由、配置、计费、asset object confirm、entity binding 和错误 envelope 映射;拼图、抓大鹅和敲木鱼提示词生成音效入口暂时关闭,通用 `/api/creation/audio/*` 对这些目标返回 `410 Gone`。敲木鱼创作只接收上传 / 录音音频资产;前端选择或录音阶段只在浏览器本地处理待提交音频,统一限制裁切后最长 1 秒、裁掉前后声音过小片段,并用浏览器端近似响度算法平衡到 `-15 LKFS` 后做峰值保护。点击生成时才直传 OSS 并确认 `asset_object`,创作 JSON 只提交轻量 `WoodenFishAudioAsset`,不得继续上传 Data URL 音频;未提供时由 `api-server` 写回内置默认木鱼音 `/wooden-fish/default-hit-sound.mp3`。
|
||||
- OSS:私有 generated legacy path 进入浏览器前必须通过 `/api/assets/read-url` 换签;不要裸请求 `/generated-*`。前端如果收到同一 OSS bucket 的完整 `https://*.oss-*.aliyuncs.com/generated-*` 地址,也必须先归一为 legacy path 后走同一换签链路,避免裸连私有 bucket 403 或绕过签名缓存。OSS 签名、读签名、HEAD 和 PUT 的结构化日志由 `platform-oss` 输出,排查资产写入 / 确认失败时优先按 `operation`、`object_key` / `key_prefix`、`status_class`、`error_kind` 和 `elapsed_ms` 下钻。新上传 generated 私有对象默认写入 `Cache-Control: public, max-age=31536000, immutable`;旧对象若缺该头,只能依赖 `ETag` / `Last-Modified` 协商缓存,应通过 OSS 元数据刷新或 CDN 配置补齐,不要恢复 api-server 静态代理。`editor-agent/` 前缀只用于服务端内部读写画布 Agent 会话消息文档,不属于浏览器直传 legacy public prefix;`/api/assets/direct-upload-tickets` 必须拒绝 `legacyPrefix=editor-agent`,内部读取只允许 `editor-agent/{conversationId}.json` 形态。
|
||||
- OSS:私有 generated path 进入浏览器前必须通过 `/api/assets/read-url` 换签;不要裸请求 `/generated-*`。请求参数的安全语义不能混用:`legacyPublicPath` 是历史公开作品兼容口,只允许 `platform_oss::LEGACY_PUBLIC_PREFIXES` 中的 curated 前缀匿名换签;`objectKey` 是正式对象引用,绝不能复用该前缀旁路,必须查询 `asset_object` 并校验配置 bucket、精确 key、`PublicRead` 或当前 owner。External OpenAPI 的 `/api/external/v1/assets/read-url` 还必须有 `editor:asset` scope,并始终以 API Key 绑定的 `owner_user_id` 执行同一 owner 校验;后台跨账号预览只能走管理员鉴权后的 `/admin/api/assets/read-url`。`/api/assets/read-bytes` 与主站 read-url 共用完全相同的授权,默认仍应由浏览器使用 signed URL 直读,bytes 只作跨域字节读取 fallback。前端如果收到同一 OSS bucket 的完整 `https://*.oss-*.aliyuncs.com/generated-*` 地址,也必须先归一为 legacy path 后走同一换签链路,避免裸连私有 bucket 403 或绕过签名缓存。OSS 签名、读签名、HEAD 和 PUT 的结构化日志由 `platform-oss` 输出,排查资产写入 / 确认失败时优先按 `operation`、`object_key` / `key_prefix`、`status_class`、`error_kind` 和 `elapsed_ms` 下钻。新上传 generated 私有对象默认写入 `Cache-Control: public, max-age=31536000, immutable`;旧对象若缺该头,只能依赖 `ETag` / `Last-Modified` 协商缓存,应通过 OSS 元数据刷新或 CDN 配置补齐,不要恢复 api-server 静态代理。`editor-agent/` 前缀只用于服务端内部读写画布 Agent 会话消息文档,不属于浏览器直传 legacy public prefix;`/api/assets/direct-upload-tickets` 必须拒绝 `legacyPrefix=editor-agent`,内部读取只允许 `editor-agent/{conversationId}.json` 形态。
|
||||
- 外部 API 失败审计:外部供应商调用未成功时,`api-server` 必须发送 OTLP 失败事件并写入 `tracking_event`。VectorEngine 图片 provider 在 `platform-image` 内输出结构化日志和 `PlatformImageFailureAudit`,覆盖 `request_send`、`response_body`、`upstream_status`、`response_parse`、`missing_image` 和 `image_download` 阶段;编辑器 `screenColor=auto` 的 gpt-5-mini 背景色决策同样必须审计每次已发出的 LLM 调用失败,包括传输 / 超时、上游拒绝、响应体解析、空响应和返回候选外颜色;即使随后降级默认背景色并继续主流程也不得只记 warning。`api-server` 将这些失败映射成 `external_api_call_failure`,`scope_kind = module`、`scope_id = provider`、`module_key = external-api`。metadata 固定包含 provider、endpoint、operation、failureStage、statusCode、statusClass、timeout、retryable、errorMessage、latencyMs、promptChars、referenceImageCount、imageModel、rawExcerpt,以及在调用方可获得上下文时补充的 `userId`(触发者)和 `profileId`(草稿 / 作品 / 场景作用域)。图片生成入口应优先把 owner user id 和 profile id 透传到失败审计,不要只保留 provider 级聚合,否则很难按“谁触发、哪个作品触发”定位问题。入库优先复用 tracking outbox,outbox 不可写或保护阈值拒绝时回退同步写 SpacetimeDB;不得新增前端兜底或在 SpacetimeDB reducer 内做外部 I/O。
|
||||
- 外部生成运行记录:所有外部生成编排的完成态统一写入 `tracking_event`,`event_key = external_generation_run`,`scope_kind = module`,`scope_id = provider`,`module_key = external-generation`。metadata 固定包含 `runId`、`provider`、`operation`、`requestLabel`、`requestPayload`、`status`、`success`、`failureReason`、`providerRequestId`、`resultPayload`、`startedAtMicros`、`completedAtMicros` 和 `durationMs`。这类记录只用于运行审计和排障,不再走 `ai_task` 旧表。
|
||||
|
||||
@@ -297,6 +298,7 @@ npm run check:server-rs-ddd
|
||||
|
||||
- Rust 结构体:`AssetObject`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/asset_metadata/objects.rs`
|
||||
- 说明:对象 metadata 以 bucket / key 标识正式对象及其 owner、访问策略。确认接口的 owner 必须来自登录会话或 External API Key 绑定的认证主体,不接受请求体指定 owner;同 bucket / key 首次登记后,重复 confirm 不得改变 owner。资产读取必须先查询同 bucket / key metadata,存在时严格执行 `PublicRead` / owner ACL;只有 metadata 不存在的历史对象才能进入 curated legacy 白名单兼容。
|
||||
|
||||
### `auth_identity`
|
||||
|
||||
@@ -464,6 +466,7 @@ npm run check:server-rs-ddd
|
||||
|
||||
- Rust 结构体:`DatabaseMigrationOperator`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/migration.rs`
|
||||
- 说明:migration operator 与在线 runtime writer 必须身份互斥。当前 runtime writer 不能被授权为 operator,任何已登记 operator 也不能成为 runtime writer;一旦已有 operator,bootstrap secret 不得新增或接管 operator,后续授权只能由既有 operator 完成。
|
||||
|
||||
### `external_api_key`
|
||||
|
||||
@@ -536,6 +539,20 @@ npm run check:server-rs-ddd
|
||||
- 说明:`陶泥儿精选` 首位固定活动卡配置表,当前使用固定 `config_id = global`。后台可配置启用状态、标题、图片地址、提示词、作者和成本文案;上传按钮通过后台受控上传票据把图片写入 OSS,保存时同时落 `image_src`、内部图片 OSS `image_object_key`、`image_width` 和 `image_height`。公开精选接口只在启用时返回该配置,前端优先用 `image_object_key` 走签名读地址展示,并按记录的图片宽高决定活动卡比例。
|
||||
- 索引:主键 `config_id`。
|
||||
|
||||
### `editor_generation_pricing_config`
|
||||
|
||||
- Rust 结构体:`EditorGenerationPricingConfig`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/editor_project_storage.rs`
|
||||
- 说明:图片画布生成类模型定价全局配置表,当前使用固定 `config_id = global`。`models: Vec<EditorGenerationModelPricing>` 强类型保存模型、定价单位、单价或档位列表,procedure / `spacetime-client` 边界不传递不透明 JSON;模块事务会再次校验完整正式模型矩阵、必需档位、单位、正价格和重复键。`writer_identity` 记录首次初始化的真实 `ctx.sender()`;后续 `upsert_editor_generation_pricing_config_and_return` 只允许同一 identity 或已授权迁移操作员修改价格,但始终保留原 writer。表为空时 `api-server` 通过 `initialize_editor_generation_pricing_config_if_missing_and_return` 在单事务内仅缺失时种子入库;表存在时 bootstrap secret 不能接管 writer。原始 bootstrap secret 固定为 64 位十六进制;WASM 只嵌入其 SHA-256,procedure 对入参原文重新计算摘要并做常量时间比较。runtime queue / 钱包 procedure 只接受精确 writer,迁移操作员不自动获得在线运行权限,且 operator / writer 身份必须互斥。SpacetimeDB 不可达时仅使用默认 JSON 或旧 override 缓存兜底。
|
||||
- 索引:主键 `config_id`。
|
||||
|
||||
### `editor_generation_runtime_identity_rotation`
|
||||
|
||||
- Rust 结构体:`EditorGenerationRuntimeIdentityRotation`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/editor_project_storage.rs`
|
||||
- 说明:模型生成运行时服务 identity 显式轮换审计表。只有已授权迁移操作员可调用 `rotate_editor_generation_runtime_service_identity_and_return`,且新 writer 不能等于当前 writer,也不能是任一已登记 migration operator。每次记录旧 writer、新 writer、迁移操作员 identity、操作人、原因和服务端时间;轮换只修改 writer,不覆盖已有模型价格。生产人工入口为 `scripts/deploy/production-runtime-writer-identity-rotate.mjs`,CLI 会核对当前登录 operator identity 并要求双录新 identity。
|
||||
- 索引:自增主键 `rotation_id`。
|
||||
|
||||
### `inventory_slot`
|
||||
|
||||
- Rust 结构体:`InventorySlot`
|
||||
@@ -759,6 +776,13 @@ npm run check:server-rs-ddd
|
||||
- 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs`
|
||||
- 说明:账号钱包流水表。`metadata_json` 为可选 JSON 对象字符串,旧行缺失时读取层按 `{}` 归一;外部生成扣费 / 退款写入 `externalGenerationJobId`,使退款记录可以追溯到对应 `external_generation_job`。
|
||||
|
||||
### `asset_operation_wallet_settlement`
|
||||
|
||||
- Rust 结构体:`AssetOperationWalletSettlement`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs`
|
||||
- 说明:资产操作 consume/refund 配对结算事实表,主键为 consume ledger ID,并保存配对 refund ledger、用户、金额和结算时间。退款先到且 consume 尚不可见时,该表作为持久化取消 intent;迟到 consume 必须检测该行并拒绝扣费,避免 worker 崩溃重领期间双扣。
|
||||
- 索引:主键 `consume_ledger_id`。
|
||||
|
||||
### `profile_wallet_config`
|
||||
|
||||
- Rust 结构体:`ProfileWalletConfig`
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -7,10 +7,10 @@
|
||||
## 配置来源
|
||||
|
||||
- 默认配置文件:`server-rs/crates/api-server/config/editor-generation-pricing.default.json`。
|
||||
- 运行时覆盖文件:默认 `.app/editor-generation-pricing.override.json`。
|
||||
- 生产或特殊环境可通过 `GENARRATIVE_EDITOR_GENERATION_PRICING_OVERRIDE_PATH` 指定可写覆盖文件路径。
|
||||
- 运行时事实源: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,作为空 override 或 override 丢失时的兜底。覆盖文件属于运行态配置,不提交 Git。
|
||||
默认文件进入 Git,作为空表或 SpacetimeDB 暂不可达时的兜底。首次读取发现表为空时,`api-server` 会用当前本地缓存尝试写入 `editor_generation_pricing_config`;本地缓存可来自默认 JSON,也可来自旧 override 文件。生产 API 发布脚本会在切换 `current` 前把旧 release 下的 `.app/editor-generation-pricing.override.json` 迁移到 `/var/lib/genarrative/editor-generation-pricing/`,避免既有后台自定义价格被默认值覆盖。
|
||||
|
||||
## 配置结构
|
||||
|
||||
@@ -49,13 +49,29 @@
|
||||
|
||||
后端保存前校验当前正式模型、必要尺寸和必要分辨率都存在且大于 0。
|
||||
|
||||
SpacetimeDB 模块会在事务内重复执行同等强度的校验,并拒绝重复模型、重复档位和单位不匹配。procedure 与 `spacetime-client` 之间传递强类型模型 / 档位列表,不传递 `pricing_json` 字符串。
|
||||
|
||||
## 后端契约
|
||||
|
||||
- `GET /api/editor/generation-pricing`:主站读取当前模型定价。
|
||||
- `GET /admin/api/editor-generation-pricing`:后台读取当前模型定价。
|
||||
- `POST /admin/api/editor-generation-pricing`:后台保存完整模型定价,并写入 override 文件。
|
||||
- `POST /admin/api/editor-generation-pricing`:后台保存完整模型定价,并写入 SpacetimeDB `editor_generation_pricing_config`;只有 procedure 入库成功后才更新进程内缓存并返回成功,不能把“仅内存生效”当作保存成功。
|
||||
|
||||
后端 `AppState` 启动时加载默认配置和 override。所有会调用外部生成 provider 的编辑器生成请求都必须由后端以运行时配置重新计算价格,前端请求不提交价格字段;计算完成后统一进入 `execute_billable_asset_operation_with_cost` 预扣泥点,预扣失败不得继续调用上游。普通图片、规范、角色、UI 设计、宣发素材、快速编辑 / 图片修改、图标 spritesheet、UI 设计图提取素材、视频、角色动作、音效和背景音乐均遵循该规则。背景色决策(gpt-5-mini)本身也是一次上游调用,同样必须在预扣泥点之后发起:预扣前只做颜色无关的算价 / 校验(动画用默认色占位算价),决策放进 billable 闭包,余额不足则决策不跑、决策失败走失败退款。需要向前端展示实际扣费时,由后端在响应中返回 `priceMudPoints`。
|
||||
后端 `AppState` 启动时加载默认配置和旧 override 作为本地缓存;接口读取优先走 SpacetimeDB。表为空时调用 `initialize_editor_generation_pricing_config_if_missing_and_return`,在单事务内仅缺失时种子入库,不能使用“先读空、再无条件 upsert”的两事务流程。首次写入把真实 `ctx.sender()` 保存为 `writer_identity`;表已存在时 initializer 只接受同一 writer,bootstrap secret 和迁移操作员都不能借该入口接管既有 writer。后续后台保存只允许同一 writer identity 或已授权迁移操作员,但即使由迁移操作员修复价格也必须保留原 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`。
|
||||
|
||||
## 运行时身份首次授权
|
||||
|
||||
模型定价 writer、外部生成队列和钱包调用都以真实 SpacetimeDB `ctx.sender()` 校验运行时服务 identity。原始 bootstrap secret 固定为 64 位十六进制;首次授权使用与当前 `spacetime_module.wasm` 构建时注入 SHA-256 摘要对应的原始值,模块收到原始值后重新计算 SHA-256 并做常量时间比较,WASM 只嵌入摘要、不嵌入原文。bootstrap secret 只能在配置表为空时建立首个受信身份,表存在后不能重复使用。queue 和钱包 runtime guard 只接受精确 `writer_identity`,迁移操作员身份不自动获得在线生成或钱包权限。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` 过滤器都不包含原文,Stdb `release-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` 或插值打印原文。
|
||||
|
||||
## 管理端
|
||||
|
||||
@@ -73,6 +89,8 @@
|
||||
|
||||
## 验证
|
||||
|
||||
- 后端配置解析、override、路由保存与公开读取测试。
|
||||
- 后端配置解析、模块强校验与 writer 授权、SpacetimeDB 保存 / 重启读取、路由保存与公开读取测试。
|
||||
- 外部生成任务在配置变更前后仍按入队价格扣费并落资产的测试;旧 attempt 退款先于迟到 consume 时,持久化 settlement intent 必须阻止迟到扣费;最终 attempt lease 过期后必须直接失败结算且不再返回 provider executor。
|
||||
- 部署脚本 Bash 语法、生产运维静态门禁、64 位十六进制 secret 校验、manifest 摘要匹配、Build / Publish credential ID 一致性、bootstrap secret 明文日志扫描和 FILE 权限 / 服务重启检查。
|
||||
- 前端价格读取、运行时覆盖、图片尺寸档位计算测试。
|
||||
- 管理端模型定价页面单位展示和档位保存测试。
|
||||
|
||||
@@ -78,8 +78,9 @@
|
||||
- 画板内所有会提交外部生成任务的按钮,展示价格都必须从模型定价配置函数推导,不允许在按钮文案中散落固定泥点数字;生成请求不提交 `priceMudPoints`,修改后端模型定价配置后,后端实际扣费和前端下一次拉取到的按钮展示应同步变化。
|
||||
- 后端所有编辑器外部生成入口必须按运行时模型定价配置计算价格后进入 `execute_billable_asset_operation_with_cost`:`/api/editor/images/generations`、`/api/editor/images/edits`、`/api/editor/icon-spritesheets/generations`、`/api/editor/ui-designs/assets/extractions`、`/api/editor/videos/generations`、`/api/editor/character-animations/generations`、`/api/editor/audios/sound-effects/generations`、`/api/editor/audios/background-music/generations` 都不能只展示价格而不真实预扣钱包。
|
||||
- 当前前端展示价统一收口在 `ImageCanvasGenerationModel.ts`:生成图片、生成角色、快速编辑、重绘、宣发素材走 `calculateEditorImageModelPrice` / `calculateEditorImageGenerationPrice`;生成图标素材走 `calculateEditorIconSpritesheetPrice`;生成 UI 设计图走 `calculateEditorUiDesignPrice`;生成规范走 `calculateEditorSpecGenerationPrice`;生成视频走 `calculateEditorVideoPrice`;角色动作走 `calculateCharacterAnimationPrice`;音效 / 背景音乐分别走 `calculateEditorSoundEffectPrice` / `calculateEditorBackgroundMusicPrice`。这些函数启动时会被后端下发配置覆盖,接口失败时才使用内置兜底。定价配置只按模型区分,不按图片 / 规范、视频 / 动作用途拆分;图片类价格必须同时传入模型和 `imageSize`,规范固定读取 `gpt-image-2` 的 `2K` 定价。
|
||||
- 泥点配置默认值独立收口到 `server-rs/crates/api-server/config/editor-generation-pricing.default.json`,JSON 结构为 `models[model] = { unit, price | prices }`;后台“模型定价”页面通过 `POST /admin/api/editor-generation-pricing` 保存完整 override 到 `.app/editor-generation-pricing.override.json`(可由 `GENARRATIVE_EDITOR_GENERATION_PRICING_OVERRIDE_PATH` 覆盖路径),主站通过 `GET /api/editor/generation-pricing` 动态读取当前配置。后台必须展示定价单位:`perGeneration` 显示“按次”,`perSecond` 显示“按秒”。
|
||||
- 泥点配置默认值独立收口到 `server-rs/crates/api-server/config/editor-generation-pricing.default.json`,JSON 结构为 `models[model] = { unit, price | prices }`;后台“模型定价”页面通过 `POST /admin/api/editor-generation-pricing` 保存完整配置到 SpacetimeDB `editor_generation_pricing_config` 全局表,主站通过 `GET /api/editor/generation-pricing` 动态读取当前配置。后台必须展示定价单位:`perGeneration` 显示“按次”,`perSecond` 显示“按秒”。
|
||||
- 生成图标素材、生成视频、角色动画、音效和背景音乐请求只提交生成参数,不提交价格字段;后端按归一后的模型、清晰度、时长或音频模型重新计算并扣费。
|
||||
- 进入外部生成队列的任务在入队时冻结后端计算出的价格;worker 的预扣、退款、响应 `priceMudPoints` 和资产 `generationCostMudPoints` 必须使用同一入队价格,后台修改模型定价只影响之后入队的任务。
|
||||
- `提取素材` 点击后进入 UI 素材提取态:右侧框选工具对齐底部工具栏按钮风格,素材下方显示与生成新素材一致宽度的提取面板。提取面板显示短提示语、框选截图预览、固定模型 `gpt-image-2`、计划规格 `1:1·1K/2K` 和 `提取 · N泥点` 按钮;前端提交 `aspectRatio / imageSize` 等生成参数,后端按 `editor_generation_config` 计算扣费,不能写死。
|
||||
- 画板 UI 统一显示 `nanobanana2`,它代表上游真实模型 `gemini-3.1-flash-image-preview`;历史输入或旧布局中的 `nano-banana` 必须先归一为 `nanobanana2` 对应的真实模型 ID 后再提交和计费。定价表仍以真实模型 ID `gemini-3.1-flash-image-preview` 和 `gpt-image-2` 为准,别名不能作为新增正式模型绕过定价表。
|
||||
|
||||
@@ -128,7 +129,7 @@
|
||||
生成背景音乐:chirp-v5 按次 12 泥点
|
||||
```
|
||||
|
||||
当前必须显式覆盖的正式模型定价配置(默认值来自 `editor-generation-pricing.default.json`,运行时可由后台 override 修改):
|
||||
当前必须显式覆盖的正式模型定价配置(默认值来自 `editor-generation-pricing.default.json`,运行时事实源为 SpacetimeDB `editor_generation_pricing_config`,可由后台模型定价页面修改):
|
||||
|
||||
- 图片模型:`gemini-3.1-flash-image-preview`(UI 显示与历史别名统一为 `nanobanana2`)必须配置 `0.5K / 1K / 2K`;`gpt-image-2` 必须配置 `1K / 2K`。
|
||||
- 视频模型:`seedance2.0-fast`、`seedance2.0`、`kling3.0`、`kling3.0-omni` 按 `480p / 720p / 1080p` 配每秒泥点。兼容旧布局回放的 `veo3.1`、`veo3.1-fast` 也要保留同样分辨率定价配置,但前端模型菜单不展示。
|
||||
|
||||
Reference in New Issue
Block a user