Compare commits

..

71 Commits

Author SHA1 Message Date
kdletters 95a3b9f593 后台灰度预设补上游戏发布开关
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m14s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m45s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 6m46s
Project CI / Backend tests (pull_request) Successful in 6m23s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 8m36s
Project CI / Native shell tests (pull_request) Successful in 7m8s
Project CI / Frontend tests (pull_request) Successful in 3m0s
Project CI / Repository checks (pull_request) Successful in 3m49s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m11s
- admin-web:灰度发布配置的固定目标新增「游戏分发 → 游戏发布」(game-distribution:publish),前缀标签与描述说明「未配置或关闭时默认开放,开启后只放行白名单/灰度命中」。
- 默认语义:选中预设后保持 enabled=false,避免误把发布收紧成全关;需要灰度时再显式打开并填白名单/比例。
- 用例:新增「可选择游戏发布开关且默认不收紧」与「保存写入白名单与比例」两条后台用例。
- 文档:开发运维文档说明该键配置前不会出现在已有列表、必须从预设或手填新建一行。
2026-09-22 20:06:37 +08:00
k88936 944ae9a6eb Merge pull request '调试支持:开发模式下增加 Codex CLI 执行器锚定逻辑,确保裸命令名按 PATH 解析为绝对路径' (#475) from fix/linux-dev into master
Project CI / AI game creator shell Rust crates (push) Successful in 1m14s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m38s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 6m30s
Project CI / Backend tests (push) Successful in 6m47s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 8m16s
Project CI / Native shell tests (push) Successful in 7m14s
Project CI / Frontend tests (push) Successful in 3m21s
Project CI / AI game creator shell web tests (push) Successful in 3m0s
Project CI / Repository checks (push) Successful in 3m42s
Reviewed-on: #475
2026-09-22 19:53:06 +08:00
lhk229 7bc411119c 修复 CI 镜像脚本的 Buildx 驱动检查兼容性
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 smoke (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
Project CI / Frontend tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / AI game creator shell Rust smoke (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
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
读取 buildx inspect 的 Driver 字段,移除不受支持的 --format 参数。
收紧 Docker 测试替身参数检查,并覆盖错误驱动拒绝行为。
更新部署文档与共享记忆,记录真实 Buildx 兼容性验证要求。
2026-09-22 11:45:22 +00:00
lhk229 079466b29b 合并最新 master 到 CI 下载缓存优化分支
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 smoke (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
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / AI game creator shell Rust smoke (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Frontend 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
合入 origin/master 的浏览器进程清理与引用输入框重构。
保留 CI 下载缓存、最小构建上下文和维护日志优化。
通过 17 项工作流测试、2 项 npm 缓存导出测试及编码检查。
2026-09-22 11:33:40 +00:00
lhk229 1e6b0e5684 优化 Gitea CI 镜像下载缓存与维护日志
使用专用 BuildKit builder 持久复用 Cargo 与 npm 下载缓存,并支持从可信镜像导入
按当前依赖物化镜像下载快照,配置独立缓存回收策略
补齐 AGC vendor 本地依赖清单并缩小构建上下文
增加缓存维护阶段、耗时和失败日志路径
补充下载缓存与构建上下文测试,同步部署说明和共享记忆
2026-09-22 11:23:46 +00:00
k88936 a0d72bbef2 调试支持:开发模式下增加 Codex CLI 执行器锚定逻辑,确保裸命令名按 PATH 解析为绝对路径
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 smoke (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
2026-09-22 19:19:01 +08:00
k88936 da0df57ebc Merge pull request '重构/抽取带引用功能的输入框' (#464) from refactor/extract-dep-from-ref-inputer into master
Project CI / AI game creator shell Rust crates (push) Successful in 1m28s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m0s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 7m4s
Project CI / Backend tests (push) Successful in 7m8s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 8m47s
Project CI / Native shell tests (push) Successful in 8m26s
Project CI / Frontend tests (push) Successful in 3m50s
Project CI / AI game creator shell web tests (push) Successful in 3m28s
Project CI / Repository checks (push) Successful in 3m45s
Reviewed-on: #464
2026-09-22 19:05:31 +08:00
lhk229 e8db0d1c87 Codex/agc browser orphan cleanup (#461)
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / AI game creator shell Rust smoke (push) Has been cancelled
Reviewed-on: #461
2026-09-22 19:05:20 +08:00
k88936 9ae9ed362a Merge remote-tracking branch 'refs/remotes/origin/master' into refactor/extract-dep-from-ref-inputer
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m25s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m56s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 6m51s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 8m30s
Project CI / Backend tests (pull_request) Successful in 7m17s
Project CI / Native shell tests (pull_request) Successful in 7m34s
Project CI / Frontend tests (pull_request) Successful in 4m4s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m9s
Project CI / Repository checks (pull_request) Successful in 3m36s
# Conflicts:
#	docs/project-memory/shared-memory/decision-log.md
2026-09-22 18:51:27 +08:00
kdletters 060a850f74 修复 AGC 更新摘要泄漏提交说明
Project CI / AI game creator shell Rust smoke (push) Successful in 1m28s
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Frontend 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
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
自动摘要改为读取提交消息首行,仅展示标题并忽略后续说明

更新摘要不再附带短 SHA

补充多行提交消息与摘要格式回归测试

同步客户端更新检查与开发运维文档
2026-09-22 18:44:55 +08:00
lhk229 b1be6947b0 契约检查补齐现有 OSS 上传白名单,保留精确校验。
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/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
Project CI / Repository checks (pull_request) Has been cancelled
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 / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Successful in 1m21s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m53s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 6m29s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 8m32s
Project CI / Frontend tests (push) Successful in 3m43s
Project CI / Backend tests (push) Successful in 8m59s
Project CI / Native shell tests (push) Successful in 9m45s
Project CI / Repository checks (push) Successful in 4m23s
Project CI / AI game creator shell web tests (push) Successful in 3m19s
修正过期注释并更新排障记录。
Native shell 契约检查、5 条素材上传测试、编码及格式检查全部通过。
2026-09-22 10:23:29 +00:00
lhk229 b7ca94334f 修正 CI 缓存部署的网络地址与验收流程
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (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
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Successful in 1m24s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m57s
Project CI / Frontend 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
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
区分任务容器、Runner 网关和宿主源码拉取地址
记录内层 Docker 独立地址池与遗留网络定向清理方式
补充真实任务网络中的源码拉取和 API 连通性验收
同步团队开发流程中的部署约定
2026-09-22 10:05:23 +00:00
kdletters 54a5401cce 将正式 Full Build 纳入 release 每日调度
Project CI / AI game creator shell Rust crates (push) Successful in 1m22s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m54s
Project CI / Native shell tests (push) Failing after 4m4s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 6m23s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 7m53s
Project CI / Backend tests (push) Successful in 7m54s
Project CI / Frontend tests (push) Successful in 5m34s
Project CI / Repository checks (push) Successful in 5m31s
Project CI / AI game creator shell web tests (push) Successful in 4m16s
按服务端与客户端 scope 分别触发 release Full Build 和 AGC 双平台
等待并汇总三类下游结果,按成功 lane 推进 revision
同步 release 参数、生产门禁与运维文档
2026-09-22 17:59:57 +08:00
kdletters 4d45afbd6e 修复 release 渠道 macOS 测试的默认环境隔离
隔离默认渠道测试中的 AGC_UPDATE_CHANNEL 环境变量
补充 Jenkins 渠道参数进入 Node 测试进程的排障记录
2026-09-22 17:59:57 +08:00
k88936 1ecb92c53d Merge branch 'master' into refactor/extract-dep-from-ref-inputer
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m17s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m51s
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
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / Backend 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
2026-09-22 17:57:51 +08:00
suzmii 4990dae829 Merge pull request '运行页入口收口:过程提示退出对话区,顶栏统一承载运行入口' (#457) from feat/agc-run-preview-tip into master
Project CI / AI game creator shell Rust crates (push) Successful in 1m25s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m55s
Project CI / Native shell tests (push) Failing after 3m12s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 6m35s
Project CI / Backend tests (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Reviewed-on: #457
2026-09-22 17:47:59 +08:00
k88936 c42e087a30 Merge remote-tracking branch 'origin/master' into refactor/extract-dep-from-ref-inputer
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Failing after 21s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Failing after 20s
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
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
# Conflicts:
#	docs/project-memory/shared-memory/decision-log.md
2026-09-22 17:41:24 +08:00
suzmii 9b0fcd48d2 Merge branch 'master' into feat/agc-run-preview-tip
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m24s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m53s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Failing after 20s
Project CI / Backend tests (pull_request) Failing after 21s
Project CI / Native shell tests (pull_request) Failing after 21s
Project CI / Frontend tests (pull_request) Failing after 21s
Project CI / Repository checks (pull_request) Failing after 21s
Project CI / AI game creator shell web tests (pull_request) Failing after 21s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
2026-09-22 17:31:32 +08:00
kdletters 56fd5a98f7 修复 release 客户端下载清单的编码空格校验
Project CI / AI game creator shell Rust lane 1/2 (push) Failing after 21s
Project CI / Backend tests (push) Failing after 21s
Project CI / Native shell tests (push) Failing after 21s
Project CI / Frontend tests (push) Failing after 21s
Project CI / Repository checks (push) Failing after 20s
Project CI / AI game creator shell web tests (push) Failing after 14s
Project CI / AI game creator shell Rust lane 2/2 (push) Failing after 1m56s
Project CI / AI game creator shell Rust crates (push) Failing after 1m54s
Project CI / AI game creator shell Rust smoke (push) Failing after 1m56s
放行客户端下载清单 URL 中百分号编码的空格
补充 release 安装包文件名回归测试
更新客户端下载规范与排障文档
2026-09-22 17:29:19 +08:00
suzmii c2d756a772 Merge branch 'master' into feat/agc-run-preview-tip
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m28s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m59s
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
Project CI / AI game creator shell Rust lane 2/2 (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 / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
2026-09-22 17:28:39 +08:00
kdletters 67e95eee4e Merge pull request '游戏分发与在线游玩:封面/截图冻结、网页可见可玩与 AGC 发布接入' (#466) from codex/game-distribution into master
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / AI game creator shell Rust smoke (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Frontend 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
Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/466
2026-09-22 17:26:59 +08:00
kdletters c0c5255b31 Merge branch 'master' into codex/game-distribution
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 smoke (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
2026-09-22 17:26:50 +08:00
k88936 d5fb34209a 重构:候选懒加载改为菜单回调,match 收成纯函数
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
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 / 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
- ReferenceProvider 新增可选 onMenuQueryChange(query):输入区在 useEffect 里回调(菜单关闭时收到 null),作为懒加载的唯一入口
- ProviderMentionMenu 不再让 provider 在渲染期产生副作用:match 只负责过滤已就绪的数据,契约里写明它必须是纯函数
- useSkillReferenceProvider 把目录读取从 match 搬进 onMenuQueryChange,第一次敲出 $ 才读;失败仍放开重试(下一次菜单查询变化时重试)并保留 console.warn
- referenceSourceProviders 用例改为经菜单回调驱动,并显式断言 match 不触发任何 Tauri invoke、菜单关闭(null)也不触发
- 同步 ADR、决策记录与功能说明里 provider 契约的那段描述
2026-09-22 17:24:24 +08:00
lhk229 ff7c5f3589 补齐 CI 缓存维护器请求与任务契约校验
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 smoke (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
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / AI game creator shell Rust smoke (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Frontend 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
要求 API 请求显式指定 HTTP 方法并更新全部调用点
增加 workflow 全部任务名称与缓存导出映射的契约测试
从 Rust 任务映射推导集合和数量,消除重复硬编码
同步开发流程中的维护约定
2026-09-22 09:22:54 +00:00
lhk229 705bb1e6b3 CI缓存自动维护与清理 (#462)
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / AI game creator shell Rust smoke (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Frontend 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
Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/462
2026-09-22 17:19:11 +08:00
kdletters e5460280ce 发布入口接入灰度开关
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 smoke (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
- 后端:GET /api/runtime/frontend-config 新增 gameDistributionPublishEnabled,复用 `game-distribution:publish` 判据(未配置或 enabled=false 时对已登录作者默认开放,显式收紧后只放行白名单/灰度命中用户,匿名恒为 false),前端入口与写入口共用同一事实源。
- 后端用例:新增 frontend_runtime_config_game_distribution_publish_is_scoped_to_authenticated_gate,覆盖默认开放、enabled=true 无白名单、白名单命中、deny 名单、enabled=false 回退与 rolloutPercent=100。
- 网页:平台壳按灰度隐藏「发布游戏 / 发布新版本」入口;/games/publish 直接访问时渲染「发布功能正在灰度中」并提供重新检查;读取失败按放行处理,由后端写入口把关并返回可读文案。
- AGC:新增 readGamePublishAvailability(同一运行时配置字段),只有命中才把发布回调交给 DirectProject 聊天头;字段缺失或读取失败按不开放处理。
- 文档:玩法链路、后端数据契约与实施计划记录灰度口径、入口行为与验证命令。
2026-09-22 17:12:04 +08:00
k88936 aa973ea34d 同类问题:另外两个宿主的引用 provider 也按内容签名记忆
- PlanningChatView 的 chatProjectAssets 由工作台壳每次渲染重新 .filter() 派生、DirectProjectComposer 的 assets 由 manifest 订阅每次渲染重新派生,两处的 [assets] 依赖同样从不命中缓存
- 两处改用 assetsSignature(...) 做依赖,provider 身份不再随宿主渲染抖动(流式回复时每帧都换),输入区不再每帧重注册 Lexical 更新监听
2026-09-22 17:12:02 +08:00
suzmii f7250f5dd1 合并 master(bba4f0eb2):DirectProject 聊天三态与快照文档并入运行页入口收口
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (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
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
- 只冲突两份共享记忆的尾部追加(pitfalls / decision-log):保留双方新增条目,直接合并非取舍
- App.tsx / styles.css 自动合并通过;本分支的运行页入口收口与 onRunNotice 通道未受影响
2026-09-22 17:08:03 +08:00
k88936 fbfc7e5e62 修复:候选菜单的 key 改用引用身份,同名素材不再撞 key
- 导出 resourceReferences 的 chatReferenceKey(只认稳定身份、不含显示名的身份键),并加注释说明它为什么不能省
- ReferenceMentionOption 由 super(chatReferenceMentionToken(...)) 改为 super(chatReferenceKey(...)):characters/hero.png 与 enemies/hero.png 都展开成 @hero,拿显示 token 当 key 会让 React 撞 key、复用错 DOM,键盘高亮与选中落到另一条候选上
- referenceSourceProviders 用例补一批同名素材:断言两条候选的显示 token 相同、身份键不同
2026-09-22 17:06:08 +08:00
k88936 3fdde6f51d 修复:并发导入附件时仍然守住 8 个上限
- useDirectProjectChatController 增加 attachmentsPendingCountRef:已放行但还没落成正文芯片的条数先在预算里占住名额,放行的部分在 finally 还回去
- 原来的预算只减草稿里已有的附件芯片数,两次并发导入会各自按同一份快照放行,最终插进正文的芯片可以超过 MAX_CHAT_COMPOSER_ATTACHMENTS(回归自「待发附件列表」被删掉的那次重构)
- 新增 tests/chatComposerAttachmentCap.test.tsx:两次并发各带 5 个文件,断言放行 5 + 3,并补 jsdom 缺失的 Blob.arrayBuffer
2026-09-22 16:59:36 +08:00
kdletters 1bd5fe9ff0 合并 master:AGC 项目快照整目录上传
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (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
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
- 合并 origin/master(bba4f0eb2:项目快照整目录上传 .agent,保留项目身份与 Agent 历史)。
- 自动合并无冲突;共享记忆与实施计划文档经复核保留双方条目,游戏分发功能面未受影响。
- 同步目的:Gitea PR 门禁要求 PR head 包含目标分支最新提交,合并后重新推送触发 Project CI。
2026-09-22 16:59:05 +08:00
k88936 39ac337569 诊断:Skill 目录读取失败不再静默
- skillReferenceProvider 的 loadSkillCatalog 失败分支补 console.warn('[skill-reference] …'),带上是哪一次重试、错在哪,排障时不再只能看到「敲 $ 没反应」
- referenceSourceProviders 用例断言这条日志(并用 vi.restoreAllMocks 收尾),把「失败可重试」与「失败留痕」两件事一起钉住
2026-09-22 16:56:31 +08:00
k88936 28bacfc125 画布生成面板的引用 provider 按素材内容签名记忆
- ResourceCanvasAssetGenerationPanelView 的 referenceProviders 改依赖 assetsSignature(referenceAssets):referenceAssets 由 .filter() 派生、每次渲染都是新数组,原来的 [referenceAssets] 依赖永远命中不了缓存
- 注释写明「provider 身份一变,输入区就重新注册 Lexical 更新监听」,避免以后又改回数组依赖
2026-09-22 16:55:39 +08:00
kdletters d047b16b91 合并 master:DirectProject 回合三态与 AGC release 每日调度
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 smoke (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
- 合并 origin/master 11 个提交(DirectProject 回合三态与投影 memo、宿主崩掉后的回合收口、权限拒绝后的忙态、策划对话布局修复、AGC release 每日调度)。
- 本次自动合并无冲突;游戏分发发布入口(App 的 requestGamePublish、DirectProjectChatView 透传、聊天头「发布到游戏广场」按钮)在新聊天状态机下保持接线。
- 验证:root / AGC / admin-web 三端 typecheck、全量 vitest、游戏分发 Rust 测试、encoding、doc-index、rustfmt 与 SpacetimeDB schema guard。
2026-09-22 16:54:38 +08:00
kdletters f1b282f3cd 合并 master 并接入 DirectProject 新聊天架构
- 合并 origin/master(304 个提交:DirectProject 聊天容器重构、Project Supervisor 退役、策划附件导入、CI 隔离编译缓存等)。
- 接受 master 对 ProjectSupervisorView / SupervisorChatOnlyView 的退役与预览快捷测试收敛;发布入口改由 DirectProject 聊天头承载。
- DirectProjectChatHeader 新增「发布到游戏广场」入口(无回调不渲染、回合忙态禁用),DirectProjectChatView 透传 onRequestGamePublish。
- App.tsx 继续由工作台壳持有试玩包导出与 GameDistributionPublishPanel,沿用 project.export_package 权限确认队列;check-config 把该命令从 native-only 清单移回 App invoke。
- 后台游戏审核 API / 类型 / 路由测试与 master 新增的 AGC 模板管理按双方保留合并,并修掉拼接造成的接口与用例闭合缺陷。
- 修正 master 自带的 viteProxyConfig 断言:/api/creation-entry 属退役路由,测试改为断言不进入代理。
- 记录合并踩坑:语法结构内部的冲突不能简单按「双方保留」拼接,必须按某一侧骨架重建并跑 tsc 与单文件测试。
- 验证:全量 vitest 393 文件 / 4374 用例通过,root / AGC / admin-web 三端 typecheck,cargo check 与游戏分发 Rust 测试,encoding、doc-index、rustfmt、SpacetimeDB schema guard。
2026-09-22 16:45:29 +08:00
kdletters bba4f0eb24 项目快照整目录上传 .agent,保留项目身份与 Agent 历史
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m16s
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
Project CI / AI game creator shell Rust lane 1/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 / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Successful in 1m17s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m47s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 7m3s
Project CI / Backend tests (push) Successful in 5m59s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 8m6s
Project CI / Native shell tests (push) Successful in 7m23s
Project CI / Frontend tests (push) Successful in 3m48s
Project CI / Repository checks (push) Successful in 3m37s
Project CI / AI game creator shell web tests (push) Successful in 3m40s
- 新增 should_skip_project_snapshot_sync_path:快照同步放行 .agent 组件,并放行其中 .db / .db-wal / .db-shm 的 Agent 状态数据库
- 组件与后缀排除表提取为常量;should_skip_project_snapshot_path 行为不变,项目索引、checkpoint、Agent 上下文与 git 检查继续排除整个 .agent
- project_snapshot 扫描入口切到新口径,.agent 的 manifest、agent.db、会话、日志、checkpoint、workbench 与 project.lock 全部参与同步
- 新增整目录扫描用例与策略用例,覆盖 .agent 内凭据、.env、版本库与构建目录仍被排除
- 主规范更新上传内容边界与后台归档范围:工程 ZIP 可用于还原项目身份与 AGC 对话历史
- 里程碑、实施计划与共享记忆决策记录同步 .agent 全量口径、真实上传与后台 ZIP 的运行时验证证据,以及 v1/v2 键布局边界
2026-09-22 16:39:12 +08:00
kdletters fdc14404b2 新增 AGC release 每日调度
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Frontend 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
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / AI game creator shell Rust smoke (push) Has been cancelled
新增 Genarrative-Scheduled-Release-Trigger,每天 04:00 发布 Windows/macOS release 渠道

release 调度复用统一发号源,并为 macOS 启用过期构建让位

现有小时 dev 调度保持同时触发 Windows 与 macOS

更新生产运维门禁、发号权限、技术方案与共享记忆
2026-09-22 16:32:11 +08:00
lhk229 3775e05970 修复策划对话布局与流式显示 (#460)
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / AI game creator shell Rust smoke (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Frontend 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
恢复固定消息区域和工作区状态条样式,修正窄屏单列布局
限制长阶段提示、澄清卡片和文件导入结果高度,保证输入区可达
补齐实时正文和思考的滚动跟随,保留用户上滚意图
移除读取策划历史时补造的发送时间
补充布局与显示回归测试,同步技术方案和共享排障约定

Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/460
2026-09-22 16:25:51 +08:00
k88936 742bad4fee Merge pull request 'Fix/unfinished turn' (#463) from fix/unfinished-turn into master
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / AI game creator shell Rust smoke (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Frontend 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
Reviewed-on: #463
2026-09-22 16:23:25 +08:00
suzmii 0434647978 修复快速编辑草稿回填用例的竞态:面板先出现、Lexical 的值后落
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 smoke (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
- 同一条用例在 CI run 2702 与本地一次全量里各命中过一次「expected '' to include '把角色头发改成红色'」:面板(dialog)一出现就同步读 textContent,编辑器值还没落
- 回填类断言统一改走 expectPanelPromptContains(内部 waitFor),不再赌时序
- 「提交后重开是空面板」那条否定断言改用 composerValue 读(它先让编辑器落值),避免空串让断言空过
- 连跑该文件 9 次 + 全量 3 次(173 文件 / 1826 通过 / 18 跳过)均全绿
2026-09-22 16:13:10 +08:00
k88936 a6a938d5cc 文档:新增引用输入区重构与宿主注入的实施计划
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m34s
Project CI / Backend tests (pull_request) Failing after 17s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m25s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 8m23s
Project CI / Repository checks (pull_request) Failing after 16s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 10m26s
Project CI / Frontend tests (pull_request) Successful in 5m37s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m6s
Project CI / Native shell tests (pull_request) Successful in 10m39s
- 记录一句话交付、五条验收判据、六个提交切分与验证方式
- 追加执行状态:六项已落地内容、顺带清理、验证结果(全绿门禁与定向用例)与未完成项(CSS 类名重命名、真机手感、粘贴解析)
2026-09-22 16:07:06 +08:00
k88936 e6b6a0aeca 文档:记录引用候选由宿主注入的决策、术语与功能说明
- 新增 ADR《引用候选由宿主注入》:注入式 provider、面板外移、附件并入正文的理由、备选与影响
- decision-log 增加 2026-09-22 条目:注入 / 分离 / 附件 / 显示口径四项决策、未纳入项、验证方式,并记录顺带清理 activeVersionId
- 功能说明《AGC聊天素材引用》同步:Provider 注入、ResourceReferencePicker 自持数据、$ Skill 只出现在注入该 provider 的宿主、附件进入正文与上限口径、显示名注入
- CONTEXT.md 补引用来源 / provider / 选择器 / 附件芯片等术语与关系
- docs/README.md 登记新 ADR
2026-09-22 16:06:40 +08:00
k88936 1fbce7ffc5 清理工作台壳到聊天壳已经传不过去的 activeVersionId
- ProjectChatComponentProps 删除 activeVersionId:它只在策划输入盒的 @ 面板里生效,而策划输入盒的 @ 触发钮早就不渲染,参数从壳传进聊天后无人消费
- WorkspaceLauncher 不再把这个参数透传给 ProjectChat;工作台壳自己那份 activeVersionId 保留,仍供 ProjectDevelopmentView 的游戏运行版本使用
- App 的 AppProps 与解构同步删除该参数
- styles.css 里那条「ResourceReferenceInput 收到 showTriggerButton={!directCodex}」的注释改成当前口径:@ 触发钮在控制排的 .project-chat-reference-trigger,输入区不注入 ResourceReferencePickerAction
2026-09-22 16:05:08 +08:00
k88936 7e009a4c94 引用输入区改为宿主注入引用来源,素材选择器与输入区分离
- 新增 features/project-workspace/reference-source(types + 资源/Skill/附件/运行画面区域四个 provider 工厂),输入区按 providers 顺序取第一个非空回答,不再判断引用种类
- ResourceReferenceInput 删除 assets / versions / activeVersionId / skills / showTriggerButton / openPicker,改为 providers / inputActions / submitSuppressed / containerRef 四个通用接缝
- 新增 ResourceReferencePicker 与 ResourceReferencePickerAction:面板自己拿数据(素材、版本、缩略图预览 invoke)并自管筛选态,由宿主渲染,确认后只经 insertReferences 交回输入区
- 四个宿主改为按需注入 provider 并各自渲染选择器:DirectProject 注入资源+Skill+附件+运行区域,策划输入盒 / 画布生成面板 / 资源卡快速编辑只注入资源+两个静默 provider,非 DirectProject 宿主不再出现 $ Skill 候选
- 附件并入 ChatReference,编辑器收敛为单一 ResourceReferenceNode(删除 AttachmentReferenceNode),附件 chip 的 DOM 契约(data-attachment-reference / data-attachment-status / title / 移除按钮 aria-label)逐字保留
- 附件改为导入成功即以芯片进入正文、失败与异常一律不插入;控制器删除待发附件状态与 removeAttachment,ComposerPendingAttachments 与其提交时拼接一并删除,单次上限按草稿中的附件芯片数计算,导入进行中禁止发送
- directCodexContentToPromptText 的引用 token 前后各留一个空白(不重复),显示名反查改由调用方注入 resourceLabelResolver(assets),并补 TODO:后续改为按组件渲染引用
- 队列 chip 文案、快速编辑出站提示词、润色回填的引用恢复统一走 joinMentionText 的同一份 token 口径
- 新增 tests/referenceSourceProviders.test.ts 规则矩阵单测(触发符、候选过滤与截断、toReference、refresh、mentionToken、Skill 目录懒加载与失败重试),既有输入区/聊天用例迁移为注入 provider
- styles.css 删除随面板外移与待发附件列表一起失效的规则
2026-09-22 16:01:54 +08:00
k88936 00f36b4863 Merge remote-tracking branch 'origin/master' into fix/unfinished-turn
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m18s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m33s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 8m5s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 6m54s
Project CI / Backend tests (pull_request) Successful in 5m42s
Project CI / Native shell tests (pull_request) Successful in 7m25s
Project CI / Frontend tests (pull_request) Successful in 3m43s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m10s
Project CI / Repository checks (pull_request) Successful in 3m34s
2026-09-22 15:52:19 +08:00
k88936 a35956f3ec 宿主崩掉不再留下永远开着的回合:本地命令失败按身份兜底收口
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 smoke (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
- directThreadChat 新增 commandClosedTurnUserItemId 与 stopDirectThreadTurn:本地命令返回即这一轮在宿主那边收场,只放掉「是否在跑」、不写终态时间
- 同身份迟到的 turn.started 不再把这一轮拉回运行态;迟到的 turn.completed 例外放行,仍要拿它补上真正的结束时间
- 身份不同的回合不动:「正在跑的是另一轮」这种拒绝不能顺手抹掉别人的回合
- useDirectThreadChatSubscription 暴露 stopCommandTurn;controller 在真失败分支按本轮身份调用,主动终止与 another-turn-running 不走这条
- 新增用例:reducer 两条(不复活 / 不碰别人的回合)、appSurface 一条(失败后不再显示「陶泥儿正在处理」、输入盒回到发送态);变异去掉兜底收口后该用例变红
- decision-log 记录本次决策,以及仍未做的宿主侧根因(Rust 侧 drop 守卫补终态事件)
2026-09-22 15:34:48 +08:00
suzmii 3b2a6c8853 Merge remote-tracking branch 'origin/master' into feat/agc-run-preview-tip 2026-09-22 15:26:54 +08:00
k88936 cab31613f2 权限被拒不再抹掉下一轮的忙态:出队统一交给 finally
- startTurn 的权限被拒分支不再自己 endTurnCommand + dispatchNextQueuedTurn,改为标记 queueAdvance 交给 finally 收口
- finally 固定顺序:刷新清单 → endTurnCommand() → 出队,保证清忙态早于出队,不再清掉下一轮刚设上的忙态与 pendingUserItemId
- 模块注释把这条顺序写成契约,并说明权限被拒这种「本轮从未发出但要继续出队」的走法
- 新增 appSurface 用例:权限被拒 + 后面排着消息时,被出队那一轮在飞期间输入区仍是终止态;改前该用例红
2026-09-22 15:20:38 +08:00
k88936 67d2f08421 窗口期渲染用例先等 invoke 发出,别让断言在命令未发出时就过
- chat-composer 的 does not report a finished turn 用例捕获 invoke 并先等它被调用,再断言窗口期不出现终态文案
- 写权限门是异步的:不等待时断言会在 invoke 之前通过、收尾的 pending[0]?.resolve 变成空操作,用例盖不住它声称覆盖的窗口
2026-09-22 14:59:13 +08:00
k88936 c8a8b0b0b8 DirectProject 聊天投影补 memo,让下游 memo 不再空转
- DirectProjectChatView 的 directTurns 改用 useMemo 缓存,判据为 directEntries / localMessages / directTurnRunning / pendingUserItemId
- 注释写明不缓存时每次渲染都是新数组引用,useDirectProjectTurnStatus 里以 turns 为判据的 useMemo 永远命中不了,读者会误以为 turnStatus 引用稳定
2026-09-22 14:56:50 +08:00
k88936 417137baf8 把 DirectProject 聊天状态与数据流的说明写进代码注释
- useDirectProjectChatController 模块注释补三层地图、三份原始输入、一次发送的时序(含空窗步骤)与状态变量归属
- DirectChatTurnState 注明三态各自能断言什么、判据的四个输入及来路、state 真值表、两个易读错的点与「finished 拿不到终态时间」的已知边界
- DirectThreadChatState.turnRunning 注明它只是原生真相,不等于界面上的「这一轮在跑吗」
- DirectProjectTurnUsage 注释写清两类「finished 却拿不到终态时间」的回合、覆盖范围与修法口径,说明不再甩给任何文档
- decision-log 的未修边界条目改为指向上述代码位置,避免同一套说明在代码与文档里各留一份
2026-09-22 14:20:27 +08:00
k88936 cfcd3cd604 DirectProject 空窗期不再谎报本轮结束:三态接入渲染
- DirectProjectTurnUsage 渲染判据改为 state !== 'finished',awaiting-start 不再显示「本轮结束于 … 耗时 0.0秒」
- DirectProjectTurn 的过程折叠判据同样改为 state !== 'finished';流式正文与工具卡片仍按 state === 'running'
- 组件注释写明「否定式读 !== 'finished'、肯定式读 === 'running'」这条分工,并标出仍由 A 负责的另一半兜底与覆盖范围
- 新增 tests/directProjectTurn.test.tsx:awaiting-start / running 不显示终态文案且不折叠、finished 有终态时显示结束时间与耗时
- appSurface chat-composer 新增窗口期用例:invoke 挂起且无任何原生事件时不出现「本轮结束于」
- decision-log 追加本条决策、保守卡片口径的理由、未修边界与变异验证结论
2026-09-22 14:07:07 +08:00
k88936 860dcb1bf5 DirectProject 聊天状态显式化:回合三态与「在跑吗」唯一派生入口
- DirectChatTurn 的 active 布尔改为三态 state(running / awaiting-start / finished),三态判定收在投影里、用身份而不是时间戳
- buildDirectChatTurns 新增 pendingUserItemId 入参;controller 的 beginTurnCommand / endTurnCommand 与 turnBusy 同生共死地维护它
- 新增 useDirectProjectTurnStatus 作为「在跑吗」的唯一派生入口:nativeRunning / commandInFlight / displayBusy / latestTurnState
- DirectProjectChatView 不再手拼 busy:header 与 composer 读 displayBusy,「正在处理」卡片读 nativeRunning
- DirectProjectConversation 的 running 入参改名 nativeRunning,语义写在类型注释里
- DirectProjectTurn 改读 turn.state;渲染条件保持不变,awaiting-start 仍按已结束渲染(空窗缺陷另开改动)
- directTurnPresentation 的起点优先级与三态判定由嵌套三元改为逐级 if 覆盖
- 新增 directProjectTurnStatus 用例 4 条;directTurnPresentation 补三态用例 4 条并改按 state 断言
- ADR 补「活动回合唯一判据约束的是原生回合」澄清与代码指针
- decision-log 记录本次重构决策、未修的窗口期缺陷与验证证据
2026-09-22 14:06:20 +08:00
suzmii 6dde321bd1 修 review 发现:运行反馈全面退出对话区,并补齐顶栏与守卫细节
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m5s
Project CI / Backend tests (pull_request) Failing after 11s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m13s
Project CI / Frontend tests (pull_request) Successful in 3m15s
Project CI / Repository checks (pull_request) Failing after 15s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m56s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 7m55s
Project CI / AI game creator shell web tests (pull_request) Failing after 3m21s
Project CI / Native shell tests (pull_request) Successful in 8m53s
- executeRunLocal 的三条失败路径(缺 Tauri / 缺项目 / 启动预览报错)也改走 onRunNotice 失败色提示,对话区不再出现过程行;随之删除已无调用方的 announceProjectChatMessage 与不再有意义的 announceToChat 参数
- RunNoticeToast 的失败提示延长到 6 秒(成功仍 2.6 秒),避免错误一闪而过
- 版本入口在 UI 编辑器壳里不渲染;版本名改由一层 span 承载省略号,长版本名不再被硬裁
- 视图拿不到 onNotice 时把「在浏览器打开失败」写进 console,不再静默吞掉
- check-native-shells 增加「运行页视图里 openUrl( 只有一个调用点」的判据,堵住自动开浏览器的口子
- 新增外壳级接线用例 runNoticeShellWiring;previewActivation 补「启动失败走失败色提示且不写对话区」;runNoticeToast 补失败时长;样式守卫补省略号
- decision-log 收敛到最终口径(成功与失败都出对话区、失败留 6 秒、编辑器不挂版本入口)
2026-09-22 13:51:30 +08:00
suzmii 82de6d46a9 合并 master(48eb3d500):CI 缓存与运行时测试重组并入运行页入口收口
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m9s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m4s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 8m26s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 7m59s
Project CI / Backend tests (pull_request) Successful in 6m44s
Project CI / Native shell tests (pull_request) Successful in 7m55s
Project CI / Frontend tests (pull_request) Successful in 3m32s
Project CI / Repository checks (pull_request) Successful in 3m6s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m33s
- 只冲突 docs/project-memory/shared-memory/pitfalls.md:保留双方新增的排障记录(本分支的 Codex 原生包版本 + master 的 Rust 对象快照)
- 其余 master 改动(CI 缓存脚本、process_session 测试拆分等)自动合并
2026-09-22 12:56:22 +08:00
suzmii 0a8debef56 合并 master:运行页入口收口并入 DirectProject 重构
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 smoke (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
- 保留 master 的 DirectProject / ProjectChat 重构与测试重组:原 appSurface 的 project-commands / project-preview / supervisor-runtime 三个 suite 已被 master 删除,接受删除并把相关断言重写到 tests/previewActivation.test.tsx 等新用例
- 本分支改动重放到 master 结构:运行与预览成功的反馈改走 onRunNotice toast(带 tone),对话区不再写过程提示
- 运行页顶栏统一:预览地址改成「在浏览器打开」按钮,版本入口搬进同一动作区复用同一套按钮皮,状态行与预览地址小字退役
- 生成任务入口、面板与锚点不在运行页渲染,placement 里的 run 档一并删除
- 手工合并 App.tsx / WorkspaceLauncher.tsx / model.ts / check-config.mjs / decision-log / pitfalls,并把两条决策记录与一条排障记录补进 master 版本文档
2026-09-22 12:50:19 +08:00
suzmii 5a87c5f965 运行页顶栏收口:预览地址改按钮,版本入口并入顶栏同一套样式
- 删除运行区域上方的状态行与预览地址小字,运行画面回到两行栅格
- 预览地址改成顶栏动作区里的「在浏览器打开」按钮(opener 插件 openUrl),只在有活预览时渲染
- 版本入口搬进同一个顶栏动作区,外观复用该容器的基础按钮样式,去掉自带的边框、底色与 hover
- 打开失败走 onNotice 的失败色 toast:onRunNotice 通道改成带 tone 的对象,四处调用点同步
- 补顶栏入口正向守卫,删掉状态行的旧样式用例并新增顶栏与浏览器打开的用例
2026-09-22 11:51:51 +08:00
suzmii 8e6ee50ad8 运行页收口:状态行同在画面外,生成任务不进运行页
- 运行区域上方新增状态行,预览地址小字与版本入口同排:两者都在游戏画面之外,版本入口不再绝对定位压在画面上
- 小字单行省略并用 title 给出完整地址,状态行高度恒定 30px,没有版本入口时也不塌
- 运行页不再渲染生成任务入口、面板与锚点;[data-generation-tasks-placement='run'] 那一档坐标与 placement 里的 run 一并删除
- 按新口径改写两侧用例,新增运行区域状态行的声明级守卫用例
- 记录决策到 shared-memory/decision-log.md
2026-09-22 10:29:53 +08:00
suzmii e6083050a8 运行/预览提示退出对话区,改 toast 与运行区域小字
- 删除播放、/preview、/open-preview 与生成后自动启动预览写进对话区的成功提示,对话区只保留对话内容
- 新增 onRunNotice 通道,工作台壳用 RunNoticeToast 弹 2.6 秒浮层,同一句连续触发会重新计时
- 运行表现层在预览框上方常驻小字「已载入客户端运行视图:URL」,没有活预览时不占位
- 更新 appSurface 四处用例,新增 onRunNotice、小字与 RunNoticeToast 用例
- 同步 check-config 与 check-native-shells 的字符串判据,并记录决策到 shared-memory
2026-09-22 10:00:38 +08:00
suzmii cc8c03a40d 运行游戏不再往聊天写"运行通过"提示
- 删掉 AGC 播放/运行本地游戏成功后的聊天提示:它每次播放都会堆在对话底部遮挡运行画面,成功与否由客户端运行视图本身体现,失败仍照原样回报
- appSurface 三处用例改为断言 start_local_game_preview 真被调用且聊天里不再出现该提示
- 补排障记录:本地 node_modules 内置 Codex 原生包过旧会让 AGC build script panic,需在仓库根目录 npm ci
2026-09-21 19:08:43 +08:00
kdletters 5ac7f65c92 合并 master 并保留游戏分发功能
- 合并 origin/master(含 429f991bd 定向回滚、Godot 原生绑定迁移与策划 Agent 模型控件)。
- 回滚提交删除的游戏分发代码按功能面补回:SpacetimeDB 模块注册与迁移表、api-server 路由与权限映射、后台审核页、AGC 发布命令与 payload 类型、SelectionStage 的 games 阶段、ProjectSupervisorView 的 overlay 挂载点、主站路由与标题、Cargo 依赖与锁、vite/vitest 代理与 tailwind source。
- 保留 master 的 Godot C++ 迁移(移除旧 vendored GDExtension、新的 composer 控件布局)与其它 master 侧改动。
- 修正 master 自带的红灯断言:/api/creation-entry 已被 api-server 列入退役路由,dev 中间件按退役路径 404,测试改为断言不进入代理。
- 验证:cargo check(api-server / spacetime-module / spacetime-client / AGC 壳)、cargo test(api-server 游戏分发 17、module-game-distribution 11、AGC 本地导出 7)、全量 vitest 4557 passed、两端 typecheck、eslint、rustfmt、编码、文档索引与 SpacetimeDB schema guard。
2026-09-21 00:20:45 +08:00
kdletters 2f0e2a48a3 把游戏分发客户端纳入 ESLint 边界
- `.eslintrc.cjs`:`src/services/gameDistributionClient.ts` 及其测试加入活跃服务白名单,不再被遗留忽略列表吞掉。
- 客户端:按仓库规则修掉排序与未使用类型导入(顺带移除两个只在 re-export 中出现的多余导入)。
- 修复 `scripts/vite-retired-css-plugin.test.ts` 的边界门禁:活跃模块不得停留在 ESLint 忽略名单里。
2026-09-20 22:56:43 +08:00
kdletters 00b96f244d 补齐 AGC 素材直传的错误文案与能力作用域验证
- AGC:直传对象存储抛错(能力作用域拒绝、网络不可达、请求被取消)时统一转成中文可操作文案,不再把英文插件错误原样暴露给作者。
- 测试:新增直传抛错的单测,确认失败时不再登记素材。
- 文档:记录用 tauri-plugin-http 同一套 urlpattern 逻辑验证 `https://*.aliyuncs.com/*` 命中 dev/生产 OSS 桶且拒绝无关主机,并说明桌面端真实执行仍需在装有 AGC 的机器上补跑。
2026-09-20 22:49:56 +08:00
kdletters 0dde6ee501 打磨封面与截图的文件选择交互
- 网页发布页:封面与截图改用透明 input 覆盖自定义胶囊按钮,点击命中原生控件;上传后不再出现「未选择任何文件」与「已选择」互相矛盾的状态。
- AGC 发布面板:同一处理,按钮文案随状态在「选择封面图片 / 更换封面图片」「添加截图 / 截图已满 6 张」之间切换。
- 文档:实施计划补充真实浏览器发布链路证据(网页上传封面 → my-games 待审 → 版本 frozenMetadata 指向本次上传素材)与交互打磨记录。
2026-09-20 22:45:50 +08:00
kdletters 4a91e5865a 创建游戏同步校验封面与截图素材归属
- 服务端:抽出 `resolve_owned_game_media`,创建游戏与创建版本复用同一素材校验;不存在的素材返回 400,借用他人素材返回 403,避免游戏行先落一个无效素材 ID。
- 脚本:新增 `npm run check:game-distribution-media-e2e`(`scripts/check-game-distribution-media-e2e.mjs`),在本地 dev 栈上跑真实素材直传、冻结、审核生效、匿名换签读与发行网关的可重复检查。
- 文档:后端数据契约补充创建游戏的素材复核口径,实施计划记录真实本地栈 27 项媒体验证与真实浏览器封面/截图展示证据。
2026-09-20 22:34:33 +08:00
kdletters 4a2b270714 补齐游戏分发封面与截图的冻结、上传与展示
- 服务端:游戏表追加可空 cover_object_key / screenshots_json,版本表追加可空 metadata_json,创建版本时校验必需封面、最多 6 张截图与图片素材归属,并从素材记录派生对象键生成冻结资料。
- 服务端:审核通过时把冻结资料整体生效到游戏行;只有已公开且存在有效活动版本的游戏,其封面/截图素材才在 /api/assets/read-url 获得匿名读授权。
- 服务端:作者与管理员回读版本时追加 frozenMetadata,带回封面与截图素材 ID 供续发复用,公开投影仍只暴露对象键。
- 网页:/games/publish 新增「封面与截图」区,复用平台图片直传与 confirm 通道,缺封面或截图超量在发请求前拦截,更新版本时按冻结资料回填且不要求重新上传。
- 网页:游戏广场卡片与详情页 hero 换签展示真实封面,详情页新增可点击的截图缩略图条,无素材或换签失败时静默回退原有渐变占位。
- AGC:发布面板支持封面必选与截图最多 6 张,新增素材直传服务并在创建游戏前拦截缺封面,同一文件重复提交复用素材 ID。
- AGC:http 能力作用域新增阿里云 OSS 直传地址,客户端同时校验直传目标必须是平台素材存储。
- 契约与文档:同步 shared-contracts、packages/shared 契约,更新 SpacetimeDB 迁移注释、后端架构、玩法链路与实施计划文档,并补齐网页端与 AGC 定向测试。
2026-09-20 22:19:50 +08:00
kdletters 789aef4ce5 补齐游戏分发结构化可观测事件
- api-server 游戏分发模块新增 package_confirmed/package_rejected/version_submitted/version_cancelled 事件,带 request_id、game_id、version_id、耗时与体积/文件数(不记录凭据、URL 与文件内容)
- 新增 review_backlog_listed(审核积压)、review_decided(审核结论与公开修订号)、game_unpublished/game_suspended(撤销传播)与 publish_switch_blocked/unavailable(开关拦截)事件
- 发行网关拒绝路径补 debug 级 release_rejected(cookie、扩展名、未公开、非公开版本)便于排查发行 4xx
- 运维文档给出可直接 grep 的事件名与关键字段,实施计划记录真实栈复跑证据
2026-09-20 21:30:32 +08:00
kdletters 7ff6e46774 修复分支上的格式与导入排序门禁问题
- 用 rustfmt 修正 AGC Tauri 导出包测试的断言排版
- 按 simple-import-sort 修正 AGC App.tsx 导入顺序与 packages/shared 导出顺序
2026-09-20 21:17:27 +08:00
kdletters 2254831db5 补充游戏内真实触屏输入的运行时证据
- 用 CDP Input.dispatchTouchEvent 向游玩页 iframe 派发真实 touchStart/touchEnd,沙箱内探测包收到 touchstart/touchend/pointerdown(pointerType=touch)/click 并命中目标元素
- 记录触摸模拟必须在 iframe 文档创建前启用的踩坑口径
2026-09-20 21:11:37 +08:00
kdletters 64425e8d1a Merge commit 'a2acfa9a6' into codex/game-distribution 2026-09-20 20:59:58 +08:00
kdletters 328ac31844 恢复游戏分发完整实现(特性分支)
- 主站:游戏广场、详情、在线游玩、网页发布与作者中心,以及共享契约与客户端服务
- 后端:module-game-distribution 领域层、SpacetimeDB 表/迁移/绑定、spacetime-client facade、api-server 路由与发行网关
- 后台:游戏审核页(待审列表、通过/拒绝、安全下架)
- AGC:发布面板、本地导出包读取命令与发布服务,含默认跳过的真实链路测试
- 运维:发行来源 nginx 模板与门禁、game-distribution:publish 灰度发布开关、OSS PutObject 受控重试
- 文档:主规范、里程碑与实施计划、决策日志与踩坑记录
2026-09-20 20:49:42 +08:00
357 changed files with 36488 additions and 4736 deletions
@@ -102,7 +102,7 @@ Use OpenAPI as the final authority; these common values are a routing aid:
- Image `kind`: `spec`, `character`, `quick-edit`, `ui-design`, `publication-material`; ordinary image generation may omit it.
- External v1 currently has no structured game-scene generation operation. Do not send `kind: "scene"` or `assetKind: "scene"` through generic image generation; the server rejects both before queueing.
- Image `model`: `gpt-image-2.5`, `gemini-3.1-flash-image-preview`, `nanobanana2`, `nano-banana`. Persisted `gpt-image-2` is a legacy value resolved only when submitting a new task; the retired `gpt-image-2-c` is no longer accepted and is handled as an unsupported value.
- Image `model`: `gpt-image-2`, `gemini-3.1-flash-image-preview`, `nanobanana2`, `nano-banana`.
- Image `aspectRatio`: `1:1`, `2:3`, `3:2`, `9:16`, `16:9`.
- Image `imageSize`: `0.5K`, `1K`, `2K`.
- Video `model`: `seedance2.0`, `seedance2.0-fast`, `kling3.0`, `kling3.0-omni`, `veo3.1`, `veo3.1-fast`.
+14 -22
View File
@@ -1,17 +1,11 @@
---
name: gpt-image-2-apimart
description: Generate or inspect project image assets through this repository's Tiantoken GPT Image 2.5 workflow. Use when Codex needs to create puzzle template sample images, reproduce the server-rs image request body, dry-run image prompts, batch-generate local project thumbnails, or debug TIANTOKEN_BASE_URL / TIANTOKEN_API_KEY image-generation configuration without exposing secrets. The directory name is historical.
description: Generate or inspect project image assets through this repository's VectorEngine gpt-image-2 workflow with gpt-image-2-c fallback. Use when Codex needs to create puzzle template sample images, reproduce the server-rs image request body, dry-run image prompts, batch-generate local project thumbnails, or debug VECTOR_ENGINE_BASE_URL / VECTOR_ENGINE_API_KEY image-generation configuration without exposing secrets. The directory name is historical.
---
# GPT Image 2.5 project image workflow
# gpt-image-2 VectorEngine
Use this skill for project-local image asset generation that must match the repository's image request contract. Provider routing is owned by `server-rs`; when this skill talks to the provider directly it must send the concrete provider model and the matching provider credentials, because the provider side only accepts concrete models:
- generation (no reference images): `gpt-image-2.5-flare-c` through Tiantoken
- edits (any reference image): `gpt-image-2.5-sunburst-c` through Tiantoken
- nanobanana (`gemini-3.1-flash-image-preview`) stays on VectorEngine
The business model name `gpt-image-2.5` is resolved to a concrete key by `server-rs` at the task boundary and is never sent to a provider directly. This client must not perform a cross-model fallback; the only retry the bundled scripts do is a same-model retry on transient upstream statuses (`408` / `429` / `5xx`, 1s/2s backoff, at most 3 attempts), which never changes the model. The folder still contains `apimart` in its name for compatibility with existing local plugin references.
Use this skill for project-local image asset generation that must match the repository's `server-rs` VectorEngine image path. Keep the product/price model identifier and primary provider request as `gpt-image-2`, then fall back once to `gpt-image-2-c` for eligible provider failures. The folder still contains `apimart` in its name for compatibility with existing local plugin references.
## Workflow
@@ -30,15 +24,15 @@ The business model name `gpt-image-2.5` is resolved to a concrete key by `server
```
5. Save final project assets under `public/` or another explicitly requested workspace path.
6. Never print `TIANTOKEN_API_KEY`. Report only whether configuration exists.
6. Never print `VECTOR_ENGINE_API_KEY`. Report only whether configuration exists.
## Request Contract
The repository image path uses:
```text
POST {TIANTOKEN_BASE_URL}/v1/images/generations
Authorization: Bearer {TIANTOKEN_API_KEY}
POST {VECTOR_ENGINE_BASE_URL}/v1/images/generations
Authorization: Bearer {VECTOR_ENGINE_API_KEY}
Content-Type: application/json
```
@@ -46,7 +40,7 @@ Default body:
```json
{
"model": "gpt-image-2.5-flare-c",
"model": "gpt-image-2",
"prompt": "<prompt>",
"n": 1,
"size": "1024x1024"
@@ -56,22 +50,22 @@ Default body:
For visual references, use the edit endpoint instead of the create endpoint:
```text
POST {TIANTOKEN_BASE_URL}/v1/images/edits
Authorization: Bearer {TIANTOKEN_API_KEY}
POST {VECTOR_ENGINE_BASE_URL}/v1/images/edits
Authorization: Bearer {VECTOR_ENGINE_API_KEY}
Content-Type: multipart/form-data
```
Multipart fields:
```text
model=gpt-image-2.5-sunburst-c
model=gpt-image-2
prompt=<prompt>
n=1
size=1024x1024
image=@reference.png
```
In this repository, calls with no reference images use `POST /v1/images/generations` with `gpt-image-2.5-flare-c`; calls with any reference image use `POST /v1/images/edits` with `gpt-image-2.5-sunburst-c` and pass references as one or more `image` form parts. Server-side calls still submit the business model `gpt-image-2.5` and let `server-rs` resolve the concrete key; provider routing and retry policy remain server-owned, and pricing follows the same reference-image rule (generation with a reference image is charged at the edit tier). Match3D container UI generation embeds `public/match3d-background-references/pot-fused-reference.png` into the edit request as an `image` part.
In this repository, calls with no reference images use `POST /v1/images/generations`; calls with any reference image use `POST /v1/images/edits` and pass references as one or more `image` form parts. Both paths prefer `gpt-image-2`; on an eligible upstream/model failure they retry with `gpt-image-2-c`. Do not fall back for authentication, local validation, request-budget exhaustion, uncertain send/connection failure, content-safety rejection, or a generated image URL download failure. Match3D container UI generation embeds `public/match3d-background-references/pot-fused-reference.png` into the edit request as an `image` part.
Accept image output from `data[].url`, `data[].b64_json`, or direct nested `url` fields. VectorEngine image generation currently returns synchronously; do not poll APIMart task endpoints.
@@ -81,14 +75,12 @@ Load environment values from process env first, then `.env.secrets.local`, `.env
Required for live generation:
- `TIANTOKEN_BASE_URL`
- `TIANTOKEN_API_KEY`
- `VECTOR_ENGINE_BASE_URL`
- `VECTOR_ENGINE_API_KEY`
Optional:
- `TIANTOKEN_IMAGE_REQUEST_TIMEOUT_MS`
`VECTOR_ENGINE_*` values no longer serve GPT Image 2.5: those credentials belong to VectorEngine, which only serves nanobanana. Do not fall back from `TIANTOKEN_*` to `VECTOR_ENGINE_*`.
- `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`
If the key or base URL is missing, stop after dry-run or explain the missing configuration. Do not ask the user to paste the key in chat.
@@ -9,9 +9,8 @@ const skillRoot = path.resolve(__dirname, '..');
const repoRoot = path.resolve(skillRoot, '..', '..', '..');
const defaultOutDir = path.join(repoRoot, 'public', 'anthro-cat-illustrations');
const defaultTimeoutMs = 1000000;
// GPT Image 2.5 生成任务只接受 concrete provider model;业务模型名 `gpt-image-2.5`
// 由 server-rs 在任务边界解析,脚本直连 provider 时必须自己给出 concrete key。
const imageModel = 'gpt-image-2.5-flare-c';
const preferredImageModel = 'gpt-image-2';
const fallbackImageModel = 'gpt-image-2-c';
const prompts = [
{
@@ -102,18 +101,18 @@ function resolveEnv() {
...process.env,
};
return {
baseUrl: String(loaded.TIANTOKEN_BASE_URL || '')
baseUrl: String(loaded.VECTOR_ENGINE_BASE_URL || '')
.trim()
.replace(/\/+$/u, ''),
apiKey: String(loaded.TIANTOKEN_API_KEY || '').trim(),
apiKey: String(loaded.VECTOR_ENGINE_API_KEY || '').trim(),
timeoutMs: Number.parseInt(
String(loaded.TIANTOKEN_IMAGE_REQUEST_TIMEOUT_MS || defaultTimeoutMs),
String(loaded.VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS || defaultTimeoutMs),
10,
),
};
}
function buildTiantokenImagesGenerationUrl(baseUrl) {
function buildVectorEngineImagesGenerationUrl(baseUrl) {
return baseUrl.endsWith('/v1')
? `${baseUrl}/images/generations`
: `${baseUrl}/v1/images/generations`;
@@ -223,15 +222,6 @@ function inferExtensionFromBytes(bytes) {
return null;
}
// Tiantoken 偶发 408/429/5xx 时重试同一个 concrete model;跨模型回退仍由
// server-rs 负责,脚本不做任何模型替换。
const transientRetryMaxAttempts = 3;
const transientRetryBaseDelayMs = 1000;
function isTransientProviderStatus(status) {
return status === 408 || status === 429 || status >= 500;
}
async function fetchJson(url, options, timeoutMs) {
const abortController = new AbortController();
const timer = setTimeout(() => abortController.abort(), timeoutMs);
@@ -243,21 +233,22 @@ async function fetchJson(url, options, timeoutMs) {
const text = await response.text();
if (!response.ok) {
const error = new Error(
`Tiantoken ${response.status}: ${text.slice(0, 600)}`,
`VectorEngine ${response.status}: ${text.slice(0, 600)}`,
);
error.providerStatus = response.status;
error.vectorEngineStatus = response.status;
error.vectorEngineBody = text;
throw error;
}
try {
return JSON.parse(text);
} catch (error) {
throw new Error(
`Tiantoken 返回无法解析的响应体:${error.message};响应片段:${text.slice(0, 600)}`,
);
error.vectorEngineResponseParse = true;
error.vectorEngineBody = text;
throw error;
}
} catch (error) {
if (error?.name === 'AbortError') {
throw new Error(`Tiantoken request timed out after ${timeoutMs}ms`);
throw new Error(`VectorEngine request timed out after ${timeoutMs}ms`);
}
throw error;
} finally {
@@ -265,55 +256,81 @@ async function fetchJson(url, options, timeoutMs) {
}
}
async function fetchJsonWithRetry(url, options, timeoutMs, requestId) {
for (let attempt = 1; ; attempt += 1) {
try {
return await fetchJson(url, options, timeoutMs);
} catch (error) {
if (
attempt >= transientRetryMaxAttempts ||
!isTransientProviderStatus(error?.providerStatus)
) {
throw error;
}
const delayMs = transientRetryBaseDelayMs * 2 ** (attempt - 1);
console.warn(
`Tiantoken ${error.providerStatus} on ${requestId}, retrying the same model in ${delayMs}ms (attempt ${attempt + 1}/${transientRetryMaxAttempts})`,
);
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
function shouldFallbackImageModel(error) {
const raw =
`${error?.message || ''}\n${error?.vectorEngineBody || ''}`.toLowerCase();
if (error?.vectorEngineResponseParse) {
return !containsContentRejection(raw);
}
const status = Number(error?.vectorEngineStatus || 0);
if (status === 408 || status >= 500) {
return true;
}
if (status === 429) {
return !containsContentRejection(raw);
}
const mentionsImageModel =
raw.includes('model') ||
raw.includes('模型') ||
raw.includes(preferredImageModel) ||
raw.includes(fallbackImageModel);
return (
[400, 404, 422].includes(status) &&
mentionsImageModel &&
/(not found|not supported|unsupported|unavailable|does not exist|invalid model|unknown model|不存在|不支持|不可用|未开通)/u.test(
raw,
)
);
}
function containsContentRejection(raw) {
return /(invalid_prompt|safety|content[_ ]policy|moderation|prompt rejected|content rejected|prompt refusal|content refusal|rejected by safety|rejected by moderation|敏感|违规|安全策略|内容审核|提示词拒绝|内容拒绝)/u.test(
raw,
);
}
async function requestImagePayload(env, entry) {
const model = imageModel;
const requestBody = {
model,
prompt: buildPrompt(entry),
n: 1,
size: '1024x1024',
};
const payload = await fetchJsonWithRetry(
buildTiantokenImagesGenerationUrl(env.baseUrl),
{
method: 'POST',
headers: {
Authorization: `Bearer ${env.apiKey}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
},
env.timeoutMs,
entry.id,
);
const base64Image = decodeStrictBase64Image(extractBase64Images(payload)[0]);
if (extractImageUrls(payload)[0] || base64Image) {
return payload;
for (const model of [preferredImageModel, fallbackImageModel]) {
const requestBody = {
model,
prompt: buildPrompt(entry),
n: 1,
size: '1024x1024',
};
try {
const payload = await fetchJson(
buildVectorEngineImagesGenerationUrl(env.baseUrl),
{
method: 'POST',
headers: {
Authorization: `Bearer ${env.apiKey}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
},
env.timeoutMs,
);
const base64Image = decodeStrictBase64Image(
extractBase64Images(payload)[0],
);
if (extractImageUrls(payload)[0] || base64Image) {
return payload;
}
const error = new Error(`VectorEngine returned no image for ${entry.id}`);
error.vectorEngineResponseParse = true;
error.vectorEngineBody = JSON.stringify(payload).slice(0, 600);
throw error;
} catch (error) {
if (model !== preferredImageModel || !shouldFallbackImageModel(error)) {
throw error;
}
console.warn(
`VectorEngine ${preferredImageModel} failed, retrying with ${fallbackImageModel}: ${error.message}`,
);
}
}
throw new Error(
`Tiantoken returned no image for ${entry.id}${JSON.stringify(payload).slice(0, 600)}`,
);
throw new Error(`VectorEngine returned no image for ${entry.id}`);
}
async function downloadUrl(url, timeoutMs) {
@@ -356,7 +373,7 @@ async function generateOne(env, entry, outDir) {
const bytes = decodeStrictBase64Image(b64Images[0]);
if (!bytes) {
throw new Error(
`Tiantoken returned invalid base64 image for ${entry.id}`,
`VectorEngine returned invalid base64 image for ${entry.id}`,
);
}
image = {
@@ -364,7 +381,7 @@ async function generateOne(env, entry, outDir) {
extension: inferExtensionFromBytes(bytes),
};
} else {
throw new Error(`Tiantoken returned no image for ${entry.id}`);
throw new Error(`VectorEngine returned no image for ${entry.id}`);
}
mkdirSync(outDir, { recursive: true });
@@ -391,8 +408,9 @@ if (dryRun) {
requests: selectedPrompts.map((entry) => ({
id: entry.id,
title: entry.title,
fallbackModel: fallbackImageModel,
body: {
model: imageModel,
model: preferredImageModel,
prompt: buildPrompt(entry),
n: 1,
size: '1024x1024',
@@ -411,7 +429,7 @@ if (!env.baseUrl || !env.apiKey) {
console.error(
JSON.stringify({
ok: false,
error: 'Missing TIANTOKEN_BASE_URL or TIANTOKEN_API_KEY',
error: 'Missing VECTOR_ENGINE_BASE_URL or VECTOR_ENGINE_API_KEY',
hasBaseUrl: Boolean(env.baseUrl),
hasApiKey: Boolean(env.apiKey),
}),
@@ -18,9 +18,8 @@ const defaultOutDir = path.join(
'puzzle-creation-templates',
);
const defaultTimeoutMs = 1000000;
// GPT Image 2.5 生成任务只接受 concrete provider model;业务模型名 `gpt-image-2.5`
// 由 server-rs 在任务边界解析,脚本直连 provider 时必须自己给出 concrete key。
const imageModel = 'gpt-image-2.5-flare-c';
const preferredImageModel = 'gpt-image-2';
const fallbackImageModel = 'gpt-image-2-c';
const args = new Map();
for (let index = 2; index < process.argv.length; index += 1) {
@@ -72,18 +71,18 @@ function resolveEnv() {
...process.env,
};
return {
baseUrl: String(loaded.TIANTOKEN_BASE_URL || '')
baseUrl: String(loaded.VECTOR_ENGINE_BASE_URL || '')
.trim()
.replace(/\/+$/u, ''),
apiKey: String(loaded.TIANTOKEN_API_KEY || '').trim(),
apiKey: String(loaded.VECTOR_ENGINE_API_KEY || '').trim(),
timeoutMs: Number.parseInt(
String(loaded.TIANTOKEN_IMAGE_REQUEST_TIMEOUT_MS || defaultTimeoutMs),
String(loaded.VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS || defaultTimeoutMs),
10,
),
};
}
function buildTiantokenImagesGenerationUrl(baseUrl) {
function buildVectorEngineImagesGenerationUrl(baseUrl) {
return baseUrl.endsWith('/v1')
? `${baseUrl}/images/generations`
: `${baseUrl}/v1/images/generations`;
@@ -193,15 +192,6 @@ function inferExtensionFromBytes(bytes) {
return null;
}
// Tiantoken 偶发 408/429/5xx 时重试同一个 concrete model;跨模型回退仍由
// server-rs 负责,脚本不做任何模型替换。
const transientRetryMaxAttempts = 3;
const transientRetryBaseDelayMs = 1000;
function isTransientProviderStatus(status) {
return status === 408 || status === 429 || status >= 500;
}
async function fetchJson(url, options, timeoutMs) {
const abortController = new AbortController();
const timer = setTimeout(() => abortController.abort(), timeoutMs);
@@ -213,21 +203,22 @@ async function fetchJson(url, options, timeoutMs) {
const text = await response.text();
if (!response.ok) {
const error = new Error(
`Tiantoken ${response.status}: ${text.slice(0, 600)}`,
`VectorEngine ${response.status}: ${text.slice(0, 600)}`,
);
error.providerStatus = response.status;
error.vectorEngineStatus = response.status;
error.vectorEngineBody = text;
throw error;
}
try {
return JSON.parse(text);
} catch (error) {
throw new Error(
`Tiantoken 返回无法解析的响应体:${error.message};响应片段:${text.slice(0, 600)}`,
);
error.vectorEngineResponseParse = true;
error.vectorEngineBody = text;
throw error;
}
} catch (error) {
if (error?.name === 'AbortError') {
throw new Error(`Tiantoken request timed out after ${timeoutMs}ms`);
throw new Error(`VectorEngine request timed out after ${timeoutMs}ms`);
}
throw error;
} finally {
@@ -235,55 +226,83 @@ async function fetchJson(url, options, timeoutMs) {
}
}
async function fetchJsonWithRetry(url, options, timeoutMs, requestId) {
for (let attempt = 1; ; attempt += 1) {
try {
return await fetchJson(url, options, timeoutMs);
} catch (error) {
if (
attempt >= transientRetryMaxAttempts ||
!isTransientProviderStatus(error?.providerStatus)
) {
throw error;
}
const delayMs = transientRetryBaseDelayMs * 2 ** (attempt - 1);
console.warn(
`Tiantoken ${error.providerStatus} on ${requestId}, retrying the same model in ${delayMs}ms (attempt ${attempt + 1}/${transientRetryMaxAttempts})`,
);
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
function shouldFallbackImageModel(error) {
const raw =
`${error?.message || ''}\n${error?.vectorEngineBody || ''}`.toLowerCase();
if (error?.vectorEngineResponseParse) {
return !containsContentRejection(raw);
}
const status = Number(error?.vectorEngineStatus || 0);
if (status === 408 || status >= 500) {
return true;
}
if (status === 429) {
return !containsContentRejection(raw);
}
const mentionsImageModel =
raw.includes('model') ||
raw.includes('模型') ||
raw.includes(preferredImageModel) ||
raw.includes(fallbackImageModel);
return (
[400, 404, 422].includes(status) &&
mentionsImageModel &&
/(not found|not supported|unsupported|unavailable|does not exist|invalid model|unknown model|不存在|不支持|不可用|未开通)/u.test(
raw,
)
);
}
function containsContentRejection(raw) {
return /(invalid_prompt|safety|content[_ ]policy|moderation|prompt rejected|content rejected|prompt refusal|content refusal|rejected by safety|rejected by moderation|敏感|违规|安全策略|内容审核|提示词拒绝|内容拒绝)/u.test(
raw,
);
}
async function requestImagePayload(env, template) {
const model = imageModel;
const requestBody = {
model,
prompt: buildPrompt(template),
n: 1,
size: '1024x1024',
};
const payload = await fetchJsonWithRetry(
buildTiantokenImagesGenerationUrl(env.baseUrl),
{
method: 'POST',
headers: {
Authorization: `Bearer ${env.apiKey}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
},
env.timeoutMs,
template.id,
);
const base64Image = decodeStrictBase64Image(extractBase64Images(payload)[0]);
if (extractImageUrls(payload)[0] || base64Image) {
return payload;
for (const model of [preferredImageModel, fallbackImageModel]) {
const requestBody = {
model,
prompt: buildPrompt(template),
n: 1,
size: '1024x1024',
};
try {
const payload = await fetchJson(
buildVectorEngineImagesGenerationUrl(env.baseUrl),
{
method: 'POST',
headers: {
Authorization: `Bearer ${env.apiKey}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
},
env.timeoutMs,
);
const base64Image = decodeStrictBase64Image(
extractBase64Images(payload)[0],
);
if (extractImageUrls(payload)[0] || base64Image) {
return payload;
}
const error = new Error(
`VectorEngine returned no image for ${template.id}`,
);
error.vectorEngineResponseParse = true;
error.vectorEngineBody = JSON.stringify(payload).slice(0, 600);
throw error;
} catch (error) {
if (model !== preferredImageModel || !shouldFallbackImageModel(error)) {
throw error;
}
console.warn(
`VectorEngine ${preferredImageModel} failed, retrying with ${fallbackImageModel}: ${error.message}`,
);
}
}
throw new Error(
`Tiantoken returned no image for ${template.id}${JSON.stringify(payload).slice(0, 600)}`,
);
throw new Error(`VectorEngine returned no image for ${template.id}`);
}
async function downloadUrl(url, timeoutMs) {
@@ -326,7 +345,7 @@ async function generateOne(env, template, outDir) {
const bytes = decodeStrictBase64Image(b64Images[0]);
if (!bytes) {
throw new Error(
`Tiantoken returned invalid base64 image for ${template.id}`,
`VectorEngine returned invalid base64 image for ${template.id}`,
);
}
image = {
@@ -334,7 +353,7 @@ async function generateOne(env, template, outDir) {
extension: inferExtensionFromBytes(bytes),
};
} else {
throw new Error(`Tiantoken returned no image for ${template.id}`);
throw new Error(`VectorEngine returned no image for ${template.id}`);
}
mkdirSync(outDir, { recursive: true });
@@ -365,8 +384,9 @@ if (dryRun) {
requests: selectedTemplates.map((template) => ({
id: template.id,
title: template.title,
fallbackModel: fallbackImageModel,
body: {
model: imageModel,
model: preferredImageModel,
prompt: buildPrompt(template),
n: 1,
size: '1024x1024',
@@ -385,7 +405,7 @@ if (!env.baseUrl || !env.apiKey) {
console.error(
JSON.stringify({
ok: false,
error: 'Missing TIANTOKEN_BASE_URL or TIANTOKEN_API_KEY',
error: 'Missing VECTOR_ENGINE_BASE_URL or VECTOR_ENGINE_API_KEY',
hasBaseUrl: Boolean(env.baseUrl),
hasApiKey: Boolean(env.apiKey),
}),
+2
View File
@@ -213,6 +213,8 @@ module.exports = {
'!src/services/clipboard.test.ts',
'!src/services/frontendRuntimeConfigService.ts',
'!src/services/frontendRuntimeConfigService.test.ts',
'!src/services/gameDistributionClient.ts',
'!src/services/gameDistributionClient.test.ts',
'!src/services/sseStream.ts',
'!src/services/sseStream.test.ts',
'src/AdventurePanel.tsx',
+41
View File
@@ -98,6 +98,12 @@ jobs:
- name: Report isolated Rust compilation cache
if: always()
run: bash scripts/ci-rust-cache.sh report
- name: Publish master Rust cache artifact
continue-on-error: true
if: ${{ !cancelled() && github.event_name == 'push' && github.ref == 'refs/heads/master' }}
env:
GENARRATIVE_GITEA_TOKEN: ${{ github.token }}
run: python3 scripts/export-gitea-rust-cache.py
ai-game-creator-shell-rust-lane-2:
name: AI game creator shell Rust lane 2/2
@@ -141,6 +147,12 @@ jobs:
- name: Report isolated Rust compilation cache
if: always()
run: bash scripts/ci-rust-cache.sh report
- name: Publish master Rust cache artifact
continue-on-error: true
if: ${{ !cancelled() && github.event_name == 'push' && github.ref == 'refs/heads/master' }}
env:
GENARRATIVE_GITEA_TOKEN: ${{ github.token }}
run: python3 scripts/export-gitea-rust-cache.py
# agent-run smoke 会 spawn `cargo run`(走壳自己的 manifest),同样不装 npm 依赖,
# 单独一个 job,免得把已经压到 4 分钟级的片 job 拖长。
@@ -183,6 +195,12 @@ jobs:
- name: Report isolated Rust compilation cache
if: always()
run: bash scripts/ci-rust-cache.sh report
- name: Publish master Rust cache artifact
continue-on-error: true
if: ${{ !cancelled() && github.event_name == 'push' && github.ref == 'refs/heads/master' }}
env:
GENARRATIVE_GITEA_TOKEN: ${{ github.token }}
run: python3 scripts/export-gitea-rust-cache.py
# AGC 壳依赖的共享 / 平台和编辑器插件 crate 各自预热独立 manifest,再运行对应测试。
ai-game-creator-shell-rust-crates:
@@ -291,6 +309,12 @@ jobs:
- name: Report isolated Rust compilation cache
if: always()
run: bash scripts/ci-rust-cache.sh report
- name: Publish master Rust cache artifact
continue-on-error: true
if: ${{ !cancelled() && github.event_name == 'push' && github.ref == 'refs/heads/master' }}
env:
GENARRATIVE_GITEA_TOKEN: ${{ github.token }}
run: python3 scripts/export-gitea-rust-cache.py
backend-tests:
name: Backend tests
@@ -380,6 +404,12 @@ jobs:
- name: Report isolated Rust compilation cache
if: always()
run: bash scripts/ci-rust-cache.sh report
- name: Publish master Rust cache artifact
continue-on-error: true
if: ${{ !cancelled() && github.event_name == 'push' && github.ref == 'refs/heads/master' }}
env:
GENARRATIVE_GITEA_TOKEN: ${{ github.token }}
run: python3 scripts/export-gitea-rust-cache.py
# 客户端的壳级与契约门禁:静态契约断言、H5 / 微信 / 移动 / 桌面壳运行时门禁,
# 以及依赖发布产物的构建 smoke。
@@ -442,6 +472,12 @@ jobs:
- name: Report isolated Rust compilation cache
if: always()
run: bash scripts/ci-rust-cache.sh report
- name: Publish master Rust cache artifact
continue-on-error: true
if: ${{ !cancelled() && github.event_name == 'push' && github.ref == 'refs/heads/master' }}
env:
GENARRATIVE_GITEA_TOKEN: ${{ github.token }}
run: python3 scripts/export-gitea-rust-cache.py
frontend-tests:
name: Frontend tests
@@ -524,6 +560,11 @@ jobs:
- name: Install npm dependencies
run: bash scripts/ci-npm-ci-with-retry.sh
- name: Validate CI cache maintenance behavior
run: |
python3 -m unittest discover -s scripts -p 'test_gitea_cache_*.py'
node --test scripts/export-ci-npm-download-cache.test.mjs
- name: Run repository checks
run: npm run check:repository-ci
+32 -29
View File
@@ -44,35 +44,6 @@ _Avoid_: 把同一资源的全局元数据和某一次摆放坐标混在同一
由图片生成或图片修改流程产生的画布资源,必须记录来源资源、提示词、实际提示词、模型、provider、任务 ID 和生成时间;本期 `/editor` 的生成修改先允许 mock 生成资源,但仍按生成资源元数据形状保存。
_Avoid_: 无来源的静态素材、只显示在 UI 但不落工程资源记录的生成结果
**图片模型历史值与使用端解析**:
图片资源中已持久化的 `gpt-image-2` 是历史业务事实,读回时保持原值;新任务使用业务模型值 `gpt-image-2.5`。当用户基于历史资源再次发起生成或编辑任务时,服务端只在新任务的使用端把历史值解析为当前业务模型,不改写历史资源。provider route 属于服务端执行与审计边界,前端不接收、不持久化、不展示,也不据此分支。
_Avoid_: 读取数据库时改写历史模型值、把 provider route 暴露为前端模型选项或公开 DTO
**图片 provider 显式路由**:
api-server 在任务入口按业务语义显式选择具体 provider model name(生成或编辑),并把同一具体名传给图片平台适配器和后台定价解析;图片平台适配器不从参考图数量或前端字段猜测任务。具体 provider model name 只存在于服务端调用、定价配置和审计边界。
后台管理 Web/API 是明确例外,可以查看和编辑两个具体定价 key;主站普通前端与公开定价 API 不接收这些 key。
_Avoid_: 让图片适配器隐式猜路由、让主站前端携带 provider model name
**业务模型**:
面向任务与产品契约的稳定模型值;当前 GPT 图片新任务的业务模型是 `gpt-image-2.5`。业务模型不等同于 provider 的具体计费/请求 model,也不暴露 provider 凭证或 endpoint。
_Avoid_: 把 provider concrete model 当作前端业务选项、用业务模型值直接推断 provider 凭证
**具体模型**:
服务端发送请求和定价使用的 concrete model name。GPT Image 2.5 生成与编辑分别是 `gpt-image-2.5-flare-c``gpt-image-2.5-sunburst-c`nanobanana 仍使用 `gemini-3.1-flash-image-preview`。具体模型只在服务端执行、定价和审计边界出现。
_Avoid_: 把具体模型写入普通前端 DTO、让未知字符串自动选择 provider
**provider client**:
按具体模型选出的外部图片 provider 连接配置,包含 provider identity、base URL 和 API keyVectorEngine 与 Tiantoken client 共享图片协议执行器,不复制请求/响应业务逻辑。两套 required client 在 api-server 启动时构造。
_Avoid_: 在首次请求时才创建 client、在 provider client 中复制尺寸/重试/审计逻辑、跨 provider credential fallback
**历史模型值**:
已持久化的 `gpt-image-2` 字符串,只作为历史事实原样读取和审计;基于历史资源提交新任务时,在使用端解析为当前 GPT Image 2.5 业务任务,不回写历史记录,也不把旧值作为现役 provider route。已退役的 `gpt-image-2-c` 已从代码整体删除,只有数据库里的历史审计字符串原样保留,任何入口传入该值都按不支持的值处理。
_Avoid_: 数据库批量改写历史值、把历史值重新路由到 VectorEngine、把兼容解析扩散到普通前端、为已删除的 `gpt-image-2-c` 重新加回常量或解析分支
**GPT Image 2.5 新生成展示名**:
`GPT Image 2.5` 是新生成任务的产品展示名;历史资源与既有编辑上下文不因新模型上线而改写展示语义。
_Avoid_: 把新生成展示名扩散到历史记录、历史生成器或旧编辑上下文
**系列素材图集生成**:
一组同类素材的统一批量生成方式,采用批量规划、sheet 生图、后端切图、透明化、OSS 持久化和局部重生成的通用流水线。
_Avoid_: 为每个玩法单独发明素材流水线、把系列素材建模成任一玩法专属 DTO
@@ -223,6 +194,34 @@ _Avoid_: 进度通知、快照轮询、第二套历史
把项目对话历史条目与运行态事件转换成消息气泡和工具卡片的读取期转换;不持久化,也不构成事实源。
_Avoid_: 投影缓存文件、已脱敏卡片库、第二套 reducer
**引用候选**:
输入区可以命中的对象集合(`@` 素材、`$` Skill),由宿主按种类注入;输入区不判断候选属于哪一类。
_Avoid_: 输入区自己读项目清单或应用目录、把候选取值写死在组件里
**引用 provider**:
一种引用种类向输入区提供的全部能力:触发符、候选、身份解析与正文文本形态;每种引用各一份,宿主按需选择性注入。
_Avoid_: 一个总装对象决定所有种类、输入区按种类分叉、provider 之间互相知道对方
**静默 provider**:
不提供候选、只负责已有引用身份与文本形态的 provider;附件与运行画面区域属于这一类,只能由外部插入或草稿回填进入正文。
_Avoid_: 给附件或运行画面区域造候选、为它们保留输入区内的专门分支
**引用文本语法**:
引用在正文文本里的形态(`@显示名` / `$名称` / `@附件名`)及其反解析;出站与解析必须同一口径,token 前后各留一个空白。
_Avoid_: 出站与解析各写一套、在空白边界之外再补兼容别名、让解析依赖具体种类的字段
**引用输入区**:
只负责编辑与渲染引用的共享输入组件;候选、身份解析与文本语法都来自注入的 provider,它不持有项目清单、不访问后端。
_Avoid_: 输入区自己拉 Skill 目录、把选择器面板塞在输入区内部
**引用选择器**:
宿主渲染的独立面板,自己拿数据与筛选状态,确认后把选中的引用交给输入区的插入缝。
_Avoid_: 输入区自带面板、每个宿主各画一个、绕开插入缝另开第二条通道
**附件芯片**:
聊天附件在正文里的唯一表示;附件导入成功即以芯片进入正文,正文之外不存在第二份附件状态。
_Avoid_: 待发送附件列表与正文芯片并存、提交时再拼一遍附件
## Relationships
- 一个 **汪汪声浪大作战** 单局包含多个 **有效声浪触发**
@@ -238,6 +237,10 @@ _Avoid_: 投影缓存文件、已脱敏卡片库、第二套 reducer
- **个人历史成绩** 由最近记录列表和个人最佳摘要组成,只允许本人查看;排行榜只公开入榜胜利成绩。
- **正式作品入口闭环** 必须覆盖创作入口、作品详情 CTA、广场/作品卡片、我的作品/个人作品架、稳定作品 ID runtime 路由和 `work_play_start` 埋点。
- **Phase 2 实施顺序** 固定为:契约与领域规则 → SpacetimeDB 表/reducer 与 api-server BFF → 最小前端纵切 → 投影与列表体验 → 收口验证。
- **引用输入区** 由宿主注入的若干 **引用 provider** 组成;**引用候选** 与 **引用文本语法** 都来自 provider,输入区不判断引用种类。
- **引用选择器** 不属于 **引用输入区**:它自己拿数据,确认后只通过输入区的插入缝交付引用。
- 只有带触发符的 **引用 provider** 会产生候选;**静默 provider** 没有触发符,只能由外部插入或草稿回填进入正文。
- **附件芯片** 是本轮附件的唯一事实源;附件导入失败时不产生芯片。
## Example dialogue
@@ -7,9 +7,12 @@ import {
getAdminFeatureGateConfig,
getAdminUserDetail,
importAdminAgcTemplates,
listAdminGameDistributionReviews,
listAdminRechargeOrders,
reconcileAdminUserConsumption,
resolveAdminRechargeRefundManualReview,
reviewAdminGameDistributionVersion,
suspendAdminGameDistributionGame,
updateAdminAccount,
updateAdminAgcTemplate,
uploadAdminEditorShowcaseCampaignImage,
@@ -454,3 +457,134 @@ test('退款人工复核使用独立 resolve 管理员路由', async () => {
}),
);
});
test('游戏审核列表与审核动作使用约定的 URL、方法和幂等键', async () => {
const fetchMock = vi.fn().mockImplementation(() =>
Promise.resolve(
new Response(JSON.stringify({ entries: [], nextCursor: null }), {
status: 200,
}),
),
);
vi.stubGlobal('fetch', fetchMock);
await listAdminGameDistributionReviews('admin-token');
await reviewAdminGameDistributionVersion(
'admin-token',
'gamever/1',
'game-review-key-1',
{
decision: 'approve',
expectedPublicationRevision: 3,
entryUrl: 'https://games.example.test/releases/game_1/index.html',
},
);
expect(fetchMock.mock.calls[0]?.[0]).toBe(
'/admin/api/game-distribution/reviews?limit=48',
);
expect(fetchMock.mock.calls[1]?.[0]).toBe(
'/admin/api/game-distribution/versions/gamever%2F1/review',
);
expect(fetchMock.mock.calls[1]?.[1]).toEqual(
expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({
Authorization: 'Bearer admin-token',
'Idempotency-Key': 'game-review-key-1',
}),
body: JSON.stringify({
decision: 'approve',
expectedPublicationRevision: 3,
entryUrl: 'https://games.example.test/releases/game_1/index.html',
}),
}),
);
});
test('安全下架请求携带公开修订号、原因与幂等键', async () => {
const fetchMock = vi.fn().mockImplementation(() =>
Promise.resolve(
new Response(JSON.stringify({ game: {}, replayed: false }), {
status: 200,
}),
),
);
vi.stubGlobal('fetch', fetchMock);
await suspendAdminGameDistributionGame(
'admin-token',
'game/1',
'game-suspend-key-1',
{ expectedPublicationRevision: 7, reason: '版权投诉' },
);
expect(fetchMock.mock.calls[0]?.[0]).toBe(
'/admin/api/game-distribution/games/game%2F1/suspend',
);
expect(fetchMock.mock.calls[0]?.[1]).toEqual(
expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({
Authorization: 'Bearer admin-token',
'Idempotency-Key': 'game-suspend-key-1',
}),
body: JSON.stringify({
expectedPublicationRevision: 7,
reason: '版权投诉',
}),
}),
);
expect(() =>
suspendAdminGameDistributionGame('admin-token', ' ', 'key', {
expectedPublicationRevision: 1,
}),
).toThrow('缺少游戏 ID');
expect(() =>
suspendAdminGameDistributionGame('admin-token', 'game-1', ' ', {
expectedPublicationRevision: 1,
}),
).toThrow('下架幂等键必须是 1 到 128 个字符');
expect(fetchMock).toHaveBeenCalledTimes(1);
});
test('游戏审核拒绝请求携带理由,空幂等键在本地失败关闭', async () => {
const fetchMock = vi.fn().mockImplementation(() =>
Promise.resolve(
new Response(JSON.stringify({ version: {}, replayed: false }), {
status: 200,
}),
),
);
vi.stubGlobal('fetch', fetchMock);
await reviewAdminGameDistributionVersion(
'admin-token',
'version-1',
'game-review-key-2',
{
decision: 'reject',
expectedPublicationRevision: 0,
reviewReason: '运行时报错',
},
);
expect(fetchMock.mock.calls[0]?.[1]).toEqual(
expect.objectContaining({
body: JSON.stringify({
decision: 'reject',
expectedPublicationRevision: 0,
reviewReason: '运行时报错',
}),
}),
);
expect(() =>
reviewAdminGameDistributionVersion('admin-token', 'version-1', ' ', {
decision: 'reject',
expectedPublicationRevision: 0,
reviewReason: 'x',
}),
).toThrow('审核幂等键必须是 1 到 128 个字符');
expect(fetchMock).toHaveBeenCalledTimes(1);
});
+72
View File
@@ -30,6 +30,9 @@ import type {
AdminExternalApiKeyListQuery,
AdminExternalApiKeyListResponse,
AdminFeatureGateConfigResponse,
AdminGameDistributionReviewListResponse,
AdminGameDistributionReviewRequest,
AdminGameDistributionReviewResponse,
AdminImportAgcTemplatesResponse,
AdminLoginResponse,
AdminMeResponse,
@@ -1200,6 +1203,75 @@ export function saveAgcModelCatalog(
);
}
export function listAdminGameDistributionReviews(token: string, limit = 48) {
const normalizedLimit = Number.isFinite(limit)
? Math.min(Math.max(Math.trunc(limit), 1), 48)
: 48;
return request<AdminGameDistributionReviewListResponse>(
`/admin/api/game-distribution/reviews?limit=${normalizedLimit}`,
{ token },
);
}
/**
* 审核游戏发行版本。幂等键由调用方生成并在同一次提交内复用,避免重复点击产生
* 两条审核结论。
*/
/**
* 安全下架整个游戏。管理员下架同样要求 CAS 修订号与幂等键,避免并发审核互相覆盖。
*/
export function suspendAdminGameDistributionGame(
token: string,
gameId: string,
idempotencyKey: string,
payload: import('./adminApiTypes').AdminGameDistributionSuspendRequest,
) {
const normalizedGameId = gameId.trim();
const normalizedKey = idempotencyKey.trim();
if (!normalizedGameId) {
throw new Error('缺少游戏 ID');
}
if (!normalizedKey || normalizedKey.length > 128) {
throw new Error('下架幂等键必须是 1 到 128 个字符');
}
return request<
import('./adminApiTypes').AdminGameDistributionSuspendResponse
>(
`/admin/api/game-distribution/games/${encodeURIComponent(normalizedGameId)}/suspend`,
{
method: 'POST',
token,
headers: { 'Idempotency-Key': normalizedKey },
body: payload,
},
);
}
export function reviewAdminGameDistributionVersion(
token: string,
versionId: string,
idempotencyKey: string,
payload: AdminGameDistributionReviewRequest,
) {
const normalizedVersionId = versionId.trim();
const normalizedKey = idempotencyKey.trim();
if (!normalizedVersionId) {
throw new Error('缺少发行版本 ID');
}
if (!normalizedKey || normalizedKey.length > 128) {
throw new Error('审核幂等键必须是 1 到 128 个字符');
}
return request<AdminGameDistributionReviewResponse>(
`/admin/api/game-distribution/versions/${encodeURIComponent(normalizedVersionId)}/review`,
{
method: 'POST',
token,
headers: { 'Idempotency-Key': normalizedKey },
body: payload,
},
);
}
export function getAdminAgcTemplates(token: string, signal?: AbortSignal) {
return request<AdminAgcTemplateLibraryResponse>('/admin/api/agc-templates', {
token,
+45
View File
@@ -1043,6 +1043,51 @@ export interface AdminAgcModelCatalog {
models: AdminAgcModel[];
}
export interface AdminGameDistributionReviewEntry {
versionId: string;
gameId: string;
versionNumber: number;
packageSha256: string;
packageBytes: number;
status: string;
publicationRevision: number;
reviewReason: string | null;
createdAt: string;
updatedAt: string;
}
export interface AdminGameDistributionReviewListResponse {
entries: AdminGameDistributionReviewEntry[];
nextCursor: string | null;
}
export interface AdminGameDistributionReviewRequest {
decision: 'approve' | 'reject';
expectedPublicationRevision: number;
reviewReason?: string;
entryUrl?: string;
}
export interface AdminGameDistributionReviewResponse {
version: AdminGameDistributionReviewEntry;
replayed: boolean;
}
export interface AdminGameDistributionSuspendRequest {
expectedPublicationRevision: number;
reason?: string;
}
export interface AdminGameDistributionSuspendResponse {
game: {
id: string;
title: string;
status: string;
publicationRevision: number;
};
replayed: boolean;
}
export interface AdminAgcTemplatePayload {
id: string;
title: string;
+7
View File
@@ -27,6 +27,7 @@ import { AdminEditorAssetQueryPage } from '../pages/AdminEditorAssetQueryPage';
import { AdminEditorGenerationPricingPage } from '../pages/AdminEditorGenerationPricingPage';
import { AdminEditorShowcaseReviewPage } from '../pages/AdminEditorShowcaseReviewPage';
import { AdminErrorReportsPage } from '../pages/AdminErrorReportsPage';
import { AdminGameDistributionReviewPage } from '../pages/AdminGameDistributionReviewPage';
import { AdminGrayReleaseConfigPage } from '../pages/AdminGrayReleaseConfigPage';
import { AdminInviteCodePage } from '../pages/AdminInviteCodePage';
import { AdminLoginPage } from '../pages/AdminLoginPage';
@@ -307,6 +308,12 @@ export function AdminApp() {
onUnauthorized={handleUnauthorized}
/>
) : null}
{activeRouteId === 'game-distribution' ? (
<AdminGameDistributionReviewPage
token={token}
onUnauthorized={handleUnauthorized}
/>
) : null}
{activeRouteId === 'editor-assets' ? (
<AdminEditorAssetQueryPage
token={token}
+2
View File
@@ -5,6 +5,7 @@ import {
Coins,
Database,
FolderArchive,
Gamepad2,
GitBranch,
Images,
LayoutDashboard,
@@ -49,6 +50,7 @@ const routeIcons = {
'recharge-orders': ReceiptText,
'editor-generation-pricing': Coins,
'editor-showcase': Star,
'game-distribution': Gamepad2,
'editor-assets': Images,
'project-snapshots': FolderArchive,
accounts: Users,
@@ -148,6 +148,27 @@ test('项目工程入口对 owner 与已授权 member 开放且可分配权限',
).not.toContainEqual(route);
});
test('后台游戏审核路由可通过导航和 hash 访问', () => {
expect(adminRoutes).toContainEqual({
id: 'game-distribution',
label: '游戏审核',
hash: '#game-distribution',
});
expect(resolveAdminRoute('#game-distribution')).toBe('game-distribution');
expect(routeHash('game-distribution')).toBe('#game-distribution');
});
test('member 可单独获得游戏审核 Tab 权限', () => {
const routes = getAccessibleAdminRoutes({
accountRole: 'member',
tabPermissions: ['game-distribution'],
});
expect(routes.map((route) => route.id)).toEqual(['game-distribution']);
expect(resolveAccessibleAdminRoute('#game-distribution', routes)).toBe(
'game-distribution',
);
});
test('模板管理只对 owner 或具有 agc-templates 权限的 member 可见', () => {
expect(adminRoutes).toContainEqual({
id: 'agc-templates',
+2
View File
@@ -15,6 +15,7 @@ export type AdminRouteId =
| 'recharge-orders'
| 'editor-generation-pricing'
| 'editor-showcase'
| 'game-distribution'
| 'editor-assets'
| 'project-snapshots'
| 'agc-models'
@@ -56,6 +57,7 @@ export const adminRoutes: AdminRouteDefinition[] = [
{ id: 'agc-models', label: 'AGC 模型', hash: '#agc-models', ownerOnly: true },
{ id: 'agc-templates', label: '模板管理', hash: '#agc-templates' },
{ id: 'editor-showcase', label: '精选审核', hash: '#editor-showcase' },
{ id: 'game-distribution', label: '游戏审核', hash: '#game-distribution' },
{ id: 'editor-assets', label: '素材查询', hash: '#editor-assets' },
{ id: 'project-snapshots', label: '项目工程', hash: '#project-snapshots' },
{ id: 'accounts', label: '账号管理', hash: '#accounts', ownerOnly: true },
@@ -0,0 +1,186 @@
/* @vitest-environment jsdom */
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { beforeEach, expect, test, vi } from 'vitest';
import {
listAdminGameDistributionReviews,
reviewAdminGameDistributionVersion,
suspendAdminGameDistributionGame,
} from '../api/adminApiClient';
import type { AdminGameDistributionReviewEntry } from '../api/adminApiTypes';
import {
AdminGameDistributionReviewPage,
resolveGameReleaseEntryUrlError,
} from './AdminGameDistributionReviewPage';
vi.mock('../api/adminApiClient', () => ({
isAdminApiError: vi.fn(
(error: unknown) =>
typeof error === 'object' &&
error !== null &&
'status' in error &&
typeof error.status === 'number',
),
formatAdminApiError: vi.fn((error: unknown) =>
error instanceof Error ? error.message : '请求失败',
),
listAdminGameDistributionReviews: vi.fn(),
reviewAdminGameDistributionVersion: vi.fn(),
suspendAdminGameDistributionGame: vi.fn(),
}));
const entry: AdminGameDistributionReviewEntry = {
versionId: 'version-1',
gameId: 'game_1',
versionNumber: 2,
packageSha256: 'a'.repeat(64),
packageBytes: 2048,
status: 'pending_review',
publicationRevision: 4,
reviewReason: null,
createdAt: '2026-09-20T08:00:00Z',
updatedAt: '2026-09-20T08:00:00Z',
};
beforeEach(() => {
vi.mocked(listAdminGameDistributionReviews).mockReset();
vi.mocked(reviewAdminGameDistributionVersion).mockReset();
vi.mocked(suspendAdminGameDistributionGame).mockReset();
vi.mocked(listAdminGameDistributionReviews).mockResolvedValue({
entries: [entry],
nextCursor: null,
});
});
test('发行入口必须是带完整来源的 HTTPS 地址', () => {
expect(resolveGameReleaseEntryUrlError('')).toBe('请填写发行入口');
expect(
resolveGameReleaseEntryUrlError('http://games.test/a/index.html'),
).toBe('发行入口必须以 https:// 开头');
expect(
resolveGameReleaseEntryUrlError('https://games.test/a/index.html?token=1'),
).toBe('发行入口不能包含 query 或 fragment');
expect(
resolveGameReleaseEntryUrlError('https://u:p@games.test/a/index.html'),
).toBe('发行入口不能包含凭据');
expect(
resolveGameReleaseEntryUrlError('https://games.test/a/index.html'),
).toBe('');
});
test('通过审核时提交当前 publicationRevision 与发行入口并刷新列表', async () => {
vi.mocked(reviewAdminGameDistributionVersion).mockResolvedValue({
version: { ...entry, status: 'published' },
replayed: false,
});
render(
<AdminGameDistributionReviewPage
token="admin-token"
onUnauthorized={vi.fn()}
/>,
);
await screen.findByText('game_1');
fireEvent.change(screen.getByLabelText('发行入口'), {
target: { value: 'https://games.test/releases/game_1/index.html' },
});
fireEvent.click(screen.getByRole('button', { name: '通过' }));
await waitFor(() =>
expect(reviewAdminGameDistributionVersion).toHaveBeenCalledTimes(1),
);
const [token, versionId, idempotencyKey, payload] =
vi.mocked(reviewAdminGameDistributionVersion).mock.calls[0] ?? [];
expect(token).toBe('admin-token');
expect(versionId).toBe('version-1');
expect(String(idempotencyKey)).toContain('version-1');
expect(payload).toEqual({
decision: 'approve',
expectedPublicationRevision: 4,
entryUrl: 'https://games.test/releases/game_1/index.html',
});
await waitFor(() =>
expect(vi.mocked(listAdminGameDistributionReviews)).toHaveBeenCalledTimes(
2,
),
);
});
test('缺少拒绝理由时不调用审核接口', async () => {
render(
<AdminGameDistributionReviewPage
token="admin-token"
onUnauthorized={vi.fn()}
/>,
);
await screen.findByText('game_1');
fireEvent.click(screen.getByRole('button', { name: '拒绝' }));
expect(await screen.findByText('拒绝审核必须填写理由')).toBeTruthy();
expect(reviewAdminGameDistributionVersion).not.toHaveBeenCalled();
});
test('安全下架需要二次确认,并携带公开修订号与原因', async () => {
vi.mocked(suspendAdminGameDistributionGame).mockResolvedValue({
game: {
id: 'game_1',
title: '测试游戏',
status: 'suspended',
publicationRevision: 5,
},
replayed: false,
});
render(
<AdminGameDistributionReviewPage
token="admin-token"
onUnauthorized={vi.fn()}
/>,
);
await screen.findByText('game_1');
fireEvent.change(screen.getByLabelText('下架原因'), {
target: { value: '盗用素材' },
});
fireEvent.click(screen.getByRole('button', { name: '安全下架' }));
// 第一次点击只弹出确认面板,不直接调用后端。
expect(suspendAdminGameDistributionGame).not.toHaveBeenCalled();
expect(await screen.findByRole('dialog')).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: '确认' }));
await waitFor(() =>
expect(suspendAdminGameDistributionGame).toHaveBeenCalledTimes(1),
);
const [token, gameId, idempotencyKey, payload] =
vi.mocked(suspendAdminGameDistributionGame).mock.calls[0] ?? [];
expect(token).toBe('admin-token');
expect(gameId).toBe('game_1');
expect(String(idempotencyKey)).toContain('game_1');
expect(payload).toEqual({
expectedPublicationRevision: 4,
reason: '盗用素材',
});
expect(await screen.findByText(//u)).toBeTruthy();
});
test('取消确认时不下架', async () => {
render(
<AdminGameDistributionReviewPage
token="admin-token"
onUnauthorized={vi.fn()}
/>,
);
await screen.findByText('game_1');
fireEvent.click(screen.getByRole('button', { name: '安全下架' }));
await screen.findByRole('dialog');
fireEvent.click(screen.getByRole('button', { name: '取消' }));
await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull());
expect(suspendAdminGameDistributionGame).not.toHaveBeenCalled();
});
@@ -0,0 +1,365 @@
import { RefreshCcw } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
import {
listAdminGameDistributionReviews,
reviewAdminGameDistributionVersion,
suspendAdminGameDistributionGame,
} from '../api/adminApiClient';
import type { AdminGameDistributionReviewEntry } from '../api/adminApiTypes';
import { useAdminWriteConfirm } from '../components/useAdminWriteConfirm';
import { handlePageError } from './pageUtils';
interface AdminGameDistributionReviewPageProps {
token: string;
onUnauthorized: (message?: string) => void;
}
function formatBytes(value: number) {
if (value >= 1024 * 1024) {
return `${(value / (1024 * 1024)).toFixed(1)} MiB`;
}
if (value >= 1024) {
return `${(value / 1024).toFixed(1)} KiB`;
}
return `${value} B`;
}
function formatTime(value: string) {
const parsed = new Date(value);
if (Number.isNaN(parsed.getTime())) return value;
return parsed.toLocaleString('zh-CN', { hour12: false });
}
function createSuspendIdempotencyKey(gameId: string) {
const random =
typeof crypto !== 'undefined' && 'randomUUID' in crypto
? crypto.randomUUID()
: `${Date.now()}-${Math.random().toString(16).slice(2)}`;
return `game-suspend-${gameId}-${random}`.slice(0, 128);
}
function createReviewIdempotencyKey(versionId: string) {
const random =
typeof crypto !== 'undefined' && 'randomUUID' in crypto
? crypto.randomUUID()
: `${Date.now()}-${Math.random().toString(16).slice(2)}`;
return `game-review-${versionId}-${random}`.slice(0, 128);
}
export function resolveGameReleaseEntryUrlError(value: string) {
const normalized = value.trim();
if (!normalized) return '请填写发行入口';
if (!normalized.startsWith('https://')) {
return '发行入口必须以 https:// 开头';
}
if (normalized.includes('?') || normalized.includes('#')) {
return '发行入口不能包含 query 或 fragment';
}
try {
const parsed = new URL(normalized);
if (parsed.username || parsed.password) {
return '发行入口不能包含凭据';
}
} catch {
return '发行入口不是合法 URL';
}
return '';
}
export function AdminGameDistributionReviewPage({
token,
onUnauthorized,
}: AdminGameDistributionReviewPageProps) {
const [entries, setEntries] = useState<AdminGameDistributionReviewEntry[]>(
[],
);
const [isLoading, setIsLoading] = useState(false);
const [busyVersionId, setBusyVersionId] = useState('');
const [errorMessage, setErrorMessage] = useState('');
const [statusMessage, setStatusMessage] = useState('');
const [entryUrlByVersion, setEntryUrlByVersion] = useState<
Record<string, string>
>({});
const [reasonByVersion, setReasonByVersion] = useState<
Record<string, string>
>({});
const [suspendReasonByGame, setSuspendReasonByGame] = useState<
Record<string, string>
>({});
const [busyGameId, setBusyGameId] = useState('');
const writeConfirm = useAdminWriteConfirm();
const loadReviews = useCallback(async () => {
setIsLoading(true);
setErrorMessage('');
try {
const response = await listAdminGameDistributionReviews(token);
setEntries(response.entries);
} catch (error) {
handlePageError(error, onUnauthorized, setErrorMessage);
} finally {
setIsLoading(false);
}
}, [token, onUnauthorized]);
useEffect(() => {
void loadReviews();
}, [loadReviews]);
async function submitReview(
entry: AdminGameDistributionReviewEntry,
decision: 'approve' | 'reject',
) {
const entryUrl = (entryUrlByVersion[entry.versionId] ?? '').trim();
const reason = (reasonByVersion[entry.versionId] ?? '').trim();
if (decision === 'approve') {
const invalid = resolveGameReleaseEntryUrlError(entryUrl);
if (invalid) {
setErrorMessage(invalid);
return;
}
} else if (!reason) {
setErrorMessage('拒绝审核必须填写理由');
return;
}
setBusyVersionId(entry.versionId);
setErrorMessage('');
setStatusMessage('');
try {
await reviewAdminGameDistributionVersion(
token,
entry.versionId,
createReviewIdempotencyKey(entry.versionId),
decision === 'approve'
? {
decision,
expectedPublicationRevision: entry.publicationRevision,
entryUrl,
}
: {
decision,
expectedPublicationRevision: entry.publicationRevision,
reviewReason: reason,
},
);
setStatusMessage(
decision === 'approve'
? `版本 v${entry.versionNumber} 已通过审核`
: `版本 v${entry.versionNumber} 已拒绝`,
);
await loadReviews();
} catch (error) {
handlePageError(error, onUnauthorized, setErrorMessage);
} finally {
setBusyVersionId('');
}
}
/**
* 管理员安全下架:先二次确认,再带当前公开修订号调用后端;并发审核导致修订号变化时
* 由服务端返回冲突,前端只提示刷新,不静默重试。
*/
async function suspendGame(entry: AdminGameDistributionReviewEntry) {
const reason = (suspendReasonByGame[entry.gameId] ?? '').trim();
const confirmed = await writeConfirm.confirmWrite({
action: '安全下架游戏',
target: `${entry.gameId}(版本 v${entry.versionNumber}`,
});
if (!confirmed) return;
setBusyGameId(entry.gameId);
setErrorMessage('');
setStatusMessage('');
try {
await suspendAdminGameDistributionGame(
token,
entry.gameId,
createSuspendIdempotencyKey(entry.gameId),
{
expectedPublicationRevision: entry.publicationRevision,
...(reason ? { reason } : {}),
},
);
setStatusMessage(`游戏 ${entry.gameId} 已安全下架,发行入口已关闭`);
setSuspendReasonByGame((current) => ({ ...current, [entry.gameId]: '' }));
await loadReviews();
} catch (error) {
handlePageError(error, onUnauthorized, setErrorMessage);
} finally {
setBusyGameId('');
}
}
return (
<section className="admin-page admin-page-wide">
<div className="admin-page-heading">
<h1></h1>
<button
type="button"
className="admin-secondary-button"
onClick={() => void loadReviews()}
disabled={isLoading}
>
<RefreshCcw aria-hidden="true" />
</button>
</div>
{errorMessage ? (
<div className="admin-alert admin-alert-warning" role="alert">
{errorMessage}
</div>
) : null}
{statusMessage ? (
<div className="admin-alert admin-alert-success" role="status">
{statusMessage}
</div>
) : null}
<div className="admin-panel">
<div className="admin-panel-heading">
<h2></h2>
<span className="admin-muted-text"> {entries.length} </span>
</div>
{isLoading ? (
<p className="admin-muted-text"></p>
) : null}
{!isLoading && entries.length === 0 ? (
<p className="admin-muted-text"></p>
) : null}
{!isLoading && entries.length > 0 ? (
<div className="admin-table-wrap">
<table className="admin-table admin-table-wide">
<thead>
<tr>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{entries.map((entry) => {
const busy = busyVersionId === entry.versionId;
return (
<tr key={entry.versionId}>
<td>
<code>{entry.gameId}</code>
</td>
<td>
v{entry.versionNumber}
<div className="admin-muted-text">{entry.status}</div>
{entry.reviewReason ? (
<div className="admin-muted-text">
{entry.reviewReason}
</div>
) : null}
</td>
<td>
{formatBytes(entry.packageBytes)}
<div className="admin-muted-text">
<code>{entry.packageSha256.slice(0, 12)}</code>
</div>
</td>
<td>{formatTime(entry.createdAt)}</td>
<td>
<div className="admin-action-row">
<div className="admin-field">
<label
htmlFor={`game-release-url-${entry.versionId}`}
>
</label>
<input
id={`game-release-url-${entry.versionId}`}
value={entryUrlByVersion[entry.versionId] ?? ''}
placeholder="https://"
onChange={(event) =>
setEntryUrlByVersion((current) => ({
...current,
[entry.versionId]: event.target.value,
}))
}
disabled={busy}
/>
</div>
<button
type="button"
className="admin-primary-button"
disabled={busy}
onClick={() => void submitReview(entry, 'approve')}
>
</button>
<div className="admin-field">
<label
htmlFor={`game-reject-reason-${entry.versionId}`}
>
</label>
<input
id={`game-reject-reason-${entry.versionId}`}
value={reasonByVersion[entry.versionId] ?? ''}
onChange={(event) =>
setReasonByVersion((current) => ({
...current,
[entry.versionId]: event.target.value,
}))
}
disabled={busy}
/>
</div>
<button
type="button"
className="admin-ghost-button"
disabled={busy}
onClick={() => void submitReview(entry, 'reject')}
>
</button>
<div className="admin-field">
<label
htmlFor={`game-suspend-reason-${entry.versionId}`}
>
</label>
<input
id={`game-suspend-reason-${entry.versionId}`}
value={suspendReasonByGame[entry.gameId] ?? ''}
onChange={(event) =>
setSuspendReasonByGame((current) => ({
...current,
[entry.gameId]: event.target.value,
}))
}
disabled={busy}
/>
</div>
<button
type="button"
className="admin-danger-button"
disabled={busy || busyGameId === entry.gameId}
onClick={() => void suspendGame(entry)}
>
{busyGameId === entry.gameId
? '正在下架…'
: '安全下架'}
</button>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
) : null}
</div>
{writeConfirm.confirmDialog}
</section>
);
}
@@ -175,6 +175,86 @@ test('灰度发布页可选择模板库并默认启用零比例灰度', async ()
);
});
test('灰度发布页可选择游戏发布开关,默认保持「未开启即开放」语义', async () => {
const user = userEvent.setup();
render(
<AdminGrayReleaseConfigPage token="admin-token" onUnauthorized={vi.fn()} />,
);
await screen.findByRole('button', { name: 'editor.new-toolbar' });
await user.selectOptions(screen.getByLabelText('Gate Key 前缀'), [
'game-distribution',
]);
expect((screen.getByLabelText('Gate Key') as HTMLInputElement).value).toBe(
'game-distribution:publish',
);
expect(
(screen.getByLabelText('Gate Key 目标') as HTMLSelectElement).value,
).toBe('publish');
// 该开关的语义是「未配置/关闭 = 默认开放」,所以选中后不能默认打开收紧。
expect((screen.getByLabelText('启用') as HTMLInputElement).checked).toBe(
false,
);
expect((screen.getByLabelText('灰度比例') as HTMLInputElement).value).toBe(
'0',
);
expect(
(screen.getByLabelText('描述') as HTMLTextAreaElement).value,
).toContain('游戏发布入口灰度');
});
test('灰度发布页保存游戏发布开关时写入白名单与比例', async () => {
const user = userEvent.setup();
vi.mocked(upsertAdminFeatureGateConfig).mockResolvedValueOnce({
gates: [
...configResponse.gates,
{
gateKey: 'game-distribution:publish',
enabled: true,
rolloutPercent: 20,
allowUserIds: ['user-internal'],
allowUserTags: [],
denyUserIds: [],
description: '游戏发布入口灰度',
updatedAt: '2026-09-22T10:00:00Z',
},
],
});
render(
<AdminGrayReleaseConfigPage token="admin-token" onUnauthorized={vi.fn()} />,
);
await screen.findByRole('button', { name: 'editor.new-toolbar' });
await user.selectOptions(screen.getByLabelText('Gate Key 前缀'), [
'game-distribution',
]);
fireEvent.click(screen.getByLabelText('启用'));
fireEvent.change(screen.getByLabelText('灰度比例'), {
target: { value: '20' },
});
fireEvent.change(screen.getByLabelText('允许用户 ID'), {
target: { value: 'user-internal' },
});
fireEvent.change(screen.getByLabelText('描述'), {
target: { value: '游戏发布入口灰度' },
});
await user.click(screen.getByRole('button', { name: '保存配置' }));
await user.click(screen.getByRole('button', { name: '确认' }));
await waitFor(() =>
expect(upsertAdminFeatureGateConfig).toHaveBeenCalledWith('admin-token', {
gateKey: 'game-distribution:publish',
enabled: true,
rolloutPercent: 20,
allowUserIds: ['user-internal'],
allowUserTags: [],
denyUserIds: [],
description: '游戏发布入口灰度',
}),
);
});
test('灰度发布页保存时转换数组和百分比', async () => {
const user = userEvent.setup();
vi.mocked(upsertAdminFeatureGateConfig).mockResolvedValueOnce({
@@ -28,6 +28,7 @@ interface GateTargetOption {
const GATE_PREFIX_LABELS: Record<string, string> = {
'image-editor': '画布',
agc: '客户端',
'game-distribution': '游戏分发',
};
const FIXED_GATE_TARGETS: GateTargetOption[] = [
@@ -45,6 +46,14 @@ const FIXED_GATE_TARGETS: GateTargetOption[] = [
label: 'Agent 侧边栏',
description: '画布 Agent 入口灰度',
},
{
prefix: 'game-distribution',
suffix: 'publish',
key: 'game-distribution:publish',
label: '游戏发布',
description:
'游戏发布入口灰度:未配置或关闭时对已登录作者默认开放,开启后只放行白名单 / 灰度命中',
},
];
export function AdminGrayReleaseConfigPage({
@@ -648,6 +648,32 @@ function readHeadCommit() {
}
}
/**
* Git 的 %s 会把「标题后紧接说明行、没有空行」的整个首段拼成一行;
* 更新摘要只允许展示原始提交消息的第一行,避免把说明暴露给用户。
*/
function commitMessageTitle(message) {
return String(message ?? '')
.split(/\r?\n/u, 1)[0]
.trim();
}
/** 解析 `git log -z --format=%h%x09%B`,只保留每个 commit 的消息首行。 */
function parseReleaseCommitLog(output) {
return output
.split('\0')
.map((record) => record.trimEnd())
.filter(Boolean)
.map((record) => {
const separator = record.indexOf('\t');
if (separator < 0) return null;
const sha = record.slice(0, separator);
const subject = commitMessageTitle(record.slice(separator + 1));
return sha && subject ? { sha, subject } : null;
})
.filter(Boolean);
}
/**
* 上一次发布到本次之间的客户端相关提交。
*
@@ -675,8 +701,9 @@ export function collectReleaseCommits(
'git',
[
'log',
'-z',
'--no-merges',
'--format=%h%x09%s',
'--format=%h%x09%B',
`${previousCommit}..${headCommit}`,
'--',
...paths,
@@ -686,28 +713,22 @@ export function collectReleaseCommits(
} catch {
return null;
}
return output
.split(/\r?\n/u)
.map((line) => line.trim())
.filter(Boolean)
.map((line) => {
const [sha = '', ...subject] = line.split('\t');
return { sha, subject: subject.join('\t') };
});
return parseReleaseCommitLog(output);
}
/** 自动更新摘要:逐条列客户端相关改动,超过上限时折叠并整体截断。 */
/** 自动更新摘要:逐条列客户端相关改动标题,超过上限时折叠并整体截断。 */
export function formatReleaseNotes(
commits,
{ limit = 12, subjectLength = 80, maxLength = 900 } = {},
) {
if (!commits || commits.length === 0) return '';
const lines = commits.slice(0, limit).map(({ sha, subject }) => {
const lines = commits.slice(0, limit).map(({ subject }) => {
const title = commitMessageTitle(subject);
const trimmed =
subject.length > subjectLength
? `${subject.slice(0, subjectLength - 1)}`
: subject;
return `- ${trimmed}${sha}`;
title.length > subjectLength
? `${title.slice(0, subjectLength - 1)}`
: title;
return `- ${trimmed}`;
});
if (commits.length > limit) {
lines.push(`- 其余 ${commits.length - limit} 项客户端改动省略`);
@@ -726,20 +747,21 @@ export function collectRecentReleaseCommits({
try {
output = execFileSync(
'git',
['log', '--no-merges', `-n${limit}`, '--format=%h%x09%s', '--', ...paths],
[
'log',
'-z',
'--no-merges',
`-n${limit}`,
'--format=%h%x09%B',
'--',
...paths,
],
{ cwd, encoding: 'utf8' },
);
} catch {
return null;
}
const commits = output
.split(/\r?\n/u)
.map((line) => line.trim())
.filter(Boolean)
.map((line) => {
const [sha = '', ...subject] = line.split('\t');
return { sha, subject: subject.join('\t') };
});
const commits = parseReleaseCommitLog(output);
return commits.length > 0 ? commits : null;
}
@@ -645,17 +645,19 @@ test('Windows remains the default and explicit Windows overrides macOS environme
test('no-bundle smoke skips version writes and manifest generation', async () => {
const steps = [];
await buildRelease(['--no-bundle', '--target=aarch64-apple-darwin'], {
prepareVersion: () => {
steps.push('version');
},
build: (_args, context) => {
steps.push(context.channel);
},
generateManifest: () => {
steps.push('manifest');
},
});
await withEnv({ AGC_UPDATE_CHANNEL: undefined }, () =>
buildRelease(['--no-bundle', '--target=aarch64-apple-darwin'], {
prepareVersion: () => {
steps.push('version');
},
build: (_args, context) => {
steps.push(context.channel);
},
generateManifest: () => {
steps.push('manifest');
},
}),
);
assert.deepEqual(steps, ['dev']);
});
@@ -1075,20 +1077,17 @@ test('release entry forwards the built artifacts and dry-run mode to the uploade
assert.ok(source.includes('\n dryRun,\n'));
});
test('release notes list client commits with short sha and bound their size', () => {
test('release notes list client commit subjects only and bound their size', () => {
const notes = formatReleaseNotes([
{ sha: 'a5fd25f1', subject: '客户端更新切换到官方更新插件' },
{ sha: '55af6014', subject: '修'.repeat(120) },
]);
const lines = notes.split('\n');
assert.equal(lines.length, 2);
assert.match(lines[0], /^- 客户端更新切换到官方更新插件a5fd25f1$/u);
const truncatedSubject = lines[1]
.replace(/^- /u, '')
.replace(/55af6014$/u, '');
assert.equal(lines[0], '- 客户端更新切换到官方更新插件');
const truncatedSubject = lines[1].replace(/^- /u, '');
assert.equal(truncatedSubject.length, 80, `主题应截断到 80 字:${lines[1]}`);
assert.match(truncatedSubject, /…$/u);
assert.match(lines[1], /55af6014$/u);
const many = formatReleaseNotes(
Array.from({ length: 20 }, (_, index) => ({
@@ -1097,6 +1096,15 @@ test('release notes list client commits with short sha and bound their size', ()
})),
);
assert.match(many, /- 其余 8 项客户端改动省略$/u);
assert.equal(
formatReleaseNotes([
{
sha: 'ignored',
subject: '提交标题\n不应展示的说明一\n不应展示的说明二',
},
]),
'- 提交标题',
);
assert.equal(formatReleaseNotes([]), '');
assert.equal(formatReleaseNotes(null), '');
});
@@ -1120,8 +1128,17 @@ test('release commits cover only client paths and skip merge commits', () => {
path.join(directory, 'apps/ai-game-creator-shell/main.rs'),
'fn main() {}\n',
);
const commitMessagePath = path.join(
directory,
'.git',
'commit-message.txt',
);
writeFileSync(
commitMessagePath,
'客户端:新增更新插件接入\n补充更新插件接入的详细说明\n',
);
git('add', '.');
git('commit', '--quiet', '-m', '客户端:新增更新插件接入');
git('commit', '--quiet', '-F', commitMessagePath);
writeFileSync(path.join(directory, 'docs/readme.md'), '# 文档\n');
git('add', '.');
@@ -1147,6 +1164,7 @@ test('release commits cover only client paths and skip merge commits', () => {
const commits = collectReleaseCommits(base, 'HEAD', { cwd: directory });
assert.ok(commits, '应能在临时仓库里收集提交');
const subjects = commits.map((entry) => entry.subject);
// 标题后没有空行时,git %s 会把说明行拼进标题;摘要必须取原始首行。
// 合并提交本身被 --no-merges 排除,但它带入的客户端改动仍然计入。
assert.deepEqual(subjects, [
'客户端:侧分支改动',
@@ -200,15 +200,16 @@ const allowedUncalledTauriCommands = [
'read_agc_plugin_panel',
'set_agc_plugin_enabled',
// 下面这些命令的调用方只有随 Project Supervisor 前端链路一起删除的旧命令聊天入口;
// 现在 App 前端、工作台与策划聊天都没有接线(检查点 / 恢复 / 索引 / 导出包 /
// 现在 App 前端、工作台与策划聊天都没有接线(检查点 / 恢复 / 索引 /
// 画板同步 / 素材登记 / 权限策略 / 本地草案 / 平台美术),Rust 侧只剩注册与实现,
// `*_at` helper 仍由 Rust 用例覆盖。接回新入口还是删除属于 native 能力取舍,先按
// native-only 登记,避免孤儿检查一直报错。
// 预览不在本清单:`activate_local_game_preview` 已按 ADR 回接到 App 的「运行」入口。
// 导出试玩包同样不在本清单:发布链路(`requestGamePublish`)已把它接回
// DirectProject 聊天头的「发布」入口。
'build_local_project_index',
'control_agent_run',
'create_local_project_checkpoint',
'export_local_project_package',
'generate_local_game_draft',
'generate_platform_art_asset',
'import_canvas_asset',
@@ -1940,6 +1941,7 @@ for (const snippet of [
'官方账号服务(固定)',
'runtime_config.save',
"'/run:运行自检,启动本地 HTTP 预览并载入客户端运行视图'",
'已载入客户端运行视图',
'async function executeRunLocal',
'function needsInitializedChatProject',
"'/remember [short|long|blackboard] 内容:追加短期、长期或黑板记忆'",
@@ -16,7 +16,8 @@
{ "url": "https://www.genarrative.world/api/*" },
{ "url": "https://*/api/*" },
{ "url": "http://localhost:*/*" },
{ "url": "http://127.0.0.1:*/*" }
{ "url": "http://127.0.0.1:*/*" },
{ "url": "https://*.aliyuncs.com/*" }
]
},
"opener:default",
@@ -203,12 +203,51 @@ pub(in crate::agent) fn game_creator_codex_cli_version_at(
Ok(version.to_string())
}
/// 开发态允许从宿主 PATH 里找到 Codex,但宿主必须持有可锚定的绝对文件:
/// 裸命令名按 PATH 解析成真实路径,否则 `bind_codex_executor` 的 canonicalize 会按 CWD 解析并失败。
/// 发行构建不走这段,候选顺序、校验与返回值都与原先一致(打包环境用内置侧车/npm 绝对路径)。
#[cfg(debug_assertions)]
fn anchor_codex_cli_executable_candidate(
candidate: &Path,
path: Option<&std::ffi::OsStr>,
) -> Option<PathBuf> {
let is_bare_command_name = candidate
.parent()
.is_some_and(|parent| parent.as_os_str().is_empty());
if !is_bare_command_name {
return candidate.is_file().then(|| candidate.to_path_buf());
}
let name = candidate.as_os_str();
for directory in path.into_iter().flat_map(std::env::split_paths) {
if directory.as_os_str().is_empty() {
continue;
}
let target = directory.join(name);
if target.is_file() {
// 第一个实际命中的项就是 OS 会执行的项;锚定失败时不再从 PATH 里换另一个。
return target.canonicalize().ok();
}
}
None
}
pub(crate) fn game_creator_codex_cli_executable_path() -> Result<PathBuf, String> {
let mut last_error = None;
let mut seen = std::collections::HashSet::new();
let bundled =
game_creator_bundled_codex_cli_path(game_creator_bundled_resource_dir().as_deref());
for candidate in game_creator_codex_cli_executable_candidates() {
#[cfg(debug_assertions)]
let candidate = match anchor_codex_cli_executable_candidate(
&candidate,
std::env::var_os("PATH").as_deref(),
) {
Some(candidate) => candidate,
None => {
last_error = Some("候选执行器不是可锚定的文件".to_string());
continue;
}
};
let identity = candidate.to_string_lossy().to_ascii_lowercase();
if !seen.insert(identity) {
continue;
@@ -6,6 +6,7 @@ mod model;
mod network_policy;
mod playtest;
mod process;
mod sweep;
pub use discovery::discover_chrome_or_edge;
#[allow(unused_imports)]
@@ -22,6 +23,7 @@ pub(crate) use process::validate_local_preview_in_browser_with_cancellation;
pub use process::{
validate_local_preview_in_browser, validate_local_preview_in_browser_with_interaction,
};
pub(crate) use sweep::sweep_stale_browser_processes;
pub(crate) use model::required_viewport_playtests_passed;
pub(crate) use playtest::browser_playtest_scenario_fingerprint;
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More