合并 origin/master(52c83cc2a,248 个提交)到 feat/game-purchase
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 5m28s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m26s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 4m41s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m56s
Project CI / Backend tests (pull_request) Successful in 9m4s
Project CI / Repository checks (pull_request) Successful in 7m13s
Project CI / Native shell tests (pull_request) Successful in 10m16s
Project CI / Frontend tests (pull_request) Successful in 2m26s

- 同步 master 侧 248 个提交:会员与泥点计费(PR #588 / #603)、创作者主页与关注粉丝(PR #638)、运行页刷新真重载(PR #640)、AGC 模板 templateVersion 递增(PR #643)等;确认 master 未触碰发行网关与 `api-server/src/modules/game_distribution.rs`。
- 冲突 1(server-rs/crates/module-runtime/src/domain.rs:1157-1162)`RuntimeProfileWalletLedgerSourceType` 两侧各自在末尾追加变体:保留 master 的 `MembershipUpgradeGrant` 为索引 16(线上已落库口径),本分支 `GamePurchase` 顺延为索引 17,并保留其「每账号每游戏一次扣费」注释。
- 冲突 2(server-rs/crates/module-runtime/src/domain.rs:1184-1188)`as_str()` 同时保留 `membership_upgrade_grant` 与 `game_purchase` 两个分支。
- 冲突 3(server-rs/crates/spacetime-client/src/active/mapper/runtime_profile.rs:95-101)bindings→领域枚举映射同时保留 `MembershipUpgradeGrant` 与 `GamePurchase` 两支,按新索引顺序排列。
- 冲突 4(server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_wallet_ledger_source_type_type.rs:43-47)生成枚举按模块源码顺序合并为 …`MembershipUpgradeGrant`、`GamePurchase`;随后 `npm run spacetime:generate` 复核该文件与生成结果逐字节一致。
- 冲突 5(server-rs/crates/api-server/src/admin.rs:4849-4853)sats 索引映射改为 `16 => membership_upgrade_grant` 与 `17 => game_purchase` 两条并存。
- 冲突 6(server-rs/crates/api-server/src/admin.rs:6368-6461)索引断言并入 master 的 `[16] => membership_upgrade_grant`,并把 master 的「17 尚未映射」占位断言替换为 `[17] => game_purchase`;本分支的后台消耗统计两个用例原样保留。
- 冲突 7(server-rs/crates/api-server/src/runtime_profile.rs:219-226)来源文案格式化同时保留 `MembershipUpgradeGrant` 与 `GamePurchase` 两支,注释随 `GamePurchase` 保留。
- 冲突 8(docs/project-memory/shared-memory/decision-log.md:3-24)两条决策记录并列保留:本分支 2026-10-05「播放会话前缀在三处入口清空 Cookie」在前,master 2026-10-03「每日免费发放额为 0 时前端隐藏该池」在后。
- 保住 master 侧:发行响应 ETag/304/gzip(`release_asset_response_with_cache` 五参形态与 `release_asset_etag` / `if_none_match_matches` / `accepts_gzip_encoding` / `gzip_release_asset` / `release_asset_not_modified_response`)、共享加载面 `PlatformGameLoadingSurface` 及其使用方、三池钱包与会员档位目录改造。
- 保住本分支侧:付费作品 404 守卫(`price_mud_points > 0` 且位于 `load_package` 之前)、购买与播放会话网关路由、nginx×3 / Pingora / Vite 的播放会话 Cookie 清空与 `nginx-route-parity.matrix.json` 登记、viewer 感知公开详情、后台审核价格、`PlatformGamePricingField` 共享定价组件与两端接入、AGC 项目版本只读 + 平台派发下一版本号、schema 与迁移白名单、钱包来源 `game_purchase`。
- 验证:`git merge-base --is-ancestor origin/master HEAD` 返回 0;`check:encoding`、`check:doc-index`、`check:spacetime-schema`、`check:server-rs-ddd`、`check:rustfmt`、`check:npm-workspaces`、`check:game-distribution-dto-parity`、`check:game-distribution-price-limit-parity`、`check:pingora-route-parity`、`check:nginx-spa-routes`、根/admin-web/AGC typecheck、`git diff --check` 全绿。
- 测试:`cargo test -p module-game-distribution` 30 passed,`cargo test -p api-server game_distribution` 61 passed,`cargo test -p spacetime-module game_distribution` 7 passed;定向 vitest 28 个文件 252 个用例全通过。
- `npm run spacetime:generate` 除 11 个 procedure 文件的纯空白格式差异外无任何语义改动(`git diff -w` 为空),已保留 master 的生成产物形态,提交内无 schema/绑定语义变化。
This commit is contained in:
2026-10-06 13:12:32 +08:00
201 changed files with 15890 additions and 13199 deletions
+11
View File
@@ -107,6 +107,17 @@
- [画布 Agent 会话消息存 OSS](./adr/【ADR】画布Agent会话消息存OSS-2026-07-03.md)
- [编辑器模型定价配置](./【编辑器】模型定价配置管理方案-2026-06-22.md)
## 钱包与会员计费
- [双余额泥点与会员补差升级实施计划](./technical/【实施计划】双余额泥点与会员补差升级-2026-10-02.md):每日免费 / 月度 / 永久三池并列投影、按开通日自然月计期、年付 12 期与补差升级的规则细则、里程碑与验收判据;已落地(会员购买与升级的 C 端 UI 未做)。
- [泥点三池与会员计费后端设计](./technical/【技术设计】泥点三池与会员计费后端设计-2026-10-02.md):后端 DB schema、账本与幂等、领域数据结构与 HTTP 契约;会员档位改为枚举、目录表以枚举为主键,含破坏性迁移计划、遗留清理清单与落地状态;已落地。
- [会员与泥点前端改造](./technical/【技术设计】会员与泥点前端改造-2026-10-03.md):共享充值弹层改为「会员与泥点」双 Tab(套餐卡 / 周期切换 / 补差升级 / 六档充值 / 套餐对比 / 确认页),金额只读后端目录与报价接口;Web 平台入口、图片编辑器与 AGC 桌面壳三处共用;含已确认决策与验收判据。
- [泥点三池以单一总额为权威](./adr/【ADR】泥点三池以单一总额为权威-2026-10-02.md):存储层保留 `wallet_balance` 单一权威总额,对外并列三池;永久泥点暂为推导余数,另记独立存储的 TODO。
- [会员账期按开通日自然月](./adr/【ADR】会员账期按开通日自然月-2026-10-02.md):北京时间开通日锚点、自然月推进与月末夹取、年付 12 期的期数表达与取整口径。
- [会员订单复用充值订单与补差升级幂等锚点](./adr/【ADR】会员订单复用充值订单与补差升级幂等锚点-2026-10-02.md):会员订单复用 `profile_recharge_order`、`MembershipUpgradeGrant` 账本幂等、后端只读报价与退款人工复核边界。
- [会员档位以枚举为权威](./adr/【ADR】会员档位以枚举为权威-2026-10-02.md):档位身份改为 Rust 枚举、目录表 `profile_membership_plan` 以枚举为主键、删除两处 `tier` 列与旧档位枚举、并发用哨兵值 `128` 表示不设上限。
- [模型权限与并发上限的强制边界](./adr/【ADR】模型权限与并发上限的强制边界-2026-10-03.md):并发上限只在认领事务按账号过滤队列 job、只算 `running`;模型权限只约束 AGC LLM 模型、缺省 `Basic` 由后台人工标 `Full`;raw gpt-image-2 等直连同步生成暂不计入并发。
## 后端、运维与测试
- [BgFilter 受限资源调度方案](./technical/【后端架构】BgFilter受限资源调度方案-2026-07-21.md)
@@ -0,0 +1,57 @@
# 【ADR】会员档位以枚举为权威
状态:已接受(2026-10-02;字段级设计与迁移见
[`【技术设计】泥点三池与会员计费后端设计-2026-10-02`](../technical/【技术设计】泥点三池与会员计费后端设计-2026-10-02.md))
## 背景
- 会员档位今天由 `RuntimeProfileMembershipTier` 枚举承载,但同一个「级别」有两套拼写:旧时长卡命名
`Month/Season/Year` 与现档位命名 `Starter/Basic/Pro/Ultimate` 互为别名(`runtime_profile_membership_tier_rank`
把它们映射到同一 rank);其中 `profile_recharge_product_config.tier` 这个枚举列还长在**在线**表上。
- 会员目录今天不是表:它是 `profile_recharge_product_config` 的四行会员商品(`member_*`)加一组
`module-runtime` 静态查表(rank / 每期泥点 / 排队上限 / 折扣 / 权益矩阵)。
- 新产品档位为 `Starter ¥39/400/¥390 · Plus ¥99/1150/¥990 · Pro ¥299/3650/¥2990 · Max ¥699/8650/¥6990`,
并新增**模型权限**(基础 / 基础+高性能)与**独立任务并发**(1 / 3 / 5 / 不设上限);旧命名与旧档位全部作废。
- 会员侧未上线,无存量会员行;但 SpacetimeDB 的枚举按变体索引编码,删变体会让已落库的行错位。
## 决策
1. **档位身份是 Rust 枚举**:新增 `RuntimeProfileMembershipPlan { Normal, Starter, Plus, Pro, Max }`,
替代字符串 plan id;`Normal` 表示非会员,必须存在。对外 JSON 只传枚举 token(如 `"pro"`),
服务端**严格解析**,不接受自由字符串。
2. **目录表以该枚举为主键**:新建 `profile_membership_plan`,主键 `plan`,字段含
`title` / `rank` / `month_price_cents` / `year_price_cents` / `period_points` / `model_access` /
`concurrent_job_limit` / `sort_order` / `enabled`。一行一档;年价是独立可配置字段,
不做 `月价 × 10` 推导。后台按档位改价与权益。
3. **删除旧档位枚举与两处 `tier` 列**:`profile_membership.tier` 与 `profile_recharge_product_config.tier`
一并删除,`RuntimeProfileMembershipTier` 整体退役;`profile_membership` 新增 `plan`(枚举)与
`cycle_index` / `cycle_count` / `cycle_kind`。
4. **并发用哨兵值而不是 `Option`**:`concurrent_job_limit: u32`,`128` 表示「不设上限」;
`Normal = 1`、`Starter = 1`、`Plus = 3`、`Pro = 5`、`Max = 128`。
5. **模型权限本期只落字段**:`RuntimeProfileMembershipModelAccess { Basic, Full }` 进目录与投影,
但服务端提交路径本期不做拦截。
6. **会员订单复用充值订单**,变更快照字段一律用枚举(`RuntimeProfileMembershipPlan` /
`RuntimeProfileMembershipCycleKind` / `RuntimeProfileMembershipChangeKind`),不用字符串。
## 影响与代价
- 这是对会员侧的一次**破坏性 schema 变更**(删列 + 删枚举),需要明确授权覆盖仓库「枚举只追加」规则:
会员侧无存量行,`profile_recharge_product_config` 是种子配置,可重建 + 重播种。
- 账本来源枚举仍只允许**末尾追加**(新增 `MembershipUpgradeGrant`),钱包侧形状不动。
- 档位比较从「按 `tier` 的别名查表」改为「按目录 `rank`」,升级判断与金额解析都要改到枚举 + 目录。
- 会员目录成为可配置数据后价格不再写死在代码;代价是必须保证 `Normal` 与上下架字段的种子正确,
否则会出现不可购买或语义错误的档位。
## 备选方案与取舍
1. **保留 `plan: String` 当权威,`tier` 只标废弃**:改动最小;代价是档位身份仍有字符串与枚举两套真相,
且后台改价与升级比较继续绑在旧枚举上。已作废。
2. **目录写死在代码(枚举 + 静态映射)**:少一张表、价格可审计;代价是后台无法改价改权益。已作废。
3. **只追加枚举变体(保留 `Month/Season/Year/Basic/Ultimate`)**:最保守;代价是把两套命名与废弃档位
永久留在权威模型里,与新目录语义直接冲突。已作废。
## 明确不做
- 不做 `tier → plan` 的存量回填(会员侧无存量)。
- 本期不执行模型权限与并发上限的服务端拦截,只落字段与展示。
- 不删除任何 `points_*` 充点商品行(历史订单与首充资格引用 `product_id`)。
@@ -0,0 +1,57 @@
# 【ADR】会员订单复用充值订单与补差升级幂等锚点
状态:已接受(2026-10-02;规则细则、里程碑与验收见
[`【实施计划】双余额泥点与会员补差升级-2026-10-02`](../technical/【实施计划】双余额泥点与会员补差升级-2026-10-02.md))
## 背景
现有 `profile_recharge_order` 是「商品 + 金额 + 支付状态 + `points_delta`」的形状,微信支付下单与回调、
未支付订单过期监听、退款结算与对账、后台订单列表五套链路全部围绕它。会员侧未上线,表里没有会员订单;
但会员订单要额外承载「变更前后的档位、周期、补点数、有效期」,而且升级补点必须防重复回调重复发点。
现有钱包幂等全靠**稳定账本 id**:扣费与退款是同一 id 的前缀互换(`asset_operation_consume:` ↔
`asset_operation_refund:`),池子级退款回填依赖这条对称性。账本来源枚举里没有任何「升级补点」类型。
另外,权威契约已经把泥点支付视为「只能扣永久泥点的负余额不变量」,并明确会员充值没有可逆 grant 快照、
退款统一 `manual_review`;新方案引入补差升级后,一条订单对应的权益变化不再只是「加一档」。
## 决策
1. **会员购买 / 升级订单复用 `profile_recharge_order`**,新增 kind 值(会员购买 / 会员升级),
变更快照放到表末尾新增的带默认值字段里;`points_delta` 仍然只表示「钱包里加了几个泥点」。
不新建会员订单专表——复制五套支付与对账链路的代价远大于加宽一张表,而会员侧未上线没有存量要兼容。
2. **补差升级新增账本来源类型 `MembershipUpgradeGrant`**,账本 id 用订单维度
`membership-upgrade-grant:{user}:{order_id}`。重复回调落在同一个账本 id 上,天然幂等;
同一期内连续升级(Starter→Plus→Pro)产生不同 `order_id`,因此不会互相吞掉。
3. **升级报价由后端只读接口计算**(入参:目标档位 + 目标周期),返回应付金额、补点、变更后月度余额、有效期;
下单时后端按同一组输入**重算并落订单快照**,前端只展示。取整规则(金额向上到分、补点向下取整)
只允许有一处实现,前端展示价不是真相源。
4. **支付成功才变更**:失败 / 关闭不动原状态;以 `profile_recharge_order` 的支付成功作为唯一事实,
补点在支付成功回调里落账本并发点。
5. **会员退款维持 `manual_review`**,但订单快照要能回答「变更前后档位、周期、补点数、有效期」,
让人工复核有据可依。自动回滚不做。
6. **连续升级以当前套餐为比较基准**,不回到最初套餐;降档、月转年、年转月一律等到期后再选。
## 影响与代价
- 订单表继续变宽;`kind` 取值会成为契约的一部分,需要在 `shared-contracts` 与后台筛选里同步。
- 报价接口是纯读路径,但必须有契约测试保证「报价返回值 == 下单重算值」,否则会出现「看到的价和扣的钱不一致」。
- 升级补点写的是月度池,退款回填的账本对称性不适用于升级(它没有对应的「扣费」半边),
因此升级的补点只能靠 `manual_review` 回收,不能自动回缴——这是刻意的。
- 账本来源枚举扩容后,后台按来源类型展示与筛选要一起更新;契约文档里的来源全集清单必须同批修订。
## 备选方案与取舍
1. **新建 `profile_membership_order` 专表**:语义最干净、字段不多不少;代价是支付 / 回调 / 退款 /
过期监听 / 后台列表五套链路都要再实现一遍。已作废。
2. **复用 `MembershipPeriodGrant` + metadata `action=upgrade`**:少一个枚举值;代价是来源类型语义变宽,
后台按来源类型统计会混。已作废。
3. **前端本地计算报价,后端下单时校验**:少一个接口;代价是两处取整实现必须逐字段对齐,且报价可被篡改。已作废。
4. **升级走自动回收(像泥点退款一样扣永久池 + 欠账冻结)**:体验一致;代价是要定义「月度点已花掉」的
回滚顺序,且与「只有永久池可被回收」的现有契约冲突。已作废。
## 明确不做
- 不做会员权益的自动回滚与 grant 快照反向执行。
- 不为会员订单新建独立支付通道;不改变现有微信支付回调语义。
- 不做「重复购买」:当前同套餐同周期只显示「当前套餐」,不产生订单。
@@ -0,0 +1,58 @@
# 【ADR】会员账期按开通日自然月
状态:已接受(2026-10-02;规则细则、里程碑与验收见
[`【实施计划】双余额泥点与会员补差升级-2026-10-02`](../technical/【实施计划】双余额泥点与会员补差升级-2026-10-02.md))
## 背景
现有会员周期池是**固定 30 天**的:`profile_membership.cycle_period_days` 默认
`PROFILE_MEMBERSHIP_DEFAULT_PERIOD_DAYS = 30`,`refresh_profile_membership_cycle` 按固定天数步进
`cycle_started_at` / `cycle_resets_at`,并且内置会员商品的 `duration_days` 全是 30——**当前不存在年付,
也没有「一次发 12 期」这回事**。
新产品要求:按开通日计期(9/20 开通 → 10/20 到期,不按自然月)、年付一次购买后按 11 期 + 首发共 12 期逐月发放、
每期刷新先清上期余额再发新额度、到期未续清月度余额但保留永久泥点。同时年付升级要按
「后续完整月数 + 本期剩余时间比例」折算补付金额与补点。
会员侧未上线,没有存量会员数据需要迁移;但 `cycle_*` 字段、惰性刷新与退款回填已经围绕现有口径写好,
改造要保住这三条链路。
## 决策
1. **账期锚点 = 北京时间的开通日时刻**,按自然月推进,且**每一期都从原始开通日重算**而不是从上一期递推,
月末夹取到当月最后一天(1/31 开通 → 2/28 → 3/31,不会漂移成 3/28)。
业务日与每日免费池已经在北京时间口径上,账期必须同口径。
2. **年付表达为「同一 `expires_at` 内的第 n 期」**:新增 `cycle_index`(第几期)与 `cycle_count`(12 或 1),
`cycle_resets_at` 继续当刷新锚点,`profile_membership` 仍然只有一行活跃记录。
`expires_at` = 开通时刻 + 12 个月(同样夹取)。
3. **每期刷新先清上期余额再发新额度**;到期未续:清月度余额、停发,永久泥点保留可用;重新选择套餐
(含月转年、年转月、降档)一律在到期后进行,本期不即时转换。
4. **取整口径固定**:金额向上取到分、补点向下取整。
年付应付 = `(新年价 − 当前年价) / 12 × (后续完整月数 + 本期剩余毫秒 / 本期实际毫秒)`;
年付本期补点 = `(新档每期额度 − 当前档每期额度) × 本期剩余比例`;
后续各期按新档额度发放,原刷新日与到期日不变。
5. **升级后月度余额的上限是自动成立的**:余额 ≤ 当期已发放额度、新档每期额度 ≥ 旧的,因此
「原剩余 + 补点 ≤ 新档每期额度」不需要额外夹取;`cycle_granted_points` 同步补上本次补点,
否则退款回填的上限判据会算错。
## 影响与代价
- 惰性刷新(无 cron,靠每次带符号变动与快照构建触发)保持不变,改造点是推进函数与期数字段。
- 月末夹取与闰年把「本期剩余比例」的分母绑定到当期实际毫秒,需要在领域层用测试固化
(1/31、2/28、闰年 2 月、12/31 跨年)。
- `cycle_period_days` 不再是权威口径,需要明确它是兼容残留还是被 `cycle_count` / 自然月规则取代。
- 旧档位枚举(`Month` / `Season` / `Year` 与 `Starter` / `Basic` / `Pro` / `Ultimate`)与会员权益表里的
按档位列出的字段要一并退役,否则后台会出现无法解释的选项。
## 备选方案与取舍
1. **继续固定 30 天,年付做成 12 次 30 天**:改动最小;代价是与「按开通日计期」和年付周年到期直接矛盾。
已作废。
2. **自然月 + 每期一行会员记录**:期数可直接查询;代价是主键冲突、活跃行唯一性与扣减选行都要重做。已作废。
3. **UTC 锚点**:与每日免费池口径不一致,会出现「同一天两个到期时间」。已作废。
## 明确不做
- 不做自动续费;本期是单次购买,到期后由用户重新选择套餐。
- 不支持本期即时月转年、年转月与降档。
- 不引入 cron 或后台定时任务来推进期数,继续用现有的惰性刷新。
@@ -0,0 +1,37 @@
# 模型权限与并发上限的强制边界
## Status
accepted(2026-10-03)
## 背景与决策
`profile_membership_plan` 的 `model_access` 与 `concurrent_job_limit` 此前只落字段、只用于展示,服务端不做拦截。2026-10-03 决定升级为服务端强制,并划清本期边界:
- **并发上限**只在已有的 `claim_external_generation_jobs_tx`(唯一的 `pending → running` 事务点)按账号过滤,只统计 `status = running` 的 `external_generation_job`;不新建队列、不设排队深度上限、不新增计数表(加 `(owner_user_id, status)` 组合索引现算)。上限读后台目录 `profile_membership_plan`,缺行失败关闭到 `Normal`(=1),`128` 哨兵表示不设上限。
- **模型权限**只约束 AGC 的 LLM 模型:`AgcModel` 末位加权限档,**缺省 `Basic`(失败开放)**,由人工在后台「AGC 模型」页手动标 `Full`;在 `GET /api/llm/models` 与代理解析处校验,越权返回 4xx `MODEL_NOT_AVAILABLE_FOR_PLAN`。
- **直连同步生成暂不计入并发**:`/api/raw/v1/images/edit`(gpt-image-2)、`editor_project` 直连图片 / 图标 / 背景、角色资源与音频等路径不产生 job 行,`running` 计数覆盖不到;统一「账号在飞生成」口径留作后续设计。
- 不设灰度开关,直接对所有账号生效。
## 考虑过的替代方案
- 新增 SpacetimeDB「在飞租约表」统一统计队列 job 与直连同步生成:更完整,但需要租约、心跳与回收语义,本期未做。
- 把直连同步生成统一改走 `external_generation_job`:最一致,但要重做 raw / editor 直连接口,风险最大。
- 模型档位缺省 `Full`(失败关闭):更安全,但上线即需先标注全部基础模型,运营成本高。
## 后果
- 模型限制在人工标注 `Full` 之前不会拦截任何请求。
- `raw` gpt-image-2 与其它直连同步生成不受 `concurrent_job_limit` 约束(TODO)。
- 无灰度开关;出问题只能回滚发版。
## 修订:列表改为可用 / 不可用分桶(2026-10-03)
原实现让 `GET /api/llm/models` 只返回本档可用模型(`Basic` 看不到 `Full`),降级用户在客户端只会看到「所选模型已停用」的笼统提示,且产品无法在列表里露出可升级的高档模型。修订为:
- 返回两个平行数组:`models` 只放本档可用(`enabled` 且档位允许);`unavailableModels` 放目录里其余全部,元素额外带 `reason` 枚举。
- `reason` 取值 `plan_required`(档位不够)/ `disabled`(后台停用)/ `unknown`(前向兜底);同时停用且档位不够时取 `disabled`(升级也解锁不了,不能标 `plan_required`)。
- 上游真实模型名(`modelId`)两桶都不下发;`defaultModelId` 必须落在 `models` 内。
- 分桶只是展示层的信息补充,**不放松服务端强制**:`/api/llm/responses`、`/api/llm/chat/completions`、`/api/llm/anthropic/*` 仍按档 4xx。
- `unavailableModels` 是加法字段:旧客户端忽略后行为不变;`reason` 在 wire 侧带 `unknown` 兜底,后端新增取值不会让旧客户端解析失败。
- 目录 DTO 与枚举改为 ts-rs 单一真源(whole DTO 导出),`agentMode` / `protocol` 由 `String` 收敛为 Rust 枚举。
@@ -0,0 +1,60 @@
# 【ADR】泥点三池以单一总额为权威
状态:已接受(2026-10-02;规则细则、里程碑与验收见
[`【实施计划】双余额泥点与会员补差升级-2026-10-02`](../technical/【实施计划】双余额泥点与会员补差升级-2026-10-02.md))
## 背景
钱包今天只有一个权威数字:`profile_dashboard_state.wallet_balance`。它**包含**每日免费泥点和会员月度泥点,
不是「永久余额」,也不是「充值余额」。三个池子不是三份余额,而是对这一个数字的三种解读:
- 每日免费池真实存储(`profile_daily_free_points.remaining_points`,北京时间 0 点清零重发);
- 会员周期池真实存储(`profile_membership.cycle_remaining_points`,到 `cycle_resets_at` 换期清零);
- 永久泥点在读取时由 `wallet_balance − 每日免费剩余 − 月度剩余` 推导
(`build_profile_mud_point_balance_response`,`api-server/src/runtime_profile.rs`)。
也就是说,现在的「永久」不是一种来源,而是「没人认领的余数」:充值、邀请奖励、兑换码、每日任务、注册赠送,
以及任何回不到原池的退款都落在这里。由此产生两个后果:对外投影里 `totalPoints` 带着池语义
(= 含每日与月度的总额),而 `permanentPoints` 是减法的产物,产品要的「两张独立余额卡片」在契约上并不成立。
边界:每日免费池、永久余数口径、账本与历史数据、充点商品与历史订单都在线上;只有会员侧(周期池、档位枚举、
会员商品与权益)未上线,可以重做。
## 决策
1. **存储层保持单一权威总额。** 不新增 `permanent_balance` 列:每日免费存 `profile_daily_free_points`,
月度存 `profile_membership.cycle_remaining_points`,永久泥点 = 总额 − 每日免费 − 月度。
只有一处写入锚点(`apply_profile_wallet_signed_delta`),三池不会互相打架。
2. **对外投影把三池做成并列一等字段**:`dailyFreePoints` / `monthlyPoints` / `permanentPoints`,
各自带自己的元数据(每日重置时间;月度到期日与刷新日、期数;永久无到期)。
`totalPoints` 不再承担池语义,永久泥点也不允许表现为「总额的剩余」。
3. **扣减顺序固定为 每日免费 → 月度 → 永久**;可用总额 = 月度 + 永久,每日免费单独展示与单独计量。
每日免费当天 24 点清零,先扣它对用户最有利,退款回填的账本感知逻辑也不用改。
4. **非会员来源一律落入永久泥点**:充值、邀请、兑换码、每日任务、注册赠送,以及跨天 / 跨周期回填失败的退款。
5. **TODO(本 ADR 的已知临时性)**:后续把永久泥点升级为独立存储列。触发条件是出现「必须区分充值本金与
奖励点数」(例如按充值金额精确回缴)或出现第二个需要独立写永久池的副作用。届时迁移口径为
「存量用户回填 `permanent = 现有余数` + 双写切换 + 保留回滚方案」,不改变本 ADR 的对外三池投影。
## 影响与代价
- 池层面的 bug 会表现为**永久泥点虚高**,而不是余额对不上;排查必须回到账本的 `permanentPointsDelta`。
- 存储层不强制「永久只能由充值 / 奖励增加」,语义靠账本来源类型白名单保证。
- 对外字段改名(月度从 `limitedPoints` 变为 `monthlyPoints`)会同步影响 `shared-contracts`、
前端共享组件、AGC 客户端与后台页面,必须同批改完。
- 每日免费与永久余数在线,任何口径变化都是**在线契约变更**;`totalPoints` 的旧含义需要一次性退役干净,
不留「有时含每日、有时不含」的歧义。
## 备选方案与取舍
1. **永久独立存储列(在线迁移)**:永久成为一等字段、可加白名单校验;代价是为存量用户回填、
双写切换、准备回滚,并让每日免费与月度的每次变动都要保证三处一致。已作废,记为 TODO。
2. **三池都是真相源,`total` 改为派生投影**:领域模型最干净;代价是 `wallet_balance` 的所有读路径
(账本 `balance_after`、后台快照、冻结 / 预留、对账工具)与全部历史账本数据都要改,在线风险最大。已作废。
3. **保持现状(永久继续是余数且不改对外字段)**:零改动;代价是产品要的「两张独立余额卡片」在契约上
永远表达不出来。已作废。
## 明确不做
- 本轮不改 `wallet_balance` 的口径与在线数据结构,不做永久泥点独立存储。
- 不给每日免费泥点引入过期以外的第二种语义(不并入可用总额、不参与「月度不足时补扣」)。
- 不为奖励点数发明独立过期规则。
@@ -17,6 +17,8 @@
2. 拉不到就报错、不写替代目录,并在下一次启动继续重试,直到目录里有数据。
3. **保持既有格式与契约不变**:目录字段(`id`/`alias`/`modelId`/`defaultModelId`)、后台页面与 DTO、`GET /api/llm/models` 形状、客户端模型标识校验都不变,不引入不兼容变更。
> 2026-10-03 修订:本条「`GET /api/llm/models` 形状不变」只约束本里程碑自身;后续「模型权限」变更已把该接口改为可用 / 不可用分桶(新增 `unavailableModels` 与 `reason` 枚举),见 `docs/adr/【ADR】模型权限与并发上限的强制边界-2026-10-03.md` 的修订节。
## 不在本里程碑内
- 不改目录字段语义与后台维护方式,不删别名/稳定标识概念。
@@ -11,6 +11,15 @@
- 影响面:`deploy/nginx/{genarrative.conf,genarrative-dev-http.conf,README.md}`、`deploy/container/nginx.conf`、`vite.config.ts`、`server-rs/crates/pingora-gateway/src/main.rs`、`deploy/pingora/nginx-route-parity.matrix.json`、`scripts/check-{nginx-spa-routes,pingora-route-parity,pingora-gateway-smoke}.mjs`、`docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。
- 关联:`docs/project-memory/shared-memory/pitfalls.md`「付费游戏播放会话前缀落在 `/api/*`」条。
## 2026-10-03 每日免费发放额为 0 时前端隐藏该池
- 背景:运营需要一个可逆的「不提供每日免费泥点」状态。不给它新增 `retired` 状态位或新字段,直接把后台配置 `daily_free_points_per_day` 配成 0,让「每日免费发放额」这个普通数值自己表达;前端据此隐藏每日免费相关入口。
- 决策(信号取 `dailyFreeResetPoints` 而非剩余):`ProfileMudPointBalance.dailyFreeResetPoints <= 0` 表示不提供该池。`dailyFreePoints`(当天剩余)为 0 是每天用完后的正常状态,不能当信号。
- 决策(显示规则 `remaining > 0 || reset > 0`):当天仍有存量(配置中途改成 0,但当天已发未用完)时必须继续展示——扣点顺序不变,这部分存量会被优先扣减,隐藏后用户会「有余额看不见、还被先扣」。判定收口到 `packages/shared/src/utils/mudPoints.ts` 的 `shouldShowProfileDailyFreePool`,三端共享组件共用,消费方 controller 不改。
- 决策(后端放开校验、不新增契约字段):`server-rs/crates/module-runtime/src/commands.rs` 的 `daily_free_points_per_day` 由「必须 > 0」放宽为 `0..=i64::MAX`,错误变体改名为 `DailyFreePointsPerDayOverflow`;`0` 本就是 `u64` 合法值,`shared-contracts` / ts-rs 生成物 / OpenAPI 不改。发放与账本逻辑不变(`amount_delta == 0` 时后端本就跳过写账)。
- 影响范围:`packages/shared/src/utils/mudPoints.ts`、`PlatformMudPointWalletEntry`、`PlatformProfileRechargeModal`(池概览 3 列变 2 列、扣点顺序提示与泥点确认页文案随可见性切换)、`apps/admin-web/src/pages/AdminProfileWalletConfigPage.tsx`(每日免费允许填 0)、`server-rs/crates/module-runtime/{commands,errors,lib}.rs`;`daily_free_grant` / `daily_free_reset` 账本文案保留,历史流水不改写。
- 验证:`cargo check -p module-runtime`、`cargo test -p module-runtime profile_wallet_config_allows_zero_daily_free_points`、`cargo fmt --check`、根 `npm run typecheck`、`npm run admin-web:typecheck`、共享层与 admin 定向 vitest 22/22、`npm run check:encoding`、`git diff --check` 通过。
## 2026-10-05 创作者主页与关注粉丝的产品边界
- 产品已确认:桌面第四项“创作者主页”默认进入当前账号主页,“我的”移到第五项;他人的关注/粉丝列表公开可查看,自己或他人的两类列表均可点击用户进入其创作者主页。
@@ -205,6 +214,25 @@
- 影响范围:`apps/ai-game-creator-shell/src/styles.css`、`src/view/home/index.tsx`、`src/view/template-library/index.tsx`。
- 验证:Chromium 真机量测(1440x800 / 1440x560 / 390x844 / 390x560,带与不带横幅)——首页 / 模板库 / 项目页改动前后高度一致,帮助页从「被裁 112~150px 且无可滚动祖先」变为正好等于 `.window-chrome__content` 高度并可滚动到底;`npm run typecheck`、`npm run check:encoding`、`git diff --check`、`eslint` 通过。
## 2026-10-02 泥点双余额与会员补差升级方案(拷问定稿)
- 决策(对外投影):余额接口并列暴露每日免费泥点 / 月度泥点 / 永久泥点三池,永久泥点不再表现为「总额的剩余」;`totalPoints` 的池语义退役,`limitedPoints` 更名 `monthlyPoints`。存储层不动——`wallet_balance` 仍是唯一权威总额,永久泥点 = 总额 − 每日免费 − 月度,并留 TODO 待后续改为独立存储(触发条件见 ADR)。
- 决策(扣减与展示):扣减顺序维持「每日免费 → 月度 → 永久」,可用总额 = 月度 + 永久,每日免费保留并单独计量与展示。
- 决策(会员侧重做):会员侧未上线,旧档位 `Month/Season/Year` 与 `Basic/Ultimate` 及后台按档位权益字段直接删除;账期改为按开通日自然月(当月最后一天夹取),年付 12 期用 `cycle_index` / `cycle_count` 表达,仍只有一行活跃会员记录。
- 决策(充点侧):6 档充点、取消首充赠送;复用旧 4 档 `points_*` id 并把赠送置 0,新增两档(`points_1280` / `points_3280`),不删除任何行(历史订单与首充资格按 `user_id + product_id` 引用)。
- 决策(升级):后端只读报价接口 + 下单重算并落订单快照;会员订单复用 `profile_recharge_order`(新增 kind 与末位变更快照字段);新增账本来源 `MembershipUpgradeGrant`,幂等锚 `membership-upgrade-grant:{user}:{order_id}`;金额向上取到分、补点向下取整,年付按「后续完整月数 + 本期实际月长占比」;会员退款维持 `manual_review`,只靠订单快照支撑人工复核。
- 边界:每日免费池、永久余数口径、账本与充点订单**在线上**;只有会员侧未上线,因此会员侧零迁移、钱包侧不允许口径漂移。
- 决策(会员档位枚举化):会员档位身份改为 Rust 枚举 `RuntimeProfileMembershipPlan { Normal, Starter, Plus, Pro, Max }`,绝不字符串;新增 `profile_membership_plan` 目录表并**以该枚举为主键**(后台按档位改价 / 改权益);`profile_membership.tier` 与 `profile_recharge_product_config.tier` 两列删除、旧 `RuntimeProfileMembershipTier` 整体退役。会员侧无存量行,属经用户授权的破坏性变更;账本来源枚举仍只允许末尾追加 `MembershipUpgradeGrant`。
- 决策(目录权益):目录一行一档,含月价、**独立可配置年价** `year_price_cents`、每期额度、模型权限 `Basic` / `Full`(本期只落字段、不执行拦截)、并发上限 `u32`(**哨兵 128 = 不设上限**,不用 `Option`);`Normal` = 0 点 / `Basic` / 并发 1 / rank 0。
- 决策(目录数值):`Starter` ¥39 / 400 / ¥390 · `Plus` ¥99 / 1150 / ¥990 · `Pro` ¥299 / 3650 / ¥2990 · `Max` ¥699 / 8650 / ¥6990。
- 权威入口:[实施计划](../../technical/【实施计划】双余额泥点与会员补差升级-2026-10-02.md);设计见 [`【技术设计】泥点三池与会员计费后端设计`](../../technical/【技术设计】泥点三池与会员计费后端设计-2026-10-02.md);决策见 [`【ADR】泥点三池以单一总额为权威`](../../adr/【ADR】泥点三池以单一总额为权威-2026-10-02.md)、[`【ADR】会员账期按开通日自然月`](../../adr/【ADR】会员账期按开通日自然月-2026-10-02.md)、[`【ADR】会员订单复用充值订单与补差升级幂等锚点`](../../adr/【ADR】会员订单复用充值订单与补差升级幂等锚点-2026-10-02.md)、[`【ADR】会员档位以枚举为权威`](../../adr/【ADR】会员档位以枚举为权威-2026-10-02.md)。
- 状态:**已落地**。后端(领域模块、`profile_membership_plan` 表与播种、两处 `tier` 列删除、订单变更快照、自然月账期、6 档充点、三池余额投影、升级报价接口、后台档位接口)、契约生成(ts-rs → `packages/shared/src/contracts/generated/`)、共享组件与平台入口三池展示、AGC 壳、后台「会员档位」页与充值商品页收敛均已实现并通过门禁。明确不做:模型权限与并发上限只落字段、不提供 `availableTotalPoints`、会员购买 / 升级的 C 端 UI。
- 实现期补充决断(未改变上述范围):
- 余额 DTO 由 Rust 单一真源经 ts-rs 导出,前端 re-export 而非手写第二份字段列表;`u64` 标 `ts(type = "number")` 避免 `bigint` 分叉,`check:generated-bindings` 以「重新生成逐字节比对」守漂移。
- 「合计」由前端派生(`packages/shared/src/utils/mudPoints.ts` 的 `resolveProfileMudPointBalanceView`),月度刷新时刻固定按北京时间格式化,不把第二份口径放回后端。
- 会员订单 `product_id` 编码「档位 + 周期」(`membership-{plan}-{cycleKind}`),会员商品不进 `profile_recharge_product_config`;支付确认不做出售校验,避免付款后档位被下架导致权益丢失。
- 后台 `rank` 不可改(升级比较基准由代码内目录决定),`normal` 档位的价格与额度由后端强制归零。
## 2026-10-01 游戏广场评分展示边界
- 用户确认广场卡片增加一位小数的 10 分制平均分与评分人数,无有效评价显示“暂无评分”;保留现有排序、筛选、卡片打开详情及返回上下文。
@@ -9635,6 +9663,136 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
- 边界(未验证):macOS 真机的 sidecar 加载与 `check-macos-bundle.mjs` 包内容门禁未在本机验证;Linux 门禁按新配置不再要求 sidecar 资源,需 CI 实跑确认转绿。
- 关联:issue #519、master `fb130d184`、`docs/technical/【技术方案】AGC随包资源staging归位-2026-09-26.md`、CI run 3083。
## 2026-10-03 后台补全会员计费运维面:订单快照、用户会员状态、三池命名与表标签
- 背景:`docs/technical/【技术设计】泥点三池与会员计费后端设计-2026-10-02.md` §5.6 声称「订单列表 / 退款人工复核读取 §2.4 快照」,但管理端 `AdminRechargeOrderEntryPayload` 没有任何会员字段;用户详情不展示会员档位与账期,钱包月度池仍叫「会员限时」,通用表浏览器缺 `profile_membership_plan` 标签。
- 决策(订单快照透出):新增 `AdminMembershipOrderChangePayload`(变更类型 / 前后档位 / 前后账期 / 前后每期泥点 / 补点增量 / 期数 / 前后到期与重置 / 价格拆分 JSON)挂到 `AdminRechargeOrderEntryPayload.membershipChange`,由 `map_membership_order_change` 从 module-runtime 既有的 `RuntimeProfileMembershipOrderChangeSnapshot` 直接映射;非会员订单为 `null`。只读,不改下单 / 退款语义。
- 决策(用户会员状态):`AdminUserDetailResponse` 新增 `membership: AdminProfileMembershipPayload`,api-server 复用 `get_profile_recharge_center` 的 `membership` 投影,不再新开一条 admin 专用读路径。
- 决策(三池命名只在契约 / UI 层):`AdminProfileWalletPayload.membership_limited_points` 改名为 `monthly_points`(TS `monthlyPoints`),后台钱包区块「会员限时」改为「月度泥点」。**SpacetimeDB 列名 `membership_limited_points` 与生成绑定保持不动**——持久化改名属破坏性变更,须先确认迁移计划。
- 决策(展示复用):新建 `apps/admin-web/src/config/membershipDisplay.ts` 收敛档位 / 账期展示名与 `format*`,`AdminMembershipPlanPage`、`AdminRechargeOrderPage`、`AdminUserDetailDialog` 共用,不再各留一份 `planLabels`。
- 影响范围:`server-rs/crates/shared-contracts/src/admin.rs`、`server-rs/crates/api-server/src/admin_recharge.rs`、`apps/admin-web/src/{api/adminApiTypes.ts,config/membershipDisplay.ts,pages/AdminMembershipPlanPage.tsx,pages/AdminRechargeOrderPage.tsx,components/AdminUserDetailDialog.tsx}`、`scripts/admin-web-fake-api.mjs`、设计文档 §5.6/§10.2、本文件。
- 验证:`cargo check -p api-server`;`npm run admin-web:typecheck`;`npx vitest run apps/admin-web` → 26 files / 248 tests passed(新增「会员订单在发放泥点列展示会员变更快照」及用户详情会员断言)。
- 边界:`/admin/api/*` 不在 `/api/external/v1` OpenAPI 门禁内,本次未补接口级契约测试,会员快照只在管理端页面用例层面取证。
## 2026-10-03 模型权限与并发上限的强制边界(拷问定案)
- 背景:`model_access` 与 `concurrent_job_limit` 此前只落字段、只用于展示,服务端零拦截;用户点出 raw gpt-image-2 这类同步生成,确认「只统计 job 的 `running`」覆盖不到它,因此先把「并发单位」定义清楚再强制。
- 决策(并发上限只约束队列 job):强制点放在唯一的 `pending → running` 事务点 `claim_external_generation_jobs_tx`,认领时按账号统计 `running` 数、达上限则跳过(任务留在 `pending`,天然排队,不新建队列)。只算 `running`(含过期待回收的 `expired_running`);失败退回 `pending` 重试释放名额;自身过期 `running` 回收豁免;上限读 `profile_membership_plan.concurrent_job_limit`,缺行失败关闭到 `Normal`(=1),`128` 哨兵为不设上限;计数用新增 `(owner_user_id, status)` 组合索引现算,不建计数表;`/api/external/v1` 任务同样计入;不设排队深度上限。
- 决策(模型权限只约束 AGC LLM 模型):`AgcModel` 末位加权限档、缺省 `Basic`(失败开放),由人工在后台「AGC 模型」页手动标 `Full`;`GET /api/llm/models` 按档过滤并把 `defaultModelId` 取可用集合首个,`/api/llm/responses` 经 `resolve_requested` 后校验、越权返 `MODEL_NOT_AVAILABLE_FOR_PLAN`;本地残留旧选择明确 4xx,不静默回退;不设灰度开关。
- 边界(本轮不做):直连同步生成(raw gpt-image-2、`editor_project` 直连图片 / 图标 / 背景、角色资源、音频)没有 job 行,`running` 计数覆盖不到;统一「账号在飞生成」口径(在飞租约表 / 或统一改走 job)留作后续 TODO。
- 影响范围:`docs/technical/【技术设计】泥点三池与会员计费后端设计-2026-10-02.md`(新增 §11,修订 §9/§10)、`CONTEXT.md`(并发上限 / 新增「独立生成任务」)、`docs/adr/【ADR】模型权限与并发上限的强制边界-2026-10-03.md`、`docs/README.md`。未改代码。
- 验证:`npm run check:encoding`、`npm run check:doc-index`、`git diff --check`。
## 2026-10-03 模型权限与并发上限落地实现
- 背景:承接同日「模型权限与并发上限的强制边界(拷问定案)」,把定案按 §11.4 编码约定落地;本文记实现拆分与验证,不再重复决策理由。
- 实现(`module-runtime`,`b78003143`):新增 `agc_model_access.rs`(`AgcModelAccess::Basic/Full`、`MODEL_NOT_AVAILABLE_FOR_PLAN`、`AgcModelResolveError::{Unavailable, NotAvailableForPlan}`、`RuntimeProfileMembershipModelAccess → AgcModelAccess`);`AgcModel` 末位追加 `#[serde(default)] access`(存量 JSON 缺字段即 `Basic`,失败开放);`AgcModelCatalog` 增加 `available_models_for / default_model_id_for / resolve_requested_for`,默认模型优先取登记项、不在档内则回退目录顺序首个可用项、无可用项返回 `Unavailable`。
- 实现(`spacetime-module`,`db8d467b8`):`external_generation_job` 追加 btree 组合索引 `(owner_user_id, status)`;`claim_external_generation_jobs_tx` 在 `pending → running` 前按 owner 现算 `running` 行数并过滤,达上限留在 `pending`;只算 `running`(含过期待回收),自身过期 `running` 回收豁免,`pending` 不占名额;`effective_profile_concurrent_job_limit` 走 `profile_membership.plan → concurrent_job_limit`,缺行失败关闭到 `Normal`(=1),`128` 为不限;抽取纯函数 `external_generation_claim_within_concurrency_limit` 并补单测。
- 实现(后台,`919b4b01d`):`AdminAgcModel` 新增 `access`(缺省 `basic`),api-server 保存时只接受 `basic/full`,`GET` 回读 / `PUT` 写入;后台「AGC 模型」页新增「权限档」列与选择器,fake API 与页面用例覆盖。
- 实现(`api-server`,`9b1a538d2`):新增 `llm/model_access.rs`,用现有 `get_profile_recharge_center` 解析账号档位到 `AgcModelAccess`,缺会员行 / 缺目录行 / 读失败统一失败关闭到 `Basic`;`GET /api/llm/models` 按档过滤且默认项取该档可用集合;`/api/llm/responses`、`/api/llm/chat/completions`、`/api/llm/anthropic/*` 全改按档解析;越权映射 403 + `MODEL_NOT_AVAILABLE_FOR_PLAN`,目录外 / 停用仍为 422。
- 术语口径:`Basic` / `Full` 是模型对会员档的要求,`Normal` 等是会员档位;缺省语义统一「失败开放到基础档、失败关闭到 1 并发」——模型档缺省 `Basic` 是失败开放(不拦),并发上限缺行失败关闭到 `Normal`(=1)。
- 验证:`cargo check -p api-server`、`cargo check -p api-server --tests`;`cargo test -p api-server llm` 49 passed、`model_catalog_tests` 3 passed(含 Basic 档过滤 Full 模型、403 错误码映射);`npm run check:spacetime-schema` 的失败项全部是分支既有 `profile_membership` / `profile_recharge_product_config` 字段变更(对照 `origin/master` merge-base `25beb3ad5`),与本次仅新增 btree 索引无关,guard 不比较索引;`npm run check:encoding`、`git diff --check`。
- 边界 / TODO:账号档位仍复用带周期刷新写入的 `get_profile_recharge_center`,后续替换为轻量专用读;AGC 客户端仍靠「目录按档过滤 + 本地选择对账回退默认项」,未加专门的 403 重选提示;统一「账号在飞生成」口径(含无 job 行的同步生成)仍为后续 TODO。
## 2026-10-03 模型列表改为可用 / 不可用分桶(修订)
- 背景:`GET /api/llm/models` 原按档过滤,降级用户在客户端只看到「所选模型已停用」的笼统提示,且无法在列表里露出可升级的高档模型。
- 决策(契约):返回 `models`(本档可用)与 `unavailableModels`(目录里其余全部,带 `reason` 枚举)两个平行数组,**不新增** `available` 布尔;`reason` 取 `plan_required` / `disabled` / `unknown`,同时停用且档位不够取 `disabled`;`unavailableModels` 保持目录顺序、恒定下发(无内容为空数组);上游真实模型名两桶都不下发;`defaultModelId` 必须落在 `models` 内;某档一个可用模型都没有时保留 503 `MODEL_UNAVAILABLE`。
- 决策(兼容):`unavailableModels` 是加法字段,旧客户端忽略后行为不变,无需能力协商;`reason` 在 wire 侧带 `unknown` 兜底,后端将来新增取值不会让旧客户端解析失败;服务端按档强制不变。
- 决策(客户端):AGC 桌面选择器把 `unavailableModels` 渲染为不可点项,仅 hover / 键盘 focus 出 tooltip(`plan_required`→「订阅计划不支持」、`disabled`→「该模型已下线」、`unknown`→「暂不可用」);降级仍自动回退默认,提示按 `reason` 取;后台直接删除(两桶都没有)用泛化文案「所选模型已不可用,已切回默认模型」;不再新增「当前已选不可用」的 API 字段,前端自行从 `unavailableModels` 推断。
- 决策(ts-rs 单一真源):整个目录 DTO 与枚举导出到 `packages/shared/src/contracts/generated/`;新增 barrel `packages/shared/src/llm/modelCatalog.ts` 并由 `packages/shared/src/index.ts` 转发;`AgcAgentMode` / `AgcModelProtocol` 从 `module-runtime` 迁入 `shared-contracts`(`module-runtime` re-export);`revision: u64` 标 `#[ts(as = "f64")]`;AGC 前端删除手写 `ClientLlmModel`,改从 `@genarrative/shared` 根导入。
- 影响范围:`docs/adr/【ADR】模型权限与并发上限的强制边界-2026-10-03.md`(修订节)、技术设计 §11.2/§11.4、`docs/technical/【技术方案】AGC后台模型别名与对话选择-2026-09-05.md`、`docs/project-memory/plans/【里程碑】AGC模型目录上游同步-2026-09-24.md`、本文件。
- 验证:`npm run check:encoding`、`npm run check:doc-index`、`git diff --check`。
## 2026-10-03 模型权限改走只读 procedure(性能收口)
- 背景:`api-server` 的 AGC 模型权限解析原复用 `get_profile_recharge_center`,它带幂等的账期 / 免费点刷新与钱包 / 商品等无关快照字段,位于每个 `/api/llm/*` 请求热路径;同日「落地实现」条目已把它列为 TODO。
- 决策:新增只读 procedure `get_profile_agc_model_access_and_return`(输入沿用 `RuntimeProfileMembershipGetInput`,结果 `RuntimeProfileAgcModelAccessSnapshot { user_id, plan, model_access }`,仅 editor generation runtime service identity 可调):只读会员账期投影 + `profile_membership_plan.model_access`,缺会员行 / 已过期按 `Normal`,缺档位目录行失败关闭到 `Basic`,不刷新、不写库;`api-server` 改调它,失败关闭语义不变。
- 影响范围:`module-runtime`(领域类型 `9e397fe9c`)、`spacetime-module`(procedure,同提交)、生成绑定、`spacetime-client`(`get_profile_agc_model_access`)、`api-server/src/llm/model_access.rs`(`b0dc94b46`)、技术设计 §11.4。
- 验证:`cargo check -p spacetime-module`、`cargo check -p spacetime-client`、`cargo check -p api-server --tests`;`cargo test -p api-server llm` 51 passed;`npm run check:encoding`、`git diff --check`;`npm run check:spacetime-schema` 失败项仍为分支既有 `profile_membership` / `profile_recharge_product_config` 表字段变更,与本次仅新增 procedure、未改表无关。
## 2026-10-03 会员链路错误分类改为机器可读错误码(typed error)
- 背景:`api-server/src/runtime_profile.rs` 的 `is_runtime_profile_membership_domain_error` 用中文 `error_message` 前缀 / 精确匹配决定返回 400 还是 502;文案一改(或新增拒绝点)就静默退化,且分类逻辑与 module 的错误文案跨层重复。
- 决策(错误码随 procedure 结果透传):`module-runtime` 新增 `RuntimeProfileMembershipErrorCode`(`UpgradeRejected` / `NotMember` / `AlreadyActive` / `CycleKindLocked` / `MembershipExpired` / `StateChanged` / `PlanNotPurchasable` / `PlanCatalogMissing` / `ProductIdUnparseable` / `MembershipProductRetired` / `InvalidPlanConfig`)与内部 `RuntimeProfileMembershipDomainError { code, message }`;三个会员 procedure 结果(升级报价、充值中心 / 下单、后台档位 upsert)末尾追加 `error_code: Option<...>`,拒绝点显式赋码,不再做字符串分类。
- 决策(客户端错误类型):`spacetime-client` 新增 `SpacetimeClientError::ProcedureRejected { code, message }`;`api-server` 据此返回 400(provider `runtime-profile`),基础设施错误保持 502。字段校验类错误不产码,仍落 502。
- 决策(行为变化):`会员目录缺少档位 ...` 与「已迁移的会员商品」原先落 502,现在明确 400;兑换码链路本轮未改造,仍保留一个文案匹配函数。procedure result 新增字段属 wire 契约变更,要求 module 与 api-server 同版本部署。
- 影响范围:`server-rs/crates/module-runtime/src/domain.rs`、`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`、`server-rs/crates/spacetime-client/src/{active.rs,active/mapper/runtime_profile.rs}`、生成绑定、`server-rs/crates/api-server/src/{runtime_profile.rs,asset_billing.rs,editor_project.rs}`、`docs/technical/【技术设计】泥点三池与会员计费后端设计-2026-10-02.md` §5.8、本文件。
- 验证:`npm run spacetime:generate`;`cargo check --workspace`;`api-server runtime_profile::tests` 41 passed;`module-runtime --lib membership::` 39 passed;`npm run check:encoding`、`npm run check:spacetime-schema`、`cargo fmt -- --check`、`git diff --check` 通过。
## 2026-10-03 会员目录播种策略定为「保留懒播种 + 明示副作用」
- 背景:评审第 20 项指出「只读」procedure(报价 / 充值中心 / 后台查询)首次读取时会经 `ensure_default_profile_membership_plan` 写库,与代码注释「不写库」矛盾。三条候选:①保留懒播种并承认副作用;②只靠 init + 发布期 migration 播种;③只读投影不落表、内存回退代码内目录。
- 决策:选 ①。`init_profile_membership_plan_catalog` 只在数据库首次创建时执行,增量发布不重跑;「表已存在但为空」的存量库必须靠读取 helper 懒播种兜底,否则目录为空会让下单 / 报价失败。保留懒播种,并把「只读入口在目录为空的首次调用会写库、表非空后幂等只读」明确写进代码注释与设计文档 §7,消除注释与实现的矛盾。
- 未选 ②/③ 的原因:②依赖发布流程保证存量库被种上,当前没有这条保证;③改动读路径回退逻辑,风险与收益不匹配。将来若做发布期 migration,再删除懒播种。
- 影响范围:`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`(`ensure_default_profile_membership_plan` / `init_profile_membership_plan_catalog` / `membership_plan_row` / `membership_plan_records` / 报价与建单函数注释)、`docs/technical/【技术设计】泥点三池与会员计费后端设计-2026-10-02.md` §7/§10.2、本文件。
## 2026-10-03 兑换码链路补机器可读错误码(typed error 收口)
- 背景:评审第 12 项指出 `SpacetimeClientError::ProcedureRejected` 的 `Display` 丢掉 `code`,而 `api-server/src/runtime_profile.rs` 的 `is_runtime_profile_redeem_code_domain_error` 仍在精确匹配 7 条中文 `error_message` 来判断兑换码拒绝该返回 400 还是 502,文案一改就静默退化,与会员链路已落地的 typed error 不一致。
- 决策(module 侧赋码):`module-runtime` 新增 `RuntimeProfileRewardCodeRedeemErrorCode`(`NotFound` / `Disabled` / `NotStarted` / `Expired` / `UsesExhausted` / `NotAllowedForUser` / `InvalidReward`);`RuntimeProfileRewardCodeRedeemProcedureResult` 末尾追加 `error_code: Option<...>`;`spacetime-module` 用 `ProfileRewardCodeRedeemProcedureError::{Classified, Plain}` 在拒绝点显式赋码,入参与钱包类错误保持 `error_code = null`。
- 决策(客户端 / BFF):`SpacetimeClientError::ProcedureRejected` 的 `code` 改为 `RuntimeProfileProcedureRejectionCode`(聚合 `Membership` 与 `RewardCodeRedeem` 两类),`Display` 输出 `[code] message` 保留机器原因;`api-server` 的 `map_runtime_profile_client_error` 只读变体的 `code` / `message`,`AppError.code` 取 `code.as_str()`,删除 `is_runtime_profile_domain_error` 与 `is_runtime_profile_redeem_code_domain_error` 两个文案匹配函数。
- 决策(行为变化):兑换码的 `兑换码不存在` / `已停用` / `未生效` / `已过期` / `次数已用完` / `不适用于当前账号` / `奖励无效` 由原来的 502 变为 400 且带机器码;procedure result 新增字段属 wire 契约变更,要求 module 与 api-server 同版本部署。
- 影响范围:`server-rs/crates/module-runtime/src/domain.rs`、`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`、`server-rs/crates/spacetime-client/src/{active.rs,active/mapper/runtime_profile.rs}`、生成绑定、`server-rs/crates/api-server/src/runtime_profile.rs`、`docs/technical/【技术设计】泥点三池与会员计费后端设计-2026-10-02.md` §5.8、本文件。
- 验证:`npm run spacetime:generate`;`cargo check -p api-server`;`cargo test -p api-server runtime_profile::tests` 41 passed;`cargo test -p module-runtime`、`cargo test -p spacetime-client`、`cargo test -p spacetime-module` 全通过;`npm run check:spacetime-schema`、`npm run check:encoding`、`git diff --check` 通过。
## 2026-10-03 会员计费评审第 13–20 项收口:契约枚举化、生成类型、计费与结算防御
- 背景:评审剩下一批 medium / low 的契约与防御性缺陷,用户逐项确认后一次性修复;每项单独提交,便于回溯。
- 决策(契约枚举化,13/14/18):`shared-contracts` 新增 `ProfileMembershipStatusToken`(`normal` / `active`),`AdminProfileMembershipPayload` 的 `plan` / `status` / `cycle_kind` 改用 token 枚举;5 个会员 token 枚举补 `ts_rs::TS` + `ts(export, ...)`,前端 `packages/shared/src/contracts/runtime.ts` 与 `apps/admin-web/src/api/adminApiTypes.ts` 改为生成类型别名 / re-export,禁止手写联合类型;`AdminAgcModel.access` 由裸 `String` 改为 `ProfileMembershipModelAccessToken`,未知值反序列化即拒。
- 决策(计费防御,15/16):`quote_runtime_profile_membership_upgrade` 显式拒绝价格 / 每期泥点 `<=`,堵住 0 元升级;`advance_beijing_months_clamped` 去掉 `expect`,超出 i64 微秒可表示范围(约 ±29 万年)时按方向饱和到 `i64::MAX` / `i64::MIN`,不改签名以免牵动 `membership_cycle_window` / `membership_expires_at`。
- 决策(错误链 typed,17):`resolve_llm_router_client` 返回 `Result<_, AppError>`,`load_owner_llm_catalog` 的 `503 + MODEL_ACCESS_UNAVAILABLE` 与 `resolve_requested_for` 的 `MODEL_UNAVAILABLE` 不再被拍平成中文,Chat Completions 与 `/api/llm/models`、代理端点错误码对齐。
- 决策(去掉退役入参,19):后台 upsert 请求 DTO 与 SpacetimeDB procedure 入参移除 `bonus_points` / `duration_days`,`profile_recharge_product_config` 退役列保留、写死 0;删除 `RuntimeProfileFieldError::InvalidRechargeProductFields`;后台表单去掉「首充赠送」,假数据同步。
- 决策(结算归一,20):`frozen_membership_cycle_matches` 比较双方都取 `max(1)`,修复 `cycle_index == 0` 迁移行被永久判成「会员状态已变化」的问题;不新增写库副作用。
- 影响范围:`shared-contracts/{runtime,admin}.rs`、`module-runtime/{domain,commands,errors}.rs` 与 `module-runtime/src/{membership/upgrade,membership/cycle,agc_model_access}.rs`、`spacetime-client/{active,active/mapper/runtime_profile,module_bindings}`、`spacetime-module/src/runtime/active/profile.rs`、`api-server/src/{runtime_profile,llm/mod,agc_models}.rs`、`packages/shared/src/contracts/runtime.ts`、`apps/admin-web/src/api/adminApiTypes.ts`、`apps/admin-web/src/pages/AdminRechargeProductPage.tsx`、`scripts/admin-web-fake-api.mjs`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`、本文件。
- 验证:`cargo test -p api-server` 1206 passed;`-p module-runtime` / `-p spacetime-client` / `-p spacetime-module` 全通过;`npm run spacetime:generate`、`npm run check:spacetime-schema`(92 表)、`npm run check:generated-bindings`(117 文件)、`npm run admin-web:typecheck`、`npm run typecheck`、`npm run check:encoding`、`git diff --check` 通过。
## 2026-10-03 后台兑换码管理补机器可读错误码(typed error 收口)
- 背景:`bd06e4269` 删除中文文案匹配后,`admin_disable_profile_redeem_code` / `admin_upsert_profile_redeem_code` / `admin_list_profile_redeem_codes` 仍只回传裸 `error_message`。这些 procedure 的 `ok = false` 是明确的业务拒绝(停用不存在的码、入参非法、私有码缺可兑换用户),却被 `spacetime-client` 拍平成 `Procedure(String)`,在 `api-server` 落 502,与会员 / 核销两条链路已落地的 typed error 不一致。
- 决策(module 侧赋码):`module-runtime` 新增 `RuntimeProfileRedeemCodeAdminErrorCode`(`NotFound` / `InvalidField` / `AllowedUsersRequired`);`RuntimeProfileRedeemCodeAdminProcedureResult` 与 `RuntimeProfileRedeemCodeAdminListProcedureResult` 末尾追加 `error_code: Option<...>`;`spacetime-module` 用 `ProfileRedeemCodeAdminProcedureError::{Classified, Plain}` 在拒绝点显式赋码,`RuntimeProfileFieldError` 统一归 `invalid_field`,私有码缺可兑换用户归 `allowed_users_required`,停用不存在的码归 `not_found`。
- 决策(客户端 / BFF):`RuntimeProfileProcedureRejectionCode` 增加 `RedeemCodeAdmin` 聚合变体,`spacetime-client` 两个后台 mapper 有 `error_code` 时走 `ProcedureRejected`、缺失时保持 `Procedure`;`api-server` 复用既有 `map_runtime_profile_client_error`,无需再引入字符串分类。
- 决策(行为变化):后台兑换码的 `兑换码不存在`、入参校验失败、私有码缺可兑换用户由 502 变为 400 且带机器码;procedure result 新增字段属 wire 契约变更,要求 module 与 api-server 同版本部署。
- 影响范围:`server-rs/crates/module-runtime/src/domain.rs`、`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`、`server-rs/crates/spacetime-client/src/{active.rs,active/mapper/runtime_profile.rs,module_bindings,module_bindings.rs}`、`server-rs/crates/api-server/src/runtime_profile.rs`、`docs/technical/【技术设计】泥点三池与会员计费后端设计-2026-10-02.md` §5.8、本文件。
- 验证:`npm run spacetime:generate`;`cargo check`(module-runtime / spacetime-module / spacetime-client / api-server);`cargo test -p spacetime-client` 30 passed;`cargo test -p api-server runtime_profile::tests` 42 passed;`cargo test -p module-runtime --lib` 113 passed;`npm run check:spacetime-schema`、`npm run check:encoding`、`git diff --check` 通过。
## 2026-10-03 后台邀请码管理补机器可读错误码
- 背景:后台邀请码的 `admin_upsert_profile_invite_code` / `admin_list_profile_invite_codes` 与上一轮的后台兑换码同病——`map_runtime_profile_invite_code_admin_procedure_result` 把 `ok = false` 一律拍成 `Procedure(String)`,`邀请码已被其他用户占用` 这类明确业务拒绝在 `api-server` 落 502,且 `map_runtime_profile_client_error` 的注释夸大成 «profile procedure 的业务拒绝都已 typed»。
- 决策:`module-runtime` 新增 `RuntimeProfileInviteCodeAdminErrorCode`(`InvalidField` / `InUse`);两个后台邀请码 procedure 结果末尾追加 `error_code: Option<...>`;`spacetime-module` 用 `ProfileInviteCodeAdminProcedureError::{Classified, Plain}` 赋码,字段校验归 `invalid_field`、被他人占用归 `in_use`;`RuntimeProfileProcedureRejectionCode` 增加 `InviteCodeAdmin` 聚合变体,`spacetime-client` mapper 有码走 `ProcedureRejected`、缺失时保持 `Procedure`;`api-server` 复用既有映射,并把注释收窄为 «带 typed error_code 的四条链路»。
- 行为变化:后台邀请码的入参校验失败与 `邀请码已被其他用户占用` 由 502 变为 400 且带机器码;procedure result 新增字段仍要求 module 与 api-server 同版本部署。
- 影响范围:`server-rs/crates/module-runtime/src/domain.rs`、`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`、`server-rs/crates/spacetime-client/src/{active.rs,active/mapper/runtime_profile.rs,module_bindings,module_bindings.rs}`、`server-rs/crates/api-server/src/runtime_profile.rs`、`docs/technical/【技术设计】泥点三池与会员计费后端设计-2026-10-02.md` §5.8、本文件。
- 验证:`npm run spacetime:generate`;`cargo check`(module-runtime / spacetime-module / spacetime-client / api-server);`cargo test -p spacetime-client` 32 passed;`cargo test -p api-server runtime_profile::tests` 43 passed;`cargo test -p module-runtime --lib` 114 passed;`cargo test -p spacetime-module` 271 passed。
## 2026-10-03 档位无可用模型改用独立错误码 MODEL_UNAVAILABLE_FOR_TIER
- 背景:`AgcModelResolveError::Unavailable`(422)与 `NoModelForTier`(503)此前共用 `MODEL_UNAVAILABLE`(见本文件 2026-10-03 的列表契约条目),只按 `error.code` 分派的客户端无法分辨「单个模型不可用」与「整个档位无可用模型(目录 / 档位配置问题)」,两者修法不同。
- 决策:`module-runtime` 新增常量 `MODEL_UNAVAILABLE` 与 `MODEL_UNAVAILABLE_FOR_TIER`,`NoModelForTier.code()` 返回后者,`Unavailable` 仍返回 `MODEL_UNAVAILABLE`;HTTP 状态(422 / 503)与端点行为不变,仅把错误码拆细,`resolve_error_codes_are_stable` 同步断言三码两两不同。
- 影响范围:`server-rs/crates/module-runtime/src/agc_model_access.rs`、`server-rs/crates/api-server/src/llm/{mod,model_access}.rs`、`docs/technical/【技术设计】泥点三池与会员计费后端设计-2026-10-02.md` §11.4、本文件。
- 验证:`cargo test -p module-runtime --lib agc_model_access`;`cargo test -p api-server llm::tests`。
## 2026-10-03 会员响应 token 反序列化前向兼容
- 背景:`shared-contracts` 的 5 个会员 token 枚举同时被入参 / 响应 DTO 复用,此前响应侧也严格反序列化;后端一旦新增 plan / status / cycle 取值,AGC Tauri shell 的 `read_profile_recharge_center` / `read_profile_membership_upgrade_quote`(`account_api.rs` 用 `serde_json::from_value` 解析整份响应)就会得到 `响应格式无效`,整个钱包页不可用。
- 决策:只给**响应** DTO 的 token 字段挂 `#[serde(deserialize_with = "...")]` 做前向兼容,未知取值 fail-open 兜底到已知最高档(plan→`max`、status→`active`、cycle→`yearly`、model_access→`full`;真实权益 / 下单金额以后端重算为准,避免把付费会员显示成非会员后引导重复购买)。**入参** DTO 不改,非法 token 仍在反序列化即拒;不改枚举本身的 `Deserialize`,避免 `Unknown` 变体污染入参与穷尽 match。
- 影响范围:`server-rs/crates/shared-contracts/src/runtime.rs`(4 个 helper + `ProfileMembershipPlanResponse` / `ProfileMembershipResponse` / `ProfileMembershipUpgradeQuoteResponse` 的 8 个字段)、`docs/technical/【技术设计】泥点三池与会员计费后端设计-2026-10-02.md` §2.5。后台 DTO(`AdminProfileMembershipPayload` / `AdminMembershipOrderChangePayload`)只有 TS 消费方,不做 Rust 侧处理。
- 验证:`cargo test -p shared-contracts --lib` 106 passed(含 `membership_response_tokens_tolerate_unknown_values_from_newer_backend`、`membership_request_tokens_stay_strict_for_unknown_values`)。
## 2026-10-03 会员错误码 `ProductIdUnparsable` 拼写修正(未上线,直接破坏性改名)
- 背景:`RuntimeProfileMembershipErrorCode::ProductIdUnparsable` 拼写有误,生成绑定与对外 `error.code = "product_id_unparsable"` 同错。
- 决策:该 PR 尚未上线,不留兼容,直接改名为 `ProductIdUnparseable`,wire 串同步改为 `"product_id_unparseable"`;重新生成 SpacetimeDB 绑定。
- 影响范围:`server-rs/crates/module-runtime/src/domain.rs`、`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`、`server-rs/crates/spacetime-client/src/{active/mapper/runtime_profile.rs,module_bindings/runtime_profile_membership_error_code_type.rs}`、`server-rs/crates/api-server/src/runtime_profile.rs`、设计文档 §5.8、本文件(含上文旧条目中的同名标识符同步更正)。
- 验证:`cargo test -p module-runtime --lib` 114 passed;`cargo test -p spacetime-client` 32 passed;`cargo test -p api-server runtime_profile::tests` 43 passed;`cargo test -p spacetime-module` 271 passed。
## 2026-10-03 会员结构体也走 ts-rs 生成类型(去前端手写漂移)
- 背景:会员 token 枚举早已 `ts(export)`,但承载它们的结构体(`ProfileMembershipPlanResponse`、`ProfileMembershipResponse`、`ProfileRechargeCenterResponse`、`ProfileRechargeProductResponse`、`ProfileRechargeOrderResponse`、`ProfileMembershipUpgradeQuoteResponse`、`ProfileMembershipPlanAdminListResponse`、`AdminUpsertProfileMembershipPlanRequest`、`ProfileDailyFreePointsResponse`、`ProfileMembershipUpgradeQuoteRequest`)仍是前端手写,字段与后端可能漂移。
- 决策:这 10 个结构体补齐 `ts_rs::TS` + `ts(export)`;64 位整数字段统一补 `ts(type = "number")`(ts-rs 默认 `bigint`,JSON 实际是 number)。`packages/shared/src/contracts/runtime.ts` 与 `apps/admin-web/src/api/adminApiTypes.ts` 改为 re-export 生成类型,仅前端需要收窄的 `kind` / `status` 联合用 `Omit` 组合保留;`ProfileRechargeCenterResponse` 的 `pointProducts` / `latestOrder` 同样收窄。
- 影响范围:`server-rs/crates/shared-contracts/src/runtime.rs`、`packages/shared/src/contracts/generated/*`(新增 10 个文件)、`packages/shared/src/contracts/runtime.ts`、`apps/admin-web/src/api/adminApiTypes.ts`、设计文档 §2.5。
- 验证:`npm run check:generated-bindings`(shared-contracts 23 个文件);根 `npm run typecheck`、`apps/admin-web` 与 AGC shell 主 `tsc` 通过;`cargo test -p shared-contracts --lib` 106 passed;`PlatformProfileRechargeModal` / `usePlatformProfileCenterController` vitest 14 passed。
## 2026-10-04 AGC 窗口标题栏项目标签改认「当前项目」,运行中项目降级为圆点与徽标
- 背景:issue #618。窗口标题栏那枚标签原来被「正在运行的项目」整块接管——`WindowChrome` 只要存在活动回合或运行快照读取失败,就把当前工作区标题换成 `ActiveProjectRunsPanel`(titlebar 形态),主文案取开始时间最晚的在跑项目。于是「当前项目没在跑、后台别的项目在跑」时,标签显示的是别的项目(现场:当前项目 `gameagent-ff7f3240`,标签 `gameagent-c3af9c7e`),用户以为自己开错了项目;读取失败时标签还会变成「正在运行的项目读取失败」,同样顶掉当前项目名。
@@ -57,6 +57,7 @@
- 对已明确退役且无现役调用方、公开契约、持久化数据、活跃实例或迁移要求的对象,直接清理实现、专属测试和说明,将权威文档更新为当前状态;历史由 Git 保存。公开契约、持久化数据和正式迁移按实际需求保留最小兼容及对应测试。
- 修改 `/api/external/v1` 时,同批更新 `docs/openapi/genarrative-external-v1.openapi.json` 与契约测试。
- 修改 SpacetimeDB schema 时遵守字段追加/default 约束,同步 migration、表目录、生成绑定,并运行 schema 检查;删除、改名、重排或改类型前先确认迁移计划。
- `shared-contracts` 响应 DTO 只增不删:新增字段一律可选(`Option`,或带安全 `#[serde(default ...)]`),身份 / 状态字段保持必填;删除、改名、改类型或改必填属性前先确认版本化计划,不保留仅服务旧客户端的 legacy shim。详见后端架构文档「shared-contracts DTO 变更规则」。
- 日志不递归输出完整配置、应用状态或 provider client;新增字段默认不进入安全摘要。所有 provider 配置类型(`LlmConfig`、`OssConfig`、`WechatConfig`、`WechatPayConfig`、`MattingConfig`、`VolcengineSpeechConfig`、`Hyper3dSettings`、`VectorEngineImageSettings`、`VectorEngineAudioSettings`)都必须手写脱敏 `Debug`:只输出枚举、数值、布尔、有界标识与 `<redacted>` 占位,凭据、私钥、路径与 URL 一律不递归格式化,并各带一条哨兵值回归用例(`cargo test -p platform-* debug`)。新增同类配置必须照此办理,不允许直接 `derive(Debug)`。
- HTTP 横切能力集中在 Axum/Tower 中间件:正常与降级路由复用追踪层;指标与 trace 使用 `MatchedPath` 模板及固定兜底,不把请求 ID、实际资源 ID 或 query 放入指标标签。在途请求通过 RAII guard 覆盖 Future 取消与 panic unwind;请求执行和响应体存活分别计量,不能把 handler 耗时当作 SSE 全生命周期。
- 业务依赖在组合根显式装配,Axum `FromRef` 只抽取可浅拷贝的窄能力。项目元数据与 External API 鉴权不持有完整 `AppState`,测试经相同接口注入替代依赖。集中鉴权仍保留方法级 fallback、公开入口、MCP 和 body limit 顺序;Provider span 跳过完整参数,不隐藏计费、重试、幂等或事务规则。
@@ -0,0 +1,207 @@
# 【实施计划】双余额泥点与会员补差升级
> 适用范围:`server-rs/crates`(SpacetimeDB 模块、`module-runtime`、`api-server`、`shared-contracts`)
> 与前端余额展示 / 充值弹层(`packages/shared`、`apps/ai-game-creator-shell`、`apps/admin-web`)。
> 状态:方案定稿(2026-10-02),未实施。本文是开发依据;术语见 `CONTEXT.md`,决策见四份 ADR。
> 字段级后端设计(DB schema / 账本 / 领域结构 / HTTP 契约 / 开放决策)见
> [`【技术设计】泥点三池与会员计费后端设计-2026-10-02`](./【技术设计】泥点三池与会员计费后端设计-2026-10-02.md)。
## 0. 一句话交付与验收判据
一句话:把「一个总额 + 三视图」的钱包改造成对外**并列三池**(每日免费 / 月度 / 永久)的记账,
并在此之上建成月付 / 年付会员——按开通日自然月计期、年付 12 期逐月发放、支持补差升级与到期重选。
验收判据(全部满足才算完成):
1. 余额接口并列返回每日免费泥点、月度泥点(含到期日、刷新日、期数)、永久泥点,
不再出现「永久泥点 = 总额 − 其他池」的对外口径。
2. 扣减顺序恒为 每日免费 → 月度 → 永久;可用总额 = 月度 + 永久;每日免费单独计量与展示。
3. 月付 / 年付按**开通日自然月**计期;年付共 12 期,每期刷新先清上期余额再发新额度;
到期未续清月度余额、停发,永久泥点保留可用。
4. 补差升级按 §3.3 公式计算;金额向上取到分、补点向下取整;刷新日与到期日不变;
防重复回调重复发点。
5. 在线部分零回归:每日免费池、永久泥点口径、账本历史、充点订单与首充资格不受影响;
`npm run check:spacetime-schema`、`npm run check:encoding`、`git diff --check` 通过。
## 1. 现状事实基线
| 关注点 | 现状 |
|---|---|
| 权威余额 | `profile_dashboard_state.wallet_balance`,**包含**每日免费与月度 |
| 每日免费池 | `profile_daily_free_points`(北京时间 `day_key`、`granted_points` / `remaining_points`),在线 |
| 月度池 | `profile_membership.cycle_*`(`cycle_started_at` / `cycle_resets_at` / `cycle_granted_points` / `cycle_remaining_points` / `cycle_period_days`),**固定 30 天、无年付、无期数**,会员侧未上线 |
| 永久池 | **推导余数** `wallet_balance − 每日免费剩余 − 月度剩余`,无独立表,在线 |
| 扣减顺序 | 每日免费 → 会员周期 → 永久(`apply_profile_wallet_signed_delta`) |
| 退款回填 | 账本感知,跨天 / 跨周期回填失败的金额落永久池 |
| 账本 | `profile_wallet_ledger` 只追加,幂等靠稳定 id;16 种来源类型,**无升级补点** |
| 充值商品 | `profile_recharge_product_config`:四档充点 `points_60/180/300/680`(基础 ¥1 = 10,后三档首充赠送 50%),会员商品 `duration_days` 全 30 |
| 对外投影 | `ProfileMudPointBalanceResponse`:`totalPoints = wallet_balance`、`limitedPoints = cycle_remaining_points`、`permanentPoints = 总额 − 每日 − 月度` |
| 后台权益 | `ProfileMembershipBenefitResponse` 含 `month/season/year/starter/basic/pro/ultimate` 七个旧档位字段 |
| 年付 / 补差 | 全仓无实现 |
冲突点(本次必须一次性收口):
- `totalPoints` 带池语义(含每日与月度),与「可用总额 = 月度 + 永久」直接冲突。
- `limitedPoints` 是全仓唯一出现「限时」措辞的地方,与「月度泥点」规范词冲突。
- 原细则表只写「月度优先扣减」,与权威契约的「每日免费优先」冲突——本方案维持每日免费优先。
- 权威契约把充值商品写死为四档带首充赠送,与本方案的 6 档无赠送冲突。
## 2. 术语
规范词见 `CONTEXT.md` 的「泥点与会员计费」:每日免费泥点、月度泥点、**永久泥点**、永久余数、会员账期、
补差升级、可用总额。实现侧只保留一个规范词:**永久泥点**(`_Avoid_: 不限时泥点`、充值余额、无限泥点)。
## 3. 规则细则(合并去重,开发以此节为准)
### 3.1 双余额记账
| 规则 | 月度泥点 | 永久泥点 |
|---|---|---|
| 来源 | 会员每期额度 | 直接充值 + 非会员奖励(历史赠点保留) |
| 有效期 | 当期有效,到期清零 | 永不过期 |
| 扣减顺序 | 每日免费耗尽后优先 | 月度不足时补扣 |
| 刷新 | 下期发新额度,旧余额不结转 | 不刷新,保留余额 |
| 升级时 | 剩余保留 + 按 §3.3 补发差额 | 完全不变 |
| 展示 | 独立卡片 + 到期 / 刷新日期 | 独立卡片 + 永久标记 |
**每日免费泥点保留**,并位于扣减顺序最前(本方案对原细则的补充):
每日免费当天 24 点清零,先扣它对用户最有利,且不用改动现有的退款回填语义。
可用总额 = 月度 + 永久,每日免费单独一行展示,不并入可用总额。
示例:月度剩 100 + 永久 500,消费 150 → 先扣 100 月度再扣 50 永久。
「永久」指不过期,创作照常扣点。
### 3.2 账期与刷新
- 按**开通日**计期(9/20 开通 → 10/20 到期),不按自然月;锚点为北京时间的开通日时刻。
- 每一期从**原始开通日**重算并夹取月末(1/31 开通 → 2/28 → 3/31),不从上一期递推。
- 年付:开通发首期,后续按月再发 11 期,共 12 期;`expires_at` = 开通时刻 + 12 个月(同样夹取)。
- 每期刷新:先清上期余额,再发新额度。
- 到期未续:清月度余额、停发;永久泥点保留可用。
- 本期为单次购买,不自动续费;到期后由用户重新选择套餐(月转年、年转月、降档一律到期后再选)。
### 3.3 补差升级
```
月付应付 = 新月价 − 当前月价
月付补点 = 新套餐每期额度 − 当前套餐每期额度
年付应付 = (新年价 − 当前年价) / 12 × (后续完整月数 + 本期剩余时间比例)
年付本期补点 = (新每期额度 − 当前每期额度) × 本期剩余时间比例
后续各期额度 = 新档每期额度
```
- 本期剩余时间比例 = 本期剩余毫秒 / 本期实际毫秒(`cycle_started_at` → `cycle_resets_at`);
后续完整月数 = 从本期 `cycle_resets_at` 到 `expires_at` 之间剩余的完整自然月期数。
- 费用向上取到分,补点向下取整。
- 升级后月度余额 = 原剩余月度余额 + 补点(已用点不返还,永久点不动);
`cycle_granted_points` 同步 +补点,否则退款回填的上限判据会算错。
- 有效期 = 原刷新日与原到期日不变。
- 连续升级以**当前套餐**为比较基准(Starter→Plus 再 →Pro 时按 Plus vs Pro 算,不回到最初套餐)。
- 支付成功才变更;失败 / 关闭不动原状态;防重复回调重复发点。
参考示例(口径来自产品细则,实施时用领域测试固化):
| 变更 | 补付 | 补点 | 变更后月度余额 | 有效期 |
|---|---|---|---|---|
| Plus 月 → Pro 月 | ¥200 | 2,500 | 3,115(原剩 615) | 原到期日不变 |
| Plus 年 → Pro 年 | ¥1,083.34 | 1,250 | 1,865(原剩 615) | 原到期日不变 |
### 3.4 允许操作矩阵
| 当前状态 | 可操作 | 不可操作 |
|---|---|---|
| 无会员 / 已到期 | 开任意月付或年付 | — |
| 月付会员 | 升更高月付 | 即时月转年、降档(到期后再选) |
| 年付会员 | 升更高年付 | 年转月、降档(到期后再改) |
| 同套餐同周期 | — | 重复购买(显示「当前套餐」) |
### 3.5 与现状的差异清单
1. `limitedPoints` → `monthlyPoints`(全仓改名,含 `shared-contracts`、`packages/shared` 契约、AGC、后台)。
2. `totalPoints` 的池语义退役:对外只给三个池;不提供「可用总额」后端字段,前端按需相加。
3. 会员周期从固定 30 天改为按开通日自然月;新增 `cycle_index` / `cycle_count`。
4. 新增年付(12 期)与补差升级;新增后端只读报价接口。
5. 账本来源枚举新增 `MembershipUpgradeGrant`。
6. 会员订单复用 `profile_recharge_order` + 末位变更快照字段。
7. 充点商品复用旧 4 档 id 并把赠送置 0(取消首充赠送),新增 ¥128 / ¥328 两档,共 6 档;不删除任何行(历史订单与首充资格引用 `product_id`)。
8. 会员档位改为枚举 `RuntimeProfileMembershipPlan { Normal, Starter, Plus, Pro, Max }`;
会员目录迁到 `profile_membership_plan` 并以**该枚举为主键**(后台按档位改价);
删除 `RuntimeProfileMembershipTier`、`profile_membership.tier` 与 `profile_recharge_product_config.tier`。
## 4. 数据模型与账本
- **不动**:`profile_dashboard_state.wallet_balance`(在线唯一权威总额)、`profile_daily_free_points`、
`profile_wallet_ledger` 既有形状与 id 方案、永久余数推导口径。
- `profile_membership`:**删除** `tier` / `cycle_period_days`;新增 `plan`(`RuntimeProfileMembershipPlan` 枚举,字段名 `plan`、类型必须是枚举)/ `cycle_index` / `cycle_count` / `cycle_kind`。
- 新增 `profile_membership_plan`:**以档位枚举为主键**,含标题、rank、月价、年价(独立可配置字段)、每期额度、模型权限、并发上限(`128` = 不设上限)、排序与上下架;一行一档,月 / 年共享每期额度。
- `profile_recharge_product_config`:**删除** `tier` 与 `membership_period_points` / `membership_period_days` / `membership_queue_limit` / `membership_discount_bps`;删 4 行 `member_*`;充点复用旧 4 档 id 并置 `bonus_points = 0`,新增两档(`points_1280` / `points_3280`)。
- 新增账本来源类型 `MembershipUpgradeGrant`;幂等锚 `membership-upgrade-grant:{user}:{order_id}`。
- `profile_recharge_order` 末尾追加会员变更快照字段(变更前 / 后档位、周期、补点数、有效期、kind);快照字段全部用枚举,不用字符串。
- schema 变更必须同步 `migration.rs`、表目录、生成绑定,并运行 `npm run check:spacetime-schema`;
已有表新增字段一律放在结构体最后并带明确默认值。
- 会员侧删列 / 删枚举属**破坏性变更**(会员未上线,已授权):迁移计划与回退见技术设计 §2.7;
账本来源枚举仍只允许末尾追加 `MembershipUpgradeGrant`。
## 5. 接口契约
| 接口 | 变更 |
|---|---|
| `GET /api/profile/recharge-center` 等余额投影 | 并列三池;`monthlyPoints` 带到期日 / 刷新日 / 期数;不再暴露池语义的 `totalPoints` |
| `POST /api/profile/membership/upgrade-quote`(新增) | 只读报价:入参目标档位 + 目标周期,返回应付金额、补点、变更后月度余额、有效期;目标档位 token 严格解析进枚举 |
| 会员购买 / 升级下单 | **复用** `POST /api/profile/recharge/orders`;`product_id` = 目标计划 id,下单时重算并落订单快照 |
| `GET|POST /admin/api/profile/membership-plans`(新增) | 后台套餐配置,按档位枚举改月价 / 年价 / 每期额度 / 模型权限 / 并发上限 |
| `GET|POST /admin/api/profile/recharge-products` | 6 档充点商品的上下架与新档位 |
`/api/external/v1` 不涉及本方案;若实施中触及其路由、DTO 或语义,必须在同一次变更中同步修订
`docs/openapi/genarrative-external-v1.openapi.json` 与契约测试。
## 6. 里程碑
### M1 双余额与月度会员(不含年付与补差)
- 交付物:三池并列投影(含 `monthlyPoints` 改名与 `totalPoints` 退役)、自然月账期、
`profile_membership_plan`(档位枚举主键)与月度会员购买、充点 6 档(复用旧 id + 新增 2 档)、
会员档位枚举化与旧 `tier` / 旧权益字段退役。
- 验收判据:§0 的判据 1、2、5;月付会员可购买、可到期清零、可在新期收到新额度。
- 必测点:三池恒等式(总额 = 每日 + 月度 + 永久)、扣减顺序与可用总额、月末夹取(1/31 → 2/28 → 3/31)、
每日免费与月度同时存在时的扣减拆分元数据、退款跨期落永久池、旧充点商品历史订单仍可展示。
### M2 年付 12 期与补差升级
- 交付物:年付(12 期)、后端只读报价接口、`MembershipUpgradeGrant` 与订单维度幂等、
订单变更快照、后台套餐配置页。
- 验收判据:§0 的判据 3、4;年付 12 期逐月发放;升级补付 / 补点与 §3.3 示例一致;
重复回调不重复发点;退款进 `manual_review` 且快照可读。
- 必测点:金额向上取到分 / 补点向下取整的边界(含 0.5 期、闰年、12/31 跨年)、
连续升级基准、报价与下单重算一致、支付失败不动原状态、年付到期未续清月度保留永久。
## 7. 风险与边界
- **永久余数是临时实现**:池层面的 bug 会表现为永久泥点虚高,排查必须回到账本 `permanentPointsDelta`(见 ADR 的 TODO)。
- **在线契约变更**:`totalPoints` / `limitedPoints` 的消费方分布在后端投影、`packages/shared` 契约与 store、
`PlatformMudPointWalletEntry`、`PlatformProfileRechargeModal`、AGC `AccountWallet` / `useAccountWallet`、
admin-web 多处;漏改会静默显示错数字。必须一次性改完并全量跑相关测试。
- **升级补点不可自动回收**:它只有「发」没有「扣」的对称半边,退款只能人工复核。
- **惰性刷新没有 cron**:到期与时区问题只在用户被触碰时暴露,测试必须直接构造时间。
- 跨天 / 跨周期退款仍然有损(落永久池),这是刻意行为,不要「顺手修」。
## 8. 待确认输入与遗留 TODO
- **套餐目录数值已定**:`Starter` ¥39 / 400 点 / ¥390 · `Plus` ¥99 / 1150 / ¥990 · `Pro` ¥299 / 3650 / ¥2990 · `Max` ¥699 / 8650 / ¥6990(`Normal` 全 0);模型权限 Starter 仅基础、其余含高性能;并发 1 / 3 / 5 / 128(不设上限)。
- **充点 6 档金额已定**:复用旧 4 档 id 并置赠送 0,新增 `points_1280`(¥128 / 1280 点)与 `points_3280`(¥328 / 3280 点);数值按原型 ¥6 / 18 / 30 / 68 / 128 / 328、¥1 = 10 泥点固化。
- **TODO(永久泥点独立存储)**:见 `docs/adr/【ADR】泥点三池以单一总额为权威-2026-10-02.md` 第 5 条。
- **TODO(文档同步)**:实施时同批修订权威契约
`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md` 的「账户充值数据契约」与
「用户钱包与编辑器生成扣费契约」两节,以及 `docs/【编辑器】模型定价配置管理方案-2026-06-22.md` 中
与扣费顺序相关的表述。
## 9. 需同步修订的权威文档
| 文档 | 修订点 |
|---|---|
| `docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md` | 三池投影与扣减顺序、永久泥点来源、会员账期与年付、升级补点与幂等、充值商品档位与首充赠送、账本来源全集 |
| `docs/【项目基线】当前产品与工程约束-2026-05-15.md` | 「用户钱包与编辑器生成扣费契约」的余额字段 |
| `docs/【编辑器】模型定价配置管理方案-2026-06-22.md` | 与扣费顺序 / 月度池相关的表述 |
| `docs/project-memory/shared-memory/decision-log.md` | 本次四条决策(已追加 2026-10-02 条目) |
| `CONTEXT.md` | 新增「泥点与会员计费」术语 |
@@ -42,6 +42,7 @@
- 上游变化不自动跟随:目录只在未初始化时重建;上游新增或移除模型由 owner 在后台增删条目或调整启用、默认项。
- `GET/PUT /admin/api/agc-models` 仅 owner 可用,返回完整配置;PUT 携带上次读取的 revision,冲突拒绝覆盖。
- `GET /api/llm/models` 返回启用项的 `id/displayName`、`defaultModelId` 和目录 `revision`,不返回实际模型名、Router 目录、凭据或能力原始数据。
- 2026-10-03 修订(模型权限):`GET /api/llm/models` 改为两个平行数组——`models` 只放本档可用;`unavailableModels` 放目录里其余(含后台停用与本档档位不够),元素带 `reason` 枚举(`plan_required` / `disabled` / `unknown`,同时停用且档位不够取 `disabled`)。`unavailableModels` 是加法字段,旧客户端忽略后行为不变,且不放松服务端按档强制;`defaultModelId` 仍必须落在 `models` 内。客户端把 `unavailableModels` 渲染为不可点项并 hover / focus 出原因(`plan_required`→「订阅计划不支持」、`disabled`→「该模型已下线」、`unknown`→「暂不可用」);已选模型落进不可用桶时自动回退默认并给出对应原因文案,后台直接删除(两桶都没有)用泛化文案。
- 客户端缓存最近 `revision`,在项目切换 / 对话表面挂载 / 下拉展开 / 窗口聚焦时条件刷新:`revision` 未变化不更新界面,同一时刻只保留一个在途请求,刷新失败保留上一次有效目录与本地选择。发起对话前用同一份快照校验所选模型仍启用,已停用或删除则回退默认模型并提示。
- 手动刷新立即显示进行中状态;真实刷新成功后显示完成反馈,即使 `revision` 未变化也有反馈。失败沿用有效缓存时仍显示失败,不能报告刷新成功;HTTP 状态和超时使用可辨认的提示。
- 模型目录与其它客户端 JSON API 的成功、失败响应体读取均复用 `readClientHttpResponseText` 的 15 秒上限;响应头已返回但响应体卡住时必须结束本次等待、释放目录在途请求并允许重试,迟到的响应不得覆盖新目录。
@@ -0,0 +1,342 @@
# 【技术设计】会员与泥点前端改造
> 定位:承接 [`【技术设计】泥点三池与会员计费后端设计-2026-10-02`](./【技术设计】泥点三池与会员计费后端设计-2026-10-02.md)(后端契约与接口已落地)
> 与原型 [`充值与会员.html`](../../充值与会员.html),给出前端(`packages/shared` + `src` + `apps/ai-game-creator-shell`)的改造设计。
> 范围:共享充值弹层、钱包入口、三处消费方的 controller 与客户端接口;不含后端实现。
> 状态:**已实施(2026-10-03)**。评审已通过,按 §11 清单落地;实现与验证结果见 §11、§13。
---
## 0. 一句话
把共享充值弹层从「一列泥点商品」升级为与原型一致的「会员与泥点」双 Tab 弹层
(套餐卡 + 月/年切换 + 会员状态 + 补差升级 + 六档充值 + 套餐对比 + 确认页),
金额与权益只读后端目录与报价接口,三处消费方共用同一套组件。
---
## 1. 已确认决策(拷问结论)
| # | 决策 | 结论 |
|---|---|---|
| 1 | 改造层级 | 扩展**共享** `PlatformProfileRechargeModal` 及其 controller,三处消费方共用 |
| 2 | 与原型对齐 | **完全像素级对齐**:两个 Tab、四张套餐卡、对比表、数量估算、确认页都做 |
| 3 | 上线方式 | **直接全量开放**,不加功能开关;会员 UI 对所有用户可见 |
| 4 | 钱包入口 | 改用**原型入口形态**(可用总额 + 行内月度/永久 + 「会员与泥点」按钮) |
| 5 | 入口次级功能 | 保留「使用详情」「兑换码」为次级入口,不因贴原型而砍功能 |
| 6 | 余额池 | 弹层内显示**三池**(每日免费 / 月度 / 永久)并按真实扣减顺序说明 |
| 7 | 金额真源 | **服务端报价**:购买用目录价、升级调 `POST /api/profile/membership/upgrade-quote`,前端不计费 |
| 8 | 确认页 | **做**确认页;**不做**前端倒计时,也**不做**价格 compare / 不一致提示 |
| 9 | 支付方式 | **仅微信**,渠道按运行环境自动解析,不出现支付宝 |
| 10 | 套餐文案 | 价格/每期泥点/模型/并发/标题读后端 catalog;「数量估算」「推荐之选」等营销文案放前端常量 |
| 11 | 消费方范围 | Web 平台入口、图片编辑器、AGC 桌面壳**三处都接** |
| 12 | 文档流程 | 先出本设计文档评审,再动代码 |
| 13 | `review.txt` | 后端评审问题**不作为本次门禁**,本次只做前端 |
---
## 2. 现状(已核实)
- 共享弹层 `packages/shared/src/components/PlatformProfileRechargeModal/index.tsx`(538 行)
只渲染 `center.pointProducts` 一列商品,标题「购买更多泥点」;props 只有
`center / isLoading / error / submittingProductId / nativePayment / onClose / onRetry / onBuy / onConfirmNativePayment / onCloseNativePayment`。
- 同目录 `index.test.tsx` 目前**断言不暴露会员**:`queryByRole('tablist')` 为 `null`、`queryByText('Starter')` 为 `null`,
用例名即 `shows the point-product empty state without exposing membership purchase`。本次必须改这些断言。
- Web 平台入口 `src/components/platform-entry/PlatformEntryActiveFlowShell.tsx` 与
图片编辑器 `src/components/image-editor/ImageCanvasEditorView.tsx` **共用**同一个 controller
`src/components/platform-entry/usePlatformProfileCenterController.ts`(1923 行)→ Web 端只需改一处。
- AGC 桌面壳走独立链路:`apps/ai-game-creator-shell/src/features/app-shell/useAccountWallet.ts`
→ `apps/ai-game-creator-shell/src/services/accountHost.ts`(Tauri `invoke`)
→ `apps/ai-game-creator-shell/src-tauri/src/account_api.rs`(命令注册在 `desktop.rs`)。
- 三端都**还没有**升级报价的客户端方法;Web 端现有客户端为
`getPlatformProfileRechargeCenter` / `createPlatformProfileRechargeOrder(productId, paymentChannel)` /
`confirmWechatPlatformProfileRechargeOrder` / `watchWechatPlatformProfileRechargeOrder`。
- 契约侧 `packages/shared/src/contracts/runtime.ts` 已有
`ProfileMembershipPlan` / `ProfileMembershipCycleKind` / `ProfileMembershipPlanRecord` /
`ProfileMembership` / `ProfileMembershipUpgradeQuote` / `ProfileRechargeCenterResponse`。
- 真实支付渠道只有微信(`wechat_mp` / `wechat_mp_virtual` / `wechat_jsapi` / `wechat_h5` / `wechat_native` / `mock`),
由 `resolveProfileRechargePaymentChannel` 按运行环境解析,**没有支付宝**。
- 会员下单复用 `POST /api/profile/recharge/orders`,`product_id = membership-{plan}-{cycleKind}`
(如 `membership-pro-yearly`),会员商品没有商品配置行,标题与价格来自目录表。
---
## 3. 契约与需要新增的前端接口
### 3.1 复用(不改)
- `GET /api/profile/recharge-center` → `ProfileRechargeCenterResponse`
(`mudPointBalance` 三池 / `membership` / `membershipPlans` / `pointProducts`)。
- `POST /api/profile/recharge/orders { productId, paymentChannel }` → 泥点与会员下单共用。
- `POST /api/profile/recharge/orders/{orderId}/wechat/confirm` → 微信支付确认。
### 3.2 新增(前端缺、后端已有)
Web 端 `src/services/platform-entry/platformProfileClient.ts` 新增:
```ts
export function getPlatformProfileMembershipUpgradeQuote(
targetPlan: ProfileMembershipPlan,
options: PlatformProfileRequestOptions = {},
) {
return requestPlatformProfileJson<ProfileMembershipUpgradeQuote>(
'/membership/upgrade-quote',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ targetPlan }),
},
'读取升级报价失败',
options,
);
}
```
AGC 桌面壳需要新增同名能力:`accountHost.ts` 增加 `getClientProfileMembershipUpgradeQuote(targetPlan)`,
并在 `src-tauri/src/account_api.rs` 新增 `#[tauri::command] read_profile_membership_upgrade_quote(target_plan: String)`,
在 `src-tauri/src/desktop.rs` 的命令清单里注册。这是桌面壳唯一的非纯前端改动。
### 3.3 会员下单 productId
前端按 `membership-${plan}-${cycleKind}`(`yearly` / `monthly`)拼串后走既有下单接口,
不再向后端请求商品行;`plan === 'normal'` 不可下单。
---
## 4. 信息架构与组件拆分
弹层保持「一个 `index.tsx` + 内部子组件」的现有约定,另抽一个纯常量模块。
```
PlatformProfileRechargeModal/
├── index.tsx // Shell / Tab / 余额概览 / 套餐卡 / 周期切换 / 会员状态 / 对比表 / 充值网格 / 确认页 / 成功态 / 微信二维码
├── membershipCopy.ts // 「数量估算」文案、推荐档位、省费标签等纯前端常量
├── index.css // 复用现有 class,按需补样式
└── index.test.tsx
```
信息架构(对齐原型):
```
顶部入口(PlatformMudPointWalletEntry)
└─ 弹层「会员与泥点」
├─ Tab:会员订阅 | 泥点充值
├─ 余额概览(三池:每日免费 / 月度 / 永久 + 各自失效时间)
├─ 会员订阅面板
│ ├─ 当前会员状态(非会员时隐藏)
│ ├─ 月付 / 年付切换(有效期内锁定为当前周期)
│ ├─ 套餐卡 × 4(Starter / Plus / Pro / Max,来自 catalog,enabled 过滤,normal 不出现)
│ ├─ 套餐对比表(4 行 × 4 档)
│ └─ 底栏主 CTA
├─ 泥点充值面板
│ ├─ 六档金额网格(¥6/18/30/68/128/328 ↔ 60/180/300/680/1280/3280 泥点,¥1 = 10 泥点)
│ └─ 底栏「充值 ¥X」
└─ 确认页(弹窗)
├─ 价格与权益明细(购买:目录价 + 首期额度;升级:补差 + 补点 + 有效期不变)
├─ 支付方式(仅微信,自动解析渠道)
└─ 支付成功态
└─ 微信扫码 / 跳转支付(复用现有 native 支付弹窗,不改)
```
### 4.1 新增 props(`PlatformProfileRechargeModalProps`)
```ts
export type PlatformProfileMembershipConfirmState = {
/** 目标档位与周期;normal 不会出现在这里。 */
plan: ProfileMembershipPlan;
cycleKind: ProfileMembershipCycleKind;
/** 首次购买时来自目录价;升级时来自 upgrade-quote。 */
amountCents: number;
/** 服务端返回的补点;首次购买为 0。 */
grantedPointsDelta: number;
/** 升级后的月度余额;首次购买为首期额度。 */
monthlyBalanceAfter: number | null;
mode: 'purchase' | 'upgrade';
};
// 追加到现有 props:
membershipQuote: ProfileMembershipUpgradeQuote | null;
isLoadingMembershipQuote: boolean;
membershipQuoteError: string | null;
membershipConfirm: PlatformProfileMembershipConfirmState | null;
onRequestMembershipQuote: (plan: ProfileMembershipPlan) => void;
onRequestMembershipPurchase: (plan: ProfileMembershipPlan, cycleKind: ProfileMembershipCycleKind) => void;
onCancelMembershipConfirm: () => void;
onConfirmMembershipPayment: () => void;
```
`onBuy(product)` 只服务泥点商品;会员走 `onRequestMembershipQuote` / `onRequestMembershipPurchase`。
---
## 5. 数据流与状态
- 套餐卡的价格、每期泥点、模型权限、并发上限、标题全部读 `center.membershipPlans`
(按 `sortOrder` 升序、`enabled` 过滤、排除 `normal`)。前端**不**存第二份价格。
- 点套餐判定:
- 当前有效会员且 `plan !== 当前档位` 且 `cycleKind === 当前周期` → 调 `onRequestMembershipQuote(plan)`;
- 否则(非会员 / 已到期)→ 直接 `onRequestMembershipPurchase(plan, cycleKind)` 用目录价开确认页。
- 周期切换:有效期内锁定为 `membership.cycleKind`,另一档禁用并提示「到期后可重新选择」。
- 可升级判定用 `rank`(`center.membershipPlans`),高于当前才能升;同档与降档按钮禁用。
- 确认页金额:升级取 `membershipQuote.amountCents`,购买取目录价;**不做**任何本地计费与价格比对。
- 支付:确认页「去支付」→ 控制器用既有 `createPlatformProfileRechargeOrder(productId, channel)`,
渠道走 `resolveProfileRechargePaymentChannel`;桌面/浏览器差异沿用现有 native 二维码弹窗。
- 支付成功后刷新 recharge center;成功态展示本期泥点与三池余额。
---
## 6. 交互细则
- 与原型一致,但**去掉**原型里的本地 2 分钟报价倒计时与「报价已失效」逻辑
(金额真源在后端,下单与支付确认由后端重算)。
- **不做**价格 compare:不在前端比较「页面展示价」与「服务端返回金额」,也不做不一致提示。
- 移动端优先:弹层宽度、套餐卡网格、对比表横向滚动,遵循现有 `PlatformProfileRechargeModalShell` 尺寸约定。
- 可访问性:Tab 用 `role="tablist"` / `role="tab"` / `aria-selected` + 左右方向键;
套餐卡与充值卡用 `role="radio"` + `aria-checked`;确认页与支付弹窗沿用 `role="dialog"`。
---
## 7. 文案与常量(`membershipCopy.ts`)
只放后端没有、且属于营销表达的内容,按 `plan` token 匹配:
- 每档「每月预计可完成」数量估算与说明;
- 「推荐之选」标签(原型为 `plus`);
- 年付「省 ¥X」文案(按目录 `yearPriceCents` 与 `monthPriceCents × 12` 计算,只用于展示);
- 并发文案:`concurrentJobLimit >= 128` 显示「不设套餐上限」,否则「同时运行 N 个独立任务」;
- 模型权限文案:`basic` →「基础模型」,`full` →「基础及高性能模型」。
未知 `plan` token 时给通用兜底文案,不抛错。
---
## 8. 三处消费方接入清单
| 消费方 | 文件 | 改动 |
|---|---|---|
| Web 平台入口 | `src/components/platform-entry/PlatformEntryActiveFlowShell.tsx` | 传新增 props;入口按钮文案改「会员与泥点」 |
| 图片编辑器 | `src/components/image-editor/ImageCanvasEditorView.tsx` | 同上(共用 controller) |
| Web/编辑器 controller | `src/components/platform-entry/usePlatformProfileCenterController.ts` | 新增报价状态、确认状态、会员下单 handler |
| Web 客户端 | `src/services/platform-entry/platformProfileClient.ts` | 新增 `getPlatformProfileMembershipUpgradeQuote` |
| AGC 桌面壳 | `apps/ai-game-creator-shell/src/features/app-shell/useAccountWallet.ts`、`AccountWallet.tsx` | 新增报价状态与会员下单 handler |
| AGC 客户端 | `apps/ai-game-creator-shell/src/services/accountHost.ts` | 新增 `getClientProfileMembershipUpgradeQuote` |
| AGC Tauri | `apps/ai-game-creator-shell/src-tauri/src/account_api.rs`、`desktop.rs` | 新增并注册 `read_profile_membership_upgrade_quote` |
| 共享入口 | `packages/shared/src/components/PlatformMudPointWalletEntry/index.tsx` | 入口形态按原型调整,保留次级入口 |
---
## 9. 测试计划
- 共享弹层(`index.test.tsx`):
- 改写现有两条「不暴露会员」断言为「暴露会员 Tab 与套餐卡」;
- 新增:四档套餐按 `sortOrder` 渲染、`normal` 与 `enabled=false` 不出现;
- 新增:周期切换在有效期内锁定;
- 新增:点套餐触发 `onRequestMembershipQuote`;
- 新增:确认页展示金额与补点,且**不出现**倒计时与价格 compare 文案;
- 新增:六档充值网格与底栏充值按钮触发 `onBuy`/下单;
- 新增:三池余额展示(含每日免费重置值)。
- Web controller:报价成功/失败、会员下单 productId 拼串、支付成功刷新。
- AGC 壳:`accountHost` 新命令的 invoke 参数与 `useAccountWallet` 报价状态。
- 回归:`PlatformEntryActiveFlowShell.test.tsx`、`ImageCanvasEditorView.test.tsx` 中与弹层标题/入口文案相关的断言同步更新。
- 命令:定向 vitest + `npm run check:encoding` + `git diff --check`。
---
## 10. 验收判据
1. 打开「会员与泥点」弹层可见两个 Tab、四张套餐卡、周期切换、套餐对比表与六档充值网格。
2. 套餐价格、每期泥点、模型权限、并发上限与后端 `membershipPlans` 完全一致,前端无第二份价格。
3. 点套餐进确认页:购买显示目录价,升级显示服务端报价金额与补点;确认页无倒计时、无价格不一致提示。
4. 支付方式只有微信,渠道按运行环境解析,不出现支付宝。
5. 会员与泥点下单都走既有 `POST /api/profile/recharge/orders`,会员 productId 为 `membership-{plan}-{cycleKind}`。
6. Web 平台入口、图片编辑器、AGC 桌面壳三处行为一致,入口保留「使用详情」「兑换码」。
7. 三池余额(每日免费 / 月度 / 永久)与失效时间展示正确,弹层与入口不出现 `NaN`。
8. `npm run check:encoding`、定向 vitest、`git diff --check` 通过。
---
## 11. 实施清单(按顺序)
1. `packages/shared/src/contracts/runtime.ts`:如有缺字段补齐(本轮预计只读,不改形状)。
2. `src/services/platform-entry/platformProfileClient.ts`:新增升级报价客户端方法。
3. `packages/shared/.../PlatformProfileRechargeModal/membershipCopy.ts`:新增前端常量。
4. `packages/shared/.../PlatformProfileRechargeModal/index.tsx`:双 Tab + 会员面板 + 充值网格 + 确认页。
5. `packages/shared/.../PlatformMudPointWalletEntry/index.tsx`:入口形态按原型调整。
6. `src/components/platform-entry/usePlatformProfileCenterController.ts`:报价/确认/下单状态与 handler。
7. `PlatformEntryActiveFlowShell.tsx`、`ImageCanvasEditorView.tsx`:传新 props。
8. AGC:`accountHost.ts` + `account_api.rs` + `desktop.rs` + `useAccountWallet.ts` + `AccountWallet.tsx`。
9. 更新与新增测试,跑定向 vitest 与编码检查。
---
## 12. 风险与后续
- **后端已知缺陷**(见 `review.txt`):结算不比对 `order.amount_cents`、报价接口写库、
旧 `tier` 无迁移等。按用户决定**不作为前端门禁**;作为后端跟进项单独处理。
- 确认页只展示服务端金额,不做事前拦截;若后端金额与展示不同,以支付确认结果为准。
- AGC 桌面壳新增 Tauri 命令属于壳内 Rust 改动,需要与桌面端一起回归。
- 「完全像素级对齐」意味着弹层代码量与测试量显著上升,后续可按效果评估是否精简对比表。
---
## 13. 实施与验证结果(2026-10-03)
### 13.1 落地清单
- 共享弹层 `PlatformProfileRechargeModal`:双 Tab、三池余额概览、套餐卡、周期切换(有效期内锁定)、会员状态、套餐对比表、六档充值网格与底栏充值按钮、确认页(仅微信支付,无倒计时、无价格对比);新增 `membershipCopy.ts` 承载营销文案与并发/模型展示口径。
- 钱包入口 `PlatformMudPointWalletEntry`:改为「可用总额 + 行内月度/永久 + 会员与泥点」,`使用详情` / `兑换码` 作为次级入口保留。
- Web:`platformProfileClient.ts` 新增 `getPlatformProfileMembershipUpgradeQuote`;`usePlatformProfileCenterController` 新增报价状态与 `submitMembershipCheckout`;平台入口与图片编辑器已传参。
- AGC 桌面壳:`accountHost.ts` 新增 `getClientProfileMembershipUpgradeQuote`;`account_api.rs` 新增并注册 `read_profile_membership_upgrade_quote`;`useAccountWallet` 新增报价状态与会员结算;`AccountWallet.tsx` 已传参。
### 13.2 验证证据
- `npm run typecheck` 通过(`tsconfig.typecheck-guardrails.json`)。
- `npx tsc -p apps/ai-game-creator-shell/tsconfig.json --noEmit` 通过。
- 定向 vitest:共享弹层 6/6;平台入口 22/22;图片编辑器顶栏 5/5;`usePlatformProfileCenterController.recharge` 8/8;AGC `accountHost` + `walletStore` 17/17。
- `npm run check:encoding` 通过(5249 个文件);`git diff --check` 通过。
- `cargo fmt --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml -- --check` 通过。
- ⚠ **未执行 `cargo check`**:仓库无 Rust 构建缓存,冷编译成本过高;新增 Tauri 命令只复用同文件既有 `require_session` / `build_client` / `bounded_business_id` / `request_json`,四个签名已逐一核对,风险集中在类型名拼写。
### 13.3 既有问题(非本次引入)
- `ImageCanvasEditorView.test.tsx > shows the breakdown read failure without inventing rows` 在改动前的基线同样失败(已用 `git stash` 对照验证):弹层打开时仍处于 loading 分支,断言未等待错误态。属既有时序问题,本次未修改该行为。
---
## 14. 追加变更:每日免费发放额为 0 时隐藏(2026-10-03)
### 14.1 语义
- **不引入** `retired` / 状态位 / 新字段;每日免费只是一个普通数值,发放额可以是 0。
- **唯一信号**:`ProfileMudPointBalance.dailyFreeResetPoints <= 0`(后端每日发放额,来自后台配置 `daily_free_points_per_day`)。
- **可逆**:后台把配置改回 > 0,前端下次读取自动恢复展示,无需额外开关。
- **契约不变**:`ProfileMudPointBalanceResponse` 字段仍是必填 `u64`,`0` 本就合法;不改 `shared-contracts`、ts-rs 生成物与 OpenAPI。
- **扣减与账本逻辑不变**:每日免费为 0 时不再产生发放账本(`profile.rs` 在 `amount_delta == 0` 时直接返回),创作扣点顺序仍为每日免费 → 月度 → 永久。
### 14.2 显示规则
```
showDailyFreePool = dailyFreePoints > 0 || dailyFreeResetPoints > 0
```
| 场景 | dailyFreePoints | dailyFreeResetPoints | 表现 |
|---|---|---|---|
| 当天正常用完 | 0 | 20 | 显示("每天重置为 20") |
| 每日免费发放额为 0 | 0 | 0 | 隐藏 |
| 配置中途改为 0,当天仍有存量 | > 0 | 0 | 显示(存量仍会被优先扣减,不能隐身) |
判定收敛到 `packages/shared/src/utils/mudPoints.ts` 的纯函数,三端共享组件共用,消费方 controller 不改。
### 14.3 改动点
- 后端 `server-rs/crates/module-runtime/src/commands.rs`:`daily_free_points_per_day` 由"必须 > 0"放宽为 `0..=i64::MAX`;`errors.rs` 对应文案同步。
- Admin Web `apps/admin-web/src/pages/AdminProfileWalletConfigPage.tsx`:每日免费泥点允许填 0。
- 共享层 `packages/shared/src/utils/mudPoints.ts`:新增 `shouldShowProfileDailyFreePool`。
- 共享层 `PlatformMudPointWalletEntry`:为 0 时隐藏"每日免费泥点"行。
- 共享层 `PlatformProfileRechargeModal`:池概览按可见池动态渲染(三列变两列)、隐藏扣点顺序提示中与每日免费相关的口径。
- 账本来源文案 `daily_free_grant` / `daily_free_reset` 保留,历史流水不改写。
### 14.4 测试
- 删除"每日免费必然存在"的断言(`PlatformMudPointWalletEntry`、`ImageCanvasTopbarView`、`ImageCanvasEditorView`),改为在共享层新增"发放额为 0 隐藏 / 有存量仍显示"的行为用例。
- 后端与 Admin 追加"允许 0"的用例。
@@ -151,13 +151,21 @@ npm run check:server-rs-ddd
本地联调和人工排障不要使用 `spacetime --root-dir`。本地数据隔离使用项目脚本或 `--data-dir`;发布目标显式传 `--server` / `--server-url`。
## shared-contracts DTO 变更规则
1. 面向前端 / AGC 壳的**响应** DTO 只增不删:新增字段一律可选——`Option<T>`,或非 `Option` 时带 `#[serde(default ...)]` 且默认值安全(空集合 / `0` / `false` / 最小合法值)。这样「客户端先于服务端发布」的滚动升级不会因为旧服务端缺字段而整包解析失败;这是兼容未来版本的手段,不是给当前旧客户端保留 legacy shim。
2. 身份与状态类字段(例如 `ProfileMembershipResponse.plan` / `status`、`enabled`)保持必填,不给默认值:fail-open 会把付费会员误显示成非会员或反过来,静默错误比整包失败更难排查。
3. 未知 token 值继续走 `deserialize_forward_compat_*` 兜底;它与「缺字段默认值」是两条独立路径,都要有契约测试(旧服务端缺字段、新服务端多发未知取值各一条)。
4. 删除字段、改名、改类型或改必填属性属于破坏性变更,必须先确认版本化 / 迁移计划并更新权威文档;除非明确要求新旧客户端混跑,否则不为旧客户端保留 legacy shim。
5. 响应 DTO 的 serde 属性不影响 ts-rs 绑定;字段集合或类型变化必须重新生成绑定并运行 `npm run check:generated-bindings`。
## 账户充值数据契约
1. `profile_recharge_product_config` 是泥点和会员商品配置真相源,默认商品只在表为空时由 SpacetimeDB 播种。`module-runtime` 中的默认商品 helper 只作为空库种子和兼容入口,不再作为运行期业务真相。
2. 默认泥点商品固定为四档:`points_60 = 60 泥点 / 600 分`、`points_180 = 180 + 90 泥点 / 1800 分`、`points_300 = 300 + 150 泥点 / 3000 分`、`points_680 = 680 + 340 泥点 / 6800 分`。`points_60` 的首充赠送为 `0`;后三档首次购买分别赠送基础泥点的 `50%`。
3. 后台通过 `/admin/api/profile/recharge-products` 读写充值商品配置;字段覆盖 `productId`、标题、商品类型、金额分、基础泥点、首充赠送泥点、会员天数、徽标、说明、会员层级、会员每周期限时泥点、周期天数、队列上限、折扣率、启用状态和排序。
2. 默认泥点商品固定为四档:`points_60 = 60 泥点 / 600 分`、`points_180 = 180 泥点 / 1800 分`、`points_300 = 300 泥点 / 3000 分`、`points_680 = 680 泥点 / 6800 分`。所有档位的首充赠送一律为 `0`(已取消首充加赠)。
3. 后台通过 `/admin/api/profile/recharge-products` 读写充点商品配置;写入口只接受 `productId`、标题、商品类型(仅 `points`)、金额分、基础泥点、徽标、说明、启用状态和排序。`bonus_points` / `duration_days` 以及旧的会员层级、会员每周期限时泥点、周期天数、队列上限、折扣率列都是退役列:表中保留以兼容存量 schema,回读固定为 `0`,不再进入 upsert 入参。
4. 充值中心、下单校验和支付确认入账都读取 `profile_recharge_product_config`。充值中心 BFF 还必须在 `mudPointBalance` 下发 `totalPoints`、`permanentPoints`、`limitedPoints`、`limitedExpiresAt`、`dailyFreePoints`、`dailyFreeResetPoints` 和 `dailyFreeResetsAt`;前端以该 read model 为真相源,不得自行用总额相减推算余额桶。当前版本公开 UI 只渲染不限时泥点和每日免费泥点,`limitedPoints` 与 `limitedExpiresAt` 仅保留给存量兼容和后端结算。历史订单保留下单时写入的商品标题、金额、渠道、状态和 provider transaction id,不随配置改动回写。
5. 泥点首充资格按 `user_id + product_id` 的历史 `paid` 订单独立判断。某个档位已支付后,只隐藏该档位的首充赠送;其它未购买档位仍展示和结算首充赠送。
5. 泥点首充资格按 `user_id + product_id` 的历史 `paid` 订单独立判断。首充赠送当前恒为 `0`,因此该判定不改变前台展示和结算金额;兼容分支保留,待后续彻底移除首充赠送概念时一并删除。
6. `hasPointsRecharged` 只保留为账号是否发生过任一泥点充值的兼容字段,不得驱动所有商品展示隐藏或结算金额计算。前端只渲染后端返回的商品快照。
7. 当前版本公开充值 UI 只展示泥点商品,不渲染会员购买页签、会员商品、购买会员或升级会员入口。充值中心响应中的会员商品兼容字段、默认会员商品、`profile_membership` 和周期刷新逻辑继续保留;存量会员的 `cycle_remaining_points` 仍通过充值中心 read model 下发用于兼容和结算,但不作为限时泥点在当前版本前台展示。
8. 默认会员商品为空库播种时使用 `Starter / Basic / Pro / Ultimate` 四档,默认有效期均为 30 天,每周期限时泥点分别为 `200 / 800 / 2500 / 6000`,队列上限分别为 `2 / 2 / 5 / 10`。
@@ -292,6 +300,7 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复
- Rust 结构体:`ExternalGenerationJob`
- 源码:`server-rs/crates/spacetime-module/src/external_generation.rs`
- 现役覆盖:worker claim 只允许 `source_module = editor-canvas`;任务领取、续租、阶段更新和结果完成必须携带并校验同一 `worker_id + lease_token`。
- 2026-10-03 会员并发上限:新增 btree 组合索引 `by_external_generation_job_owner_status (owner_user_id, status)`。`claim_external_generation_jobs_tx` 在 `pending → running` 前按任务 owner 的 `profile_membership.plan → profile_membership_plan.concurrent_job_limit` 现算该 owner `status = running` 的行数(含租约过期待回收的 `expired_running`),达上限的 pending 保持排队;账号自身过期的 running 回收豁免上限,避免超限账号的卡死任务无法回收。`pending` 不占名额,失败退回 `pending` 会自然释放名额;不新增计数表。缺会员行按 `Normal`(上限 1)失败关闭,目录行缺失同样按 1;`128` 哨兵由 `is_unlimited_concurrency` 解释为不限。
- 用途:外部生成 worker 的内部持久任务队列;`GENARRATIVE_EXTERNAL_GENERATION_MODE=queue` 时,`api-server` HTTP 角色只入队,`external-generation-worker` 角色通过 claim lease 领取、续租、执行,并用 `lease_token` 栅栏回写阶段、完成 / 失败。队列行继续保存 worker 执行、计费与滚动发布兼容所需字段,末尾可选 `phase` 只取 `generating / processing`;claim 写 `generating`,真实进入抠图处理时由受 `job_id + worker_id + lease_token` 保护的 procedure 写 `processing`。phase procedure 以结构化结果区分 `LeaseFencingRejected` 与 `OtherRejected`;`LeaseFencingRejected` 立即终止,`OtherRejected` 以及 SDK 的 `Procedure` / `Runtime` 错误不重试,只有 `Build` / `ConnectDropped` / `Timeout` 在同一个 job attempt 内重试一次。该重试只重新上报 phase,不把任务写回 `pending`,也不重新调用 provider;编辑器 job 入队固定 `max_attempts=1`,第二次传输失败后任务进入 `failed`,不会回到 `pending` 或从 provider 生成起点重跑。用户可见任务列表、价格、状态、阶段、未确认终态数量和通知确认时间的正式读取事实源已经迁到 `external_generation_job_summary`;BFF 不得再为列表 / 详情 / acknowledge 读取该大表。图片画布编辑器的 `editor_image_generation`、`editor_image_edit`、`editor_background_removal`、`editor_icon_spritesheet_generation`、`editor_ui_design_asset_extraction`、`editor_character_animation_generation`、`editor_video_generation`、`editor_sound_effect_generation` 和 `editor_background_music_generation` 复用同一队列表。结构化 canvas 激活后,当前 worker completion 先以读取时 canvas revision 执行 CAS,并发冲突时拒绝覆盖并保留可诊断失败;目标是进一步收口为受 lease 栅栏保护的单事务幂等写入 `editor_project_resource`、结果 `editor_canvas_layer`、`editor_canvas_generation_dialog` 终态和 canvas revision。未激活 canvas 在 2 MiB 上限内继续走 `editor_canvas.layers_json` 兼容写回。前端只通过 BFF job 状态轮询和项目快照读取恢复完成态。`GENARRATIVE_EXTERNAL_GENERATION_MODE=inline` 时不创建该队列行,三个 external generation guard 字段必须同时为空才允许 api-server 受控同步写回,半空 guard 仍会拒绝。worker 成功写回业务事实后才能 complete job;业务失败态写回成功后才能 fail job,失败态未写回时保留租约等待后续重领。
- 2026-08-06 收口覆盖:上一条用途描述中“当前先 CAS、单事务仍是目标”的旧句已作废。现役编辑器生成不再组合调用 object confirm、resource create、asset create、canvas save 和 job complete。`api-server` 只准备稳定候选,再经 `spacetime-client` 调用 `persist_editor_generation_result_and_return`;procedure 在同一 `try_with_tx` 内写入可选 `asset_object`、全部 `editor_project_resource`、`editor_asset`、可选 `asset_entity_binding`、可选 canvas V2 CAS、queue job 终态和 `editor_generation_operation` receipt。结构化 canvas 的 layer / dialog / revision 与未激活 canvas 的 legacy `layers_json` 仍经既有 V2 布局验证分流,前端不直接发明正式完成态。queue 首次提交在同一快照验证 owner、job kind、request fingerprint 和有效 `job_id + worker_id + lease_token`;统一 procedure 已完成 job 后 worker 不得再单独 complete。
- 载荷约束:本次先对 `source_module = editor-canvas` 的 `request_payload_json` / `result_payload_json` 实施有限大小合法 JSON、任意层级禁止 `data:` / `blob:` 的双层门禁,只保存 worker 执行必需的普通参数和已登记媒体引用。画布 Agent 来源的任务可在 `result_payload_json.editor-agent-tool-call-result` 中保存有界的轻量结果和已登记媒体引用,供后端按已有 `externalJobId + owner_user_id` 定向懒回填;其它编辑器任务保持元数据结果,并可保存有界的 `warning.code/reason`。未登记的 source module 不由本次门禁静默改变既有请求契约。该主表只供 worker claim / 执行、受控维护以及画布 Agent 的定向结果回填读取;正式用户任务列表、单任务状态、队列概览与 acknowledge 不得返回或解析这两个 payload。画布 Agent 懒回填必须经对应工具 formatter 归一为有界轻量媒体引用后写入 OSS 会话,不能把原始 payload 直接透传前端。
@@ -745,144 +754,154 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复
### `profile_dashboard_state`
- Rust 结构体:`ProfileDashboardState`
- 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs`
- 源码:`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`
### `profile_daily_free_points`
- Rust 结构体:`ProfileDailyFreePoints`
- 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs`
- 源码:`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`
- 作用:每日免费泥点事实源。`day_key` 使用北京时间业务日,基础发放量读取 `profile_wallet_config.daily_free_points_per_day`(未配置时默认 `20`),`remaining_points` 保存当日剩余额度;跨业务日退款的每日免费消费部分会叠加到退款当日,使 `granted_points` 和 `remaining_points` 可暂时超过当前基础发放量,下一业务日首次触达时旧余额与叠加量一并失效并按当时最新配置重置。
### `profile_feedback_submission`
- Rust 结构体:`ProfileFeedbackSubmission`
- 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs`
- 源码:`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`
### `profile_invite_code`
- Rust 结构体:`ProfileInviteCode`
- 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs`
- 源码:`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`
- 生效时间:`starts_at` / `expires_at` 均可为空;两者同时存在时必须满足 `starts_at < expires_at`。开始时刻计入有效区间,截止时刻不计入有效区间。
### `profile_code_operation`
- Rust 结构体:`ProfileCodeOperation`
- 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs`
- 源码:`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`
- 作用:后台兑换码 / 邀请码的持久操作记录,记录新增、更新、停用的码值、操作人和操作时间。
### `profile_membership`
- Rust 结构体:`ProfileMembership`
- 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs`
- 作用:会员有效期和当前周期限时泥点事实源。`started_at/expires_at` 表示会员有效期,`cycle_started_at/cycle_resets_at/cycle_period_days` 表示当前周期,`cycle_granted_points/cycle_remaining_points` 表示当前周期已发放和剩余限时泥点。
- 源码:`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`
- 作用:会员有效期和当前周期限时泥点事实源。`started_at/expires_at` 表示会员有效期,`cycle_started_at/cycle_resets_at` 表示当前周期,`cycle_granted_points/cycle_remaining_points` 表示当前周期已发放和剩余限时泥点;`plan/cycle_index/cycle_count/cycle_kind` 是档位身份与自然月期数的权威字段,末位追加。
- 兼容列:旧 `tier`(`RuntimeProfileMembershipTier`)与固定 30 天的 `cycle_period_days` 已退役,仅为兼容存量 schema 保留在原始位置,运行时不读不写。
### `profile_membership_plan`
- Rust 结构体:`ProfileMembershipPlan`
- 源码:`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`
- 作用:会员计划目录真相源,**主键是档位枚举** `RuntimeProfileMembershipPlan`(`Normal` / `Starter` / `Plus` / `Pro` / `Max`),一行一档,供后台按档位直接改价和改权益。
- 字段:`month_price_cents` 与 `year_price_cents` 是两个独立可配置价格(年价不做月价 ×10 推导);`period_points` 是每期(一个自然月)额度,年付 = 12 次同额度发放;`model_access` 与 `concurrent_job_limit` 是档位权益,并发上限 `128` 表示“不设上限”。
- 索引:主键 `plan`。
- 说明:`Normal` 是不可购买的非会员占位档位(`enabled = false`);模型权限与并发上限当前只落字段、只用于展示,服务端不做拦截。
### `profile_recharge_product_config`
- Rust 结构体:`ProfileRechargeProductConfig`
- 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs`
- 作用:泥点和会员充值商品配置真相源,供充值中心展示、下单校验、支付确认和后台“充值商品”页维护;当前公开充值中心只展示四档泥点商品,会员商品配置留存但不公开购买或升级入口。
- 字段补充:会员商品追加 `membership_period_points`、`membership_period_days`、`membership_queue_limit`、`membership_discount_bps`;泥点商品这些字段必须为 `0`。
- 源码:`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`
- 作用:泥点充值商品配置真相源,供充值中心展示、下单校验、支付确认和后台“充值商品”页维护;当前公开充值中心只展示六档泥点商品(`points_60` ~ `points_3280`),会员购买已下线。
- 兼容列:旧 `tier`(`RuntimeProfileMembershipTier`)与会员商品权益列 `membership_period_points`、`membership_period_days`、`membership_queue_limit`、`membership_discount_bps` 已退役,仅为兼容存量 schema 保留在原始位置,运行时不读不写。
### `profile_played_world`
- Rust 结构体:`ProfilePlayedWorld`
- 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs`
- 源码:`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`
### `profile_recharge_order`
- Rust 结构体:`ProfileRechargeOrder`
- 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs`
- 源码:`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`
- 作用:账户充值订单事实源。`status` 包含 `pending`、`paid`、`failed`、`closed`、`refunded`、`expired`;过期补偿字段 `expired_at`、`expiration_checked_at`、`expiration_provider_state`、`expiration_last_error` 用于记录本地过期和微信查单结果。
### `profile_recharge_refund`
- Rust 结构体:`ProfileRechargeRefund`
- 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs`
- 源码:`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`
- 作用:普通微信支付 V3 退款单聚合。以 `out_refund_no` 为主键、`provider_refund_id` 唯一,保存原订单/微信支付单、四个分金额、微信状态、最近观察、权益目标/已回收/未回收量和人工处理错误码。管理员读取契约同步暴露微信交易号、订单总额和人工复核获批错误码。交易号或订单总额冲突经管理员确认归属后,追加保存处理管理员、非空原因、处理时间和本次获批的错误码;四字段只写一次,作为重新执行正式结算的审计事实。
- 索引:`by_profile_recharge_refund_order_id`、`by_profile_recharge_refund_status_updated_at`。
### `profile_recharge_refund_observation`
- Rust 结构体:`ProfileRechargeRefundObservation`
- 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs`
- 源码:`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`
- 作用:退款事实的追加观察记录。保存 callback / api_request / query / trade_bill 来源、按“来源 + 事实指纹”稳定生成的 observation ID、金额、状态、脱敏通知引用、事实指纹和 resolution code;同一事实从不同来源进入时使用不同 ID,不保存回调密文、签名、密钥、原始 CSV 或短时下载 URL。
- 索引:`by_profile_recharge_refund_observation_out_refund_no`、`by_profile_recharge_refund_observation_order_id`。
### `profile_recharge_order_refund_settlement`
- Rust 结构体:`ProfileRechargeOrderRefundSettlement`
- 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs`
- 源码:`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`
- 作用:原充值订单维度的退款与权益结算摘要,保存累计成功退款金额、目标/已回收/未回收泥点、权益状态和钱包冻结事实。部分退款不改订单终态;累计全额才把订单改为 `refunded`。
- 索引:主键 `order_id`,`by_profile_recharge_order_refund_settlement_user_id` 用于钱包消费前检查退款欠款。
### `profile_recharge_refund_hold`
- Rust 结构体:`ProfileRechargeRefundHold`
- 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs`
- 源码:`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`
- 作用:后台主动退款调用微信前的永久泥点占用。活动占用保护退款所需泥点但不改钱包总额;匹配退款成功后结算,关闭或确认未创建的退款释放。相同 `out_refund_no` 只允许内容完全一致的幂等重放。
- 索引:主键 `out_refund_no`,`by_profile_recharge_refund_hold_order_id`、`by_profile_recharge_refund_hold_user_id`、`by_profile_recharge_refund_hold_status`。
### `profile_wallet_manual_restriction`
- Rust 结构体:`ProfileWalletManualRestriction`
- 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs`
- 源码:`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`
- 作用:后台人工钱包冻结当前态,保存冻结原因、创建/更新管理员和时间。它不承载退款欠账;退款欠账仍来自订单退款 settlement,两个来源任一有效都阻断普通消费。
- 索引:主键 `user_id`。
### `profile_recharge_refund_bill_checkpoint`
- Rust 结构体:`ProfileRechargeRefundBillCheckpoint`
- 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs`
- 源码:`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`
- 作用:按交易账单日期记录已完成的退款 reconciliation,保存账单 SHA1(确认稳定无账单时保存 `NO_STATEMENT_EXIST`)、处理退款行数和完成时间。任一退款行失败时不写完成 checkpoint,成功前缀依赖 observation 幂等重放;重复下载、多实例执行或进程重启不会重复回收权益。
### `profile_recharge_order_expiration_schedule`
- Rust 结构体:`ProfileRechargeOrderExpirationSchedule`
- 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs`
- 源码:`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`
- 作用:旧普通微信充值订单到期查单调度表,保留 schema 兼容但当前不再写入;当前过期路径改由原生 scheduled 表 `profile_recharge_order_expiration_timer` 触发。
### `profile_recharge_order_expiration_timer`
- Rust 结构体:`ProfileRechargeOrderExpirationTimer`
- 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs`
- 源码:`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`
- 作用:充值订单原生 scheduled 表,`scheduled_at` 到点触发 `expire_profile_recharge_order_timer`;`order_id` 唯一,支付成功、本地关闭或 scheduled reducer 执行后删除对应行。
### `profile_redeem_code`
- Rust 结构体:`ProfileRedeemCode`
- 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs`
- 源码:`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`
- 生效时间:复用邀请码时间窗口语义,`starts_at` / `expires_at` 均可为空;两者同时存在时必须满足 `starts_at < expires_at`。开始时刻计入有效区间,截止时刻不计入有效区间。用户兑换时以后端接收的 `redeemed_at_micros` 判定:未到开始时间拒绝为“兑换码未生效”,到达或超过截止时间拒绝为“兑换码已过期”。
- 后台契约:`AdminUpsertProfileRedeemCodeRequest` 通过可空 `startsAt` / `expiresAt` 接收 RFC3339 时间,列表与保存响应同步返回这两个字段;后台页只负责输入、回填和显示,真正兑换判定留在后端事务路径。`reward_points` 是奖励泥点(兑换成功后按 `redeem_code_reward` 流水进入泥点钱包),不是金额;后台奖励输入与列表必须带泥点单位。
### `profile_redeem_code_usage`
- Rust 结构体:`ProfileRedeemCodeUsage`
- 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs`
- 源码:`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`
### `profile_referral_relation`
- Rust 结构体:`ProfileReferralRelation`
- 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs`
- 源码:`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`
### `profile_save_archive`
- Rust 结构体:`ProfileSaveArchive`
- 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs`
- 源码:`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`
### `profile_task_config`
- Rust 结构体:`ProfileTaskConfig`
- 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs`
- 源码:`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`
### `profile_task_progress`
- Rust 结构体:`ProfileTaskProgress`
- 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs`
- 源码:`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`
### `profile_task_reward_claim`
- Rust 结构体:`ProfileTaskRewardClaim`
- 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs`
- 源码:`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`
### `profile_wallet_ledger`
@@ -901,7 +920,7 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复
### `asset_operation_wallet_settlement`
- Rust 结构体:`AssetOperationWalletSettlement`
- 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs`
- 源码:`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`
- 说明:资产操作 consume/refund 配对结算事实表,主键为 consume ledger ID,并保存配对 refund ledger、用户、金额和结算时间。退款先到且 consume 尚不可见时,该表作为持久化取消 intent;迟到 consume 必须检测该行并拒绝扣费,避免 worker 崩溃重领期间双扣。
- 索引:主键 `consume_ledger_id`。
@@ -915,18 +934,18 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复
### `profile_wallet_config`
- Rust 结构体:`ProfileWalletConfig`
- 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs`
- 源码:`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`
- 作用:账号钱包全局配置真相源,当前维护新账号注册初始泥点数;表为空时业务回退 `100` 泥点。
### `public_work_like`
- Rust 结构体:`PublicWorkLike`
- 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs`
- 源码:`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`
### `public_work_play_daily_stat`
- Rust 结构体:`PublicWorkPlayDailyStat`
- 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs`
- 源码:`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`
### api-server 长期订阅读模型
@@ -962,7 +981,7 @@ private `asset_object` 不进入长期订阅;安全判断通过受限 procedur
### `tracking_daily_stat`
- Rust 结构体:`TrackingDailyStat`
- 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs`
- 源码:`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`
- 写入:由单条或批量 tracking procedure 在同一事务中随 `tracking_event` 更新,作为运营查询和个人任务进度的聚合投影。
### `agc_tracking_event`
@@ -976,7 +995,7 @@ private `asset_object` 不进入长期订阅;安全判断通过受限 procedur
### `tracking_event`
- Rust 结构体:`TrackingEvent`
- 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs`
- 源码:`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`
- 写入:关键业务埋点同步调用单条 procedure;普通 HTTP route tracking 由 `api-server` 本机 outbox 批量调用 `record_tracking_events_and_return`。outbox 到达批量阈值时先封存 active 文件并切新 active,后台 worker 异步 flush sealed 文件,HTTP 请求线程不等待 SpacetimeDB。`FLUSH_INTERVAL_MS` 只负责兜底封存长时间未满批的 active 文件,`MAX_BYTES` 只做磁盘保护阈值。`event_id` 必须稳定且全局唯一,批量重试时用唯一索引做幂等跳过。
- 外部 API 失败:`event_key = external_api_call_failure` 使用同一张表落库;它是供应商失败审计事实,不新增 SpacetimeDB 表,查询时按 `module_key = 'external-api'` 或 `scope_kind = module AND scope_id = '<provider>'` 过滤。