Feat/pricing plan limit #603

Merged
k88936 merged 25 commits from feat/pricing-plan-limit into feat/pricing-plan 2026-10-04 13:36:52 +08:00
Member

close #587

close #587
Author
Member
  • 1. 模型权限解析走带写库刷新的重快照(performance · medium)

    • 现状(修复前):api-server/src/llm/model_access.rs 每个 /api/llm/* 请求都调 get_profile_recharge_center;它内部 build_profile_membership_snapshot 会先 refresh_profile_wallet_expiring_points(每日免费点 + 会员账期刷新)再读投影,并多读钱包 / 商品 / 最新订单等与模型权限无关的字段。
    • 问题:热路径上的带写能力重快照。刷新虽只在账期 / 每日边界真正写,但每次多读无关字段、延迟高;这正是「读写能力混在一起」的坏味道。
    • 已修:新增只读 procedure get_profile_agc_model_access_and_return(输入沿用 RuntimeProfileMembershipGetInput,输出 RuntimeProfileAgcModelAccessSnapshot { user_id, plan, model_access },仅 editor generation runtime service identity 可调)。它只读 build_profile_membership_snapshot_readonly + membership_plan_row(...).model_access,缺会员行 / 已过期按 Normal,缺档位目录行失败关闭到 Basic,不刷新、不写库;api-server 改调 spacetime_client().get_profile_agc_model_access(...),失败关闭语义不变。
    • 提交:9e397fe9c(领域类型 + procedure + 生成绑定 + client 访问器)、b0dc94b46(api-server 切换)、4a3e258f3(文档)。
    • 验证:cargo test -p api-server llm 51 passed;cargo check -p spacetime-module / -p spacetime-client / -p api-server --tests 通过;npm run check:encoding 通过。
  • 2. 外部生成认领逐候选行重查并发上限与在飞计数(performance · low)

    • 已修(c3c3fc5a2):ExternalGenerationOwnerConcurrencyCache 在单次 claim 事务内按账号缓存有效 limit 与 running 计数;认领 / lease 耗尽终结后只失效该账号 running 缓存,判定结果与逐行现查逐位一致,不改变认领行为。
  • 3. 认领窗口 take(limit) + continue 造成跨账号队头阻塞(bug · high,决定不修,已加代码内 TODO a1e2415fa)

    • 澄清(回答「是不是吵闹邻居?」):不是同一个问题。
      • 吵闹邻居 = 单账号抢占共享资源;本项目已用 concurrent_job_limit 按账号封顶来治(达上限只能留 pending 排队,拿不到更多 worker 槽)。
      • 本条是队头阻塞(head-of-line blocking):候选集按 available_at / created_at 全局时间序物化后,只取前 limit 行检查。
    • 窗口大小(修正):limit = work_slots.available_permits().max(1)(external_generation_worker.rs:137),池子 = GENARRATIVE_EXTERNAL_GENERATION_WORKER_CONCURRENCY(默认 2,release 8)。所以 limit ∈ [1, 8]:空载时 8,越忙越小;负载高时(7/8 worker 占用)limit = 1,正是饥饿最要命的时刻。
    • 影响:
      • 空载、limit = 8:需要同一饱和账号在队头占满 8 行才整轮领不到;热门账号(正因为积压才饱和)这很常见,但不是必然。
      • 高负载、limit = 1:队头任意 1 行属于饱和账号,本轮 claimed 就为空,其它账号全部饿死。这是真实稳态,不是理论边界。
    • 结论:被并发上限拦下的行只做了 continue,却已经吃掉一个检查名额;问题不是「某账号跑太多」,而是「饱和账号的旧行把别人挡在窗口外」,且窗口越忙越小。
    • 两种修法(需你选):
      • A. 保持全局时间队列(推荐,最小改动):把 take(limit) 改为遍历全部候选,成功认领一行后才 claimed += 1,if claimed.len() >= limit { break; };被跳过 / 被 lease 耗尽终结的行不消耗认领名额(终结不占 worker 槽)。仍然是 FIFO、先到先得,只是不让挡路的行占名额;配合第 2 条的按账号缓存,查询上界从 O(候选行) 降到 O(不同账号数)。
      • B. 按账号轮转(round-robin / 每账号公平队列):让不同账号交替出队,而不是纯时间序。这是更大的策略变更:需要按 owner 维护游标或先按 owner 分组再轮询,改变现有「全局按 available_at 排序」的语义,还要处理每账号上限与不限档(128 哨兵)的交互。
    • 我的建议:先做 A(修掉 bug,不改变公平策略);只有当你确实想要「不同账号平均穿插」时才做 B。按账号上限已经限制了吵闹邻居的并发总量,做完 A 后其它账号不会被永久挡住。
    • 决定(2026-10-03):不修,改为在 claim_external_generation_jobs_tx 的 take(limit) 上方留 TODO(a1e2415fa),说明触发条件与未决修法,避免遗忘。真要做时:A 改动小但会放宽单事务写集上界(需确认预算),B 属策略变更。
  • 1. resolve_requested_for 默认模型分支漏掉档位校验(maintainability · medium)

    • 现状:module-runtime/src/agc_model_access.rs 的 None 分支先 default_model_id_for(access) 拿 id,再 find(|m| m.id == id && m.enabled),只校验 enabled。
    • 问题:授权边界自身不做档位校验,安全性依赖 default_model_id_for 的隐式不变量;它一旦被重构或查找漂移,可能把 Full 模型发给 Basic 账号。
    • 已修(7c57f5666):改用 m.is_available_for(access)(= enabled && access.allows),边界自洽。当前行为不变(default_model_id_for 本就只返回同档模型),属防御性收紧。
    • 验证:cargo test -p module-runtime --lib agc_model_access 9 passed。
  • 2. 锁定模型选项的 aria-label 覆盖了模型名(bug · low)

    • 现状:ConversationModelSelect.tsx 的不可用项 role="option" 写 aria-label={reasonLabel},覆盖了可见子文本 {model.displayName}。
    • 问题:读屏只报「订阅计划不支持」,用户不知道是哪个模型被锁。
    • 已修(a02a80ad1):改成 `${model.displayName}:${reasonLabel}`,名称与原因都能朗读。
    • 验证:app tsc --noEmit 通过。
  • 3. 读链路失败被静默降级为 Basic(bug · medium)

    • 现状(修复前):api-server/src/llm/model_access.rs 对 get_profile_agc_model_access 的任何 Err 都 warn! 后返回 Basic。
    • 问题:module procedure 已对「缺会员行 / 缺档位目录行」返回 ok: true + Basic,所以 Err 只剩传输 / 超时 / 鉴权失败;把它降级成 Basic 会让付费 Full 账号收到 403 plan_required、列表里丢掉 Full 模型,且只有一条 warn、没有对外降级信号。
    • 已修(7b014bdc0):新增独立错误码 MODEL_ACCESS_UNAVAILABLE(503),与 403 MODEL_NOT_AVAILABLE_FOR_PLAN、422/503 MODEL_UNAVAILABLE 区分;resolve_owner_agc_model_access 返回 Result 并由 load_owner_llm_catalog 透传,不再静默降级;入口注释逐条列出三类错误码的语义与状态码;补错误码映射单测。
    • 验证:cargo test -p api-server llm 52 passed。
  • 4. 目录与权限档两次串行 SpacetimeDB 往返(performance · medium)

    • 现状:load_owner_llm_catalog 先 await 目录 procedure,再 await 权限 procedure,两次独立往返串行。
    • 问题:热路径上延迟直接叠加。
    • 已修(576d2a91a):tokio::join! 并发发起两者;catalog? 仍返回目录错误。
    • 验证:cargo test -p api-server llm 52 passed。
    • 备注:合并成单个 procedure 也能省一次往返,但会改 procedure 契约,留作后续。
  • 5. revision: u64 导出成 TS number 的精度(maintainability · low)

    • 现状:shared-contracts/src/llm_catalog.rs 的 revision: u64 带 ts(type = "number"),前端消费 number。
    • 核对:revision 是目录乐观锁自增计数器(测试里是 4),不是时间戳,2^53 不可达;decision-log 明确选了 number(#[ts(as = "f64")])以免前端 BigInt。
    • 结论:建议不改;真要保险是后端序列化成字符串,也是契约变更。留给你定。
  • 6. 过期会员仍保留高并发上限(bug · high)

    • 你的问题「为什么到期不把 plan 自动改成 Normal?」:设计上 profile_membership.plan 故意保留「最近一次购买的档位」——refresh_profile_membership_cycle 到期只把 status 置 Normal、清 cycle_remaining_points,plan 不动,供展示「已失效的套餐」;对外「当前档位」由 build_profile_membership_snapshot_from_row 读取时按 expires_at 派生(过期 → Normal,代码注释也这么写)。
    • 问题:effective_profile_concurrent_job_limit 直接读了这个原始列,绕过派生,于是过期 Max 继续拿 128(不限并发)哨兵,与快照口径不一致。
    • 已修(0d71c1dd0):读取时加 active_membership_row_at(row, ctx.timestamp) 过滤(等价于快照的 expires_at > now),过期回落 Normal。
    • 为什么不在写入侧改:把 plan 写成 Normal 会丢掉「曾购买档位」信息,且存量已过期行要回填;读取侧派生是现有设计,改一处即可。若你要「列即当前档位」,那是另一种数据模型,需要迁移 + 回填。
    • 验证:cargo test -p spacetime-module 265 passed / 1 ignored。
  • 7. 跨账号队头阻塞(bug · medium)

    • 现状:claim_external_generation_jobs_tx 仍 candidates.into_iter().take(limit),被上限拦下 / lease 耗尽终结的行只 continue,却占掉一个检查名额。limit = worker 空闲槽(默认 2 / release 8,负载高时降到 1)。
    • 问题:饱和账号占住队头时,本轮其它账号的可认领任务看不到,claimed 可能为空。
    • 决定:上一轮你说不修,已在代码留 TODO(当前提交 ad7de7304)。
    • 建议(未决):遍历全部候选,成功认领才计数、claimed.len() >= limit 才 break,被跳过 / 终结的行不占名额;仍是全局 FIFO。改前确认单事务写集上界。
  • 8. running 计数把 status 当 Rust 后置过滤(performance · medium)

    • 现状:先 .filter(&owner_user_id.to_string()) 再 .filter(|row| row.status == RUNNING),owner 的 pending 积压也被物化(每次缓存 miss)。
    • 已修(ad59f420a):.filter((owner_user_id, EXTERNAL_GENERATION_STATUS_RUNNING)),status 推进 (owner_user_id, status) btree 范围,语义不变。
    • 验证:cargo test -p spacetime-module 265 passed。
  • 9. input.user_id.clone() 可以改成 move(maintainability · low)—— 评审不成立

    • 现状:get_profile_agc_model_access_and_return 里 build_runtime_profile_membership_get_input(input.user_id.clone())。
    • 核对:ctx.try_with_tx 签名是 body: impl Fn(&TxContext),闭包借用捕获 input,move 出字段报 E0507(实测编译失败)。保持 .clone(),取消该建议。
  • 10. 锁定模型原因提示长文案会溢出气泡(style · low)

    • 现状:.conversation-model-menu-option-locked[data-tooltip]::after 用 width: max-content; max-width: 220px; white-space: nowrap,长文案不换行、溢出 220px 气泡。
    • 已修(ec73b9cae):white-space: normal。
    • 验证:app tsc --noEmit 通过。
  • 11. 额外发现:前端模型选择测试套原本全红

    • 现状:ConversationModelSelect.tsx 的 applyCatalog 直接解引用 catalog.unavailableModels(setUnavailableModels 与按 reason 取文案两处);目录响应缺该字段(旧缓存 / 测试 mock)时 state 变 undefined,.find / .map 抛 TypeError。
    • 问题:conversationModelSelect.test.tsx 26 failed / 3 passed,且用例还在断言旧降级文案 所选模型已停用,已切换为默认模型。
    • 已修(4c7ef1673):applyCatalog 归一化 catalog.unavailableModels ?? [](与 initialCatalog 的既有守卫一致,契约注释也写明「旧服务端缺字段时为空」);用例跟进新文案,disabled 用例补上下线桶并断言「该模型已下线,已切换为默认模型」。
    • 验证:该文件 29 passed;modelAvailabilityCopy.test.ts 2 passed;chatDialogFrameLayout.test.ts + 组件自带用例 14 passed;app tsc --noEmit 通过。
- [x] 1. 模型权限解析走带写库刷新的重快照(performance · medium) - 现状(修复前):`api-server/src/llm/model_access.rs` 每个 `/api/llm/*` 请求都调 `get_profile_recharge_center`;它内部 `build_profile_membership_snapshot` 会先 `refresh_profile_wallet_expiring_points`(每日免费点 + 会员账期刷新)再读投影,并多读钱包 / 商品 / 最新订单等与模型权限无关的字段。 - 问题:热路径上的带写能力重快照。刷新虽只在账期 / 每日边界真正写,但每次多读无关字段、延迟高;这正是「读写能力混在一起」的坏味道。 - 已修:新增只读 procedure `get_profile_agc_model_access_and_return`(输入沿用 `RuntimeProfileMembershipGetInput`,输出 `RuntimeProfileAgcModelAccessSnapshot { user_id, plan, model_access }`,仅 editor generation runtime service identity 可调)。它只读 `build_profile_membership_snapshot_readonly` + `membership_plan_row(...).model_access`,缺会员行 / 已过期按 `Normal`,缺档位目录行失败关闭到 `Basic`,不刷新、不写库;`api-server` 改调 `spacetime_client().get_profile_agc_model_access(...)`,失败关闭语义不变。 - 提交:`9e397fe9c`(领域类型 + procedure + 生成绑定 + client 访问器)、`b0dc94b46`(api-server 切换)、`4a3e258f3`(文档)。 - 验证:`cargo test -p api-server llm` 51 passed;`cargo check -p spacetime-module` / `-p spacetime-client` / `-p api-server --tests` 通过;`npm run check:encoding` 通过。 - [x] 2. 外部生成认领逐候选行重查并发上限与在飞计数(performance · low) - 已修(`c3c3fc5a2`):`ExternalGenerationOwnerConcurrencyCache` 在单次 claim 事务内按账号缓存有效 limit 与 running 计数;认领 / lease 耗尽终结后只失效该账号 running 缓存,判定结果与逐行现查逐位一致,不改变认领行为。 - [x] 3. 认领窗口 `take(limit)` + `continue` 造成跨账号队头阻塞(bug · high,**决定不修**,已加代码内 TODO `a1e2415fa`) - 澄清(回答「是不是吵闹邻居?」):**不是同一个问题**。 - 吵闹邻居 = 单账号抢占共享资源;本项目已用 `concurrent_job_limit` 按账号封顶来治(达上限只能留 `pending` 排队,拿不到更多 worker 槽)。 - 本条是**队头阻塞(head-of-line blocking)**:候选集按 `available_at / created_at` 全局时间序物化后,只取前 `limit` 行检查。 - 窗口大小(修正):`limit = work_slots.available_permits().max(1)`(`external_generation_worker.rs:137`),池子 = `GENARRATIVE_EXTERNAL_GENERATION_WORKER_CONCURRENCY`(默认 2,release 8)。所以 `limit ∈ [1, 8]`:空载时 8,越忙越小;**负载高时(7/8 worker 占用)`limit = 1`**,正是饥饿最要命的时刻。 - 影响: - 空载、`limit = 8`:需要同一饱和账号在队头占满 8 行才整轮领不到;热门账号(正因为积压才饱和)这很常见,但不是必然。 - 高负载、`limit = 1`:队头任意 1 行属于饱和账号,本轮 `claimed` 就为空,其它账号全部饿死。这是真实稳态,不是理论边界。 - 结论:被并发上限拦下的行只做了 `continue`,却已经吃掉一个检查名额;问题不是「某账号跑太多」,而是「饱和账号的旧行把别人挡在窗口外」,且窗口越忙越小。 - 两种修法(需你选): - A. 保持全局时间队列(推荐,最小改动):把 `take(limit)` 改为遍历全部候选,成功认领一行后才 `claimed += 1`,`if claimed.len() >= limit { break; }`;被跳过 / 被 lease 耗尽终结的行不消耗认领名额(终结不占 worker 槽)。仍然是 FIFO、先到先得,只是不让挡路的行占名额;配合第 2 条的按账号缓存,查询上界从 O(候选行) 降到 O(不同账号数)。 - B. 按账号轮转(round-robin / 每账号公平队列):让不同账号交替出队,而不是纯时间序。这是更大的策略变更:需要按 owner 维护游标或先按 owner 分组再轮询,改变现有「全局按 `available_at` 排序」的语义,还要处理每账号上限与不限档(`128` 哨兵)的交互。 - 我的建议:先做 A(修掉 bug,不改变公平策略);只有当你确实想要「不同账号平均穿插」时才做 B。按账号上限已经限制了吵闹邻居的并发总量,做完 A 后其它账号不会被永久挡住。 - 决定(2026-10-03):**不修**,改为在 `claim_external_generation_jobs_tx` 的 `take(limit)` 上方留 TODO(`a1e2415fa`),说明触发条件与未决修法,避免遗忘。真要做时:A 改动小但会放宽单事务写集上界(需确认预算),B 属策略变更。 - [x] 1. `resolve_requested_for` 默认模型分支漏掉档位校验(maintainability · medium) - 现状:`module-runtime/src/agc_model_access.rs` 的 `None` 分支先 `default_model_id_for(access)` 拿 id,再 `find(|m| m.id == id && m.enabled)`,只校验 `enabled`。 - 问题:授权边界自身不做档位校验,安全性依赖 `default_model_id_for` 的隐式不变量;它一旦被重构或查找漂移,可能把 `Full` 模型发给 `Basic` 账号。 - 已修(`7c57f5666`):改用 `m.is_available_for(access)`(= `enabled && access.allows`),边界自洽。当前行为不变(`default_model_id_for` 本就只返回同档模型),属防御性收紧。 - 验证:`cargo test -p module-runtime --lib agc_model_access` 9 passed。 - [x] 2. 锁定模型选项的 `aria-label` 覆盖了模型名(bug · low) - 现状:`ConversationModelSelect.tsx` 的不可用项 `role="option"` 写 `aria-label={reasonLabel}`,覆盖了可见子文本 `{model.displayName}`。 - 问题:读屏只报「订阅计划不支持」,用户不知道是哪个模型被锁。 - 已修(`a02a80ad1`):改成 `` `${model.displayName}:${reasonLabel}` ``,名称与原因都能朗读。 - 验证:app `tsc --noEmit` 通过。 - [x] 3. 读链路失败被静默降级为 `Basic`(bug · medium) - 现状(修复前):`api-server/src/llm/model_access.rs` 对 `get_profile_agc_model_access` 的任何 `Err` 都 `warn!` 后返回 `Basic`。 - 问题:module procedure 已对「缺会员行 / 缺档位目录行」返回 `ok: true` + `Basic`,所以 `Err` 只剩传输 / 超时 / 鉴权失败;把它降级成 `Basic` 会让付费 `Full` 账号收到 403 `plan_required`、列表里丢掉 `Full` 模型,且只有一条 warn、没有对外降级信号。 - 已修(`7b014bdc0`):新增独立错误码 `MODEL_ACCESS_UNAVAILABLE`(503),与 403 `MODEL_NOT_AVAILABLE_FOR_PLAN`、422/503 `MODEL_UNAVAILABLE` 区分;`resolve_owner_agc_model_access` 返回 `Result` 并由 `load_owner_llm_catalog` 透传,不再静默降级;**入口注释逐条列出三类错误码的语义与状态码**;补错误码映射单测。 - 验证:`cargo test -p api-server llm` 52 passed。 - [x] 4. 目录与权限档两次串行 SpacetimeDB 往返(performance · medium) - 现状:`load_owner_llm_catalog` 先 await 目录 procedure,再 await 权限 procedure,两次独立往返串行。 - 问题:热路径上延迟直接叠加。 - 已修(`576d2a91a`):`tokio::join!` 并发发起两者;`catalog?` 仍返回目录错误。 - 验证:`cargo test -p api-server llm` 52 passed。 - 备注:合并成单个 procedure 也能省一次往返,但会改 procedure 契约,留作后续。 - [ ] 5. `revision: u64` 导出成 TS `number` 的精度(maintainability · low) - 现状:`shared-contracts/src/llm_catalog.rs` 的 `revision: u64` 带 `ts(type = "number")`,前端消费 `number`。 - 核对:`revision` 是目录乐观锁自增**计数器**(测试里是 4),不是时间戳,2^53 不可达;decision-log 明确选了 `number`(`#[ts(as = "f64")]`)以免前端 BigInt。 - 结论:建议**不改**;真要保险是后端序列化成字符串,也是契约变更。留给你定。 - [x] 6. 过期会员仍保留高并发上限(bug · high) - 你的问题「为什么到期不把 `plan` 自动改成 `Normal`?」:设计上 `profile_membership.plan` **故意**保留「最近一次购买的档位」——`refresh_profile_membership_cycle` 到期只把 `status` 置 `Normal`、清 `cycle_remaining_points`,`plan` 不动,供展示「已失效的套餐」;对外「当前档位」由 `build_profile_membership_snapshot_from_row` 读取时按 `expires_at` 派生(过期 → `Normal`,代码注释也这么写)。 - 问题:`effective_profile_concurrent_job_limit` 直接读了这个原始列,绕过派生,于是过期 `Max` 继续拿 `128`(不限并发)哨兵,与快照口径不一致。 - 已修(`0d71c1dd0`):读取时加 `active_membership_row_at(row, ctx.timestamp)` 过滤(等价于快照的 `expires_at > now`),过期回落 `Normal`。 - 为什么不在写入侧改:把 `plan` 写成 `Normal` 会丢掉「曾购买档位」信息,且存量已过期行要回填;读取侧派生是现有设计,改一处即可。若你要「列即当前档位」,那是另一种数据模型,需要迁移 + 回填。 - 验证:`cargo test -p spacetime-module` 265 passed / 1 ignored。 - [ ] 7. 跨账号队头阻塞(bug · medium) - 现状:`claim_external_generation_jobs_tx` 仍 `candidates.into_iter().take(limit)`,被上限拦下 / lease 耗尽终结的行只 `continue`,却占掉一个检查名额。`limit` = worker 空闲槽(默认 2 / release 8,负载高时降到 1)。 - 问题:饱和账号占住队头时,本轮其它账号的可认领任务看不到,`claimed` 可能为空。 - 决定:上一轮你说不修,已在代码留 TODO(当前提交 `ad7de7304`)。 - 建议(未决):遍历全部候选,成功认领才计数、`claimed.len() >= limit` 才 break,被跳过 / 终结的行不占名额;仍是全局 FIFO。改前确认单事务写集上界。 - [x] 8. `running` 计数把 status 当 Rust 后置过滤(performance · medium) - 现状:先 `.filter(&owner_user_id.to_string())` 再 `.filter(|row| row.status == RUNNING)`,owner 的 `pending` 积压也被物化(每次缓存 miss)。 - 已修(`ad59f420a`):`.filter((owner_user_id, EXTERNAL_GENERATION_STATUS_RUNNING))`,status 推进 `(owner_user_id, status)` btree 范围,语义不变。 - 验证:`cargo test -p spacetime-module` 265 passed。 - [x] 9. `input.user_id.clone()` 可以改成 move(maintainability · low)—— 评审不成立 - 现状:`get_profile_agc_model_access_and_return` 里 `build_runtime_profile_membership_get_input(input.user_id.clone())`。 - 核对:`ctx.try_with_tx` 签名是 `body: impl Fn(&TxContext)`,闭包借用捕获 `input`,move 出字段报 E0507(实测编译失败)。保持 `.clone()`,取消该建议。 - [x] 10. 锁定模型原因提示长文案会溢出气泡(style · low) - 现状:`.conversation-model-menu-option-locked[data-tooltip]::after` 用 `width: max-content; max-width: 220px; white-space: nowrap`,长文案不换行、溢出 220px 气泡。 - 已修(`ec73b9cae`):`white-space: normal`。 - 验证:app `tsc --noEmit` 通过。 - [x] 11. 额外发现:前端模型选择测试套原本全红 - 现状:`ConversationModelSelect.tsx` 的 `applyCatalog` 直接解引用 `catalog.unavailableModels`(`setUnavailableModels` 与按 reason 取文案两处);目录响应缺该字段(旧缓存 / 测试 mock)时 state 变 `undefined`,`.find` / `.map` 抛 TypeError。 - 问题:`conversationModelSelect.test.tsx` 26 failed / 3 passed,且用例还在断言旧降级文案 `所选模型已停用,已切换为默认模型`。 - 已修(`4c7ef1673`):`applyCatalog` 归一化 `catalog.unavailableModels ?? []`(与 `initialCatalog` 的既有守卫一致,契约注释也写明「旧服务端缺字段时为空」);用例跟进新文案,`disabled` 用例补上下线桶并断言「该模型已下线,已切换为默认模型」。 - 验证:该文件 29 passed;`modelAvailabilityCopy.test.ts` 2 passed;`chatDialogFrameLayout.test.ts` + 组件自带用例 14 passed;app `tsc --noEmit` 通过。
k88936 closed this pull request 2026-10-03 19:30:50 +08:00
k88936 reopened this pull request 2026-10-03 19:32:35 +08:00
k88936 changed target branch from master to feat/pricing-plan 2026-10-03 19:33:08 +08:00
k88936 added 97 commits 2026-10-04 13:30:07 +08:00
- 按 vitest 0.34 的参数元组口径改写 mock 泛型
- 修正 noUncheckedIndexedAccess 下的下标访问
- 补齐与当前类型脱节的测试桩字段
- 按 scripts/*.mjs 的运行时契约补局部类型声明
- 新增 apps/ai-game-creator-shell/tsconfig.tests.json,覆盖 src、tests 与 vite.config.ts
- 新增 check:tests:types 脚本执行 tsc -p tsconfig.tests.json --noEmit
- 将 check:tests:types 并入 AGC 的 typecheck
文档:记录 AGC 测试类型门禁与 Node 环境口径
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 5m15s
Project CI / Backend tests (pull_request) Failing after 23s
Project CI / AI game creator shell Rust crates (pull_request) Failing after 2m47s
Project CI / Repository checks (pull_request) Failing after 38s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 5m20s
Project CI / Frontend tests (pull_request) Successful in 2m38s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m12s
Project CI / Native shell tests (pull_request) Successful in 6m52s
2922b94594
- development-workflow 增补 AGC 测试类型门禁一节
- pitfalls 记录测试未进 tsconfig 时类型断言为空写的经验
- pitfalls 记录 Node 24+ Web Storage 顶掉 jsdom localStorage 的环境口径
- 新增 docs/adr/【ADR】游戏游玩次数计数-2026-10-03.md:点开始游戏触发、api-server 纯内存 5s flush、30min 去重、IP+game 限流、批量 procedure 自增 play_count、不 bump updated_at、失败少计优于双计
- docs/README.md:登记该 ADR 到当前产品与平台
- spacetime-module/game_distribution.rs:新增 GameDistributionPlayCountIncrement(Input) 与 increment_game_distribution_game_play_counts_and_return,事务内只对 published 且有 active_version_id 的游戏 saturating_add,且不 bump updated_at
- 重新执行 npm run spacetime:generate,新增 3 个绑定并同步 module_bindings.rs 等生成产物
- game_distribution.rs:新增 GameDistributionPlayCountIncrementRecordInput 与 increment_game_distribution_game_play_counts,把批次映射为绑定输入并调用增量 procedure,错误转 procedure_failed
- game_play_counter.rs:新增纯内存计数器(30min 身份去重、IP+game 固定窗口限流、按时/按量取增量、requeue、过期清理)与 9 个单测
- game_play_counter_worker.rs:新增 flush worker,Build 失败放回重试、Timeout/ConnectDropped 丢弃并记录丢失量,关停前强制 flush
- main.rs:HTTP 角色注册 worker,并在 finalize_shutdown 内按 outbox 超时强制落库
- state.rs/config.rs:AppState 持有计数器,新增 GENARRATIVE_GAME_PLAY_COUNTER_FLUSH_INTERVAL_MS(默认 5s)
- request_context.rs:抽出 client_ip_from_headers 并补测试,runtime_profile.rs 改为复用
- modules/game_distribution.rs:新增 POST /api/game-distribution/games/{game_id}/plays,公开可带 bearer,非公开 404、限流 429、成功 200 {recorded}
- 身份组成:登录用 userId、匿名用 clientId、都缺失回退 IP+UA;无效 bearer 按匿名处理不影响计数
- 请求体读取原始 Bytes,空体或畸形体不阻断计数
- gamePlayClientId.ts:新增匿名游玩身份,localStorage 持久并在不可用时退化为会话内存值
- gameDistributionClient.ts:新增 recordGamePlay,携带 clientId 且按后台请求处理,不刷新会话、不改全局登录态
- GamePlayPage.tsx:点击开始游戏即 fire-and-forget 上报,失败静默不阻断进入游戏
- 补 clientId 稳定性用例与点击上报断言,并把新服务文件加入 eslint 白名单
- 玩法链路:路由表新增 POST /games/{gameId}/plays,并新增「游玩计数(已实现)」小节说明触发、落点、缓冲延迟、去重限流与写入语义
- 后端架构:game_distribution_game 节补 play_count 的批量 procedure 写入路径与参数
- decision-log:记录内存去重缓冲 + 批量 procedure 落库的决策、失败语义与影响范围
- modules/game_distribution.rs:新增公开路由在未连接 SpacetimeDB 时可达、无需登录且 no-store 的断言
- 补 clientId 去空白/截断 128 字符与缺失 UA 回退的单测
Merge branch 'master' into fix/typecheck-test
Project CI / AI game creator shell Rust crates (pull_request) Successful in 3m4s
Project CI / Backend tests (pull_request) Failing after 4m30s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 4m43s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 5m23s
Project CI / Frontend tests (pull_request) Successful in 3m21s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m17s
Project CI / Repository checks (pull_request) Failing after 4m28s
Project CI / Native shell tests (pull_request) Successful in 7m43s
88abe52c3d
- 「渲染函数子节点改编进 props」改为「改传进 props」
- asTauriListen 形参返回类型由 Promise<unknown> 改为 Promise<() => void>,与 TauriListen 的 unlisten 契约一致
- 合并 chat-composer / project-conversation / project-development / runtime-settings / design-agent 五处重复的 installTauriRuntime 定义为 harness 中一处
- 安装参数由 unknown 收成可调用下界并声明 event 可缺省,非函数的误装会在编译期被挡下
- design-agent 改为薄适配 installChatRuntime,调用点与运行时行为不变
- gameDistributionClient.ts:删除与 BACKGROUND_AUTH_REQUEST_OPTIONS 逐字重复的 PLAY_REPORT_REQUEST_OPTIONS,改为直接复用该常量,避免两处 local/background 鉴权语义日后漂移
- gameDistributionClient.ts:recordGamePlay 标注 Promise<{ recorded?: boolean }>,空 id 分支也返回同形状对象,消除 Promise<void> 与对象 Promise 的联合类型
- game_play_counter.rs:drain 只负责在锁内取走增量,新增 sort_deltas 在释放锁后排序,避免 O(n log n) 排序阻塞并发 record
- game_play_counter.rs:seen/rate 的键由 U+001F 拼接字符串改为 (identity, game_id)/(ip, game_id) 元组,clientId 携带分隔符时不再发生键碰撞
- 补分隔符碰撞回归测试
- game_play_counter.rs:lock 仍从中毒互斥锁恢复,但先 warn 一次,便于关联留下部分更新状态的 panic
后端:关停 flush 不再假承诺重试
Project CI / Backend tests (pull_request) Failing after 32s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 4m37s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m46s
Project CI / Repository checks (pull_request) Failing after 34s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 5m21s
Project CI / Frontend tests (pull_request) Successful in 3m16s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m3s
Project CI / Native shell tests (pull_request) Successful in 6m51s
2122dc9469
- game_play_counter_worker.rs:flush_deltas 增加 requeue_on_build;worker 循环仍把 Build 失败放回下一轮,关停路径改为直接记丢失量,不再把增量放回一个不会再被 flush 的缓冲
- harness 新增 createTauriEventBridge,只负责事件登记与派发,不碰 window、不做类型转换
- home.suite 44 处内联 window.__TAURI__ 赋值改为 installTauriRuntime,并删除 asTauriInvoke / asTauriListen 两个适配器
- auth.suite 删除 installTauriBridge 的 as never,改为 createTauriEventBridge + installTauriRuntime 组合
Merge remote-tracking branch 'origin/master' into fix/typecheck-test
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m30s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 5m2s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 3m46s
Project CI / Frontend tests (pull_request) Successful in 3m20s
Project CI / Backend tests (pull_request) Failing after 4m41s
Project CI / Repository checks (pull_request) Failing after 3m59s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m34s
Project CI / Native shell tests (pull_request) Successful in 7m16s
55cca04971
- 新增 tests/tauriRuntimeFake.ts,作为唯一的 window.__TAURI__ 安装点与类型转换点,并提供 createTauriEventBridge 与 resetTauriRuntime
- appSurface/harness.ts 改为从该模块导入并转出 installTauriRuntime / createTauriEventBridge,既有 ./harness 导入路径不变
- pickProjectFromLauncher 不再直接写 window.__TAURI__,改走 installTauriRuntime 保留原有 core/event
修复 AGC 画布素材卡「引用」无消费者:新增活跃聊天输入区注册表
Project CI / Backend tests (pull_request) Failing after 18s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 5m44s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 3m2s
Project CI / Repository checks (pull_request) Failing after 30s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 5m10s
Project CI / Frontend tests (pull_request) Successful in 2m43s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m16s
Project CI / Native shell tests (pull_request) Successful in 5m58s
a22e8ee447
新增 features/project-workspace/activeChatComposer.ts:模块级保存当前挂载的输入区句柄,registerActiveChatComposer 注销时校验身份,insertChatReferences 空批次或无句柄返回 false
DirectProjectComposer 用 useImperativeHandle 暴露 DirectProjectComposerHandle(按 ref 转发,句柄稳定),新增可选 ref 入参
DirectProjectChatView 挂载期间注册、卸载注销,并把 composerHandleRef 传给 DirectProjectComposer
PlanningChatView 同样注册(句柄按 composerRef 转发),两条链路互斥渲染,同一时刻只有一个句柄
App.tsx 收敛为一处监听:单条「引用」与批量拖拽两个事件都走 insertChatReferences,返回 false 时 dev 下 console.warn
chatComposerRef 只保留给策划输入盒自己的 getDraft / clear,不再承担跨面板插入
project-development.suite.ts:工具条「引用」用例改为渲染真实 DirectProject 聊天面,断言草稿里出现引用芯片(键盘 + 鼠标两条通路、光标留在插入之后)
resourceCanvasChatReferenceDrop.test.tsx:换成真实 DirectProject 聊天面,批量拖拽断言整批一次落进草稿且顺序 = 拖动集合顺序;新增未登记素材不出「引用」按钮、拖拽只给原因的用例
design-agent.suite.ts:新增策划链路引用插入不回归用例
同步 docs/【功能说明】AGC聊天素材引用-2026-09-08.md、shared-memory 的 pitfalls 与 decision-log
测试:全仓测试统一 Tauri 运行时替身安装点
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m58s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 6m34s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 5m29s
Project CI / Backend tests (pull_request) Failing after 4m38s
Project CI / Frontend tests (pull_request) Successful in 3m33s
Project CI / Repository checks (pull_request) Failing after 4m4s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m13s
Project CI / Native shell tests (pull_request) Successful in 6m35s
25c5ecdf6e
- 78 处内联 window.__TAURI__ 赋值改为 installTauriRuntime,覆盖 appSurface 之外的 37 个测试文件
- 31 处 delete window.__TAURI__ / window.__TAURI__ = undefined 复位改为 resetTauriRuntime
- 删除 recentProjectsHook、projectCreationDirectory、projectResourceLiveIntegration、useProjectResourceCardPreviews、resourceVersionReplacementModel、unityProjectOpen 里的本地 asTauriInvoke / asTauriListen 适配器与相关类型别名
- designProjectRestore 的 setup 参数由 ReturnType<typeof vi.fn> 收窄为具体 invoke 签名
- 迁移后仅 tests/tauriRuntimeFake.ts 直接读写 window.__TAURI__
Merge pull request 'Fix/typecheck test' (#601) from fix/typecheck-test into master
Project CI / AI game creator shell Rust crates (push) Successful in 3m9s
Project CI / Backend tests (push) Failing after 4m6s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 4m59s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 5m49s
Project CI / Frontend tests (push) Successful in 3m5s
Project CI / AI game creator shell web tests (push) Successful in 2m41s
Project CI / Repository checks (push) Failing after 3m36s
Project CI / Native shell tests (push) Successful in 6m18s
ccd2715726
Reviewed-on: #601
自审加固:注册表按 ref 转发注册、插入成功语义与失败留痕对齐
Project CI / Backend tests (pull_request) Failing after 17s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 4m50s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 5m19s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m43s
Project CI / Repository checks (pull_request) Failing after 32s
Project CI / Frontend tests (pull_request) Successful in 2m35s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m23s
Project CI / Native shell tests (pull_request) Successful in 6m26s
f4ada50612
ActiveChatComposerHandle.insertReferences 改为返回 boolean:由句柄回答这一批有没有真的递到输入区,注册表不再用「有句柄」冒充「插进去了」
insertChatReferences 增加第三种 false(句柄报落空),且只有插入成功才 focus,失败不抢焦点
DirectProjectChatView 改为注册「按 ref 转发」的句柄,注册时不再读 composerHandleRef.current,去掉对父子 effect 顺序的隐式依赖(内层输入区重挂载也不会留下死句柄)
DirectProjectComposer 与 PlanningChatView 的转发句柄同步返回 boolean
App.tsx 的 dev 失败线索文案改为「没有可用的聊天输入区(未挂载或已卸载)」,覆盖句柄落空这一种
新增 tests/activeChatComposer.test.ts:钉住注册表合同(空批次 / 无输入区 / 句柄报落空不聚焦 / 注销身份校验);反向证伪:去掉身份校验后该用例变红
同步 docs/【功能说明】AGC聊天素材引用-2026-09-08.md 与 shared-memory 决策记录
master 侧(PR #601)新增 tests/tauriRuntimeFake.ts(全仓唯一 window.__TAURI__ 安装点)与 tsconfig.tests.json,测试统一改用 installTauriRuntime / resetTauriRuntime,并新增 check:tests:types 门禁
冲突三处测试文件(resourceCanvasChatReferenceDrop.test.tsx、appSurface/project-development.suite.ts、appSurface/design-agent.suite.ts)按语义两边保留:采用 master 的替身安装点与类型收窄(ResourceReference 过滤),保留本分支的真实 DirectProject 聊天面挂载与端到端草稿断言
App 的 props 类型未导出,新增的两处 chat 节点改用 AppComponent 组件类型别名(与 design-agent.suite.ts 同一写法),通过新的 check:tests:types
docs 侧 pitfalls.md / decision-log.md 自动合并:本分支的 issue #602 条目与 master 的「根因 7/8」条目都保留
未在本分支修 master 既有红(check:nginx-spa-routes 缺 /pay 与 /profile/payment、external_mcp::semantic 32 vs 30、http_tracing 偶发、AGC Rust shard 4/4),由上游 fix/ci-master-red 处理
按独立评审的三条 P2 收口:空批次不误报、重复注册留线索、文档与实测对齐
Project CI / AI game creator shell Rust crates (pull_request) Failing after 1m35s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 4m20s
Project CI / Backend tests (pull_request) Failing after 4m39s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 5m18s
Project CI / Frontend tests (pull_request) Successful in 3m19s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m43s
Project CI / Native shell tests (pull_request) Successful in 7m28s
Project CI / Repository checks (pull_request) Failing after 4m32s
3f18e3dc77
App.tsx 的监听处对空批次直接返回:空批次没有要插的东西,不能报成「没有可用的聊天输入区」(原因指向错了方向)
activeChatComposer.registerActiveChatComposer 增加重复注册检测:dev 下 console.warn 指出后注册者顶替了前者,不改运行时语义(仍然后注册者接管、身份校验照旧兜住乱序卸载)
tests/activeChatComposer.test.ts 补两条:空批次不打扰已挂载输入区;重复注册出现告警且乱序注销清不掉更新的句柄
tests/resourceCanvasChatReferenceDrop.test.tsx 补一条:直接构造空批次引用事件时不产生「没有可用的聊天输入区」告警
decision-log 的「影响范围」补记 tests/activeChatComposer.test.ts,「验证」换成合并 master 后的实跑数字;功能说明的用例表补两行
反向证伪:去掉空批次短路、去掉重复注册告警后,上面两条新用例各红一处
- src/routing/activeAppPageRoutes.ts 的 STAGE_ROUTE_ENTRIES 已把 payment-checkout→/pay、
  profile-payment→/profile/payment 定义为主站对外可直达的 SPA 路由(分别渲染
  PaymentCheckoutView 与 PlatformPaymentServiceView),但三份 Nginx 模板的 SPA allowlist
  仍停留在加入支付入口之前的集合,导致 check:nginx-spa-routes 在 master 上失败
- deploy/nginx/genarrative.conf、deploy/nginx/genarrative-dev-http.conf、
  deploy/container/nginx.conf 的 SPA regex 补上 pay、profile/payment,
  保持既有形状(锚定完整路径 + 大小写不敏感 + 允许一个尾部斜杠)
- 本地实跑:node scripts/check-nginx-spa-routes.mjs → OK (14 SPA routes, 3 Nginx templates)
- 断言 MCP_OPERATIONS.len() == 30 自 #592 起写死。4b529a895(新增支付服务接入与订单收银台)
  在内置 OpenAPI 中新增 createExternalPaymentOrder、getExternalPaymentOrder 两条未被
  x-mcp-excluded 排除的操作,legacy 可调用工具由 30 合法增至 32(非重复注册、非覆盖)
- 断言更新为 32 并注明来源(内置 OpenAPI 去掉 5 条元数据/入口操作后的可调用集合),
  保留“语义工具是增量、不得覆盖 legacy 工具”的原意;总数断言改为
  MCP_OPERATIONS.len() + TOOLS.len() 派生,避免再次写死
- 本地实跑:cargo test --locked -p api-server --no-fail-fast --manifest-path server-rs/Cargo.toml semantic
  → 18 passed(含 semantic_catalog_adds_fifteen_tools_without_replacing_legacy_tools)
- 根因:span!/info_span! 宏在 callsite 的缓存 interest 为 never 时会静默返回空 span
  (tracing-0.1.44/src/macros.rs 的 span! 分支),而 DefaultCallsite::register 只在调用点
  首次被命中时计算一次 interest,且当进程里只注册过一个 dispatcher 时会退化成
  dispatcher::get_default()——也就是命中线程自己的 dispatcher(tracing-core-0.1.36
  callsite.rs 的 Rebuilder::JustOne)。libtest 默认并发下,没有 subscriber 的普通测试线程
  一旦抢到 http.request / llm.request 调用点的首次注册,就会把它永久缓存成 never,
  于是 CI 偶发看到 0 个 span(app.rs:603、observability_tests.rs:98)
- 关键:set_default 与 with_subscriber 在 interest 缓存这件事上**等价**(都只是新建 Dispatch
  并触发一次 rebuild_interest,只能纠正“已经注册过”的调用点,纠正不了 JustOne→get_default
  这条首次注册分支),所以真正起作用的是**在自家 subscriber 下命中同一个调用点做热身**,
  把首次注册的顺序握在自己手里;register_callsite 覆写只用于避免本 subscriber 触发的重建
  把其它调用点永久标记成 never
- api-server app::tests::http_tracing:不再用 with_subscriber,改为在请求前用同一
  http.request 调用点热身到连续两轮采集成功,并在整个请求期间持有 scoped default
  (已注明 set_default 是线程绑定,只适用于默认的 current_thread #[tokio::test])
- platform-llm observability_tests:新增 run_under_capture 固定整段流程的 scoped default,
  并新增 warm_up_provider_span_callsite(用指向刚释放 loopback 端口的最小失败请求命中同一
  llm.request 调用点,连续两轮采集成功才继续)
- 断言未放宽:仍要求每个被拒绝请求恰好 1 个 HTTP span、每次 Provider 调用恰好 1 个
  llm.request span;未使用 sleep
- 本地实跑:cargo test -p api-server --bin api-server app::tests::http_tracing(默认并发与
  --test-threads=1 各 20 次全绿)、app::tests:: 91 用例并发 10 次全绿、--skip bgfilter_worker
  --skip wallet_refund_outbox 的 1133 用例全量 bin 2 次全绿;cargo test -p platform-llm --lib
  (162 passed,含 3 条观测用例)连跑 20 次全绿
- agent::thread_manager::tests::active_turn_changes_publish_one_notification_per_real_change
  偶发 left 8/right 7:测试计数器 DIRECT_ACTIVE_TURNS_EVENT_TEST_COUNT 曾在 2026-10-01 按线程
  作用域隔离,2026-10-02 退役 runtime_driver 把它搬进 agent/direct_events.rs 时降级回进程级
  static AtomicU64,于是断言会取到宿主 tauri::async_runtime 后台回合在别的线程上的广播
  (--test-threads=1 只串行测试线程)。改回 thread_local! Cell,并注明跨线程口径的覆盖取舍
- process_session::tests::process_session_graceful_terminate_keeps_wrapper_alive_for_target_cleanup
  偶发 left "exited"/right "terminated":测试命令里 leader 打印 READY 后立刻 exit 0,同组后代
  仍存活,trampoline 从 leader 被回收起开始 800ms 宽限;客户端只要晚于宽限才发出 terminate
  就只能读到既成事实。改为 leader 用 wait 等后台子进程,让 terminate 必然落在会话仍 running 时
  (trap / sleep 0.4 / marker / 断言均未改),并注明该用例的确定性来自 400ms < 800ms 的时间余量
- docs/project-memory/shared-memory/pitfalls.md:更新 graceful terminate 那条已过时的验证口径,
  补三条 2026-10-04 条目(tracing interest 竞态、AGC 两条偶发的串台根因、SPA 深链前缀路由)
- 本地实跑:修复前把计数器临时改回 AtomicU64 时同一并行口径 17/20 红;修复后并行 20 次全绿、
  agent::thread_manager:: 连跑 5 次 72 passed;第 2 条用例是 #[cfg(target_os = "linux")],
  Windows 本机跑不到,已用真实 Linux 内核(WSL Alpine)验证命令形状(leader 活到 TERM、
  同组后代完成 400ms 延迟清理 marker=done 0.41s、清理后组内零残留),CI 侧仍需跑
  node apps/ai-game-creator-shell/scripts/run-rust-shell-test-shards.mjs --shards=4 --shard-index=4 复核
- 现状:只把 `/pay`、`/profile/payment` 加进 allowlist 只能让 check:nginx-spa-routes 变绿;
  payment.rs 生成的 checkoutUrl 是 `/pay/<checkoutToken>`,深链仍落默认 location 的
  try_files → 404。同一批漂移里 check:pingora-route-parity 也是红的(Pingora MAIN_SPA_PATHS
  缺 /pay、/profile/payment),只是被 lint 链里先失败的门禁掩盖,修一条要跑到链尾
- 真相源:src/routing/activeAppPageRoutes.ts 新增 APP_PREFIX_ROUTE_ENTRIES
  ('/pay' → payment-checkout),resolveSelectionStageFromPath 改用它
- 门禁:scripts/check-nginx-spa-routes.mjs 要求三份模板都有锚定前缀 location
  `location ~* "^/pay/[^/]+/?$"`(裸前缀仍由精确 location 负责;前缀 location 必须镜像精确
  location 的维护闸与 try_files 回退);新增 scripts/check-nginx-spa-routes.test.mjs 正/反用例
  (把前缀写成精确匹配或过宽裸前缀都会红),由 npm run check:nginx-spa-routes 一起执行;
  check-pingora-route-parity 新增 MAIN_SPA_PREFIX_PATHS 与前端前缀路由的逐条比对
- 模板:deploy/nginx/genarrative.conf、deploy/nginx/genarrative-dev-http.conf、
  deploy/container/nginx.conf 各加一条锚定前缀 location
- Pingora:MAIN_SPA_PATHS 补 /pay、/profile/payment;新增 MAIN_SPA_PREFIX_PATHS 与
  is_main_spa_prefix_path(大小写不敏感,只认「前缀 + 恰好一段」),矩阵新增
  pay_checkout_spa_fallback 用例,并给网关补一条前缀正/反单测
- 文档:Pingora 试点文档的路由表与门禁说明、deploy/nginx/README 与本地开发/生产运维文档
  同步前缀路由口径与线上 curl 复验方式
- 本地实跑:node --test scripts/check-nginx-spa-routes.test.mjs(4 passed)、
  node scripts/check-nginx-spa-routes.mjs(OK,14 SPA routes / 1 prefix routes / 3 templates)、
  npm run check:pingora-route-parity(OK,25 routes)、
  cargo test -p pingora-gateway -- pay_checkout_deep_link matches_nginx_route_parity_matrix(2 passed)
语义目录测试的总数断言改回字面量,避免恒真
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m50s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 4m10s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 5m5s
Project CI / Backend tests (pull_request) Successful in 6m30s
Project CI / Frontend tests (pull_request) Successful in 3m12s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m44s
Project CI / Native shell tests (pull_request) Successful in 6m33s
Project CI / Repository checks (pull_request) Successful in 5m5s
8b11dc4e7a
- names.len() == MCP_OPERATIONS.len() + TOOLS.len() 在 BTreeSet 去重构造下恒真、没有判别力;
  改回固定 47(32 条 legacy 工具 + 15 条语义工具),并注明上面的插入断言已保证两组名字互不覆盖
- 本地实跑:cargo test --locked -p api-server --manifest-path server-rs/Cargo.toml --bin api-server semantic
  → 18 passed
合并 fix/ci-master-red(PR #609)到 fix/agc-canvas-reference-insert:取得 master 级 CI 修复
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m54s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 4m22s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 5m21s
Project CI / Backend tests (pull_request) Successful in 6m30s
Project CI / Frontend tests (pull_request) Successful in 3m29s
Project CI / Native shell tests (pull_request) Successful in 7m10s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m37s
Project CI / Repository checks (pull_request) Successful in 6m16s
4fc13bcfe3
把 #609 的 8/8 全绿修复并进本分支,满足「PR head 必须包含最新 base 提交」的门禁,同时带上共享红修复:check:nginx-spa-routes 的 /pay/<checkoutToken> 前缀路由与 Pingora 路由矩阵、external_mcp::semantic 真值 32、api-server/platform-llm 的 tracing interest 竞态、AGC 壳 Rust shard-4 两条偶发断言
本次 merge 无冲突自动完成:pitfalls.md 两边条目都保留(本分支的 issue #602 条目 + #609 的 2026-10-04 tracing/shard-4/SPA 深链三条)
未改动 #609 引入的任何文件
Merge pull request '修复 master CI 红:收银台深链前缀路由、外部 MCP 工具数断言、并发 span 采集竞态、AGC 壳 shard-4 断言' (#609) from fix/ci-master-red into master
Project CI / AI game creator shell Rust crates (push) Successful in 2m58s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 3m58s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 4m45s
Project CI / Backend tests (push) Successful in 7m9s
Project CI / Frontend tests (push) Successful in 3m12s
Project CI / Native shell tests (push) Successful in 7m9s
Project CI / AI game creator shell web tests (push) Successful in 3m13s
Project CI / Repository checks (push) Successful in 6m31s
afc2068fb3
Reviewed-on: #609
- parsePositiveInteger / parseNonNegativeInteger 改用 ^\d+$ 校验原始字符串
- 拒绝 1.0、1e2、0x10、+5 等 Number 会隐式转换的写法,与'严格整数'注释一致
- 新增 parseSafeIntegerText 统一十进制校验,四个导出函数改为薄包装
- 注明 parsePositiveIntegerOrZero / parseNonNegativeIntegerOrZero 语义等价
- 说明缺失档位会让倒挂静默通过,要求新增调用方传全量目录表
- 注明与升级报价显式拒绝 cycle_index=0 的差异及兼容存量行的原因
- 用 if/else 预计算 actionButtonLabel,明确 禁用/提交中/默认 的优先级
- 页签列表与泥点档位网格共用同一个 roving index 解析函数
- 用 switch 取代深层嵌套三元,Home/End/左右键语义保持不变
- 新增 amountPending:升级报价缺失时金额区显示占位符,CTA 只显示「补差升级」
- 补一条回归测试覆盖报价加载中的确认页
- 微信支付是纯展示的单一选项,不可交互的 radio 会误导读屏
- 改为普通文本容器,视觉样式不变
- 在注释里给出 limits.rs 与 MEMBERSHIP_UNLIMITED_CONCURRENCY 定义,便于后端改动时同步
- 说明极端日序号会饱和并给出无意义日期,要求调用方保证输入范围
- 年份导致微秒溢出时返回 None,符合函数文档,不再饱和成看似合法的值
- 补测试覆盖 i64 溢出年份返回 None
- upsert 解析 rank 失败改为显式报错,避免与 Normal 占位同档
- 与 module-runtime 用 Option 暴露缺失档位的契约保持一致
- build_wechat_virtual_pay_params 的档位校验补齐 enabled 与 is_purchasable
- 与 resolve_enabled_profile_recharge_order_product 的可售判定对齐,拒绝 Normal 占位与下架档位
Merge pull request '修复 AGC 画布素材卡「引用」无消费者:新增活跃聊天输入区注册表(Issue #602)' (#605) from fix/agc-canvas-reference-insert into master
Project CI / AI game creator shell Rust crates (push) Successful in 3m2s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 4m17s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 5m36s
Project CI / Backend tests (push) Successful in 6m51s
Project CI / Frontend tests (push) Successful in 3m5s
Project CI / Native shell tests (push) Successful in 6m47s
Project CI / AI game creator shell web tests (push) Successful in 3m0s
Project CI / Repository checks (push) Successful in 5m5s
f689b97d8a
Reviewed-on: #605
- game_play_counter.rs:新增只读 is_rate_limited,不消耗额度也不改状态
- modules/game_distribution.rs:record_game_play 先做内存限流预检,超限直接 429,不再让这些请求也打一次 SpacetimeDB;真正计数时 record 仍会重新判定
- 补预检只读与固定窗口作用域测试
- main.rs:finalize_shutdown 删除游戏游玩计数强制 flush,关停不再为它等待网络,内存里剩余增量随进程结束丢弃
- game_play_counter_worker.rs:删除 flush_game_play_counter_for_shutdown,flush_deltas 去掉 requeue_on_build 参数(关停路径已不存在),Build 仍放回等待下一轮
- game_play_counter.rs:take_pending 标记为仅测试使用,模块文档改指 take_pending_if_due
- game_play_counter_worker.rs:任一分片失败即停止本次 flush,不再为后续分片逐个重建连接(连接不通时每片都会各自等一次超时)
- Build(确定未发出)仍放回本批,剩余分片直接丢弃;其余错误连本批一起丢弃,并记录丢弃增量
- 补 total_delta 汇总剩余增量,模块文档写明失败即停止与丢弃口径
- ADR:修订「正常关停必须 force flush」为关停不做强制 flush,并补充 flush 任一分片失败即丢弃剩余分片;追加修订记录
- 玩法链路:缓冲与延迟改为崩溃/被杀/正常关停都允许丢一个窗口;限流节补充先做内存预检;写入语义补充失败丢弃口径
- decision-log:顶部新增 2026-10-04 修订条目,并给 2026-10-03 条目标注失效项
- docs/README:ADR 摘要去掉「关停 flush」
- advance_profile_membership_cycle_to 只在 cycle_resets_at 缺失时判定需要初始化写库
- cycle_index == 0(新列默认值)只补写期号水位,不重置 cycle_granted/remaining_points
- 补测试覆盖 cycle_index=0 且账期窗口已存在时保留本期余额
- profile_recharge_order.membership_granted_points_delta 列由 i64 改为 u64
- RuntimeProfileMembershipOrderChangeSnapshot 与 AdminMembershipOrderChangePayload 同步改 u64
- 删除 snapshot/order 之间的 as i64 与 try_from 兜底转换
- 重生成 SpacetimeDB 绑定:profile_recharge_order、订单变更快照
- request_context.rs:client_ip_from_headers 改为优先 nginx 覆盖写入的 x-real-ip,x-forwarded-for 只作回退且取最后一段(nginx 用 $proxy_add_x_forwarded_for 追加的真实对端),不再信任可伪造的首段
- 影响两个调用方:公开游玩上报的匿名身份与 IP+game 限流键、微信支付下单的 payer_client_ip
- 更新测试:X-Real-IP 优先、XFF 回退取末段、空 X-Real-IP 回退与回环兜底
- ADR:身份与去重键节写明 IP 取 nginx 覆盖的 X-Real-IP、XFF 只取末段,并追加 2026-10-04 修订记录
- 玩法链路:去重与限流节标注 IP 来源
- decision-log:2026-10-04 条目补安全修正说明与标题
- shared-contracts 新增 ProfileMembershipPlan/CycleKind/ModelAccess/ChangeKind Token 枚举
- RuntimeProfileMembershipOrderChangeSnapshot 与 UpgradeQuoteRecord 档位/周期改用模块枚举
- AdminMembershipOrderChangePayload、ProfileMembership/Plan/UpgradeQuote Response 改用 token 枚举
- module-runtime 提供模块枚举与 wire token 的双向 From 映射
- api-server / spacetime-client / spacetime-module 同步去掉 as_str().to_string() 拼装
- 重生成 SpacetimeDB 绑定:订单变更快照字段改为枚举
修复 AGC 渠道更新互相终止与快捷方式迁移
Project CI / AI game creator shell Rust crates (push) Successful in 3m5s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 4m24s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 5m20s
Project CI / Backend tests (push) Successful in 6m42s
Project CI / Frontend tests (push) Successful in 3m2s
Project CI / Native shell tests (push) Successful in 7m16s
Project CI / AI game creator shell web tests (push) Successful in 3m17s
Project CI / Repository checks (push) Successful in 6m25s
13a15fed5c
为 release 与自定义渠道注入独立主程序名,隔离 Windows 更新进程。

新增 release 旧安装目录与快捷方式迁移钩子,保护 dev 历史目录清理。

同步渠道规范、共享决策与排障记录,补充发布脚本回归测试。
- AdminUpsertProfileMembershipPlanRequest.plan/model_access 由 String 改为 token 枚举
- 未登记的档位 / 权限 token 在反序列化阶段直接拒绝,后台不能再持久化任意字符串
- admin handler 去掉字符串解析分支,改为 From 转换
- 删除仅用于手写解析的 parse_runtime_profile_membership_* 辅助函数
Merge remote-tracking branch 'origin/master' into feat/play-count-with-cache
Project CI / Backend tests (pull_request) Failing after 36s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m28s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 6m41s
Project CI / Repository checks (pull_request) Failing after 15s
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
57e259278d
# Conflicts:
#	docs/project-memory/shared-memory/decision-log.md
- membership_granted_points_delta 列类型文档由 i64 更新为 u64
- 补充 shared-contracts 新增四个会员 wire token 枚举及其与模块枚举的映射说明
Merge branch 'master' into feat/play-count-with-cache
Project CI / AI game creator shell Rust crates (pull_request) Successful in 3m7s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 4m20s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 5m28s
Project CI / Backend tests (pull_request) Successful in 7m46s
Project CI / Frontend tests (pull_request) Successful in 3m55s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m18s
Project CI / Native shell tests (pull_request) Successful in 8m37s
Project CI / Repository checks (pull_request) Failing after 6m40s
ff83b67196
- module-runtime 新增 RuntimeProfileMembershipErrorCode 与 RuntimeProfileMembershipDomainError
- 会员报价 / 充值中心 / 后台档位 upsert 的 procedure 结果末尾追加 error_code
- spacetime-module 在拒绝点显式赋码,中文 error_message 逐字保持不变
- spacetime-client 新增 SpacetimeClientError::ProcedureRejected 并映射生成绑定
- api-server 删除会员中文前缀分类,改按 error_code 返回 400,基础设施错误仍 502
- 重生成 module_bindings,并同步设计文档 5.8 与决策日志
Add gamePlayClientId.ts to module and test exclusions
Project CI / AI game creator shell Rust crates (pull_request) Successful in 3m14s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 4m54s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 6m0s
Project CI / Backend tests (pull_request) Successful in 6m54s
Project CI / Frontend tests (pull_request) Successful in 2m54s
Project CI / Native shell tests (pull_request) Successful in 6m47s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m8s
Project CI / Repository checks (pull_request) Successful in 5m9s
9ce43071ad
- is_runtime_profile_redeem_code_domain_error 顶部注明仍靠中文文案匹配判 400
- 指明后续按 RuntimeProfileMembershipErrorCode + ProcedureRejected 的做法改造
- ensure_default_profile_membership_plan 注释说明 init 与读取 helper 共用、目录空时首次读取会写库
- init_profile_membership_plan_catalog 注释改为不覆盖「表已存在但为空」的存放量库
- 报价与建单注释去掉「不写库」的绝对说法,注明目录空时的懒播种例外
- 同步设计文档 7 / 10.2 与决策日志(播种策略定为保留懒播种 + 明示副作用)
Merge pull request 'Feat/游玩计数' (#604) from feat/play-count-with-cache into master
Project CI / AI game creator shell Rust crates (push) Successful in 3m8s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 4m44s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 6m7s
Project CI / Backend tests (push) Successful in 7m12s
Project CI / Frontend tests (push) Successful in 3m3s
Project CI / Native shell tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
a6fb2f01c1
Reviewed-on: #604
Merge remote-tracking branch 'origin/master' into feat/pricing-plan
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
65b9821d25
# Conflicts:
#	docs/project-memory/shared-memory/decision-log.md
- 新增 ADR「模型权限与并发上限的强制边界」:并发只在认领事务按账号过滤 running job、模型档缺省 Basic、直连同步生成暂不计入
- 技术设计新增 §11,承接并作废 §9/§10 的「只落字段、不拦截」表述
- CONTEXT 修订「模型权限」「并发上限」词条,新增「独立生成任务」
- docs/README 与 decision-log 登记本 ADR 与拷问定案
- 新增 §11.4:module-runtime 的 AgcModelAccess / 解析错误、spacetime-module 的 (owner_user_id, status) 索引与认领过滤、api-server 的档位解析与 403 错误码、后台 access 字段
- 明确默认模型语义:登记默认项优先,不在该档可用集合里时回退目录顺序首个可用项
- 记录本期复用 get_profile_recharge_center 解析档位的性能 TODO
- 新增 agc_model_access.rs:AgcModelAccess(basic/full,缺省 basic 失败开放)、越权错误码 MODEL_NOT_AVAILABLE_FOR_PLAN、AgcModelResolveError,以及会员档位到模型权限档的映射
- AgcModel 末位追加 #[serde(default)] access,存量目录 JSON 缺字段按 basic 解析
- AgcModelCatalog 新增 available_models_for / default_model_id_for / resolve_requested_for:显式越权不静默回退,默认模型优先登记项、不可用时回退目录顺序首个可用项
- 覆盖缺省解析、档位过滤、越权与未知模型区分、无可用模型等纯函数测试
- external_generation_job 追加 (owner_user_id, status) 组合索引,认领事务内现算 running 行数,不建计数表
- claim_external_generation_jobs_tx 认领前判定会员并发上限:达上限的 pending 保持排队
- 账号自身过期 running 的回收豁免上限,避免超限账号卡死任务无法回收
- profile.rs 新增 effective_profile_concurrent_job_limit:缺会员行按 Normal、缺目录行失败关闭到 1
- 抽取纯函数 external_generation_claim_within_concurrency_limit 并补单元测试
- AdminAgcModel 新增 access 字段(缺省 basic,失败开放),GET 回读 PUT 写入
- api-server 保存时校验权限档只允许 basic/full
- 后台 AGC 模型页新增「权限档」列,可按模型选择基础/高性能模型
- fake API 与页面用例覆盖权限档选择与保存
- 新增 llm/model_access.rs:用 get_profile_recharge_center 解析账号档位到 AgcModelAccess,缺会员行/缺目录行/读失败统一失败关闭到 Basic
- GET /api/llm/models 按档过滤并给出该档默认模型;该档无可用模型时返回 503 而不是不可解析的默认项
- /api/llm/responses、/api/llm/chat/completions、/api/llm/anthropic/* 全部改按档解析;越权返回 403 + MODEL_NOT_AVAILABLE_FOR_PLAN,目录外/停用仍是 422
- 补 api-server 单测:Basic 档过滤 Full 模型并回退默认项、越权错误码与状态映射
- 数据契约 external_generation_job 增补 2026-10-03 会员并发上限:新增 (owner_user_id, status) 索引、认领只算 running、过期回收豁免、缺行失败关闭到 Normal
- 设计文档 §11.4 增补落地状态与提交号,并标注账号档位轻量读与客户端 403 重选提示为后续可选项
- decision-log 追加落地实现条目:实现拆分、Basic/Full 与 Normal 的失败语义、验证结论与边界 TODO
- 新增 ExternalGenerationOwnerConcurrencyCache:单次 claim 事务内按账号只查一次 concurrent_job_limit,running 计数按账号缓存
- 认领 / lease 耗尽终结后失效该账号 running 缓存,判定结果与逐候选行现查一致
- 回收豁免与不限档不再触发 running 计数查询;补缓存失效单测
- ADR 增补修订节:GET /api/llm/models 返回 models + unavailableModels,reason 枚举 plan_required/disabled/unknown,停用优先,不放松服务端强制
- 技术设计 §11.2 改述列表契约为分桶,§11.4 补 llm_catalog.rs 落点、域层分桶方法、AGC 客户端 hover 文案与 ts-rs 单一真源,并刷新落地提交号
- AGC 后台模型别名技术方案补 2026-10-03 修订说明
- AGC 模型目录上游同步里程碑标注「形状不变」已被后续变更取代
- decision-log 追加分桶决策、兼容与客户端口径
- module-runtime 新增 RuntimeProfileAgcModelAccessSnapshot / ProcedureResult 领域类型
- spacetime-module 新增 get_profile_agc_model_access_and_return:只读会员账期投影 + 档位目录 model_access,缺目录行失败关闭 Basic
- 重新生成绑定并补 spacetime-client get_profile_agc_model_access 与结果映射
- api-server llm/model_access.rs 改调 get_profile_agc_model_access,不再走带账期刷新的 get_profile_recharge_center
- 失败关闭语义不变:procedure 失败或档位目录缺行仍按 Basic
- 技术设计 §11.4:api-server 改调 get_profile_agc_model_access_and_return,性能说明去掉 TODO 并登记提交
- decision-log 追加「模型权限改走只读 procedure(性能收口)」条目
- shared-contracts 新增 llm_catalog.rs:LlmModelsResponse/LlmModelSummary/LlmUnavailableModel/LlmModelUnavailableReason(plan_required/disabled/unknown),未停用优先于档位的 disabled 语义留给域层
- AgcAgentMode/AgcModelProtocol 从 module-runtime 迁入 shared-contracts(前后端 + ts-rs 单一真源),module-runtime re-export 保持路径可用
- 整个目录 DTO 与枚举走 ts-rs,生成到 packages/shared/src/contracts/generated,revision 标 number
- module-runtime 新增 AgcModelCatalog::unavailable_models_for(access):保持目录顺序,停用优先于档位不足,补单测
- api-server public_model_catalog 产出 models + unavailableModels 两桶,重写 model_catalog_tests
- 新增 packages/shared/src/llm/modelCatalog.ts,re-export Rust 生成的目录 DTO 与枚举
- 根 index.ts 增加 export type * from './llm/modelCatalog',AGC 从 @genarrative/shared 根导入
- llm_catalog.rs 删掉手写 WireModelCatalog/WireModelSummary 与 map_catalog,直接反序列化 LlmModelsResponse
- 旧服务端缺 agentMode/protocol/unavailableModels 时按 Codex + OpenAI Responses + 空桶兼容
- 新增未知 reason 收口为 unknown 且 agentMode/protocol 解析为枚举的单测
- clientApi/llmModelCatalog 改用 @genarrative/shared 生成类型,自定义目录补 unavailableModels 空桶与 protocol
- 新增 modelAvailabilityCopy.ts:锁定项原因文案与按 reason 的降级提示
- ConversationModelSelect 渲染锁定项(不可点、可 focus、data-tooltip),降级提示按 reason 取值
- styles.css 补锁定项与 tooltip 样式(仅 hover / focus-visible)
- 新增 vitest 覆盖文案映射与锁定项渲染,vitest.config 纳入 features/project-workspace 用例
- claim_external_generation_jobs_tx 的 take(limit) 只检查前 limit 行,被上限拦下/lease 终结的行仍占检查名额
- 记录触发条件(负载越高 limit 越小)与未决修法(改为成功认领才计数,仍是全局 FIFO),本次不修
- resolve_requested_for 的默认分支原只校验 enabled,补回 access.allows 校验,避免默认模型解析与档位边界不一致
- count_running_external_generation_jobs_for_owner 原先把 status 当 Rust 后置过滤,owner 的 pending 积压也会被物化
- 改为 (owner_user_id, status) 复合条件,直接走 btree 范围,语义不变
- load_owner_llm_catalog 原串行等待目录与权限档两次 SpacetimeDB 往返,热路径延迟叠加
- 改为 tokio::join! 并发发起;目录错误仍由 catalog? 返回,权限档失败关闭语义不变
- role=option 的 aria-label 原只放降级原因,覆盖了可见模型名,读屏只报「订阅计划不支持」
- 改为 `${displayName}:${reasonLabel}`,名称与原因都可被朗读
- 气泡 width:max-content + max-width:220px 配 white-space:nowrap,长文案会溢出气泡
- 改为 white-space:normal,在 220px 内换行
- 新增 MODEL_ACCESS_UNAVAILABLE(503),与 403 档位不足 / 422 模型不可用区分,不再把付费账号静默降级成 Basic
- resolve_owner_agc_model_access 返回 Result 并由 load_owner_llm_catalog 透传;缺会员行 / 缺目录行仍由 module 内失败关闭并返回成功
- 入口注释列出三类错误码语义;补错误码映射单测
- effective_profile_concurrent_job_limit 原先直接读 profile_membership.plan;该列到期后仍保留最近购买档位,过期 Max 会继续拿 128 不限并发哨兵
- 补 expires_at 有效性过滤,与 build_profile_membership_snapshot_from_row 的对外口径一致;缺会员行 / 已过期按 Normal
fix(AGC): 目录缺 unavailableModels 时不再崩溃
Project CI / Backend tests (pull_request) Failing after 1m26s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 3m5s
Project CI / Native shell tests (pull_request) Failing after 2m18s
Project CI / Frontend tests (pull_request) Failing after 1m7s
Project CI / Repository checks (pull_request) Failing after 1m15s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 5m3s
Project CI / AI game creator shell web tests (pull_request) Failing after 58s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
620f6ca938
- applyCatalog 归一化 catalog.unavailableModels,修掉按 reason 取降级文案时对 undefined 解引用(此前 26 个用例挂在这)
- 用例跟进新降级文案;disabled 用例补上下线桶,断言「该模型已下线」专用文案
k88936 force-pushed feat/pricing-plan-limit from a1e2415fae to 620f6ca938 2026-10-04 13:30:07 +08:00 Compare
k88936 marked the pull request as ready for review 2026-10-04 13:33:37 +08:00
k88936 merged commit 0b687651a2 into feat/pricing-plan 2026-10-04 13:36:52 +08:00
k88936 deleted branch feat/pricing-plan-limit 2026-10-04 13:36:53 +08:00
Sign in to join this conversation.