Compare commits

..

68 Commits

Author SHA1 Message Date
k88936 a4296a4bdd 合并 origin/master 并收敛 DirectProject 引用渲染到 canonical user item
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 7m17s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 7m15s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 7m45s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 7m59s
Project CI / Backend tests (pull_request) Failing after 10s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m8s
Project CI / Repository checks (pull_request) Failing after 9s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m48s
Project CI / AI game creator shell web tests (pull_request) Successful in 6m24s
Project CI / Frontend tests (pull_request) Successful in 15m29s
Project CI / Native shell tests (pull_request) Successful in 19m38s
- .eslintrc.cjs 冲突取双方 ignore 并集,保留 contracts/generated/** 与 project-workspace/generated/**。
- 实施计划文档冲突保留双方内容:2026-09-15 资源 kind 枚举化权威口径与 master 新增计划段并存。
- resourceReferences.ts 冲突合并双方 import:kind 解析入口与 canonical DirectCodexUserItem 类型同时保留。
- project-development.suite.ts 冲突合并双方 import:UI 设计文档 kind/mediaType 常量与 ProjectSupervisorView 同时保留。
- decision-log.md 冲突取双方条目,并补记 DirectProject 引用渲染收敛到 canonical user item 的决策。
- 接受 master 退役 agent/direct_codex_references.rs,把 UI 设计文档引用生成代码上下文的 prompt 注入迁入 agent/direct_codex_user_item/wire.rs。
- assets.rs 资源登记 kind 参数收紧为 GameCreationAppAssetKind,相应测试 fixture 改用正式枚举成员。
- 同步 project-memory 实施计划、里程碑与扫描清单中的模块路径。
2026-09-16 21:01:20 +08:00
lhk229 cec971c438 修复局部修改工具的缺陷和偶发锁问题 (#386)
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 5m49s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 4m50s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m53s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 5m16s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 4m1s
Project CI / AI game creator shell Rust crates (push) Successful in 2m14s
Project CI / Frontend tests (push) Failing after 4m6s
Project CI / Repository checks (push) Successful in 4m33s
Project CI / Native shell tests (push) Successful in 6m33s
Project CI / Backend tests (push) Successful in 8m26s
Project CI / AI game creator shell web tests (push) Successful in 3m43s
Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/386
Co-authored-by: Linghong <ink29535@proton.me>
Co-committed-by: Linghong <ink29535@proton.me>
2026-09-16 19:53:23 +08:00
k88936 7eee89ded3 同步 AGC 资源 kind 枚举化的实施计划、里程碑与扫描清单
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 5m22s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 5m16s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 5m4s
Project CI / Backend tests (pull_request) Failing after 10s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m39s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 4m40s
Project CI / Repository checks (pull_request) Failing after 11s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m1s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m3s
Project CI / Frontend tests (pull_request) Successful in 4m35s
Project CI / Native shell tests (pull_request) Successful in 6m44s
- 实施计划:记录已落地内容(kind 由 `String` 收紧为枚举的破坏性变更、写入与读取统一正式成员、资源编辑源身份词汇表、`cargo test export_bindings` 生成路径、TS 穷举派生)
- 实施计划:修正「明确不修改」与验证命令,去掉与当前代码不符的表述
- 里程碑:验收标准除旧 alias 读边界一项外全部勾选,并补记 ts-rs 生成与退役类型导出两项证据
- 扫描清单:把「当前未决项」逐条落成结论(`asset`、`art-spritesheet*`、`game-*`、`ui*`、`Unknown` trace、生成路径),只保留 CI verify 接入一项待办
2026-09-16 19:08:28 +08:00
k88936 f9bc2d917c AGC 资源 kind 集合改为从生成 union 穷举派生
- assetKind.ts:新增 `GAME_CREATION_APP_ASSET_KIND_MEMBERS: Record<GameCreationAppAssetKind, true>`,生成 union 增删成员时会直接编译报错
- assetKind.ts:`GAME_CREATION_APP_ASSET_KINDS` 由该表派生,不再手写第二份字面量列表
- assetKind.ts:新增 `isGameCreationAppAssetKind` 类型守卫,`parseGameCreationAppAssetKind` 复用它
- view/project-development/index.tsx:资源引用的 `sourceSubtype` 只传正式 kind 成员,任务产物等非 manifest 来源传 `null`
- tests/assetKindCanonicalMapping.test.ts:两侧 kind 目录比对前先展开 Rust 常量引用(`ui-design-doc` 是常量),修正此前恒失败的两条;删除测试体里游离的 `'font',` 表达式
2026-09-16 19:07:58 +08:00
lhk229 70157673b6 Merge branch 'master' into fix/ci
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (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 shard 3/4 (push) Successful in 5m27s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 6m12s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 6m30s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m41s
Project CI / AI game creator shell Rust crates (push) Successful in 2m48s
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 5m46s
Project CI / Frontend tests (push) Successful in 4m49s
Project CI / Repository checks (push) Successful in 3m43s
Project CI / Native shell tests (push) Successful in 6m41s
Project CI / Backend tests (push) Successful in 7m17s
Project CI / AI game creator shell web tests (push) Successful in 3m26s
2026-09-16 19:07:53 +08:00
k88936 b96cd4b7c2 ts-rs 绑定统一走 cargo test export_bindings,删除退役类型导出
- asset_kind.rs:`GameCreationAppAssetKind` 自带 `#[ts(export, export_to = ../apps/ai-game-creator-shell/src/contracts/generated/)]`,`cargo test export_bindings` 即可复现生成文件
- binding.rs:删除手写的 `exports_ui_editor_types` 测试,不再由测试就地重写生成文件或重建退役类型
- binding.rs:`BindingChange` / `BindingDTO` 去掉 `TS` derive 与 `ts(export_to)`,只作为内部结构使用,不再导出退役的前端绑定
- binding.rs:清理仅为该测试存在的 `ts_rs` / `GameCreationAppAssetKind` 导入
- .prettierignore、.eslintrc.cjs:忽略 `src/contracts/generated/`,保留 ts-rs 原始格式使生成幂等且不污染工作树
- GameCreationAppAssetKind.ts:按生成器原始格式更新(与上述路径一致,无内容漂移)
2026-09-16 19:07:33 +08:00
k88936 f185d21f55 资源编辑源身份改用独立词汇表,只传正式 kind 成员
- resource_editor.rs:新增 `ResourceEditSourceKind`,把源身份拆成 `Manifest(GameCreationAppAssetKind)` / `ProjectVersion` / `AgentResultText`
- resource_editor.rs:`ResourceEditSourceSnapshot.asset_kind` 由 `String` 收紧为该枚举,项目版本与 Agent 回执不再冒充 manifest kind
- resource_editor.rs:`infer_resource_edit_source_asset_kind` 直接返回枚举成员,`art-image` / `svg` / `text` 收口为 `Image` / `Image` / `Document`
- resource_editor.rs:栅格源归一化只接受正式枚举成员,`task-artifact` 特判删除,未知值按 `Image` 收口
- resource_editor.rs:账本、HTTP 载荷与提示词边界改用 `as_str()`,manifest 写回改用 `manifest_kind()`
- resource_editor.rs:`source_subtype` 字段补充「只接受 manifest 正式 kind」的文档说明,测试 fixture 同步改用枚举成员
2026-09-16 19:07:23 +08:00
k88936 a61b0cce81 收敛 AGC 资源 kind 写入与读取到正式枚举成员
- assets.rs:`register_local_asset_at` / `register_local_asset_entry` 改为接收 `GameCreationAppAssetKind`,画板导入与画板同步在边界解析 kind
- assets.rs:`infer_canvas_export_asset_kind` 直接返回枚举成员,去掉仅供兜底的 canonical 归一
- commands.rs:资源登记按 `asset.register` 上下文解析 kind,字体导入写 `Font`,本地文件类型表与平台导入归一改用枚举
- direct_runtime.rs:直连游戏工程产物统一写 `Code`,图集切片读取按 `Icon` 叠加切片路径判定,陶泥儿美术身份改收枚举
- canvas_generation.rs:新增 `platform_art_asset_manifest_kind` 收口平台美术请求词汇,切片登记 category 改用 `Icon.as_str()`
- manifest.rs:必填视觉资产校验改用 `IconSpec` / `UiDesign` / `IconSpritesheet`,修正此前与写入侧对不上的比对
- project_gates.rs、autonomous_policy.rs、recovery_scan.rs、direct_tool_bridge.rs、ui_editor workflow/persistence/resource_bridge:读取侧与 fixture 改用正式枚举成员
- tests:资源相关 fixture 与断言改用正式枚举成员
2026-09-16 19:07:12 +08:00
kdletters 492e9e63af 合入 AGC 终止链路 abort 闪退修复
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 5m20s
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 smoke (push) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (push) Has been cancelled
Project CI / AI game creator shell Rust shard 3/4 (push) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (push) Has been cancelled
codex_app_server 新增 spawn_codex_app_server_task,统一经 tauri::async_runtime::spawn 派发
终止链路、CodexThreadLease::drop、CodexTurnGuard::drop 三处不再在同步命令线程上 tokio::spawn
新增回归用例 codex_app_server_task_dispatch_needs_no_tokio_runtime_context
补记 tokio::spawn 导致 abort 闪退、cargo test 重写 generated 绑定两条踩坑
2026-09-16 18:58:19 +08:00
lhk229 57fae7037a 修复 Agent 异步测试等待
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 5m13s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 5m24s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 5m52s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 6m12s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m38s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m42s
Project CI / Frontend tests (pull_request) Successful in 4m28s
Project CI / Repository checks (pull_request) Successful in 3m39s
Project CI / Native shell tests (pull_request) Successful in 6m59s
Project CI / Backend tests (pull_request) Successful in 7m37s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m55s
移除不稳定的项目页文案前置

保留刷新 Agent 与拆解任务按钮就绪检查
2026-09-16 10:52:25 +00:00
kdletters 2bcfa10647 修复 AGC 客户端点「终止」整体闪退:同步命令线程改走 Tauri 全局异步 runtime
- codex_app_server 新增 spawn_codex_app_server_task,统一经 tauri::async_runtime::spawn 派发中断与收尾任务
- 终止链路 maybe_interrupt、CodexThreadLease::drop、CodexTurnGuard::drop 三处改走该入口,避免同步 Tauri 命令线程上 tokio::spawn panic 后跨 IPC 回调边界 abort
- 新增回归用例 codex_app_server_task_dispatch_needs_no_tokio_runtime_context,把派发入口钉在没有 tokio 上下文的线程上
- 同步 agc-skills manifest 指纹,修复 master 指纹回退导致的 skill-pack:check 失败(本地打包前置门禁)
- 记录两条踩坑:同步命令线程 tokio::spawn 导致客户端 abort 闪退、cargo test 重写 generated/*.ts 后前端 typecheck 必红
2026-09-16 18:40:14 +08:00
kdletters e777236817 补齐 decision-log 两处标题前缺失的空行
恢复引言引用块、2026-09-16 图标图集条目之前的空行分隔,与文件既有排版一致
仅新增两行空行,不改动任何决策条目内容
2026-09-16 18:35:52 +08:00
lhk229 d11c74212d Merge pull request '修复 AGC skill 包与 Agent 测试' (#387) from fix/ci into master
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 5m15s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 5m16s
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 5m38s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 6m8s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m45s
Project CI / AI game creator shell Rust crates (push) Successful in 2m28s
Project CI / Frontend tests (push) Failing after 3m36s
Project CI / Repository checks (push) Failing after 3m40s
Project CI / Native shell tests (push) Successful in 6m48s
Project CI / Backend tests (push) Successful in 7m47s
Project CI / AI game creator shell web tests (push) Failing after 3m44s
Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/387
2026-09-16 18:33:52 +08:00
lhk229 82b2f853e7 修复 AGC skill 包与 Agent 测试
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 shard 3/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Has been cancelled
同步工作流和试玩 skill 指纹

等待项目页面与拆解任务按钮就绪
2026-09-16 10:31:57 +00:00
kdletters 31f83a751a 三维请求自选技术栈,不再固定 Phaser
Project CI / AI game creator shell Rust shard 2/4 (push) Failing after 4m35s
Project CI / AI game creator shell Rust shard 1/4 (push) Failing after 5m18s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m32s
Project CI / AI game creator shell Rust shard 3/4 (push) Failing after 5m7s
Project CI / AI game creator shell Rust shard 4/4 (push) Failing after 3m54s
Project CI / AI game creator shell Rust crates (push) Successful in 2m26s
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
- DirectProject 系统提示新增三维请求合同:三维请求不受 Phaser 4.2.1 约束,由 Codex 自选三维技术栈
- 工程合同的 Phaser 固定约束按二维/三维分层,点名引擎的不匹配澄清规则保持不变
- 新增三维意图识别、三维回合合同与首页三维提示;伪 3D、等轴与代码话题不触发
- 补定向用例与里程碑/实施计划文档,并在 decision-log 记录该决策
2026-09-16 18:04:44 +08:00
lhk229 a1cafde7e9 修复 AI 创作壳测试稳定性
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (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 shard 1/4 (push) Failing after 4m35s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m32s
Project CI / AI game creator shell Rust shard 3/4 (push) Failing after 4m48s
Project CI / AI game creator shell Rust shard 4/4 (push) Failing after 4m59s
Project CI / AI game creator shell Rust shard 2/4 (push) Failing after 5m37s
Project CI / AI game creator shell Rust crates (push) Successful in 2m42s
Project CI / Native shell tests (push) Failing after 2m47s
Project CI / Frontend tests (push) Failing after 3m29s
Project CI / AI game creator shell web tests (push) Failing after 49s
Project CI / Repository checks (push) Failing after 3m16s
Project CI / Backend tests (push) Successful in 7m7s
调整 Agent 项目打开测试的异步就绪等待

延长权限策略连续命令测试超时预算

同步 taonier-art-assets skill-pack 内容指纹

补充对应实施计划中的测试约定
2026-09-16 09:57:21 +00:00
kdletters 0594a90bdd 合入平台会话身份与凭据分离
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 4m15s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m32s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 4m58s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 6m13s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 5m3s
Project CI / AI game creator shell Rust crates (push) Successful in 3m1s
Project CI / Frontend tests (push) Failing after 4m24s
Project CI / Repository checks (push) Successful in 4m1s
Project CI / Native shell tests (push) Successful in 7m45s
Project CI / AI game creator shell web tests (push) Failing after 3m4s
Project CI / Backend tests (push) Successful in 8m20s
平台会话快照拆分身份代次与写入 revision,凭据续期轮换不再中断在途生成
AGC Runner 请求参数新增 platform_auth_revision,并按 revision 做单调写入判定
解决与多窗口共享 Runner 的冲突,保留 claim adopt/publish 与界面参与锁语义
同步刷新轮换竞争、appSurface 鉴权用例与客户端 API 测试
补录平台会话身份与凭据分离的实施计划、共享记忆与决策记录
2026-09-16 17:42:00 +08:00
kdletters aee862532c 删除已完成的多窗口里程碑临时文档
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 4m38s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 4m57s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m36s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 5m52s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 4m42s
Project CI / AI game creator shell Rust crates (push) Successful in 2m39s
Project CI / Repository checks (push) Successful in 3m58s
Project CI / Frontend tests (push) Successful in 5m53s
Project CI / Native shell tests (push) Successful in 6m28s
Project CI / Backend tests (push) Successful in 7m49s
Project CI / AI game creator shell web tests (push) Successful in 2m8s
- 按规范驱动开发工作流,里程碑与实施计划属临时产物,合并后删除
- 持久结论已回写 AGC 技术方案 2026-09-16 节、共享记忆决策记录与踩坑记录
2026-09-16 17:15:22 +08:00
kdletters 42be8ea060 发布 AGC 标准版 0.1.47
- 版本号同步到 0.1.47:package.json / package-lock.json / tauri.conf.json / Cargo.toml / Cargo.lock
- 构建脚本读取 OSS 清单(已有 0.1.46)后自动递增,本版打包产物为 陶泥儿_0.1.47_x64-setup.exe
- 本轮同时包含同 AppData 多窗口共享 Agent Runner 的修复
2026-09-16 17:15:15 +08:00
kdletters b48293fb1f 收敛平台会话 epoch 重装语义,避免原生计数与渲染层漂移
- GUI owner 重装与清除改为按 authority 快照重定基准,保持原生计数与渲染层认知一致
- 注释写明 epoch 交接的授权前提,以及写入单调性由渲染层 reserve 与 durable session revision 保证
- 同主体换凭据的重装保持身份代次,在途 operation 的冻结会话继续有效
- 补充 epoch 重装与换主体失效的回归用例,替换原先错误假设原生计数器单调的用例
2026-09-16 17:08:47 +08:00
kdletters e08171fd2e 补记平台会话身份与凭据分离的规范与记忆
- 主规范新增平台会话身份与凭据分离条款,并修正长回合保活的口径
- 共享记忆补充并发生图登录态冲突的排障口径与验证方式
- 决策记录补充身份代次、凭据轮换、刷新失败语义的长期决策
- 新增本次里程碑与实施计划,记录证据与未验证项
2026-09-16 17:08:47 +08:00
kdletters 479120d368 修复刷新轮换竞争被放大成登出
- refresh 接口的轮换失败不再下发清空 refresh cookie 的响应,避免删掉并发请求刚写入的有效 cookie
- 缺少 refresh cookie 的失败保持无副作用,cookie 失效只由吊销、过期或身份变更决定
- 网站前端在刷新返回 401 时先用当前 cookie 收敛重试一次,重试仍被拒绝才判权威失效
- 网站前端只在当前鉴权代次未变化时重试刷新,避免旧账号结论污染新代次
- 更新 api-server 与前端断言,覆盖轮换竞争与收敛重试两条路径
2026-09-16 17:08:47 +08:00
kdletters 4182979a19 平台会话身份与凭据分离,续期不再中断在途生成
- 原生会话快照拆成身份代次与写入 revision,安装与清除按 revision 排序、按身份归属判定
- 冻结平台会话校验改为只比身份,同账号 access token 轮换不再让在途生成、编辑、上传、确认和下载失败
- 同一身份代次禁止更换登录主体或服务 origin,换号、退出和 origin 变化继续失败关闭
- Runner attach 新增 platform_auth_revision,平台会话安装与校验按 revision 排序、按身份归属判定
- GUI owner 替换与清除改为计数器只增不减,同一主体重装保持身份代次,避免旧 epoch 迟到写入复活
- renderer 拆分平台原生身份代次与写入 revision,同账号续期只更新凭据,不推进身份代次
- 刷新失败结果新增权威失效判定,只有服务端明确 401 与 403 才清除本地会话,网络错误和 5xx 保留会话
- 收敛刷新判据,重复的刷新工具函数合并为一个对外判定,去掉一次性包装
- 补齐平台会话身份判据、续期与迟到写入的 Rust 与前端回归用例
2026-09-16 17:08:46 +08:00
k88936 2a75a2d2c8 回退资源 kind 绑定断言的误改
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (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 shard 2/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Has been cancelled
ts-rs 导出内容是双引号,测试重新生成后再校验,恢复双引号断言。
2026-09-16 17:05:03 +08:00
k88936 512f669f57 修正资源引用入口的源码断言
正则改为匹配 parseGameCreationAppAssetKind(selectedResource.subtype) 的新调用形状。
2026-09-16 16:59:51 +08:00
kdletters 737a2266b9 支持同 AppData 多窗口共享 Agent Runner
- owner 独占锁改为可多窗口同时持有的界面参与锁 agent-runner.gui-participant.lock
- Runner 启动检查、attach 门禁与 watchdog 改用参与锁存活判定,最后一个窗口退出才关停 Runner
- 窗口启动只采纳现有 owner claim,登录/refresh/退出/换号才发布新 epoch claim
- 同 claim 重复 attach 改为幂等空操作,不再清空 Runner 平台登录态
- manifest 失效与 Runtime update relay 接收端改为按 token 去重的注册表并广播,失败只淘汰该接收端
- GUI 退出先释放参与锁,仍有其它窗口时保留 Runner 并记录 retained_for_other_windows
- Runner 启动失败时按最新 endpoint 复用一次,避免两窗口同时冷启动被实例锁竞争误报
- 更新 runner 定向测试、GUI 生命周期集成测试与自检用例
- 同步 AGC 技术方案、决策记录、踩坑记录与多窗口里程碑/实施计划
2026-09-16 16:56:21 +08:00
k88936 21dbbb4c3c 清理资源桥接测试的旧 kind fixture
将有效分类投影用例的资源 kind 改为 UiDesign 枚举与 ui-design 断言。

移除遗留 ui 字符串 fixture,使 AGC 壳测试恢复编译。
2026-09-16 16:53:27 +08:00
lhk229 262deaf9b7 接入做成游戏后的策划文档指令 (#383)
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 4m46s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 5m22s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m16s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 4m17s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 4m14s
Project CI / AI game creator shell Rust crates (push) Successful in 4m1s
Project CI / Frontend tests (push) Successful in 4m32s
Project CI / Backend tests (push) Successful in 7m54s
Project CI / Repository checks (push) Successful in 4m19s
Project CI / AI game creator shell web tests (push) Failing after 4m9s
Project CI / Native shell tests (push) Successful in 9m12s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 5m35s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 5m39s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 6m3s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 6m9s
Project CI / Backend tests (pull_request) Failing after 19s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m40s
Project CI / Repository checks (pull_request) Failing after 9s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m16s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m32s
Project CI / Frontend tests (pull_request) Successful in 6m10s
Project CI / Native shell tests (pull_request) Successful in 8m50s
## 变更说明

点击策划阶段的“做成游戏”后,切换到 GameAgent 并自动发送读取 `design_artifacts/` 文档的指令。

## 验证

- npm run typecheck
- git diff --check
- 提交钩子 ESLint / Prettier

Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/383
2026-09-16 16:51:37 +08:00
kdletters c36a5170f8 AGC 新增本地自定义 LLM 配置与展示模型勾选
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 5m28s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 5m29s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 6m6s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 6m6s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m46s
Project CI / AI game creator shell Rust crates (push) Successful in 2m51s
Project CI / Frontend tests (push) Successful in 4m44s
Project CI / Native shell tests (push) Successful in 6m31s
Project CI / Repository checks (push) Successful in 4m17s
Project CI / Backend tests (push) Successful in 7m48s
Project CI / AI game creator shell web tests (push) Successful in 3m43s
- 本地配置新增 llm.customEnabled 与 llm.visibleModels,显式开启后保留自定义连接和勾选列表,官方路由下仍清空凭据
- 配置迁移始终写出 customEnabled、visibleModels、apiKey、baseUrl、model、apiKind、reasoningEffort,便于手写自定义连接
- 新增 discover_game_creator_llm_models 命令,直连自定义端点 GET /models,限制超时与响应大小并脱敏错误
- codex_app_server 自定义模式改用配置里的地址与 Key 走本地凭据代理,不再经过平台 /api/llm,也不回退官方路由
- 模型目录按官方与自定义来源隔离缓存,自定义模式只展示勾选模型,选项失效时回退第一项
- 设置页在自定义模式展示 API 地址、API Key、协议与推理档,并提供模型读取、搜索、勾选与已勾选预览
- 补齐固定项排版与读取按钮样式,同步 AGC 模型选择规范、实施计划与共享概览
2026-09-16 16:39:15 +08:00
kdletters 9663bbf911 修复 AGC 客户端登录失败原因提示
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 6m7s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 6m13s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 6m32s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 6m38s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m38s
Project CI / AI game creator shell Rust crates (push) Successful in 2m29s
Project CI / Frontend tests (push) Successful in 4m42s
Project CI / Repository checks (push) Successful in 4m2s
Project CI / Native shell tests (push) Successful in 7m45s
Project CI / Backend tests (push) Successful in 8m39s
Project CI / AI game creator shell web tests (push) Successful in 3m0s
- clientAuth 错误响应解析兼容未带 envelope 的旧形态错误体,密码错误时显示接口返回原因而不是固定“登录失败”
- 非 JSON 与结构无法识别的响应继续使用本地化兜底,不回显内部英文和原始 JSON
- appSurface 认证用例补充 envelope 与旧形态两条回归测试
2026-09-16 16:05:59 +08:00
kdletters b3e9d0a906 发布 AGC 标准版 0.1.45
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 5m11s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 5m39s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m54s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 4m35s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 4m26s
Project CI / AI game creator shell Rust crates (push) Successful in 3m17s
Project CI / Frontend tests (push) Successful in 4m41s
Project CI / Repository checks (push) Successful in 3m53s
Project CI / Native shell tests (push) Successful in 6m43s
Project CI / Backend tests (push) Successful in 7m26s
Project CI / AI game creator shell web tests (push) Successful in 3m23s
- apps/ai-game-creator-shell/package.json 版本提升到 0.1.45
- apps/ai-game-creator-shell/src-tauri/tauri.conf.json 版本提升到 0.1.45
- apps/ai-game-creator-shell/src-tauri/Cargo.toml 版本提升到 0.1.45
- apps/ai-game-creator-shell/src-tauri/Cargo.lock 同步 genarrative-ai-game-creator-shell 版本
- package-lock.json 同步 apps/ai-game-creator-shell 工作区版本
2026-09-16 11:52:03 +08:00
kdletters a60328623d 调整开发调度管线备份策略
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 5m30s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m12s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 5m36s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 5m2s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 4m52s
Project CI / AI game creator shell Rust crates (push) Successful in 2m7s
Project CI / Frontend tests (push) Successful in 4m45s
Project CI / Repository checks (push) Successful in 4m42s
Project CI / Backend tests (push) Successful in 6m58s
Project CI / Native shell tests (push) Successful in 8m22s
Project CI / AI game creator shell web tests (push) Successful in 4m5s
恢复 Full Job 备份参数原样透传
开发调度统一传入 skip,默认不上传 OSS
保持 Full 与 AGC 使用同一固定 revision 参数
2026-09-16 11:43:22 +08:00
kdletters 9e63b76991 修正开发环境备份默认映射
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 5m7s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 4m56s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 5m37s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 4m45s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m18s
Project CI / AI game creator shell Rust crates (push) Successful in 2m48s
Project CI / Repository checks (push) Failing after 1m54s
Project CI / Frontend tests (push) Successful in 5m15s
Project CI / Backend tests (push) Failing after 7m46s
Project CI / AI game creator shell web tests (push) Failing after 4m21s
Project CI / Native shell tests (push) Successful in 8m53s
保留备份参数原有枚举顺序以通过运维门禁
开发目标未显式选择时将 async 映射为 skip
开发环境默认不上传 OSS,release 仍保留显式策略
2026-09-16 11:40:22 +08:00
kdletters a98ebcf68f 调整开发环境默认备份策略
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 5m30s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 5m35s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 5m55s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 6m0s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m4s
Project CI / AI game creator shell Rust crates (push) Successful in 2m33s
Project CI / Repository checks (push) Failing after 1m16s
Project CI / Frontend tests (push) Successful in 4m59s
Project CI / AI game creator shell web tests (push) Successful in 3m28s
Project CI / Native shell tests (push) Successful in 7m12s
Project CI / Backend tests (push) Successful in 8m33s
开发环境默认跳过 OSS 备份上传
需要冷备时显式选择 async
保持 release 发布的备份策略可配置
2026-09-16 11:37:56 +08:00
kdletters 576ff07a5e 新增定时版本调度管线并收回两条下游管线的定时与去重
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 5m9s
Project CI / AI game creator shell Rust shard 1/4 (push) Failing after 5m11s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 5m34s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m1s
Project CI / AI game creator shell Rust crates (push) Successful in 2m36s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 4m20s
Project CI / Frontend tests (push) Successful in 4m23s
Project CI / Repository checks (push) Successful in 4m13s
Project CI / Native shell tests (push) Successful in 6m44s
Project CI / Backend tests (push) Successful in 8m22s
Project CI / AI game creator shell web tests (push) Successful in 3m28s
- 新增 Genarrative-Scheduled-Revision-Trigger:每小时用 git ls-remote 解析分支版本,与上一次触发过的 revision 不同才继续
- 触发时把同一个固定 COMMIT_HASH 同时传给 Full Build 与 AGC Windows Build,保证两条管线构建同一个版本
- Full Build 与 AGC Windows Build 去掉自带定时触发器与管线内版本去重
- 新增 scheduled-revision-trigger Job 配置 XML,使用本机 Git 入口与既有 SSH 凭据,定时器留在 Jenkinsfile
- 生产运维门禁改为校验调度管线约束,并拦住两条下游管线重新加 cron 或自行去重
- 同步开发运维文档、共享开发工作流与踩坑记录
2026-09-16 10:55:16 +08:00
kdletters 5aa616134c 修复 Hook 链继承仓库定位变量导致真实仓库被夹具污染
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 5m30s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 5m47s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 5m53s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 6m8s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m48s
Project CI / AI game creator shell Rust crates (push) Successful in 2m46s
Project CI / Frontend tests (push) Failing after 4m6s
Project CI / Repository checks (push) Successful in 3m57s
Project CI / Native shell tests (push) Successful in 7m40s
Project CI / Backend tests (push) Successful in 8m27s
Project CI / AI game creator shell web tests (push) Successful in 12m53s
- .husky/pre-commit 与 .husky/pre-push 入口清除 GIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILE 等仓库定位变量

- scripts/check-repository-ci.sh 增加同样的清理,保证本地门禁与 CI 走同一套隔离

- scripts/git-hooks.test.mjs 夹具 Git 调用前自检仓库归属并禁用 Hook,命令落到外部仓库时直接失败

- 守卫用例的子进程必须真的继承 GIT_DIR,避免隔离断言空转

- 更新开发运维文档与 shared-memory/pitfalls.md,记录链接工作树注入形态与配置修复步骤
2026-09-16 10:35:07 +08:00
kdletters 187b66c3b1 Jenkins 定时构建增加版本未变化去重
- Full Build 改为每小时触发,并按已解析 commit 与全部构建参数判断是否重复
- AGC Windows Build 增加每小时触发与同一套去重逻辑,跳过时不再安装依赖、构建和上传
- 跳过记录为 NOT_BUILT,越过跳过记录后最近一次实际构建成功才允许跳过,失败或中断可重试
- 生产运维门禁补充两个 Job 的去重约束与各阶段跳过检查
- 同步开发运维文档与共享开发工作流说明
2026-09-16 10:32:03 +08:00
k88936 55d0c99e2b 修正资源 kind 转义测试期望
转义后的 ui-design 应解析为 UiDesign 枚举。
2026-09-15 20:57:14 +08:00
k88936 3907a8bf74 补齐资源 kind 字符串字段转换
资源编辑请求中的 sourceSubtype 统一从 AssetKind 显式转换字符串。
2026-09-15 20:52:53 +08:00
k88936 46dfa2cffb 修复资源枚举迁移后的测试类型错误
将字体、角色和资源图 fixture 改为 AssetKind 枚举。

将资源 kind 回传到字符串字段时显式使用 to_string。
2026-09-15 20:45:55 +08:00
k88936 97e2b26900 简化资源引用 kind 来源
引用资源时直接解析已存在的 subtype,移除无效 category 回退。
2026-09-15 20:14:13 +08:00
k88936 39e33f7a5b 更新上传资源 kind 枚举测试
按 Font 与 Unknown 返回值修正上传类型断言。

使用枚举字符串视图校验 canonical kind 集合。
2026-09-15 20:01:20 +08:00
k88936 040f646480 统一 ts-rs workspace 依赖版本
在 server-rs workspace 集中声明 ts-rs 版本。

shared-contracts 通过 workspace 依赖引用可选 ts-rs。
2026-09-15 19:53:06 +08:00
k88936 8b007c637d 修正图集切片读取侧枚举判断
恢复校验和测试筛选统一识别 Icon 资源 kind。

部分登记探测限定在图集切片路径,避免误判普通图标。
2026-09-15 19:48:48 +08:00
k88936 f0a62c6306 迁移资源桥接测试 fixture 到枚举
将图集切片、角色动画和规格资源 fixture 改用对应 AssetKind 变体。
2026-09-15 19:45:51 +08:00
k88936 30edfee00e 修复 Codex 资源引用测试 fixture
将 manifest 资源 kind 字符串 fixture 迁移为 Character 枚举。
2026-09-15 19:41:41 +08:00
k88936 c2da8e184e 修正 UI 工作流源资源枚举校验
工作流源图校验与 fixture 统一使用 UiDesign。

更新失败提示中的 manifest kind 名称。
2026-09-15 19:36:29 +08:00
k88936 a19319d815 修正 UI 原型资源桥接枚举使用
生产校验与测试 fixture 改用 GameCreationAppAssetKind::UiDesign。

保留 ui-prototype 作为本地文件路径标识。
2026-09-15 19:35:04 +08:00
k88936 d07ea186fa 收紧资源 kind 测试专用导入
仅在测试构建中导入 GameCreationAppAssetKind,避免正式构建产生未使用警告。
2026-09-15 19:33:13 +08:00
k88936 80caec373a 修正资源 kind 绑定测试断言
按 ts-rs 生成的单引号联合成员校验绑定内容。
2026-09-15 19:32:12 +08:00
k88936 3caefd6716 修复资源 kind 转义字符串反序列化
为 AssetKindVisitor 增加 visit_string 转发。

补充 JSON 转义 kind 的反序列化回归测试。
2026-09-15 19:31:21 +08:00
k88936 4d0dd2d721 收紧 UI 资源桥接的 manifest kind
UI 原型与文档链接改用生成枚举边界
删除 ui-prototype 的主动 alias 使用并更新测试清单
2026-09-15 18:06:27 +08:00
k88936 2befaaef9d 接入 AGC shell TS 资源 kind 边界
新增消费 Rust 生成绑定的未知值解析模块
迁移资源引用与画布引用入口并补充日志测试
更新逐项迁移清单与实施计划
2026-09-15 18:00:34 +08:00
k88936 20b0bf4dd7 补充 AGC manifest 未知 kind 验收
验证旧值读取为 Unknown 且序列化为 unknown
明确不执行 alias 迁移
2026-09-15 17:50:33 +08:00
k88936 b4fd8d9b8b 收紧 AGC manifest 资源 kind 字段
将 manifest 条目 kind 改为 Rust 枚举并把未知值收口为 Unknown
修正字体上传与图集切片的正式资源 kind
保持 source.kind 与 generationKind 等其他字符串字段不变
2026-09-15 17:37:28 +08:00
k88936 fe85fa2a62 建立 AGC 资源 kind Rust 枚举与 TypeScript 绑定
新增 GameCreationAppAssetKind 及 Unknown 解析边界
通过 ts-rs 生成 shell 专属 TypeScript 联合类型
补充未知值上下文日志与 Rust/绑定生成测试
2026-09-15 16:20:52 +08:00
k88936 b14a533b17 新增AGC资源kind绑定实施计划
限定首个代码切片为 Rust enum 与 ts-rs 生成边界。

明确不修改 manifest 字段和其他资源字符串领域。

列出绑定、serde round-trip 与定向验证门禁。
2026-09-15 15:29:37 +08:00
k88936 42969b2218 细化AGC资源kind构造器审计
补充 Rust manifest 构造器逐处迁移清单。

补充 shell TypeScript 裸 assetKind 消费方清单。

区分图集、工作流和 manifest kind 的处理边界。
2026-09-15 15:24:46 +08:00
k88936 c4ee75a1af 补充AGC资源kind词汇审计清单
记录 manifest kind 的 Rust 写入边界与 TypeScript 消费方。

区分资源 kind 与 source/generation/workflow 等字符串领域。

标注 font、audio、sound-effect、background-music 及旧值的生产状态。

通过文档索引、编码与 diff 检查。
2026-09-15 15:19:58 +08:00
k88936 6f2137cbb0 补充AGC资源kind枚举化规范
新增资源 kind 枚举化主规范与边界。

新增词汇审计、Rust 绑定、写入清理三个里程碑。

新增 shell-only 资源 kind 扫描 todo 清单。

通过文档索引、编码与 diff 检查。
2026-09-15 15:12:24 +08:00
k88936 70a2246e8b 移除 UI 文档引用新增测试
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 4m30s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 4m59s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 4m24s
Project CI / Backend tests (pull_request) Failing after 11s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m50s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 4m28s
Project CI / Repository checks (pull_request) Failing after 13s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m7s
Project CI / AI game creator shell web tests (pull_request) Failing after 3m22s
Project CI / Frontend tests (pull_request) Failing after 3m51s
Project CI / Native shell tests (pull_request) Successful in 7m4s
删除 Direct Codex UI 文档 fixture 与专门测试用例

保留生产引用逻辑和现有业务测试
2026-09-14 21:41:53 +08:00
k88936 b7e3dac661 统一引用 UI 文档资产类型常量
前端入口、资源桥接与引用预览统一使用共享常量

Rust 持久化与测试复用共享契约常量

补齐 UI 编辑器入口 fixture 的新资产类型
2026-09-14 21:36:43 +08:00
k88936 2e4a1996c8 同步 UI 编辑器入口测试资产类型
将项目开发 UI 编辑器回程测试 fixture 更新为 ui-design-doc
2026-09-14 21:12:03 +08:00
k88936 fe4e952853 更新 UI 文档资源编辑器入口
将 ui-design-doc 资源纳入选中资源的 UI 编辑器按钮判定

保留 ui-prototype 图片资源的编辑器桥接入口
2026-09-14 21:04:08 +08:00
k88936 18654b6806 同步 UI 文档资产共享契约与前端识别
共享契约新增 ui-design-doc 分类及中英文映射

前端编辑器与资源桥接严格识别文档资产

更新技术方案、实施计划和长期决策记录
2026-09-14 20:45:04 +08:00
k88936 ee7d00c0b1 统一 UI 编辑器文档资产类型
新增 ui-design-doc 与 application/json 常量

创建、桥接、工作流和持久化统一使用严格资产类型
2026-09-14 20:44:49 +08:00
k88936 9cd1a94369 接入 UI 文档引用代码上下文生成
Direct Codex 引用 ui-design-doc 时生成带文档的 JS 文件

生成成功或失败信息追加到当前 prompt

补充成功、失败与 ui-prototype 隔离测试
2026-09-14 20:44:49 +08:00
k88936 470e85c0ff Add UI-specific prompt rendering for design assets in resource references 2026-09-14 20:44:49 +08:00
159 changed files with 7150 additions and 3791 deletions
+1
View File
@@ -164,6 +164,7 @@ module.exports = {
'server-rs/target-*',
'apps/desktop-shell/src-tauri/target',
'apps/ai-game-creator-shell/src/features/ui-editor/types/**',
'apps/ai-game-creator-shell/src/contracts/generated/**',
'apps/ai-game-creator-shell/src/features/project-workspace/generated/**',
'target',
'src/main.tsx',
+3
View File
@@ -1 +1,4 @@
# Git 在链接工作树里执行 Hook 时会注入 GIT_DIR 等仓库定位变量,优先级高于 cwd;
# 子进程(npm、lint-staged、测试夹具)会继承它们并写到真实仓库,故在入口统一清除。
unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_COMMON_DIR GIT_PREFIX GIT_CONFIG_PARAMETERS GIT_CEILING_DIRECTORIES
npm run format:staged
+3
View File
@@ -1 +1,4 @@
# Git 在链接工作树里执行 Hook 时会注入 GIT_DIR 等仓库定位变量,优先级高于 cwd;
# 钩子链(npm → check:repository-ci → 测试夹具)会继承它们并写到真实仓库,故在入口统一清除。
unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_COMMON_DIR GIT_PREFIX GIT_CONFIG_PARAMETERS GIT_CEILING_DIRECTORIES
npm run check:pre-push-master -- "$@"
+2
View File
@@ -4,6 +4,8 @@ node_modules
.codex-logs
public/Icons
apps/ai-game-creator-shell/src/features/ui-editor/types/
# ts-rs 直接落盘的生成产物:保持生成器原始格式,`cargo test export_bindings` 才可幂等复现。
apps/ai-game-creator-shell/src/contracts/generated/
apps/ai-game-creator-shell/src-tauri/resources/agc-skills/
media
*.log
-16
View File
@@ -172,20 +172,6 @@ _Avoid_: 多步骤向导、完整规则编辑器、拖拽编辑器
Bark Battle 平台作品闭环按契约与领域规则、后端存储/API、最小前端纵切、投影体验、收口验证的顺序推进。
_Avoid_: mock 先行堆积、前后端各自发散、先做排行榜 UI
## 项目开发对话(DirectProject
**项目对话历史**:
AGC 本地项目内 Codex 原始对话条目的持久集合,是聊天展示、工具卡片和线程恢复注入的唯一持久事实源。
_Avoid_: 会话缓存、展示态历史、按 UI 需要另存的对话副本
**运行态事件**:
Thread Manager 向订阅者推送的当前回合原始事件流,只服务运行期间与短期断线恢复,不替代项目对话历史。
_Avoid_: 进度通知、快照轮询、第二套历史
**聊天投影**:
把项目对话历史条目与运行态事件转换成消息气泡和工具卡片的读取期转换;不持久化,也不构成事实源。
_Avoid_: 投影缓存文件、已脱敏卡片库、第二套 reducer
## Relationships
- 一个 **汪汪声浪大作战** 单局包含多个 **有效声浪触发**
@@ -220,5 +206,3 @@ _Avoid_: 投影缓存文件、已脱敏卡片库、第二套 reducer
- “入口闭环”曾可能只指内部 demo 或单个详情 CTA;已解析为 **正式作品入口闭环**,不新增独立专区或活动页。
- “创作编辑”曾可能指多步骤向导或完整编辑器;已解析为 **轻配置编辑流程**,使用单页表单 + 预览卡片完成保存草稿、发布和发布后跳转作品详情。
- “实施顺序”曾可能按 UI 或功能并行发散;已解析为契约/领域规则先行,再做后端存储/API,随后打通最小前端纵切,最后补投影体验与收口验证。
- “回合进度事件”曾同时指 Direct turn update 与 Thread Manager 运行态事件;已解析为 AGC 项目开发对话只保留 **运行态事件**
- “哪些消息可显示”曾可能由后端历史分页判断;已解析为可见性判断属于 **聊天投影**,后端只按原始条目分页,前端负责跳过不可显示条目并推进分页锚点。
@@ -2,6 +2,8 @@
"schemaVersion": "game-creator-config.v2",
"agentMode": "codex_app_server",
"llm": {
"customEnabled": false,
"visibleModels": [],
"apiKey": "",
"baseUrl": "https://dev.genarrative.world/gpt/v1",
"model": "gpt-6-astra",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@genarrative/ai-game-creator-shell",
"private": true,
"version": "0.1.29",
"version": "0.1.47",
"type": "module",
"scripts": {
"dev": "node scripts/start-tauri-dev.mjs",
+3 -1
View File
@@ -1725,7 +1725,7 @@ dependencies = [
[[package]]
name = "genarrative-ai-game-creator-shell"
version = "0.1.29"
version = "0.1.47"
dependencies = [
"agent-runtime-core",
"axum",
@@ -4900,6 +4900,8 @@ dependencies = [
"serde",
"serde_json",
"sha2",
"tracing",
"ts-rs",
]
[[package]]
@@ -1,6 +1,6 @@
[package]
name = "genarrative-ai-game-creator-shell"
version = "0.1.29"
version = "0.1.47"
edition = "2021"
publish = false
@@ -14,7 +14,7 @@ cocos-editor-injection = ["cocos-editor-execute", "cocos-editor-bridge/windows-i
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
shared-contracts = { path = "../../../server-rs/crates/shared-contracts", default-features = false }
shared-contracts = { path = "../../../server-rs/crates/shared-contracts", default-features = false, features = ["ts-bindings"] }
tauri-build = { version = "2.6.2", features = [] }
[dependencies]
@@ -50,7 +50,7 @@ portable-pty = "0.9"
percent-encoding = "2"
reqwest = { version = "0.12", default-features = false, features = ["json", "multipart", "native-tls", "stream"] }
regex = "1"
shared-contracts = { path = "../../../server-rs/crates/shared-contracts", default-features = false }
shared-contracts = { path = "../../../server-rs/crates/shared-contracts", default-features = false, features = ["ts-bindings"] }
tauri = { version = "2.11.2", features = [] }
tauri-plugin-dialog = "2.7.1"
tauri-plugin-http = { version = "2.5.9", default-features = false, features = ["charset", "cookies", "http2", "rustls-tls"] }
@@ -2,7 +2,7 @@
{"type":"function","function":{"name":"get_workflow_status","description":"读取当前策划工作流状态,只返回阶段列表、当前阶段、已批准阶段和待审批阶段;不推进阶段、不提交审批、不修改文件。","parameters":{"type":"object","properties":{},"additionalProperties":false}}},
{"type":"function","function":{"name":"list_resources","description":"列出固定资源的逻辑目录、资源 ID、标题和简介。资源是只读的随包文档;不要猜测物理路径。","parameters":{"type":"object","properties":{},"additionalProperties":false}}},
{"type":"function","function":{"name":"read_resource","description":"读取一份固定资源文档全文。每次读取一个 resource_id;资源只读。读到未实现占位文档时由你自行判断和处理。","parameters":{"type":"object","properties":{"resource_id":{"type":"string"}},"required":["resource_id"],"additionalProperties":false}}},
{"type":"function","function":{"name":"patch_file","description":"局部修改 UTF-8 文件。使用 old_text/new_text,或使用 edits 一次进行多个独立替换;每个 old_text 必须非空且在原文件中唯一,匹配失败、重复或范围重叠时不修改文件。path 使用相对路径。","parameters":{"type":"object","properties":{"path":{"type":"string"},"old_text":{"type":"string"},"new_text":{"type":"string"},"edits":{"type":"array","items":{"type":"object","properties":{"old_text":{"type":"string"},"new_text":{"type":"string"}},"required":["old_text","new_text"],"additionalProperties":false}}},"required":["path"],"additionalProperties":false}}},
{"type":"function","function":{"name":"patch_file","description":"局部修改 UTF-8 文件。使用 old_text/new_text,或使用 edits 一次进行多个独立替换;每个 old_text 必须非空且在原文件中唯一。所有 edit 会一次性校验;任何失败都不修改文件,错误会列出各失败项及可唯一匹配的其余项。path 使用相对路径。","parameters":{"type":"object","properties":{"path":{"type":"string"},"old_text":{"type":"string"},"new_text":{"type":"string"},"edits":{"type":"array","items":{"type":"object","properties":{"old_text":{"type":"string"},"new_text":{"type":"string"}},"required":["old_text","new_text"],"additionalProperties":false}}},"required":["path"],"additionalProperties":false}}},
{"type":"function","function":{"name":"delete_path","description":"谨慎使用;永久删除工作区内的文件或目录;目录会连同全部内容递归删除,不备份。先确认目标及删除范围。path 使用相对路径,不能删除工作区根目录,也不能经过链接。","parameters":{"type":"object","properties":{"path":{"type":"string"}},"required":["path"],"additionalProperties":false}}},
{"type":"function","function":{"name":"list_dir","description":"列出工作目录内的文件和目录。path 使用相对路径。","parameters":{"type":"object","properties":{"path":{"type":"string"}},"required":["path"],"additionalProperties":false}}},
{"type":"function","function":{"name":"read_file","description":"读取工作目录内的 UTF-8 文本文件。path 使用相对路径。","parameters":{"type":"object","properties":{"path":{"type":"string"}},"required":["path"],"additionalProperties":false}}},
@@ -21,7 +21,6 @@ mod direct_project_history;
mod direct_project_turn_history;
mod direct_runtime;
mod direct_thread_manager;
mod direct_thread_wire;
mod direct_tool_bridge;
mod direct_tool_calls;
mod direct_tools_mcp;
@@ -56,7 +55,6 @@ pub(crate) use direct_project_history::*;
pub(crate) use direct_project_turn_history::*;
pub(crate) use direct_runtime::*;
pub(crate) use direct_thread_manager::*;
pub(crate) use direct_thread_wire::*;
pub(crate) use direct_tool_bridge::*;
pub(crate) use direct_tool_calls::*;
pub(crate) use direct_tools_mcp::*;
@@ -131,7 +131,6 @@ impl CodexAppServerCredential {
) -> Option<(&'a str, &'a str)> {
match self {
Self::PlatformSession { .. } => None,
#[cfg(test)]
Self::AppDataKey { .. } => (!llm.api_key.trim().is_empty())
.then_some((llm.base_url.trim_end_matches('/'), llm.api_key.trim())),
#[cfg(test)]
@@ -139,7 +138,7 @@ impl CodexAppServerCredential {
.as_deref()
.map(|api_key| (GAME_CREATOR_CODEX_AUTH_BRIDGE_API_BASE_URL, api_key)),
#[cfg(not(test))]
Self::AppDataKey { .. } | Self::AuthBridge { .. } => None,
Self::AuthBridge { .. } => None,
}
}
}
@@ -196,6 +195,20 @@ impl CodexAppServerStderrSummary {
}
}
/// 派发 app-server 的收尾与中断任务。
///
/// 这个入口会被**没有 tokio runtime 上下文的线程**调用:`cancel_direct_codex_turn`
/// 是同步 Tauri 命令,直接跑在 IPC 回调线程(Windows 上是 WebView2 的 UI 线程);
/// [`CodexThreadLease`] 与 [`CodexTurnGuard`] 的 `Drop` 也在调用方线程上执行。
/// `tokio::spawn` 在那样的线程上会经 `Handle::current()` panic"there is no reactor
/// running"),而 panic 跨不过 Tauri 的 IPC 回调边界,整个进程会以 `0xC0000409`
/// FAST_FAIL_FATAL_APP_EXITabort——现场就是"点终止,App 闪退"2026-09-16 的 WER
/// 记录:`genarrative-ai-game-creator-shell.exe`,异常代码 `0xc0000409`fail-fast
/// 参数 `7`)。一律走 Tauri 的全局异步 runtime`main` 已把深栈 runtime 装进去。
fn spawn_codex_app_server_task(task: impl std::future::Future<Output = ()> + Send + 'static) {
tauri::async_runtime::spawn(task);
}
struct CodexTurnStartCancellation {
inner: Weak<CodexAppServerInner>,
thread_id: String,
@@ -257,7 +270,7 @@ impl CodexTurnStartCancellation {
};
let connection = CodexAppServerConnection { inner };
let thread_id = self.thread_id.clone();
tokio::spawn(async move {
spawn_codex_app_server_task(async move {
let _ = connection
.request(
"turn/interrupt",
@@ -564,17 +577,6 @@ enum CodexTurnEvent {
item_id: String,
delta: String,
},
/// 思考正文增量:app-server `item/reasoning/summaryTextDelta` 的明文思考文本。
///
/// `item/reasoning/summaryTextDelta`core `ReasoningContentDelta`)与
/// `item/reasoning/textDelta`core `ReasoningRawContentDelta`)都进这条通道:前者是
/// reasoning item 的 `summary`,后者是它的 `content`,两段文本都随 `item/completed`
/// 落进 `project.jsonl`、此前也已经在完成时展示给用户。plan 文本与命令输出仍然只降级为
/// 活动状态,不下发正文。
ReasoningDelta {
item_id: String,
delta: String,
},
IntermediateText(String),
Activity(&'static str),
Item {
@@ -582,7 +584,7 @@ enum CodexTurnEvent {
params: serde_json::Value,
},
Request {
kind: DirectThreadRequestKind,
event_type: &'static str,
params: serde_json::Value,
},
RawItem(serde_json::Value),
@@ -752,14 +754,23 @@ fn direct_codex_safe_activity_for_item_value(item: &serde_json::Value) -> &'stat
direct_codex_safe_activity_for_item(item_type)
}
/// 运行态事件载荷:与历史切片同形的脱敏原始条目;拿不到身份或类型就整条跳过。
///
/// 这里不生成工具卡片形状:标题、折叠摘要和可见性都是前端投影的职责。
fn direct_thread_event_item(
root: &std::path::Path,
item: &serde_json::Value,
) -> Option<DirectThreadItem> {
direct_thread_item_from_value(root, item, direct_tool_call_now_ms())
/// Project an app-server item into the small public payload carried by the
/// DirectProject event queue. Full item contents are persisted in JSONL and
/// must not be forwarded through the runtime event stream.
fn direct_thread_item_started_payload(item: &serde_json::Value) -> serde_json::Value {
serde_json::json!({
"itemType": item
.get("type")
.and_then(serde_json::Value::as_str)
.unwrap_or("unknown"),
})
}
fn direct_thread_item_id(item: &serde_json::Value) -> Option<String> {
item.get("id")
.and_then(serde_json::Value::as_str)
.filter(|value| !value.is_empty())
.map(str::to_string)
}
fn direct_codex_command_is_game_verification(command: &str) -> bool {
@@ -976,21 +987,19 @@ fn direct_codex_safe_activity_for_notification(method: &str) -> Option<&'static
}
}
fn direct_codex_request_event_type(method: &str) -> Option<DirectThreadRequestKind> {
fn direct_codex_request_event_type(method: &str) -> Option<&'static str> {
match method {
"item/fileChange/requestApproval"
| "item/commandExecution/requestApproval"
| "item/permissions/requestApproval" => Some(DirectThreadRequestKind::ApprovalRequested),
"item/tool/requestUserInput" | "item/mcpToolCall/requestUserInput" => {
Some(DirectThreadRequestKind::AskRequested)
}
| "item/permissions/requestApproval" => Some("approval.requested"),
"item/tool/requestUserInput" | "item/mcpToolCall/requestUserInput" => Some("ask.requested"),
_ => None,
}
}
fn direct_codex_resolution_event_type(method: &str) -> Option<DirectThreadRequestKind> {
fn direct_codex_resolution_event_type(method: &str) -> Option<&'static str> {
match method {
"serverRequest/resolved" => Some(DirectThreadRequestKind::RequestResolved),
"serverRequest/resolved" => Some("request.resolved"),
_ => None,
}
}
@@ -1063,33 +1072,6 @@ fn direct_codex_notification_event(
intermediate_text: Option<String>,
safe_activity: Option<&'static str>,
) -> Option<CodexTurnEvent> {
// 思考正文走独立通道,交给 DirectProject 的运行态事件;它不因为
// "preparing 活动" 的降级规则被丢掉,否则界面只能等 item/completed 才看到思考。
//
// 两条通知都下发正文,不下发活动文本:
// - `item/reasoning/summaryTextDelta`core `ReasoningContentDelta`)→ reasoning item 的 `summary`
// - `item/reasoning/textDelta`core `ReasoningRawContentDelta`)→ reasoning item 的 `content`
// 正是 `project.jsonl` 里保存、并在此前 `item/completed` 已经展示给用户的同一段文本。
// 因此这里只是把"完成时才看到"提前为"边生成边看到",没有放宽可见文本的范围;
// 未识别的 plan 文本与命令输出仍然只降级为活动状态,不下发正文。
if matches!(
method,
"item/reasoning/summaryTextDelta" | "item/reasoning/textDelta"
) {
return params
.get("delta")
.and_then(serde_json::Value::as_str)
.filter(|value| !value.is_empty())
.map(|delta| CodexTurnEvent::ReasoningDelta {
item_id: params
.get("itemId")
.and_then(serde_json::Value::as_str)
.filter(|value| !value.is_empty())
.map(str::to_string)
.unwrap_or_else(|| "direct-missing-item".to_string()),
delta: delta.to_string(),
});
}
let (activity, intermediate_text) = match (&intermediate_text, safe_activity) {
(Some(_), Some(activity)) if activity == "preparing" => (Some(activity), None),
_ => (safe_activity, intermediate_text),
@@ -2049,7 +2031,13 @@ impl CodexAppServerConnection {
let codex_cli_version = game_creator_codex_cli_version_identity()
.map_err(platform_llm::LlmError::InvalidConfig)?;
let mut effective_llm = llm.clone();
let credential = if game_creator_official_llm_route_locked() {
let credential = if llm.custom_enabled {
crate::config::validate_custom_llm_connection(llm)
.map_err(platform_llm::LlmError::InvalidConfig)?;
CodexAppServerCredential::AppDataKey {
fingerprint: format!("custom-key:{:x}", Sha256::digest(llm.api_key.as_bytes())),
}
} else if game_creator_official_llm_route_locked() {
let session = current_platform_session().ok_or_else(|| {
platform_llm::LlmError::InvalidConfig(
"authentication-required: 请先登录陶泥儿账号".to_string(),
@@ -2217,7 +2205,8 @@ impl CodexAppServerConnection {
true,
),
_ => (
(workspace_mode == CodexAppServerWorkspaceMode::DirectProject)
(llm.custom_enabled
|| workspace_mode == CodexAppServerWorkspaceMode::DirectProject)
.then(|| credential.direct_provider_route(llm))
.flatten()
.map(|(base_url, api_key)| (base_url.to_string(), api_key.to_string())),
@@ -2957,7 +2946,18 @@ impl CodexAppServerConnection {
turn_start_guard.armed = false;
let direct_thread_id = history_root.to_string_lossy().into_owned();
if self.inner.workspace_mode == CodexAppServerWorkspaceMode::DirectProject {
append_direct_thread_event(&direct_thread_id, DirectThreadEvent::turn_started());
append_direct_thread_event(
&direct_thread_id,
DirectThreadRawEventDraft {
event_type: "turn.started".to_string(),
turn_id: turn_id.clone(),
item_id: None,
payload: serde_json::json!({
"threadId": thread_id,
"turnId": turn_id,
}),
},
);
}
let mut receiver = self.register_turn(&turn_id).await;
let mut direct_project_history = DirectProjectHistoryAccumulator::default();
@@ -3012,13 +3012,12 @@ impl CodexAppServerConnection {
direct_project_history.observe_delta(&item_id, &delta);
append_direct_thread_event(
&direct_thread_id,
// 事件自足:增量自带 item 身份与正文类别(正文 / 思考),
// 前端 reducer 不允许靠猜 itemId 的来源决定 kind。
DirectThreadEvent::item_delta(
item_id.clone(),
DirectThreadDeltaKind::Message,
delta.clone(),
),
DirectThreadRawEventDraft {
event_type: "item.delta".to_string(),
turn_id: turn_id.clone(),
item_id: Some(item_id.clone()),
payload: serde_json::json!({ "delta": delta.clone() }),
},
);
}
streamed_text.push_str(&delta);
@@ -3049,18 +3048,6 @@ impl CodexAppServerConnection {
});
}
}
Some(CodexTurnEvent::ReasoningDelta { item_id, delta }) => {
if self.inner.workspace_mode == CodexAppServerWorkspaceMode::DirectProject {
append_direct_thread_event(
&direct_thread_id,
DirectThreadEvent::item_delta(
item_id,
DirectThreadDeltaKind::Reasoning,
delta,
),
);
}
}
Some(CodexTurnEvent::IntermediateText(text)) => {
if let Some(observer) = direct_observer.as_deref_mut() {
observer(DirectCodexTurnObservation::IntermediateText(text));
@@ -3073,7 +3060,6 @@ impl CodexAppServerConnection {
"rawResponseItem/completed 缺少 item".to_string(),
));
}
let entry_item = direct_thread_event_item(history_root, &item);
let history_root = history_root.to_path_buf();
let history_item = item.clone();
tokio::task::spawn_blocking(move || {
@@ -3087,15 +3073,19 @@ impl CodexAppServerConnection {
})?
.map_err(platform_llm::LlmError::InvalidRequest)?;
direct_project_history.complete_item(&item);
if let Some(entry_item) = entry_item {
append_direct_thread_event(
&direct_thread_id,
DirectThreadEvent::item_completed(entry_item),
);
}
let item_id = direct_thread_item_id(&item);
append_direct_thread_event(
&direct_thread_id,
DirectThreadRawEventDraft {
event_type: "item.completed".to_string(),
turn_id: turn_id.clone(),
item_id,
payload: serde_json::json!({}),
},
);
}
}
Some(CodexTurnEvent::Request { kind, params }) => {
Some(CodexTurnEvent::Request { event_type, params }) => {
if self.inner.workspace_mode == CodexAppServerWorkspaceMode::DirectProject {
let request_id = params
.get("requestId")
@@ -3105,7 +3095,14 @@ impl CodexAppServerConnection {
.map(str::to_string);
append_direct_thread_event(
&direct_thread_id,
DirectThreadEvent::request(kind, request_id),
DirectThreadRawEventDraft {
event_type: event_type.to_string(),
turn_id: turn_id.clone(),
item_id: None,
payload: request_id
.map(|id| serde_json::json!({ "requestId": id }))
.unwrap_or_else(|| serde_json::json!({})),
},
);
}
}
@@ -3216,14 +3213,16 @@ impl CodexAppServerConnection {
&& self.inner.workspace_mode
== CodexAppServerWorkspaceMode::DirectProject
{
if let Some(entry_item) =
direct_thread_event_item(history_root, item)
{
append_direct_thread_event(
&direct_thread_id,
DirectThreadEvent::item_started(entry_item),
);
}
let item_id = direct_thread_item_id(item);
append_direct_thread_event(
&direct_thread_id,
DirectThreadRawEventDraft {
event_type: "item.started".to_string(),
turn_id: turn_id.clone(),
item_id,
payload: direct_thread_item_started_payload(item),
},
);
}
}
}
@@ -3265,7 +3264,12 @@ impl CodexAppServerConnection {
{
append_direct_thread_event(
&direct_thread_id,
DirectThreadEvent::turn_completed(status.to_string()),
DirectThreadRawEventDraft {
event_type: "turn.completed".to_string(),
turn_id: turn_id.clone(),
item_id: None,
payload: serde_json::json!({ "status": status }),
},
);
}
match status {
@@ -3576,7 +3580,7 @@ impl Drop for CodexThreadLease {
let connection = self.connection.clone();
let key = self.key.clone();
let thread_id = self.thread_id.clone();
tokio::spawn(async move {
spawn_codex_app_server_task(async move {
let mut threads = connection.inner.threads.lock().await;
if let Some(entry) = threads.get_mut(&key) {
if entry.thread_id == thread_id {
@@ -3603,7 +3607,7 @@ impl Drop for CodexTurnGuard {
let connection = self.connection.clone();
let thread_id = self.thread_id.clone();
let turn_id = self.turn_id.clone();
tokio::spawn(async move {
spawn_codex_app_server_task(async move {
connection.inner.turns.lock().await.remove(&turn_id);
connection.inner.turn_backlog.lock().await.remove(&turn_id);
let _ = connection
@@ -3960,8 +3964,8 @@ async fn read_game_creator_codex_app_server_stdout(
continue;
}
}
let event = if let Some(kind) = direct_codex_resolution_event_type(method) {
CodexTurnEvent::Request { kind, params }
let event = if let Some(event_type) = direct_codex_resolution_event_type(method) {
CodexTurnEvent::Request { event_type, params }
} else if let Some(activity) = safe_activity {
// Preparing notifications may carry private plan/reasoning text;
// expose only the safe activity category. Other categories may
@@ -4013,8 +4017,8 @@ async fn read_game_creator_codex_app_server_stdout(
),
method if direct_codex_request_event_type(method).is_some() => {
CodexTurnEvent::Request {
kind: direct_codex_request_event_type(method)
.expect("request kind checked above"),
event_type: direct_codex_request_event_type(method)
.expect("request event type checked above"),
params,
}
}
@@ -4499,6 +4503,19 @@ mod tests {
assert!(table.select(&key, None).is_err());
}
/// 终止路径会从同步命令线程和 `Drop` 里派发 app-server 任务:那些线程没有 tokio
/// runtime 上下文。`tokio::spawn` 在那里 panicpanic 跨不过 IPC 回调边界就把整个
/// 进程 abort0xC0000409"点终止就闪退")。这条用例把派发入口钉在没有 runtime
/// 上下文的线程上,回退到 `tokio::spawn` 时它会失败。
#[test]
fn codex_app_server_task_dispatch_needs_no_tokio_runtime_context() {
let joined = std::thread::spawn(|| spawn_codex_app_server_task(async {}));
assert!(
joined.join().is_ok(),
"没有 tokio runtime 上下文的线程也必须能派发 app-server 收尾任务"
);
}
/// 注册键:前端传的项目路径与回合注册时的路径必须归一化成同一个键(Windows 上
/// `canonicalize` 会带 `\\?\` 前缀,去掉后两边才相等)。
#[test]
@@ -4525,29 +4542,11 @@ mod tests {
"arguments": { "path": "game/index.html", "token": "secret" },
"result": { "content": "large output" }
});
// 运行态事件必须自足:载荷是脱敏原始条目,前端不需要再按 itemId 取快照。
let projected = direct_thread_event_item(std::path::Path::new("."), &item).expect("item");
assert_eq!(projected.item_id(), "item-1");
let payload = serde_json::to_value(&projected).expect("payload");
assert_eq!(direct_thread_item_id(&item).as_deref(), Some("item-1"));
assert_eq!(
payload.get("itemType").and_then(serde_json::Value::as_str),
Some("mcpToolCall")
direct_thread_item_started_payload(&item),
serde_json::json!({ "itemType": "mcpToolCall" })
);
assert_eq!(
payload.get("itemId").and_then(serde_json::Value::as_str),
Some("item-1")
);
// 卡片标题 / 折叠摘要 / kind 属于前端投影:载荷里不得出现这些 UI 语义。
assert!(payload.get("toolCall").is_none(), "{payload}");
assert!(payload.get("title").is_none(), "{payload}");
assert!(payload.get("summary").is_none(), "{payload}");
assert!(payload.get("kind").is_none(), "{payload}");
// 参数里的密钥不得随载荷下发(脱敏占位符可以保留,明文不行)。
let arguments = payload
.get("arguments")
.and_then(serde_json::Value::as_str)
.unwrap_or_default();
assert!(!arguments.contains("\"secret\""), "{payload}");
}
#[test]
@@ -4777,6 +4776,8 @@ mod tests {
fn test_llm() -> GameCreatorLlmConfig {
GameCreatorLlmConfig {
custom_enabled: false,
visible_models: Vec::new(),
api_key: "fixture-secret".to_string(),
base_url: "https://example.invalid/v1".to_string(),
model: "fixture-model".to_string(),
@@ -5562,6 +5563,59 @@ mod tests {
assert_ne!(command_token, provider_key);
}
#[tokio::test]
async fn custom_llm_broker_uses_configured_route_without_exposing_upstream_key() {
let mut llm = test_llm();
llm.custom_enabled = true;
llm.api_key = "custom-upstream-fixture-secret".into();
llm.base_url = "http://127.0.0.1:9/v1".into();
llm.model = "vendor/model.v1:latest".into();
llm.visible_models = vec![llm.model.clone()];
let credential = CodexAppServerCredential::AppDataKey {
fingerprint: "custom-fixture".into(),
};
let (base, key) = credential
.direct_provider_route(&llm)
.expect("custom route");
assert_eq!(base, llm.base_url);
assert_eq!(key, llm.api_key);
let proxy = start_codex_provider_proxy(base, key, false).await.unwrap();
for mode in [
CodexAppServerWorkspaceMode::DirectProject,
CodexAppServerWorkspaceMode::ToolHost,
] {
let mut command = tokio::process::Command::new("fixture");
configure_game_creator_codex_app_server_command_for_mode(
&mut command,
&llm,
mode,
Some(&proxy),
None,
true,
)
.unwrap();
let arguments = command
.as_std()
.get_args()
.map(|arg| arg.to_string_lossy())
.collect::<Vec<_>>()
.join("\n");
let params = codex_app_server_thread_start_params(
&llm.model,
std::path::Path::new("fixture-workspace"),
mode,
String::new(),
true,
);
assert_eq!(params["model"], "vendor/model.v1:latest");
assert!(!arguments.contains(&llm.api_key));
assert!(!arguments.contains("/api/llm"));
for (_, value) in command.as_std().get_envs() {
assert!(!value.is_some_and(|value| value.to_string_lossy().contains(&llm.api_key)));
}
}
}
#[cfg(unix)]
#[tokio::test]
async fn direct_project_spawn_restores_broker_token_after_environment_isolation() {
@@ -527,10 +527,6 @@ fn process_design_batch(
let result = if uncertain {
Err("进程在工具执行期间中断,执行结果未保存。未重复执行;请读取实际工作区确认结果后再决定下一步。".to_string())
} else {
let _write = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
root,
"design.tool",
)?;
execute_design_tool(root, resources, session, &call)
};
let error = result
@@ -1026,6 +1022,15 @@ pub(crate) async fn continue_design_agent_at(
finish_design_command(root, resources, session, active, run, emit).await
}
async fn recover_uncertain_design_batch(
root: &Path,
resources: &DesignResources,
session: DesignSession,
active: File,
) -> Result<DesignView, String> {
finish_design_command(root, resources, session, active, true, |_| {}).await
}
pub(crate) async fn decide_design_phase_at(
root: &Path,
resources: &DesignResources,
@@ -1058,7 +1063,8 @@ fn ensure_design_runtime_active(root: &Path) -> Result<(), String> {
}
#[tauri::command]
pub(crate) fn hydrate_design_agent_session(
pub(crate) async fn hydrate_design_agent_session(
app: tauri::AppHandle,
project_path: String,
) -> Result<Option<DesignView>, String> {
let root = Path::new(project_path.trim());
@@ -1084,8 +1090,33 @@ pub(crate) fn hydrate_design_agent_session(
if session.project_id != project_id {
return Err("策划会话与当前项目不匹配".into());
}
let active = try_open_game_creator_agent_runtime_task_lock_file(root, DESIGN_ACTIVE_LOCK)?;
Ok(Some(design_view(&session, active.is_none())))
let Some(active) =
try_open_game_creator_agent_runtime_task_lock_file(root, DESIGN_ACTIVE_LOCK)?
else {
return Ok(Some(design_view(&session, true)));
};
if design_session_has_uncertain_batch(&session) {
let resources = DesignResources::new(resolve_design_resources_root(&app)?)?;
let view = recover_uncertain_design_batch(root, &resources, session, active).await?;
return Ok(Some(view));
}
drop(active);
Ok(Some(design_view(&session, false)))
}
fn design_session_has_uncertain_batch(session: &DesignSession) -> bool {
let Some(batch) = session.pending_batch.as_ref() else {
return false;
};
if !batch.executing || batch.cursor >= batch.calls.len() {
return false;
}
let call_id = batch.calls[batch.cursor].id.as_str();
session.turn.as_ref().is_some_and(|turn| turn.pending)
&& !session.history.iter().any(|item| {
item.get("type").and_then(Value::as_str) == Some("function_call_output")
&& item.get("call_id").and_then(Value::as_str) == Some(call_id)
})
}
fn design_session_error_is_recoverable(error: &str) -> bool {
@@ -1958,4 +1989,94 @@ mod tests {
.any(|message| message.text.contains("重试后继续")));
assert!(next.session.last_error.is_none());
}
#[tokio::test(flavor = "current_thread")]
async fn uncertain_batch_hydrate_continues_the_original_turn_without_replaying_file_tools() {
let (_temp, root, resources) = init_design_project();
execute_design_file_tool(
&root,
"write_file",
&json!({"path":"project/00_concept/design.md","content":"概念"}),
)
.expect("write concept");
let mut session = new_design_session("design-fake", "quality");
let call = platform_llm::LlmToolCall {
id: "interrupted-call".into(),
name: "patch_file".into(),
arguments: json!({
"path":"project/00_concept/design.md",
"old_text":"概念",
"new_text":"概念设计"
})
.to_string(),
};
session.history.push(json!({
"type":"function_call",
"call_id":call.id,
"name":call.name,
"arguments":call.arguments,
}));
session.messages = vec![DesignMessage {
id: "turn:user".into(),
role: "user".into(),
text: "继续".into(),
}];
session.turn = Some(DesignTurn {
id: "turn-recovery".into(),
pending: true,
request_index: 0,
attempt: 0,
});
session.pending_batch = Some(DesignToolBatch {
calls: vec![call],
cursor: 0,
executing: true,
});
assert!(design_session_has_uncertain_batch(&session));
write_design_session(&root, &session).expect("write interrupted session");
let _fake = fake_provider::install(
vec![Ok(fake_response(
"recovered-after-uncertain-tool",
"已读取文件并确认。",
Vec::new(),
))],
0,
);
let view = recover_uncertain_design_batch(&root, &resources, session, {
try_open_game_creator_agent_runtime_task_lock_file(
&root,
".agent/design-agent/active.lock",
)
.expect("open active lock")
.expect("active lock is free")
})
.await
.expect("recover uncertain batch");
assert!(!view.running);
assert!(view.session.last_error.is_none());
let restored = read_design_session(&root)
.expect("read restored")
.expect("session");
assert!(restored.pending_batch.is_none());
assert!(!restored.turn.expect("turn").pending);
assert!(restored.history.iter().any(|item| {
item.get("type").and_then(Value::as_str) == Some("function_call_output")
&& item.get("call_id").and_then(Value::as_str) == Some("interrupted-call")
&& item
.get("output")
.and_then(Value::as_str)
.is_some_and(|output| output.contains("执行结果未保存"))
}));
assert!(restored.history.iter().any(|item| {
item.get("role").and_then(Value::as_str) == Some("assistant")
&& item.get("content").is_some()
}));
assert!(
fs::read_to_string(root.join("design_artifacts/project/00_concept/design.md"))
.expect("read target")
== "概念"
);
}
}
@@ -321,15 +321,31 @@ pub(crate) fn execute_design_file_tool(
})
.collect::<Vec<_>>();
let mut matches = Vec::new();
let mut edit_errors = Vec::new();
let mut valid_edits = 0;
for (index, (old, new)) in normalized.iter().enumerate() {
if old == new {
edit_errors.push(format!(
"edits[{index}] new_text 与 old_text 相同,不会产生修改"
));
continue;
}
let count = content.matches(old).count();
if count == 0 {
return Err(format!("edits[{index}] 原文未找到:{display}"));
edit_errors.push(format!(
"edits[{index}] 原文未找到:{}{}",
display,
design_patch_location_hint(&content, old)
));
continue;
}
if count != 1 {
return Err(format!(
"edits[{index}] 原文匹配 {count} 处,必须唯一:{display}"
let start = content.find(old).expect("count checked");
let line = design_patch_line_number(&content, start);
edit_errors.push(format!(
"edits[{index}] 原文匹配 {count} 处,必须唯一;首次位于第 {line}"
));
continue;
}
let start = content.find(old).expect("count checked");
let end = start + old.len();
@@ -337,13 +353,33 @@ pub(crate) fn execute_design_file_tool(
.iter()
.find(|(_, other_start, other_end)| start < *other_end && *other_start < end)
{
return Err(format!(
"edits[{index}] 与 edits[{other_index}] 修改范围重叠{display}"
edit_errors.push(format!(
"edits[{index}] 与 edits[{other_index}] 修改范围重叠;请合并为一个 edit 或缩短 old_text"
));
continue;
}
matches.push((index, start, end));
valid_edits += 1;
let _ = new;
}
if !edit_errors.is_empty() {
let shown = edit_errors.len().min(4);
let mut details = edit_errors[..shown].to_vec();
if shown < edit_errors.len() {
details.push(format!(
"另有 {} 个 edit 校验失败(详情省略)",
edit_errors.len() - shown
));
}
if valid_edits > 0 {
details.push(format!(
"其余 {valid_edits} 个 edit 当前可唯一匹配;本次未写入文件"
));
} else {
details.push("本次未写入文件".to_string());
}
return Err(details.join("\n"));
}
let mut updated = content.clone();
for (index, start, end) in matches.into_iter().rev() {
let (_, new) = &normalized[index];
@@ -396,6 +432,60 @@ pub(crate) fn execute_design_file_tool(
}
}
fn design_patch_line_number(content: &str, start: usize) -> usize {
1 + content[..start]
.bytes()
.filter(|byte| *byte == b'\n')
.count()
}
fn design_patch_visible_line(line: &str) -> String {
line.replace('\t', "\\t").chars().take(180).collect()
}
fn design_patch_location_hint(content: &str, old: &str) -> String {
let Some(anchor) = old.lines().map(str::trim).find(|line| !line.is_empty()) else {
return String::new();
};
let mut candidates = content
.lines()
.enumerate()
.filter(|(_, line)| line.trim() == anchor)
.map(|(index, line)| (index + 1, line))
.collect::<Vec<_>>();
if candidates.is_empty() {
let token = anchor.split_whitespace().find(|token| token.len() >= 3);
if let Some(token) = token {
candidates = content
.lines()
.enumerate()
.filter(|(_, line)| line.trim().contains(token))
.map(|(index, line)| (index + 1, line))
.collect();
}
}
if candidates.is_empty() {
return format!(
";未找到与 old_text 首个非空行相似的行(当前文件约 {} 行)",
content.lines().count()
);
}
let details = candidates
.iter()
.take(2)
.map(|(line, text)| format!("{line} 行:{}", design_patch_visible_line(text)))
.collect::<Vec<_>>()
.join("");
let suffix = if candidates.len() > 2 {
format!("{}", candidates.len())
} else {
String::new()
};
format!(";old_text 首个非空行可能对应 {details}{suffix}tab 显示为 \\t")
}
pub(crate) fn list_design_workspace_files(
root: &Path,
) -> Result<Vec<DesignWorkspaceEntry>, String> {
@@ -693,6 +783,22 @@ mod tests {
)
.expect_err("escape");
assert!(escaped.contains("路径"));
let mismatch = execute_design_file_tool(
root,
"patch_file",
&json!({
"path":"notes/design.md",
"edits":[
{"old_text":" 游戏设计","new_text":"游戏概念"},
{"old_text":"设计","new_text":"方案"}
]
}),
)
.expect_err("report all patch failures");
assert!(mismatch.contains("edits[0] 原文未找到"));
assert!(mismatch.contains("第 1 行:游戏设计"));
assert!(mismatch.contains("其余 1 个 edit 当前可唯一匹配"));
assert!(mismatch.contains("本次未写入文件"));
let patched = execute_design_file_tool(
root,
"patch_file",
@@ -1,6 +1,10 @@
use super::model::{DirectCodexUserContentPart, DirectCodexUserItem};
use super::model::{DirectCodexUserContentPart, DirectCodexUserItem, DirectCodexUserMessageItem};
use super::validation::validate_direct_codex_user_item;
use crate::agent::{read_manifest_for_project, sanitize_attachment_local_path};
use crate::ui_editor::persistence::{
generate_ui_design_code_at, GenerateUiDesignCodeInput, UI_DESIGN_DOC_ASSET_KIND,
UI_DESIGN_DOC_MEDIA_TYPE,
};
use serde_json::Value;
use std::path::Path;
@@ -111,29 +115,165 @@ pub(crate) fn direct_codex_user_item_to_prompt(
item: &DirectCodexUserItem,
) -> Result<String, String> {
let wire = direct_codex_user_item_to_wire_input(root, item)?;
wire.as_array()
let DirectCodexUserItem::Message(message) = item;
let mut prompt = wire
.as_array()
.ok_or_else(|| "DirectProject user item wire input 不是数组".to_string())
.map(|parts| {
.and_then(|parts| {
parts
.iter()
.filter_map(|part| part.get("text").and_then(Value::as_str))
.collect::<String>()
})
.and_then(|prompt| {
if prompt.trim().is_empty() {
Err("DirectProject user item 不能转换为空 prompt".to_string())
} else {
Ok(prompt)
.map(|part| {
part.get("text")
.and_then(Value::as_str)
.ok_or_else(|| "DirectProject user item wire part 缺少 text".to_string())
})
.collect::<Result<String, String>>()
})?;
if let Some(code_context) = render_ui_design_code_context(root, message)? {
prompt.push('\n');
prompt.push_str(&code_context);
}
if prompt.trim().is_empty() {
return Err("DirectProject user item 不能转换为空 prompt".to_string());
}
Ok(prompt)
}
/// 本轮 prompt 的 UI 设计文档引用上下文:复用 UI Editor 代码导出,把带文档注释的
/// JS 片段路径交给模型;生成失败只追加原始错误,不阻断本轮引用,其它引用继续处理。
///
/// 只在生成本轮 prompt 时展开:历史 item 回读走 `direct_codex_user_item_to_response_item`
/// 的纯投影,不得在这里产生项目写副作用。
fn render_ui_design_code_context(
root: &Path,
message: &DirectCodexUserMessageItem,
) -> Result<Option<String>, String> {
let referenced_ids = message
.content
.iter()
.filter_map(|part| match part {
DirectCodexUserContentPart::AgcResourceReference { resource_id } => {
Some(resource_id.trim())
}
_ => None,
})
.collect::<Vec<_>>();
if referenced_ids.is_empty() {
return Ok(None);
}
let manifest = read_manifest_for_project(root)?;
let mut lines = Vec::new();
for resource_id in referenced_ids {
let is_ui_design_doc = manifest.assets.iter().any(|asset| {
asset.id == resource_id
&& asset.kind == UI_DESIGN_DOC_ASSET_KIND
&& asset.media_type == UI_DESIGN_DOC_MEDIA_TYPE
});
if !is_ui_design_doc {
continue;
}
lines.push(
match generate_ui_design_code_at(GenerateUiDesignCodeInput {
project_path: root.to_string_lossy().into_owned(),
expected_project_id: manifest.project_id.clone(),
asset_id: resource_id.to_string(),
}) {
Ok(result) => format!("请先阅读生成的带有文档的代码片段: {}", result.relative_path),
Err(error) => format!("生成代码遇到错误{error}"),
},
);
}
if lines.is_empty() {
return Ok(None);
}
Ok(Some(lines.join("\n")))
}
#[cfg(test)]
mod tests {
use super::direct_codex_user_item_to_response_item;
use super::{direct_codex_user_item_to_prompt, direct_codex_user_item_to_response_item};
use crate::ui_editor::persistence::UI_DESIGN_DOC_MEDIA_TYPE;
use serde_json::json;
use shared_contracts::game_creation_app::{
GameCreationAppAssetKind, GameCreationAppAssetSource, GameCreationAppAssetSourceKind,
};
use std::path::Path;
const PROMPT_CONTEXT_PROJECT_ID: &str = "direct-prompt-context-project";
fn prompt_context_project() -> tempfile::TempDir {
let directory = tempfile::tempdir().expect("temp project");
crate::init_local_game_project_at(
directory.path(),
PROMPT_CONTEXT_PROJECT_ID,
"Direct prompt 上下文测试",
)
.expect("init project");
directory
}
fn register_fixture_asset(
root: &Path,
relative_path: &str,
kind: GameCreationAppAssetKind,
media_type: &str,
) -> String {
let absolute_path = crate::resolve_local_project_path(root, relative_path)
.expect("resolve fixture asset path");
std::fs::create_dir_all(absolute_path.parent().expect("fixture asset parent"))
.expect("create fixture asset parent");
std::fs::write(&absolute_path, b"{}").expect("write fixture asset file");
crate::assets::register_local_asset_at(
root,
relative_path,
kind,
media_type,
"prompt-context-test",
GameCreationAppAssetSource {
kind: GameCreationAppAssetSourceKind::Generated,
canvas_project_id: None,
resource_id: Some("prompt-context".to_string()),
asset_object_id: None,
task_id: None,
prompt: None,
model: None,
generation_route: None,
generation_kind: None,
reference_resource_ids: Vec::new(),
},
)
.expect("register fixture asset")
.id
}
fn ui_design_doc_fixture(initialize_state: bool) -> (tempfile::TempDir, String) {
let directory = prompt_context_project();
let asset_id = register_fixture_asset(
directory.path(),
"ui/design.json",
GameCreationAppAssetKind::UiDesignDoc,
UI_DESIGN_DOC_MEDIA_TYPE,
);
if initialize_state {
crate::ui_editor::persistence::initialize_ui_design_state_at(
directory.path(),
PROMPT_CONTEXT_PROJECT_ID,
&asset_id,
)
.expect("initialize UI design state");
}
(directory, asset_id)
}
fn user_item_with_resource_reference(asset_id: &str) -> serde_json::Value {
json!({
"type": "message",
"role": "user",
"id": "turn-1:user",
"content": [{"type": "agc_resource_reference", "resourceId": asset_id}],
})
}
#[test]
fn standard_response_item_passes_through_without_agc_private_parts() {
let item = json!({
@@ -174,4 +314,81 @@ mod tests {
.expect_err("history item without type must fail");
assert!(error.contains("缺少 type"), "{error}");
}
#[test]
fn ui_design_doc_reference_appends_generated_code_context() {
let (project, asset_id) = ui_design_doc_fixture(true);
let item: super::DirectCodexUserItem =
serde_json::from_value(user_item_with_resource_reference(&asset_id))
.expect("canonical user item");
let prompt =
direct_codex_user_item_to_prompt(project.path(), &item).expect("prompt projection");
assert!(
prompt.contains(&format!(
"[素材引用 resourceId={asset_id};项目路径=ui/design.json]"
)),
"{prompt}"
);
assert!(
prompt.contains("请先阅读生成的带有文档的代码片段: ui/generated-"),
"{prompt}"
);
}
#[test]
fn history_projection_never_writes_generated_ui_design_code() {
let (project, asset_id) = ui_design_doc_fixture(true);
let item = user_item_with_resource_reference(&asset_id);
direct_codex_user_item_to_response_item(project.path(), &item).expect("history projection");
let generated_root =
crate::resolve_local_project_path(project.path(), "ui").expect("resolve ui directory");
let generated_files = std::fs::read_dir(generated_root)
.expect("read ui directory")
.filter_map(|entry| entry.ok())
.filter(|entry| {
entry
.file_name()
.to_string_lossy()
.starts_with("generated-")
})
.count();
assert_eq!(
generated_files, 0,
"历史回读只做纯投影,不得生成 UI 设计代码"
);
}
#[test]
fn ui_design_generation_failure_keeps_reference_and_reports_error() {
let (project, asset_id) = ui_design_doc_fixture(false);
let item: super::DirectCodexUserItem =
serde_json::from_value(user_item_with_resource_reference(&asset_id))
.expect("canonical user item");
let prompt =
direct_codex_user_item_to_prompt(project.path(), &item).expect("prompt projection");
assert!(prompt.contains("素材引用 resourceId="), "{prompt}");
assert!(prompt.contains("生成代码遇到错误"), "{prompt}");
}
#[test]
fn other_asset_kind_reference_does_not_generate_ui_design_code() {
let project = prompt_context_project();
let asset_id = register_fixture_asset(
project.path(),
"assets/hero.png",
GameCreationAppAssetKind::Character,
"image/png",
);
let item: super::DirectCodexUserItem =
serde_json::from_value(user_item_with_resource_reference(&asset_id))
.expect("canonical user item");
let prompt =
direct_codex_user_item_to_prompt(project.path(), &item).expect("prompt projection");
assert!(prompt.contains("素材引用 resourceId="), "{prompt}");
assert!(
!prompt.contains("请先阅读生成的带有文档的代码片段"),
"{prompt}"
);
assert!(!prompt.contains("生成代码遇到错误"), "{prompt}");
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1203,7 +1203,7 @@ fn bridge_registered_resource(
// (落盘值 + 按 kind 派生 + 读时自愈),这里走 Rust 的同构实现。
// 直接透传落盘 `asset.category` 会让 `kind:"ui"` 的资产在 UI 显示「UI 交互」、
// 在 Agent 侧读到 `unclassified`(真机 55 条分歧)。
"category": game_creation_app_asset_effective_category(&asset.kind, asset.category),
"category": game_creation_app_asset_effective_category(asset.kind.as_str(), asset.category),
"tags": asset.tags,
"canvasProjectId": asset.source.canvas_project_id,
"resourceId": asset.source.resource_id,
@@ -1244,7 +1244,8 @@ fn bridge_list_registered_assets(root: &Path, arguments: &Value) -> Value {
arguments,
"kind",
DIRECT_TOOL_BRIDGE_MAX_RESOURCE_KIND_CHARS,
)?;
)?
.map(|kind| GameCreationAppAssetKind::parse_with_context(&kind, "asset.list.kind"));
let asset_id = bridge_optional_bounded_string(
arguments,
"assetId",
@@ -1264,7 +1265,7 @@ fn bridge_list_registered_assets(root: &Path, arguments: &Value) -> Value {
let mut assets = manifest
.assets
.iter()
.filter(|asset| kind.as_ref().is_none_or(|kind| asset.kind == *kind))
.filter(|asset| kind.is_none_or(|kind| asset.kind == kind))
.filter(|asset| {
asset_id
.as_ref()
@@ -1862,7 +1863,7 @@ async fn bridge_create_or_derive_resource(
source_asset_id: source_asset.as_ref().map(|asset| asset.id.clone()),
source_path: source_asset.as_ref().map(|asset| asset.local_path.clone()),
source_media_type: source_asset.as_ref().map(|asset| asset.media_type.clone()),
source_subtype: source_asset.as_ref().map(|asset| asset.kind.clone()),
source_subtype: source_asset.as_ref().map(|asset| asset.kind.to_string()),
producer_task_id: source_asset
.as_ref()
.and_then(|asset| asset.source.task_id.clone()),
@@ -3323,7 +3324,7 @@ mod tests {
fn bridge_art_resource_exposes_only_the_safe_identity_projection() {
let asset = GameCreationAppAssetManifestEntry {
id: "local-art-1".to_string(),
kind: "art-spritesheet-slice".to_string(),
kind: GameCreationAppAssetKind::Icon,
media_type: "image/png".to_string(),
local_path: "assets/art-spritesheet-slices/player.png".to_string(),
image_sequence_frames: None,
@@ -3380,7 +3381,7 @@ mod tests {
fn bridge_registered_resource_exposes_formal_sequence_without_private_generation_fields() {
let asset = GameCreationAppAssetManifestEntry {
id: "animation-1".to_string(),
kind: "character-animation".to_string(),
kind: GameCreationAppAssetKind::CharacterAnimation,
media_type: "video/mp4".to_string(),
local_path: "assets/edits/animation.mp4".to_string(),
image_sequence_frames: Some(vec![
@@ -3431,7 +3432,7 @@ mod tests {
fn bridge_registered_resource_keeps_explicit_manifest_classification() {
let asset = GameCreationAppAssetManifestEntry {
id: "spec-1".to_string(),
kind: "spec".to_string(),
kind: GameCreationAppAssetKind::Spec,
media_type: "application/json".to_string(),
local_path: "assets/specs/hero.json".to_string(),
image_sequence_frames: None,
@@ -3467,13 +3468,13 @@ mod tests {
///
/// UI 栏目走 TS 的 `gameCreationAppAssetCategory`(落盘值 + 按 kind 派生 + 读时自愈),
/// Agent 走 Rust 的同构实现 `game_creation_app_asset_effective_category`。直接透传落盘
/// `category` 会让 `kind:"ui"` 的资产在 UI 显示「UI 交互」、在 Agent 侧读到
/// `unclassified`——真机 122 条资产里有 55 条这样分叉
/// `category` 会让 `ui-design` 的资产在 UI 显示「UI 交互」、在 Agent 侧读到
/// `unclassified`,两侧分类口径必须一致
#[test]
fn bridge_registered_resource_projects_effective_category_not_raw_persisted_value() {
let asset = GameCreationAppAssetManifestEntry {
id: "ui-1".to_string(),
kind: "ui".to_string(),
id: "ui-design-1".to_string(),
kind: GameCreationAppAssetKind::UiDesign,
media_type: "application/json".to_string(),
local_path: "assets/UI 设计 1.json".to_string(),
image_sequence_frames: None,
@@ -3496,7 +3497,7 @@ mod tests {
};
let projection = bridge_registered_resource(&asset, false);
assert_eq!(projection["kind"], "ui");
assert_eq!(projection["kind"], "ui-design");
assert_eq!(projection["category"], "ui-interaction");
}
@@ -8,9 +8,11 @@
//! 为什么不复用 `project.jsonl`:那条链路的回读只投影 `role ∈ {user, assistant}` 的
//! 文本条目,而且会被注入 Codex 上下文。往里面塞新形状既装不下,又有污染模型上下文的风险。
use super::direct_thread_wire::sanitize_detail_text;
use crate::agent::redact_secret_tokens;
use crate::agent::sanitize_error_context;
use crate::config::{prepare_game_creator_private_path_for_read, write_game_creator_private_file};
use crate::project::{enforce_project_permission_policy, project_append_lock_for};
use crate::redact_absolute_path_tokens;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;
@@ -84,6 +86,134 @@ fn tool_calls_path(root: &Path) -> PathBuf {
root.join(".agent/conversations/tool-calls.jsonl")
}
/// 项目根目录之后的路径 token:分隔符统一成 `/`,返回 `(消费到的下标, 项目相对路径)`。
fn project_relative_path_segment(value: &str, start: usize) -> (usize, String) {
let mut index = start;
let mut relative = String::new();
while index < value.len() {
let character = value[index..].chars().next().unwrap_or_default();
if matches!(character, '/' | '\\') {
if !relative.is_empty() {
relative.push('/');
}
index += character.len_utf8();
continue;
}
if character.is_whitespace()
|| matches!(
character,
'\'' | '"'
| '`'
| ','
| ';'
| '|'
| '&'
| '('
| ')'
| '['
| ']'
| '{'
| '}'
| '<'
| '>'
| ':'
)
{
break;
}
relative.push(character);
index += character.len_utf8();
}
while relative.ends_with('/') {
relative.pop();
}
(index, relative)
}
/// 把项目根目录前缀换成**项目相对路径**`<root>/game/src/x.ts` → `game/src/x.ts`)。
///
/// 必须排在 `redact_absolute_path_tokens` 之前:后者会把整个绝对路径抹成
/// `<absolute-path>`,之后就再也认不出哪些路径在项目内了。
/// Windows 上同时匹配 `\` 与 `/` 两种分隔符写法,并按大小写不敏感比较(盘符大小写会变)。
fn relativize_project_root_paths(root: &Path, value: &str) -> String {
let root_text = root.to_string_lossy();
let root_text = root_text.trim_end_matches(['/', '\\']);
if root_text.is_empty() {
return value.to_string();
}
let mut needles = [
root_text.to_string(),
root_text.replace('\\', "/"),
root_text.replace('/', "\\"),
]
.into_iter()
.map(|needle| needle.to_ascii_lowercase())
.filter(|needle| !needle.is_empty())
.collect::<Vec<_>>();
needles.sort();
needles.dedup();
let lower = value.to_ascii_lowercase();
let mut output = String::with_capacity(value.len());
let mut cursor = 0usize;
while cursor < value.len() {
let mut hit: Option<(usize, usize)> = None;
for needle in &needles {
let mut search = cursor;
while let Some(relative) = lower[search..].find(needle.as_str()) {
let start = search + relative;
let end = start + needle.len();
let left_is_boundary = start == 0
|| lower[..start].chars().next_back().is_some_and(|character| {
!character.is_alphanumeric() && character != '_' && character != '-'
});
if left_is_boundary && value[end..].starts_with(['/', '\\']) {
if hit.is_none_or(|(best_start, _)| start < best_start) {
hit = Some((start, end));
}
break;
}
search = end;
}
}
let Some((start, end)) = hit else {
break;
};
output.push_str(&value[cursor..start]);
let (consumed, relative) = project_relative_path_segment(value, end);
if relative.is_empty() {
// 只写了项目根目录本身(没有后续路径段):按占位形状处理。
output.push_str("<absolute-path>");
} else {
output.push_str(&relative);
}
cursor = consumed;
}
output.push_str(&value[cursor..]);
output
}
/// 脱敏:项目内绝对路径先归一化成项目相对路径,再依次做绝对路径、密钥前缀与
/// 错误上下文脱敏。
///
/// 顺序不能反:先抹密钥会把 `sk-…` 之类的 token 换成占位符,但绝对路径里的用户名目录
/// 仍然会留下;这里先归一化路径 token,再处理密钥。
///
/// 复用既有 `agent/generation/prompt_context.rs` 的脱敏组合:`sanitize_error_context`
/// 就是 `redact_secret_tokens` + `redact_error_sensitive_assignments` +
/// `redact_error_bearer_values` + `redact_error_config_names` 的既有组合用法,覆盖
/// `Authorization: Bearer …`、`Cookie: …`、`api_key=…`、`client_secret=…` 这类键值凭据;
/// 含 `--password` / `--token` / `--secret` 这类敏感 CLI 标志的行按既有 fail-closed
/// 约定整行替换成 `[redacted sensitive context]`(与 `sanitize_agent_runtime_text` 一致)。
///
/// `pub(crate)`:回合流(`direct_turn_stream`)的文本段复用同一套脱敏,避免两处口径分叉。
pub(crate) fn sanitize_detail_text(root: &Path, value: &str) -> String {
let without_project_root = relativize_project_root_paths(root, value);
let without_absolute = redact_absolute_path_tokens(&without_project_root);
let without_secret = redact_secret_tokens(&without_absolute);
sanitize_error_context(&without_secret)
}
/// 按字符数截断(不切坏 UTF-8),并在真正截断时补省略号。
fn bounded_chars(value: &str, max_chars: usize) -> String {
if value.chars().count() <= max_chars {
@@ -49,7 +49,7 @@ struct ExternalMcpHttpState {
root: PathBuf,
token: String,
session_user_id: String,
session_generation: u64,
session_identity_generation: u64,
}
pub(crate) fn direct_tools_mcp_mode_requested(args: &[String]) -> bool {
@@ -1241,7 +1241,8 @@ fn external_mcp_session_id(root: &Path) -> String {
material.push('\0');
material.push_str(&session.user_id);
material.push('\0');
material.push_str(&session.generation.to_string());
// 用身份代次而不是 token:同一账号续期不得让 MCP 会话身份漂移。
material.push_str(&session.identity_generation.to_string());
}
format!("mcp-{:x}", Sha256::digest(material.as_bytes()))
}
@@ -1759,7 +1760,9 @@ async fn handle_external_mcp_http_request(
let Some(session) = current_platform_session() else {
return Err(StatusCode::UNAUTHORIZED);
};
if session.user_id != state.session_user_id || session.generation != state.session_generation {
if session.user_id != state.session_user_id
|| session.identity_generation != state.session_identity_generation
{
return Err(StatusCode::UNAUTHORIZED);
}
let response = EXTERNAL_MCP_BRIDGE_URL
@@ -1794,7 +1797,7 @@ pub(crate) async fn start_external_mcp_loopback(
root,
token: token.clone(),
session_user_id: session.user_id,
session_generation: session.generation,
session_identity_generation: session.identity_generation,
};
let app = Router::new()
.route(&route, post(handle_external_mcp_http_request))
@@ -11,6 +11,10 @@ use super::external_generation_state::{
retain_platform_art_generation_runtime_accepted_result, PlatformArtGenerationRuntimeState,
};
use super::*;
use crate::platform_session::{
acquire_platform_session_identity_lease, validate_platform_session_identity,
PlatformSessionIdentity,
};
use reqwest::multipart::{Form, Part};
const EXTERNAL_GENERATION_POLL_TIMEOUT: Duration = Duration::from_secs(35 * 60);
@@ -469,6 +473,27 @@ pub(crate) fn normalize_platform_art_asset_generation_kind(kind: &str) -> Option
})
}
/// 平台美术生成请求的 `assetKind` → manifest 资源 kind。
///
/// `PlatformArtAssetGenerationOptions::asset_kind` 是**平台请求词汇**`ui-prototype` /
/// `art-spritesheet` / `game-background` / `game-art` …),跟 manifest 的正式
/// `GameCreationAppAssetKind` 不是同一套词表。生成结果登记进 manifest 之前必须在这里显式
/// 转换,否则 manifest 会落进非 canonical 值,读侧只能靠 shared-contracts 的别名表兜底。
///
/// 词表外的值不猜:返回 `Unknown`,由调用方按既有未知输入口径处理。
pub(crate) fn platform_art_asset_manifest_kind(asset_kind: &str) -> GameCreationAppAssetKind {
match asset_kind.trim() {
"image" | "game-art" => GameCreationAppAssetKind::Image,
"character" | "character-art" => GameCreationAppAssetKind::Character,
"spec" | "icon-spec" => GameCreationAppAssetKind::IconSpec,
"ui-prototype" | "ui-design" => GameCreationAppAssetKind::UiDesign,
"art-spritesheet" | "icon-spritesheet" => GameCreationAppAssetKind::IconSpritesheet,
"publication-material" => GameCreationAppAssetKind::PublicationMaterial,
"game-background" | "scene" => GameCreationAppAssetKind::Scene,
_ => GameCreationAppAssetKind::Unknown,
}
}
pub(in crate::agent) fn recover_persisted_visual_generation_options(
root: &Path,
pending: &AgentRuntimePendingToolAction,
@@ -1510,10 +1535,7 @@ struct PreparedPlatformArtAssetSlice {
#[derive(Clone)]
struct PreparedPlatformSessionFence {
user_id: String,
api_base_url: String,
generation: u64,
access_token_sha256: String,
identity: PlatformSessionIdentity,
}
impl PreparedPlatformSessionFence {
@@ -1521,41 +1543,17 @@ impl PreparedPlatformSessionFence {
access
.frozen_platform_session()
.map(|session| PreparedPlatformSessionFence {
user_id: session.user_id.clone(),
api_base_url: session.api_base_url.clone(),
generation: session.generation,
access_token_sha256: format!(
"{:x}",
Sha256::digest(session.access_token.as_bytes())
),
identity: session.identity(),
})
}
fn validate(&self) -> Result<(), String> {
let matches = current_platform_session().is_some_and(|session| {
session.user_id == self.user_id
&& session.api_base_url == self.api_base_url
&& session.generation == self.generation
&& format!("{:x}", Sha256::digest(session.access_token.as_bytes()))
== self.access_token_sha256
});
if matches {
Ok(())
} else {
Err(
"authentication-required: 陶泥儿登录态已变化,旧账号请求已停止,请使用当前账号重试"
.to_string(),
)
}
// 只比较身份:同一账号的 access token 轮换不得让在途生成 operation 失败。
validate_platform_session_identity(&self.identity)
}
fn acquire_lease(&self) -> Result<ValidatedPlatformSessionLease, String> {
acquire_validated_platform_session_fingerprint(
&self.user_id,
&self.api_base_url,
self.generation,
&self.access_token_sha256,
)
acquire_platform_session_identity_lease(&self.identity)
}
}
@@ -1637,7 +1635,7 @@ async fn canonical_art_spec_reference_at(
.iter()
.find(|asset| {
asset.local_path == AGENT_RUNTIME_ART_SPEC_PATH
&& asset.kind == "icon-spec"
&& asset.kind == GameCreationAppAssetKind::IconSpec
&& asset.media_type.starts_with("image/")
&& asset.source.kind == GameCreationAppAssetSourceKind::Canvas
})
@@ -1661,7 +1659,7 @@ async fn canonical_art_spec_reference_at(
&source.id,
&format!("{:x}", Sha256::digest(&bytes)),
&source.media_type,
&source.kind,
source.kind.as_str(),
)?;
if let Some(binding) = read_external_editor_resource_binding_at(
root,
@@ -6870,7 +6868,7 @@ fn register_platform_art_slice_manifest_entries_at(
.iter_mut()
.find(|asset| asset.local_path == registration.local_path)
{
existing.kind = "art-spritesheet-slice".to_string();
existing.kind = GameCreationAppAssetKind::Icon;
existing.media_type = registration.media_type.clone();
existing.image_sequence_frames = None;
existing.image_sequence_duration_ms = None;
@@ -6884,12 +6882,14 @@ fn register_platform_art_slice_manifest_entries_at(
);
manifest.assets.push(GameCreationAppAssetManifestEntry {
id: id.clone(),
kind: "art-spritesheet-slice".to_string(),
kind: GameCreationAppAssetKind::Icon,
media_type: registration.media_type.clone(),
local_path: registration.local_path.clone(),
image_sequence_frames: None,
image_sequence_duration_ms: None,
category: game_creation_app_asset_category_for_kind("art-spritesheet-slice"),
category: game_creation_app_asset_category_for_kind(
GameCreationAppAssetKind::Icon.as_str(),
),
tags: Vec::new(),
source,
});
@@ -6921,7 +6921,7 @@ fn existing_platform_art_slice_registrations_are_complete(
return Ok(false);
};
if candidates.next().is_some()
|| asset.kind != "art-spritesheet-slice"
|| asset.kind != GameCreationAppAssetKind::Icon
|| asset.media_type != "image/png"
|| asset.source.kind != GameCreationAppAssetSourceKind::Canvas
|| asset.source.generation_route.as_deref()
@@ -7010,10 +7010,12 @@ pub(in crate::agent) fn register_existing_platform_art_slices_at(
.clone()
.filter(|value| !value.trim().is_empty())
.ok_or_else(|| "旧项目图集源资源缺少 sourceResourceId".to_string())?;
let has_partial_or_invalid_registration = manifest
.assets
.iter()
.any(|asset| asset.kind == "art-spritesheet-slice");
let has_partial_or_invalid_registration = manifest.assets.iter().any(|asset| {
asset.kind == GameCreationAppAssetKind::Icon
&& asset
.local_path
.starts_with("assets/art-spritesheet-slices/")
});
let receipt_path =
resolve_local_project_path(root, ".agent/runtime/art-spritesheet-contract.json")?;
@@ -7623,7 +7625,7 @@ fn commit_prepared_platform_art_asset_with_before_replace_hook(
let registered = match register_local_asset_entry(
root,
&local_path,
&options.asset_kind,
platform_art_asset_manifest_kind(&options.asset_kind),
&download.media_type,
"platform-art",
GameCreationAppAssetSource {
@@ -10636,7 +10638,7 @@ mod canvas_generation_tests {
}
drop(owner_a_access);
drop(frozen_owner_a);
install_platform_session("durable-owner-b", "durable-owner-b-token", &base_url, 2)
install_platform_session("durable-owner-b", "durable-owner-b-token", &base_url, 2, 2)
.expect("switch to owner B");
let error = match request_platform_art_asset_with_runtime_options_at(
@@ -10761,8 +10763,14 @@ mod canvas_generation_tests {
.recv_timeout(Duration::from_secs(3))
.expect("wait for accepted response");
std::thread::sleep(Duration::from_millis(50));
install_platform_session("post-202-user-b", "post-202-token-b", &switch_base_url, 2)
.expect("switch platform account after accepted response");
install_platform_session(
"post-202-user-b",
"post-202-token-b",
&switch_base_url,
2,
2,
)
.expect("switch platform account after accepted response");
});
let runtime_context = PlatformArtGenerationRuntimeContext {
agent_id: "art-director".to_string(),
@@ -11226,7 +11234,7 @@ mod canvas_generation_tests {
register_local_asset_at(
root,
"assets/art-spec.png",
"icon-spec",
GameCreationAppAssetKind::IconSpec,
"image/png",
"changed-reference-test",
GameCreationAppAssetSource {
@@ -12796,7 +12804,7 @@ mod canvas_generation_tests {
let first_slice_ids = registered
.assets
.iter()
.filter(|asset| asset.kind == "art-spritesheet-slice")
.filter(|asset| asset.kind == GameCreationAppAssetKind::Icon)
.map(|asset| {
assert_eq!(asset.source.kind, GameCreationAppAssetSourceKind::Canvas);
assert_eq!(
@@ -12830,7 +12838,7 @@ mod canvas_generation_tests {
.clone();
mutate_manifest_at(root, |manifest| {
manifest.assets.retain(|asset| {
asset.kind != "art-spritesheet-slice" || asset.local_path == retained_path
asset.kind != GameCreationAppAssetKind::Icon || asset.local_path == retained_path
});
Ok(())
})
@@ -12844,7 +12852,7 @@ mod canvas_generation_tests {
let backfilled_slice_ids = backfilled
.assets
.iter()
.filter(|asset| asset.kind == "art-spritesheet-slice")
.filter(|asset| asset.kind == GameCreationAppAssetKind::Icon)
.map(|asset| (asset.local_path.clone(), asset.id.clone()))
.collect::<std::collections::BTreeMap<_, _>>();
assert_eq!(backfilled_slice_ids.len(), 4);
@@ -12893,7 +12901,7 @@ mod canvas_generation_tests {
let replaced_slice_ids = replaced
.assets
.iter()
.filter(|asset| asset.kind == "art-spritesheet-slice")
.filter(|asset| asset.kind == GameCreationAppAssetKind::Icon)
.map(|asset| {
assert_eq!(
asset.source.reference_resource_ids,
@@ -12926,7 +12934,7 @@ mod canvas_generation_tests {
register_local_asset_entry(
root,
AGENT_RUNTIME_ART_SPRITESHEET_PATH,
"art-spritesheet",
GameCreationAppAssetKind::IconSpritesheet,
"image/png",
"platform-art",
GameCreationAppAssetSource {
@@ -12987,12 +12995,12 @@ mod canvas_generation_tests {
assert!(after
.assets
.iter()
.all(|asset| asset.kind != "art-spritesheet-slice"));
.all(|asset| asset.kind != GameCreationAppAssetKind::Icon));
register_local_asset_entry(
root,
"assets/art-spritesheet-slices/player.png",
"art-spritesheet-slice",
GameCreationAppAssetKind::Icon,
"image/png",
"platform-art",
GameCreationAppAssetSource {
@@ -14548,7 +14556,10 @@ mod canvas_generation_tests {
let manifest = read_manifest_for_project(root).expect("read replacement manifest");
assert_eq!(manifest.assets.len(), 1);
assert_eq!(manifest.assets[0].local_path, "assets/art-spritesheet.png");
assert_eq!(manifest.assets[0].kind, "art-spritesheet");
assert_eq!(
manifest.assets[0].kind,
GameCreationAppAssetKind::IconSpritesheet
);
assert_eq!(
fs::read_dir(root.join("assets"))
.expect("read assets directory")
@@ -1431,12 +1431,13 @@ mod external_generation_state_tests {
base_url,
);
let frozen_a = current_platform_session().expect("freeze owner A");
validate_platform_session_snapshot(&frozen_a).expect("owner A is current before switch");
validate_frozen_platform_session(&frozen_a).expect("owner A is current before switch");
replace_platform_session_for_gui_owner(
"fingerprint-owner-b",
"fingerprint-token-b",
base_url,
2,
2,
)
.expect("switch global session to owner B");
let current_b = current_platform_session().expect("owner B is current after switch");
@@ -98,7 +98,7 @@ pub(crate) fn render_local_asset_prompt_context(root: &Path) -> Result<String, S
output.push_str("- ");
output.push_str(&asset.id);
output.push_str(": ");
output.push_str(&asset.kind);
output.push_str(asset.kind.as_str());
output.push_str(" / ");
output.push_str(&asset.media_type);
output.push_str(" / ");
@@ -2061,17 +2061,23 @@ mod tests {
.expect("resume first manifest task fixture");
for (local_path, kind) in [
("assets/art-spec.png", "icon-spec"),
("assets/ui-prototype.png", "ui-prototype"),
("assets/art-spritesheet.png", "art-spritesheet"),
("assets/art-spec.png", GameCreationAppAssetKind::IconSpec),
(
"assets/ui-prototype.png",
GameCreationAppAssetKind::UiDesign,
),
(
"assets/art-spritesheet.png",
GameCreationAppAssetKind::IconSpritesheet,
),
] {
let (generation_route, generation_kind, reference_resource_ids) = match kind {
"icon-spec" => (
GameCreationAppAssetKind::IconSpec => (
"/api/external/v1/editor/images/generations",
"spec",
Vec::new(),
),
"ui-prototype" => (
GameCreationAppAssetKind::UiDesign => (
"/api/external/v1/editor/images/generations",
"ui-design",
vec!["resource-icon-spec".to_string()],
@@ -2082,7 +2088,7 @@ mod tests {
vec!["resource-icon-spec".to_string()],
),
};
let bytes = if kind == "art-spritesheet" {
let bytes = if kind == GameCreationAppAssetKind::IconSpritesheet {
use image::ImageEncoder;
let mut bytes = Vec::new();
image::codecs::png::PngEncoder::new(&mut bytes)

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