Compare commits

...

56 Commits

Author SHA1 Message Date
lhk229 1ce2c10107 删除客户端每回合四项媒体资源请求上限 (#427)
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 7m10s
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 7m28s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 7m32s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 7m48s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m1s
Project CI / AI game creator shell Rust crates (push) Successful in 1m58s
Project CI / Repository checks (push) Successful in 5m46s
Project CI / Frontend tests (push) Successful in 7m8s
Project CI / Backend tests (push) Successful in 9m19s
Project CI / Native shell tests (push) Successful in 9m42s
Project CI / AI game creator shell web tests (push) Successful in 5m5s
- direct_tool_bridge.rs 删除 DIRECT_TOOL_BRIDGE_MAX_RESOURCE_CALLS_PER_TURN 常量、回合授权状态的 resource_request_ids 计数 map 与 resource_request_ids() 方法
- agc_create_or_derive_resource(含 agc_edit_image 委托)与 agc_remove_background 统一按回合身份与请求指纹确定性派生 operation/idempotency id,同指纹重试复用与 pending 对账语义不变
- 付费提交串行仍由 resource_generation_gate 互斥保证,成本控制交由服务端计费与泥点余额兜底
- 决策记录新增 2026-09-19 删除上限条目,2026-09-18 抠图豁免条目标记为仅作追溯
- 实施计划文档同步改为「不同请求串行提交,不设每回合计数上限」

Reviewed-on: #427
Co-authored-by: Linghong <ink29535@proton.me>
Co-committed-by: Linghong <ink29535@proton.me>
2026-09-19 01:44:48 +08:00
lhk229 ea141d9f17 修复持久目标模式切换测试的异步竞态
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) Successful in 5m11s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 5m4s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 5m50s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m59s
Project CI / AI game creator shell Rust crates (push) Successful in 2m28s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 4m37s
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 / AI game creator shell web tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
- developer-agent-window.suite.ts 在切换目标模式前等待按钮脱离 disabled,避免读取历史链路在途时点击落空导致 aria-pressed 断言偶发失败
2026-09-19 01:21:05 +08:00
lhk229 769740a46d Merge pull request '取消抠图工具的每回合四项媒体请求限制' (#426) from codex/remove-bg-turn-limit into master
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 6m56s
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 7m15s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 7m24s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 7m21s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m25s
Project CI / AI game creator shell Rust crates (push) Successful in 2m55s
Project CI / Repository checks (push) Successful in 15m5s
Project CI / Backend tests (push) Successful in 18m40s
Project CI / Native shell tests (push) Successful in 20m39s
Project CI / Frontend tests (push) Failing after 19m35s
Project CI / AI game creator shell web tests (push) Successful in 13m6s
Reviewed-on: #426
2026-09-19 00:29:51 +08:00
lhk229 b472b9eec9 取消抠图工具的每回合四项媒体请求限制
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 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 1/4 (pull_request) Has been cancelled
agc_remove_background 不再经过 resource_request_ids 付费计数,直接按回合身份与请求指纹确定性派生 id

抠图请求不再挤占付费 create/derive 的四项额度,幂等复用与 pending 对账语义不变

create_or_derive(含 agc_edit_image 委托)路径逐字节不变

决策记录补充 2026-09-18 抠图豁免条目

实施计划文档同步注明抠图不占四项额度
2026-09-19 00:14:01 +08:00
suzmii 9a21690fe9 Merge pull request '修复 AGC 画布生成、卡片展示与布局交互' (#412) from feat/agc-canvas-acceptance into master
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 5m52s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 5m56s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 4m29s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 5m8s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m7s
Project CI / AI game creator shell Rust crates (push) Successful in 2m27s
Project CI / Frontend tests (push) Successful in 8m33s
Project CI / Repository checks (push) Successful in 10m19s
Project CI / AI game creator shell web tests (push) Successful in 6m51s
Project CI / Backend tests (push) Successful in 18m39s
Project CI / Native shell tests (push) Successful in 20m53s
2026-09-18 15:50:44 +08:00
suzmii deff6319e2 合并主线项目创建目录选择更新
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 6m53s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 6m31s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 7m12s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m5s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 4m54s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m12s
Project CI / Backend tests (pull_request) Successful in 9m37s
Project CI / Native shell tests (pull_request) Successful in 12m21s
Project CI / Repository checks (pull_request) Successful in 11m10s
Project CI / AI game creator shell web tests (pull_request) Successful in 12m51s
Project CI / Frontend tests (pull_request) Successful in 23m14s
合入 master ee33af7b7 的项目目录选择与模板建项说明。
保留本批画布和对话修复,自动合并无冲突,类型检查通过。
2026-09-18 15:48:10 +08:00
suzmii 2257a78d6b 收口思考等待反馈与紧凑过程样式及失败状态
思考展开时隐藏重复预览并使用灯泡图标,首个响应前显示临时思考提示。
统一分钟内小数和分钟以上整数秒,保持内外过程的紧凑间距与正文层级。
失败工具与分组显示错误色,非零退出码不再误判为成功。
补齐状态、格式、样式和交互回归,本批其余需求留待后续PR。
2026-09-18 15:46:30 +08:00
suzmii a0f6b7c67d 统一思考与执行过程折叠样式及中文耗时
思考展开复用安全Markdown渲染,折叠显示浅色单行纯文本预览与右侧箭头。
共享过程摘要组件,执行过程按真实操作数显示统计并保留各组独立计时。
工具、整轮、生成任务和策划耗时统一为中文时分秒格式。
补齐格式与Markdown回归、更新已有界面断言并验证窄屏键盘操作。
2026-09-18 14:34:10 +08:00
kdletters ee33af7b75 补充模板建项命令的项目创建目录说明
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 6m4s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 6m4s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 6m13s
Project CI / AI game creator shell Rust smoke (push) Successful in 3m3s
Project CI / AI game creator shell Rust crates (push) Successful in 2m8s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 6m0s
Project CI / Frontend tests (push) Successful in 10m59s
Project CI / Repository checks (push) Successful in 10m54s
Project CI / Native shell tests (push) Successful in 13m12s
Project CI / Backend tests (push) Successful in 14m33s
Project CI / AI game creator shell web tests (push) Successful in 5m37s
- 模板库技术方案的命令表补记 create_automatic_local_game_project_from_template 支持 projectsRoot 覆盖,未指定时仍是应用数据目录
2026-09-18 14:30:02 +08:00
kdletters be370cc615 AGC 支持用户自选项目创建目录
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 7m12s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 7m17s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 7m26s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 7m37s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m57s
Project CI / AI game creator shell Rust crates (push) Successful in 2m10s
Project CI / Frontend tests (push) Successful in 6m4s
Project CI / Native shell tests (push) Successful in 8m58s
Project CI / Backend tests (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
- 新增设置「工作区」分类,承载「项目创建目录」的选择与恢复默认入口
- 自动建项命令新增可选 projectsRoot,未指定时沿用 AppData projects 默认目录
- 新增项目创建目录校验与解析:只接受目录选择器返回且通过私有路径门禁的既有目录
- 目录选择器新增可选 title 参数,用于「选择项目创建目录」文案
- 首页自动建项与模板建项在建项时携带用户选定的项目创建目录
- 客户端本地保存项目创建目录偏好,建项时重新校验,不作为授权凭据
- 补充设置页工作区、首页建项与目录偏好单测
- 修复项目开发视图缺失的 Shapes 图标导入,恢复 agc:typecheck 通过
- 同步实施计划、决策日志与排障记忆文档
2026-09-18 14:10:05 +08:00
suzmii 21b059bd41 修正工具组独立计时并移除重复整轮耗时
每个工具组只计算自身调用跨度,已完成组立即停止增长与进行中标记。
整轮总耗时仅在回合状态或完成小结显示一次,单工具仍按百毫秒动态更新。
补齐前组已完成后组运行、边界缺失与倒序的回归及浏览器证据。
2026-09-18 13:45:27 +08:00
suzmii 2d90b73b94 合并主线Mac运行资源与发布门禁更新
合入 origin/master b137f1759,保留画布验收与计时返修。
同步主线0.1.67版本及Mac随包资源、插件能力和发布目标检查。
合并双方排障说明,不提交个人依赖锁文件改动。
2026-09-18 13:27:28 +08:00
suzmii cd8f480719 修复生成草稿隔离与回合计时恢复及布局排队
按占位身份隔离生成面板并保存草稿,阻止改变失败请求输入后伪装重试。
保留用户真实发送时间,按既有用户条目身份恢复空运行态回合的终态边界。
修复跨栏目整理队列覆盖及多选拖动预览起点漂移。
补齐六项 review 发现的回归、原生绑定、交叉审查及浏览器验收记录。
2026-09-18 13:25:18 +08:00
suzmii b137f17599 Merge pull request '修复Mac客户端随包Codex与插件资源缺失' (#417) from fix/mac-bundled-runtime into master
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 6m26s
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 6m40s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 6m35s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 4m41s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m56s
Project CI / AI game creator shell Rust crates (push) Successful in 2m22s
Project CI / Backend tests (push) Successful in 9m16s
Project CI / Repository checks (push) Successful in 7m2s
Project CI / Frontend tests (push) Successful in 8m12s
Project CI / Native shell tests (push) Successful in 11m13s
Project CI / AI game creator shell web tests (push) Successful in 5m37s
Reviewed-on: #417
2026-09-18 12:48:00 +08:00
suzmii 2ffccb844c 修复对话动态计时与初始消息重复并补齐游戏居中指引
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 5m51s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 5m45s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 5m31s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m3s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m4s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 4m38s
Project CI / Repository checks (pull_request) Successful in 5m39s
Project CI / Frontend tests (pull_request) Successful in 7m45s
Project CI / Backend tests (pull_request) Successful in 10m3s
Project CI / Native shell tests (pull_request) Successful in 8m54s
Project CI / AI game creator shell web tests (pull_request) Failing after 5m31s
区分整轮总耗时与工具实际阶段时间,以百毫秒刷新一位小数并冻结终态。
修复正式与乐观用户消息到达时初始占位重复,保留真实同文多次发送。
在开发 Agent 系统提示与 Skill 明确禁止 Phaser 和 CSS 双重居中。
补齐原生事件、前端回归与验收文档,保留未完成项和远程 CI 风险。
2026-09-18 12:17:28 +08:00
suzmii 16c905b51b 修复发布目标与插件能力门禁并同步版本0.1.67
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 7m21s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 7m27s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 7m28s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 7m46s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m53s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 3m14s
Project CI / Backend tests (pull_request) Successful in 8m6s
Project CI / Native shell tests (pull_request) Successful in 8m11s
Project CI / Repository checks (pull_request) Successful in 5m57s
Project CI / Frontend tests (pull_request) Successful in 7m53s
Project CI / AI game creator shell web tests (pull_request) Successful in 4m38s
统一显式目标对应的版本读取、构建渠道、产物目录和更新清单
无原生适配器时隐藏Cocos插件并阻止自动启动
同步单架构更新规范并增加发布和插件回归测试
更新版本与锁文件到0.1.67并记录Mac安装包验证结果
2026-09-18 12:15:00 +08:00
suzmii 6f012d419a 修复Mac客户端随包运行依赖缺失
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 5m37s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 5m46s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 5m28s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m43s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m40s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 5m20s
Project CI / Frontend tests (pull_request) Successful in 7m41s
Project CI / Repository checks (pull_request) Successful in 7m41s
Project CI / Native shell tests (pull_request) Successful in 11m20s
Project CI / Backend tests (pull_request) Successful in 12m11s
Project CI / AI game creator shell web tests (pull_request) Successful in 4m12s
统一Codex平台布局并补齐macOS原生组件、完整性清单和资源加载路径
补齐macOS插件资源并保留Windows专属原生桥接边界
增加隔离安装包验证、平台配置门禁与侧车回归测试
声明macOS 15最低系统版本并同步规范及Windows待验收计划
2026-09-18 11:11:07 +08:00
suzmii 1bd153d7f2 合并主线对话状态与抠图背景色更新
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 4m56s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 6m9s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 5m28s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m33s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m23s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 4m39s
Project CI / Repository checks (pull_request) Successful in 4m50s
Project CI / Backend tests (pull_request) Successful in 8m38s
Project CI / Frontend tests (pull_request) Successful in 8m2s
Project CI / AI game creator shell web tests (pull_request) Successful in 8m11s
Project CI / Native shell tests (pull_request) Successful in 15m27s
合入 master a2e6f205d,保留远端功能分支已有合并历史。
融合生成选项的参考素材、目标栏目与抠图背景色字段。
保留主线统一线程状态与重试策略,完成前端及原生定向验证。
2026-09-18 10:31:55 +08:00
lhk229 a2e6f205d6 修复最终回复失败测试重试超时
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 7m5s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 7m17s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 7m18s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 7m41s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m31s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m26s
Project CI / Repository checks (pull_request) Successful in 4m2s
Project CI / Frontend tests (pull_request) Successful in 5m29s
Project CI / Backend tests (pull_request) Successful in 8m18s
Project CI / Native shell tests (pull_request) Successful in 9m38s
Project CI / AI game creator shell web tests (pull_request) Successful in 5m10s
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 6m44s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 6m26s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 5m13s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m59s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 4m25s
Project CI / AI game creator shell Rust crates (push) Successful in 1m19s
Project CI / Repository checks (push) Successful in 4m54s
Project CI / Frontend tests (push) Successful in 5m52s
Project CI / Backend tests (push) Successful in 7m4s
Project CI / AI game creator shell web tests (push) Successful in 2m38s
Project CI / Native shell tests (push) Successful in 8m0s
最终回复失败测试显式关闭 Provider 重试

让非法回复 mock 按实际请求次数监听

同步运行态公共失败审计测试的 mock 参数
2026-09-18 04:55:05 +08:00
k88936 30c377d0f0 保留唯一的thread manager作为direct project的状态来源 (#384)
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 6m21s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m51s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 5m3s
Project CI / AI game creator shell Rust shard 2/4 (push) Failing after 5m54s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 4m28s
Project CI / AI game creator shell Rust crates (push) Successful in 1m53s
Project CI / Repository checks (push) Successful in 5m33s
Project CI / Frontend tests (push) Successful in 7m20s
Project CI / Native shell tests (push) Successful in 8m29s
Project CI / Backend tests (push) Successful in 8m57s
Project CI / AI game creator shell web tests (push) Successful in 3m16s
说明: 在把工作交给段哥前还没有实现direct project聊天页面的迁移, 导致在 #375 里用很复杂的实现又做了一套事件流, 实测还有会话丢失的bug, 我在这里把数据获取的部分迁移到 #367 上

---------

Co-authored-by: 孔令弘 <ink29535@proton.me>
Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/384
Reviewed-by: 孔令弘 <ink29535@proton.me>
Co-authored-by: 王德宇 <kvtodev@outlook.com>
Co-committed-by: 王德宇 <kvtodev@outlook.com>
2026-09-18 02:13:50 +08:00
lhk229 1c6e4c1e74 AGC生图工具暴露抠图背景色并补齐相关说明 (#414)
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 6m52s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 7m10s
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 7m20s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 5m50s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m12s
Project CI / AI game creator shell Rust crates (push) Successful in 2m13s
Project CI / Repository checks (push) Successful in 6m17s
Project CI / Frontend tests (push) Successful in 7m21s
Project CI / Backend tests (push) Successful in 9m49s
Project CI / AI game creator shell web tests (push) Successful in 2m41s
Project CI / Native shell tests (push) Successful in 11m8s
agc_generate_image 新增 screenColor 参数,仅 kind=character/art-spritesheet 生效,说明含 12 色板 hex 与不得接近本体颜色的约束
screenColor 计入 standalone 生成动作身份,None 时跳过序列化,未使用该字段的请求身份与升级前逐字节一致
character 生图请求体按需注入 screenColor,art-spritesheet 图集把硬编码 auto 改为透传归一化值
bridge 侧只校验 auto/#RRGGBB 格式并统一大写,色板白名单交由服务端 400 权威校验,其它 kind 携带直接拒绝
修复 sliceCount 悬空指引:MCP schema 与双层字段白名单补齐,1-256 校验后透传至图集生成请求
精简 agc_generate_image 工具说明,删除美术包限制与客户端职责两段样板句
审计投影同步记录 screenColor,补充 bridge 归一化、动作身份指纹与工具目录契约测试

Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/414
Co-authored-by: Linghong <ink29535@proton.me>
Co-committed-by: Linghong <ink29535@proton.me>
2026-09-18 01:14:02 +08:00
lhk229 f5a333b839 Merge branch 'master' into feat/agc-canvas-acceptance
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
2026-09-18 01:04:04 +08:00
kdletters 2e15a70b14 Provider 瞬态重试次数严格按设置执行
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 7m26s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 7m38s
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 7m46s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 8m4s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m19s
Project CI / AI game creator shell Rust crates (push) Successful in 2m13s
Project CI / Backend tests (push) Successful in 10m11s
Project CI / Frontend tests (push) Successful in 12m57s
Project CI / Repository checks (push) Successful in 15m23s
Project CI / AI game creator shell web tests (push) Successful in 9m18s
Project CI / Native shell tests (push) Successful in 22m24s
删除自主构建档位 12~16 的重试区间与 standard 档位 3 次上限,maxRetries 严格决定物理重试次数
上游 400 不再单独收窄到 2 次,与其它瞬态错误共用设置里的同一份预算
运行档位改为只决定上游 400 是否算瞬态,重试次数不再被档位改写
重写档位重试用例为设置值直取,同步 upstream-400 用例
decision-log 记录本次口径变更与本机既有失败边界
2026-09-18 00:01:40 +08:00
suzmii 20b1fd63de 合并最新主线抠图链路与开发监听修复
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 6m44s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 6m55s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 7m3s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m9s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m30s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 4m41s
Project CI / Native shell tests (pull_request) Successful in 15m5s
Project CI / Repository checks (pull_request) Successful in 16m54s
Project CI / Backend tests (pull_request) Successful in 19m49s
Project CI / Frontend tests (pull_request) Successful in 23m33s
Project CI / AI game creator shell web tests (pull_request) Successful in 11m6s
合入master的抠图账本链路、Vite监听边界及窗口测试更新

保留画布占位参考生成、资源展示与批量标签追加功能

合并态通过137项定向测试、AGC类型检查及Rust全目标编译
2026-09-17 22:18:23 +08:00
suzmii 105591bac5 支持画布与资源面板多选素材批量追加标签
复用标签编辑器并冻结整组选中素材,保留原标签与素材类型

原生批量校验后一次保存,覆盖权限、版本冲突与无变化边界

修复保存输入锁和切项目迟到回包,补齐混合选择入口原因

新增前后端回归并记录本地验证及待真实客户端验收事项
2026-09-17 21:51:59 +08:00
lhk229 195b7dd5dd 修复抠图401 404问题 (#394)
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 7m34s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 7m39s
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 7m50s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 7m56s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m31s
Project CI / AI game creator shell Rust crates (push) Successful in 3m19s
Project CI / Native shell tests (push) Successful in 16m33s
Project CI / Backend tests (push) Successful in 17m56s
Project CI / Repository checks (push) Successful in 15m2s
Project CI / Frontend tests (push) Successful in 23m48s
Project CI / AI game creator shell web tests (push) Successful in 23m11s
Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/394
Co-authored-by: Linghong <ink29535@proton.me>
Co-committed-by: Linghong <ink29535@proton.me>
2026-09-17 21:37:23 +08:00
suzmii da44d66dc8 明确多选素材批量追加标签的行为合同
限定追加语义、冻结对象、原子保存与类型保留边界

记录已评审里程碑及前后端并行实现计划
2026-09-17 21:12:42 +08:00
suzmii 0a85c4f87e 合并主线资源菜单和引擎预览功能
合入PR410的常用操作收纳与卡片信息类型入口

保留本分支资源名称、文档图标、占位生成与批量移动能力

整合引擎结构摘要与当前图集测试契约
2026-09-17 21:06:15 +08:00
suzmii efce7b102f 对齐主线图集契约与窗口同步测试要求
补齐图集切分声明与工具目录字段边界断言

更新原生HTTP权限守卫及稳定空快照发布次数期望

修正末尾空行并记录CI排查证据,保留待远程验证状态
2026-09-17 21:01:26 +08:00
suzmii db5edc948c 合并最新主分支并修复画布静态检查问题
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 7m19s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 7m30s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 7m28s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 7m41s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m11s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m10s
Project CI / Native shell tests (pull_request) Failing after 4m28s
Project CI / Frontend tests (pull_request) Failing after 5m12s
Project CI / Backend tests (pull_request) Successful in 7m17s
Project CI / Repository checks (pull_request) Failing after 5m43s
Project CI / AI game creator shell web tests (pull_request) Failing after 4m34s
合入master的更新摘要、策划运行边界及图集契约调整

保留画布生成引用占位、资源卡展示与批量布局功能

修正导入顺序、未使用项和稳定回调依赖
2026-09-17 20:15:38 +08:00
suzmii 45c3780bf5 记录画布验收修复的验证证据与待验边界
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
登记自动化回归和独立评审结论

保留真实客户端与付费生成待验项及远程写入确认边界
2026-09-17 19:46:49 +08:00
suzmii 6fcf42e4ac 修 appSurface 生成任务用例的异步排空:等阶段合并、等轮询退出
- 阶段文案与徽标改用 waitFor:本地队列的「排队中。」先于后端记录合并,同步断言在整套连跑时抢跑
- 用例收尾连等两个 poll 周期(2s)并断言账本读计数不再增长,确认这条轮询链在本用例内退出
- 留在线上的轮询醒来会打进下一个用例的 spy(宿主按调用时读 __TAURI__),故排空放在用例内,未动全局 harness
- 未改生产:后台任务跨面板保活保持原样
2026-09-17 19:44:48 +08:00
suzmii e0f9f811b7 生成浮层居中定位与画布安全带高度
补回 transform translateX(-50%):锚点给的是占位卡中心,少了它浮层整体右偏半个面板宽
高度只按画布安全带分配:装得下时挂在卡下面,装不下时与卡顶边对齐盖住占位,不再压成一百多像素
reveal 的面板高度按当前缩放从屏幕 px 折回世界坐标,占位与浮层并集参与可见性
浮层宽度并入可见性并集,靠边卡片不再把浮层切掉一半
新增真实矩形模拟用例(画布高 568)核对浮层高度与落点不越出安全带
2026-09-17 19:35:41 +08:00
suzmii 03b5c1c9f4 补画布生成入口宿主生命周期验收:占位提交、同 operation 重试与成功落点
- 新增测试文件 resourceCanvasGenerationHostLifecycle.test.tsx,渲染真实 ProjectDevelopmentView
- 覆盖音频入口:点工具出占位、提交后占位进入 submitted(后台仍在跑时的中间态)
- 覆盖失败后原请求重试:复用同一 operationId 与幂等键,不会变成新的付费生成
- 覆盖成功后落点:结果卡按占位位置落盘(手动接管),占位随之被撤掉
- 原生命令全部走假实现,不触发任何真实 Provider 调用
2026-09-17 19:20:31 +08:00
suzmii 58992330aa 生成浮层面板 chrome 与画布安全高度修复
浮层复用模态的面板类:边框圆角底色内边距与 header 排布不再缺失
高度上界按真实画布可用底边算(画布高减退底栏安全区与顶边),空间紧时收紧并内部滚动
提交动作行固定在浮层底部,避免长表单把提交按钮顶到底栏下面
新增浮层高度模型测试、结构 CSS 钉子与浮层 chrome 断言
新增宿主级整合用例:图片按正式归类落点并用占位最新位置、音频失败保留与幂等重试
2026-09-17 19:08:34 +08:00
suzmii 2c687b01a4 补齐生成结果分类与占位交互验收合同
明确原生目标分类、音频幂等及画布浮层可见性
2026-09-17 19:08:34 +08:00
suzmii c0e377f479 测试助手同时支持纯文本与引用输入区
appSurface 图集/图标规范入口的提示词仍是 textarea,helper 不能只走 Lexical 编辑器 API
两种字段分别用原生 change 与编辑器 update,读回同样两种口径
补 targetCategory 随任务保存与恢复的断言
2026-09-17 19:01:35 +08:00
suzmii 6001b87215 画布生成按入口栏目落盘目标分类
GUI 生成命令新增可选 targetCategory:generate_local_project_asset 与 start_local_project_asset_generation 经 options 传入完成登记
PlatformArtAssetGenerationOptions 增加 target_category,Agent 与 Direct 路径保持 None,仍按 kind 派生默认分类
新建条目与同 kind 重登记都按显式目标分类写入 manifest 既有 category 字段,不新增字段、不改数据结构
目标分类只接受 GameCreationAppAssetCategory 的六个合法值,version 与 all 等栏目伪值在提交前失败关闭
target_category 刻意不进 standalone 指纹与 durable 请求快照,分类只影响本地 manifest 落点,避免在途账本换槽重复 POST,同任务幂等恢复由调用方继续用同一栏目提交
补用例:kind=image 带 targetCategory=character 落到 character 而不是 unclassified、非法栏目值不产生生成请求、登记入口的显式分类与不传时的原 kind 派生行为
2026-09-17 18:58:53 +08:00
suzmii b08ab6ee66 生成占位可见性、落点与音频提交身份修复
打开生成浮层时把占位与浮层整块带进安全区(避开顶栏底栏),只做最小视口平移不改缩放
落点意图改为按草稿 ID 的 Map,避免并发完成互相覆盖;坐标在落点时刻取占位最新位置
落点 section 改用资源正式归类后的 category,修复图片落待归类、规范落文档时静默跳过
音频与背景音乐提交绑定占位与 operationId:在途时拒绝再次提交,失败保留输入并可同 operation 重试
宿主记录收起浮层时的草稿与音频提交身份,重开占位接着编辑而不是回到空表单
生成任务新增入口栏目 targetCategory 走原生可选入参,前端不据此伪分类
补可见性平移的定向测试
2026-09-17 18:50:29 +08:00
suzmii 7f80012d7f 清理占位重开状态里不再读取的动作 ID
生成浮层的草稿、错误与占位归属统一按 draftId 判等,动作 ID 已无读取方,删掉避免留下死状态
2026-09-17 18:50:29 +08:00
suzmii fbe95591d5 校准布局撤销作用域的行为说明
明确切模式后新操作会替换旧撤销栈但不会跨写布局
2026-09-17 18:31:16 +08:00
suzmii d222aad2ec 参考素材改为提交前预检并补参考链路用例
参考解析前增加纯本地预检:清单归属、受控路径与文件、媒体类型、可解码位图全部通过后才允许产生任何上传副作用,避免第一张参考已上传、第二张坏图才失败
明确拒绝 SVG 等矢量参考,媒体类型或文件扩展名任一命中即拒,本次不做矢量转换,坏图同样在本地失败关闭
asset.upload 门禁提前到预检阶段,显式拒绝该命令的项目连第一个上传凭证都不会签发
预检与实际上传共用同一份本地判据 helper,不新建平行流程,上传侧仍按同一判据重新读取与解码
补参考合同与恢复的账本用例:icon-spec 带用户参考、game-background 规范图加用户参考、图集超限、空白引用写入失败
补提交侧用例:拒绝路径、未登记、非图片、缺失文件、超限、SVG 与坏图,并断言零上传凭证与零生成 POST,反向验证同一张合格参考单独提交确实会签发凭证
补 asset.upload 拒绝用例,以及清单校验与 Direct 身份校验接受用户参考、拒绝用户参考顶替规范引用的覆盖
2026-09-17 18:28:19 +08:00
suzmii 13b28ebbc7 生成占位与卡下独立浮层,修复引用选择被焦点陷阱阻断
工具点击先在当前栏目创建临时占位卡,生成浮层挂在占位下沿;图片、音效、背景音乐入口一致
占位按项目与草稿 ID 归属宿主临时状态:可拖动、提交绑定任务 ID、成功结果落到占位最新位置
删除占位只隐藏展示,关闭浮层不冒充取消后台任务;失败保留占位与输入引用供重试
生成面板改为非模态浮层:ThemedModal 焦点陷阱曾阻断 portal 到 body 的引用选择器,点选不生效
引用选择器定位加顶边钳制,搜索收窄与键盘选择可用;不再越出视口
参考候选与陈旧引用校验排除 SVG(原生解码不支持),其余 image 类型不额外收窄
任务创建处对参考资产 ID 去重;补浮层真实组件整合、占位模型与拖动、参考模型回归测试
2026-09-17 18:22:39 +08:00
suzmii 30648e6b93 画布修复:重算意图各占队列槽、撤销不跨排序模式、整理判据与落盘同源
- 显式整理与「关系图首次就绪」各占一个队列槽,系统重算不再改写用户已按下的那笔整理
- 资源签名同步覆盖排队中的每一笔资源意图,先落盘的那笔不会把后一笔留成过期签名
- 撤销栈按「项目 + 排序模式」隔离:切模式后不会把另一份 sidecar 的坐标写过去
- 整理的「有没有变化」判据与落盘同源,目标栏目还有在途/排队手动落点时也算一次真实变化
- 删除本次改动退役且已无调用方的 resourceBookAllBandLocalPoint 与布局 dragX/dragY
- 补回归:排队中整理不被覆盖、切模式撤销不跨写、在途拖动后整理照常生效、分类变更后拖动按新栏目落盘
2026-09-17 18:18:32 +08:00
suzmii 9dd1052374 明确参考图片格式与上传前整组校验边界
排除不可解码SVG参考并约束上传权限与账号归属判据
2026-09-17 18:18:32 +08:00
suzmii aec9568c39 接通画布生成参考素材的原生链路
新增 referenceAssetIds 原生入参:start_local_project_asset_generation 与 generate_local_project_asset 经 prepare_local_project_asset_generation 统一收口当前项目 manifest 图片素材 id
PlatformArtAssetGenerationOptions 增加 reference_asset_ids,GUI、agent 桥、media 观察与 Direct 运行时构造点显式传空
参考解析固定规范图前置在前、用户参考按给出顺序随后,按远端资源 ID 去重,普通生成总量 5 张、有规范前置 4 张用户参考,图集拒绝额外参考而非静默丢弃
参考素材复用 manifest 到本地文件、图片解码、内容 hash、当前账号 binding 上传的既有通道,历史账号遗留远端 ID 不再复用
retained 阶段与清单校验改为与提交同一套请求合同:icon-spec、ui-prototype、game-background 允许正确引用,art-spritesheet 仍只接受唯一规范引用
修复 Part::file_name 的生命周期错误与 4 处测试构造点少传参考参数
2026-09-17 17:57:53 +08:00
suzmii 0a1f0e0b26 画布布局收口:整理只动当前栏目、多选拖动一笔提交与一次撤销
- 整理画布重排目标栏目内全部素材(含手动坐标与被筛选隐藏的卡),「所有资源」页覆盖全部栏目
- 栏外坐标逐值保留:不再把其他栏目的自动坐标丢给重算,避免被顺带规整成规范槽位
- 手动标记纳入历史快照与相等判定,撤销整理恢复原坐标与原手动标记
- 新增批量坐标写入入口:多选拖动与撤销都只排一笔 CAS、只压一条历史
- 多选拖动按同一位移整批跟随,取消 / 捕获丢失 / 失焦回滚,切项目丢弃进行中的位移
- 补测试:栏外自动坐标保留、all 页整理范围、批量撤销与取消边界
2026-09-17 17:47:11 +08:00
suzmii 5c31ae91ca 返修资源卡完整名悬停入口并收短文档卡注释
- 完整资源名改挂在覆盖整卡、真正命中指针的选中按钮 title 上,名称条自身不再挂悬停不到的 title

- 无障碍名保持原有 aria-label 不变,长名仍是 CSS 单行省略

- 断言改为校验选中按钮 title,并钉住名称条不挂 title

- 文档卡分支注释只写当前行为,去掉旧实现说明
2026-09-17 17:37:48 +08:00
suzmii 3ba6168c6a 同步资源卡名称与画布布局交互产品合同
统一卡面名称和文档图标展示要求

明确当前栏目整理与多选移动的撤销边界
2026-09-17 17:15:38 +08:00
suzmii 2628b83d4b 画布资源卡统一显示正式资源名并简化文档卡卡面
- 资源卡新增卡面名称条,所有类型卡统一显示正式资源 label,长名单行省略、完整名进 title

- 文档卡卡面改为居中图标,不再把正文前几行铺在卡面上,正文只在独立详情浮层读取

- 删除卡面正文摘要的取文本 helper 与行数/长度常量,移除对应 CSS 规则不留墓碑

- 名称条按媒体卡让出右下播放钮位置,替换血缘角标上移避免遮住名称

- 新增资源卡名称与文档卡卡面用例,并同步重命名链路与卡面正文的既有断言
2026-09-17 17:08:42 +08:00
suzmii a7b2b0e23b 画布图片生成支持真实参考图引用
新增参考图模型:候选为当前项目已登记图片,用户参考上限普通生成 5 张、带规范前置 4 张、图集为 0
生成浮层复用资源引用输入组件,提示词里的 @ 引用直接产出当前 manifest 资产 ID 作为参考
生成任务与队列新增 referenceAssetIds,派发时以 referenceAssetIds 交给原生侧解析账号绑定
图集入口不呈现参考选择器,超限在提交前给出原因并挡住提交,不做静默截断
补充参考模型、面板接线与队列派发的定向测试
2026-09-17 16:59:49 +08:00
suzmii ef982fdb78 拆分原生引用与前端生成并行实现边界
使用独立工作树处理原生引用,固定引用资产ID接缝
2026-09-17 16:59:48 +08:00
suzmii 91b65f94ca 确保工作树测试读取本地共享组件源码
补齐共享包测试别名以避免复用依赖指向其他工作树

记录工作树依赖复用的解析检查要求
2026-09-17 16:18:17 +08:00
suzmii 70513cf049 按规范评审固定参考上限并放行实现
普通生成最多五张参考,图集拒绝额外参考

复用现有引用快照并核验恢复校验,明确里程碑验收边界
2026-09-17 16:09:14 +08:00
suzmii 87aed0b765 补齐画布任务身份与引用持久化边界
明确参考图片入参、账号绑定与恢复校验

固定栏目整理撤销、多选范围与素材名称来源
2026-09-17 16:06:57 +08:00
suzmii 733a7015af 明确画布验收修复范围与行为合同
统一生成占位与参考、资源卡展示和布局交互的验收要求

补充单里程碑并行实现计划和独立评审门禁
2026-09-17 16:02:37 +08:00
196 changed files with 26763 additions and 5658 deletions
+6
View File
@@ -41,6 +41,12 @@ temp*build*/
/apps/ai-game-creator-shell/src-tauri/resources/codex/win-x64/codex-resources/
/apps/ai-game-creator-shell/src-tauri/resources/codex/win-x64/codex-package.json
/apps/ai-game-creator-shell/src-tauri/resources/plugins/
/apps/ai-game-creator-shell/src-tauri/resources/codex/mac-native/bin/
/apps/ai-game-creator-shell/src-tauri/resources/codex/mac-native/codex-path/
/apps/ai-game-creator-shell/src-tauri/resources/codex/mac-native/codex-resources/
/apps/ai-game-creator-shell/src-tauri/resources/codex/mac-native/codex-package.json
/apps/ai-game-creator-shell/src-tauri/resources/codex/mac-native/manifest.json
/apps/ai-game-creator-shell/src-tauri/resources/codex/mac-native/NOTICE.md
/plugins/agc-cocos-editor/native/payload/
/apps/ai-game-creator-shell/logs/
/apps/ai-game-creator-shell/.llm-drafts/
+16
View File
@@ -172,6 +172,20 @@ _Avoid_: 多步骤向导、完整规则编辑器、拖拽编辑器
Bark Battle 平台作品闭环按契约与领域规则、后端存储/API、最小前端纵切、投影体验、收口验证的顺序推进。
_Avoid_: mock 先行堆积、前后端各自发散、先做排行榜 UI
## 项目开发对话(DirectProject
**项目对话历史**:
AGC 本地项目内 Codex 原始对话条目的持久集合,是聊天展示、工具卡片和线程恢复注入的唯一持久事实源。
_Avoid_: 会话缓存、展示态历史、按 UI 需要另存的对话副本
**运行态事件**:
Thread Manager 向订阅者推送的当前回合原始事件流,只服务运行期间与短期断线恢复,不替代项目对话历史。
_Avoid_: 进度通知、快照轮询、第二套历史
**聊天投影**:
把项目对话历史条目与运行态事件转换成消息气泡和工具卡片的读取期转换;不持久化,也不构成事实源。
_Avoid_: 投影缓存文件、已脱敏卡片库、第二套 reducer
## Relationships
- 一个 **汪汪声浪大作战** 单局包含多个 **有效声浪触发**
@@ -206,3 +220,5 @@ _Avoid_: mock 先行堆积、前后端各自发散、先做排行榜 UI
- “入口闭环”曾可能只指内部 demo 或单个详情 CTA;已解析为 **正式作品入口闭环**,不新增独立专区或活动页。
- “创作编辑”曾可能指多步骤向导或完整编辑器;已解析为 **轻配置编辑流程**,使用单页表单 + 预览卡片完成保存草稿、发布和发布后跳转作品详情。
- “实施顺序”曾可能按 UI 或功能并行发散;已解析为契约/领域规则先行,再做后端存储/API,随后打通最小前端纵切,最后补投影体验与收口验证。
- “回合进度事件”曾同时指 Direct turn update 与 Thread Manager 运行态事件;已解析为 AGC 项目开发对话只保留 **运行态事件**
- “哪些消息可显示”曾可能由后端历史分页判断;已解析为可见性判断属于 **聊天投影**,后端只按原始条目分页,前端负责跳过不可显示条目并推进分页锚点。
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@genarrative/ai-game-creator-shell",
"private": true,
"version": "0.1.47",
"version": "0.1.67",
"type": "module",
"scripts": {
"dev": "node scripts/start-tauri-dev.mjs",
@@ -14,16 +14,71 @@ const appRoot = fileURLToPath(new URL('..', import.meta.url));
// 提交摘要里的 pathspec 与 `git log` 都以仓库根为基准,不能在应用目录里执行。
const repoRoot = path.resolve(appRoot, '..', '..');
const defaultReleaseTarget = 'x86_64-pc-windows-msvc';
const releaseTarget =
process.env.AGC_BUILD_TARGET?.trim() || defaultReleaseTarget;
const bundleRoot = path.join(
appRoot,
'src-tauri',
'target',
releaseTarget,
'release',
'bundle',
);
function defaultTarget() {
return process.env.AGC_BUILD_TARGET?.trim() || defaultReleaseTarget;
}
function explicitBuildTarget(args) {
let target;
const separator = args.indexOf('--');
const options = separator < 0 ? args : args.slice(0, separator);
for (let index = 0; index < options.length; index += 1) {
const argument = options[index];
let value;
if (argument === '--target' || argument === '-t') {
value = options[++index];
} else if (argument.startsWith('--target=')) {
value = argument.slice('--target='.length);
} else {
continue;
}
if (!value?.trim() || value.startsWith('-')) {
throw new Error('--target 缺少有效目标');
}
if (target !== undefined) throw new Error('不能重复指定 --target');
target = value.trim();
}
return target;
}
function validateReleaseTarget(target) {
if (target === 'universal-apple-darwin') {
throw new Error(
'内置 Codex 资源仅支持 macOS 单架构构建,请使用 aarch64-apple-darwin 或 x86_64-apple-darwin',
);
}
if (
![
'x86_64-pc-windows-msvc',
'aarch64-apple-darwin',
'x86_64-apple-darwin',
].includes(target)
) {
throw new Error(`不支持的发布目标:${target}`);
}
return target;
}
/** 在入口冻结目标;所有发布步骤共享同一上下文,不再各自读取默认目标。 */
export function resolveReleaseContext(args = [], env = process.env) {
const target = validateReleaseTarget(
explicitBuildTarget(args) ||
env.AGC_BUILD_TARGET?.trim() ||
defaultReleaseTarget,
);
return Object.freeze({
target,
channel: resolveReleaseChannel(env, target),
bundleRoot: path.join(
appRoot,
'src-tauri',
'target',
target,
'release',
'bundle',
),
});
}
const packageJsonPath = path.join(appRoot, 'package.json');
const rootPackageLockPath = path.resolve(appRoot, '../..', 'package-lock.json');
const tauriConfigPath = path.join(appRoot, 'src-tauri', 'tauri.conf.json');
@@ -99,7 +154,7 @@ export function nextPatchVersion(localVersion, remoteVersion) {
return `${major}.${minor}.${patch + 1}`;
}
export function resolveReleasePlatform(target = releaseTarget) {
export function resolveReleasePlatform(target = defaultTarget()) {
if (target.includes('windows')) return 'windows';
if (target.includes('apple-darwin')) return 'darwin';
if (target.includes('linux')) return 'linux';
@@ -108,7 +163,7 @@ export function resolveReleasePlatform(target = releaseTarget) {
export function resolveReleaseChannel(
env = process.env,
target = releaseTarget,
target = defaultTarget(),
) {
const platform = resolveReleasePlatform(target);
const requested = env.AGC_UPDATE_CHANNEL?.trim();
@@ -142,13 +197,10 @@ export function updateManifestUrl(channel = resolveReleaseChannel()) {
}
/**
* 更新插件按运行时平台键查找清单条目:universal macOS 包同时挂
* `darwin-aarch64` 与 `darwin-x86_64`,单架构目标只挂对应键。
* 单架构产物只登记实际目标,不能把同一原生资源映射为另一架构。
*/
export function resolveManifestPlatformKeys(target = releaseTarget) {
if (target === 'universal-apple-darwin') {
return ['darwin-aarch64', 'darwin-x86_64'];
}
export function resolveManifestPlatformKeys(target = defaultTarget()) {
validateReleaseTarget(target);
if (target === 'aarch64-apple-darwin') return ['darwin-aarch64'];
if (target === 'x86_64-apple-darwin') return ['darwin-x86_64'];
if (target.includes('windows')) {
@@ -256,8 +308,8 @@ function replaceVersionLine(source, version, pattern, label) {
return source.replace(pattern, `$1${version}$3`);
}
export async function prepareReleaseVersion() {
const channel = resolveReleaseChannel();
export async function prepareReleaseVersion(context = resolveReleaseContext()) {
const { channel } = context;
const localVersion = parseVersion(readPackageJson().version, '本地版本');
const remoteVersion = await resolveRemoteHighWaterVersion(channel);
const requestedVersion = process.env.AGC_RELEASE_VERSION?.trim();
@@ -330,18 +382,14 @@ export async function prepareReleaseVersion() {
export function buildTauriBuildArguments(
args = [],
target = releaseTarget,
target = defaultTarget(),
platform = process.platform,
) {
const noBundle = args.includes('--no-bundle');
const targetIndex = args.indexOf('--target');
const explicitTarget =
targetIndex >= 0
? args[targetIndex + 1]
: args
.find((value) => value.startsWith('--target='))
?.slice('--target='.length);
const explicitTarget = explicitBuildTarget(args);
const targetArgs = noBundle || explicitTarget ? [] : ['--target', target];
if (!noBundle || explicitTarget)
validateReleaseTarget(explicitTarget || target);
const features = defaultEditorFeatures(
explicitTarget || (noBundle ? platform : target),
);
@@ -374,18 +422,33 @@ function writeChannelConfigFile(channel) {
return configPath;
}
export function runTauriBuild(args = []) {
const tauriArguments = buildTauriBuildArguments(args);
if (!tauriArguments.includes('--config') && !tauriArguments.includes('-c')) {
const channel = resolveReleaseChannel();
const configPath = writeChannelConfigFile(channel);
console.log(
`[ai-game-creator-shell] 渠道 ${channel} 端点配置:${configPath}`,
);
tauriArguments.push('--config', configPath);
export function runTauriBuild(
args = [],
context = resolveReleaseContext(args),
{ spawn = spawnSync } = {},
) {
if (
explicitBuildTarget(args) &&
explicitBuildTarget(args) !== context.target
) {
throw new Error('构建参数与发布上下文目标不一致');
}
const tauriArguments = buildTauriBuildArguments(args, context.target);
const { channel } = context;
const configPath = writeChannelConfigFile(channel);
console.log(
`[ai-game-creator-shell] 渠道 ${channel} 端点配置:${configPath}`,
);
// 最后合并渠道配置,防止用户配置中的端点与实际发布目标分叉。
const separator = tauriArguments.indexOf('--');
tauriArguments.splice(
separator < 0 ? tauriArguments.length : separator,
0,
'--config',
configPath,
);
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
const result = spawnSync(
const result = spawn(
npmCommand,
['--prefix', '../..', 'exec', 'tauri', '--', ...tauriArguments],
{ cwd: appRoot, stdio: 'inherit', shell: process.platform === 'win32' },
@@ -402,11 +465,11 @@ function listFiles(root) {
});
}
function artifactPriority(filePath) {
function artifactPriority(filePath, target) {
const name = path.basename(filePath).toLowerCase();
if (releaseTarget.includes('windows')) return name.endsWith('.exe') ? 0 : 99;
if (target.includes('windows')) return name.endsWith('.exe') ? 0 : 99;
// 更新链路要的是 updater 产物(macOS 为 .app.tar.gz),dmg 只作人工分发。
if (releaseTarget.includes('apple-darwin')) {
if (target.includes('apple-darwin')) {
return name.endsWith('.app.tar.gz') ? 0 : 99;
}
if (name.endsWith('.appimage.tar.gz')) return 0;
@@ -416,7 +479,8 @@ function artifactPriority(filePath) {
return 99;
}
export function selectReleaseArtifact(files) {
export function selectReleaseArtifact(files, target = defaultTarget()) {
validateReleaseTarget(target);
const explicit = process.env.AGC_UPDATE_ARTIFACT?.trim();
if (explicit) {
const resolved = path.resolve(explicit);
@@ -427,9 +491,10 @@ export function selectReleaseArtifact(files) {
}
return (
[...files]
.filter((filePath) => artifactPriority(filePath) < 99)
.filter((filePath) => artifactPriority(filePath, target) < 99)
.sort((left, right) => {
const priority = artifactPriority(left) - artifactPriority(right);
const priority =
artifactPriority(left, target) - artifactPriority(right, target);
return priority || left.localeCompare(right);
})[0] ?? null
);
@@ -450,13 +515,15 @@ function readUpdaterSignature(artifactPath) {
export function createUpdateManifest(
artifactPath,
{
channel = resolveReleaseChannel(),
target = releaseTarget,
target = defaultTarget(),
channel = resolveReleaseChannel(process.env, target),
publishedAt = new Date().toISOString(),
notes = readReleaseNotes(),
commit = readHeadCommit(),
} = {},
) {
validateReleaseTarget(target);
resolveReleaseChannel({ AGC_UPDATE_CHANNEL: channel }, target);
const signature = readUpdaterSignature(artifactPath);
const version = readPackageJson().version;
const fileName = path.basename(artifactPath);
@@ -604,9 +671,11 @@ export function createLegacyUpdateManifest(
};
}
export async function generateUpdateManifest() {
const channel = resolveReleaseChannel();
const artifact = selectReleaseArtifact(listFiles(bundleRoot));
export async function generateUpdateManifest(
context = resolveReleaseContext(),
) {
const { channel, target, bundleRoot } = context;
const artifact = selectReleaseArtifact(listFiles(bundleRoot), target);
if (!artifact) {
throw new Error(`未找到可发布的 AGC 安装包:${bundleRoot}`);
}
@@ -623,7 +692,7 @@ export async function generateUpdateManifest() {
`[ai-game-creator-shell] 未生成自动更新摘要(上一发布 commit=${previousCommit ?? '未知'},客户端相关提交=${commits ? commits.length : '不可判定'},最近提交=${recentCommits ? recentCommits.length : '不可判定'}`,
);
}
const manifest = createUpdateManifest(artifact, { channel, notes });
const manifest = createUpdateManifest(artifact, { channel, target, notes });
const manifestPath = path.join(bundleRoot, 'latest.json');
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
const notesPath = path.join(bundleRoot, 'release-notes.txt');
@@ -675,12 +744,24 @@ export async function generateUpdateManifest() {
};
}
export async function buildRelease(
args = [],
{
prepareVersion = prepareReleaseVersion,
build = runTauriBuild,
generateManifest = generateUpdateManifest,
} = {},
) {
const context = resolveReleaseContext(args);
if (!args.includes('--no-bundle')) await prepareVersion(context);
build(args, context);
if (!args.includes('--no-bundle')) return generateManifest(context);
}
if (
process.argv[1] &&
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
) {
const args = process.argv.slice(2);
if (!args.includes('--no-bundle')) await prepareReleaseVersion();
runTauriBuild(args);
if (!args.includes('--no-bundle')) await generateUpdateManifest();
await buildRelease(args);
}
@@ -14,6 +14,8 @@ import { fileURLToPath } from 'node:url';
import {
agcReleasePathPatterns,
buildRelease,
buildTauriBuildArguments,
collectRecentReleaseCommits,
collectReleaseCommits,
compareVersions,
@@ -22,11 +24,14 @@ import {
createUpdateManifest,
formatRecentReleaseNotes,
formatReleaseNotes,
generateUpdateManifest,
nextPatchVersion,
resolveManifestPlatformKeys,
resolvePreviousReleaseCommit,
resolveReleaseChannel,
resolveReleaseContext,
resolveRemoteHighWaterVersion,
runTauriBuild,
selectReleaseArtifact,
updateManifestUrl,
} from './build-release.mjs';
@@ -34,6 +39,21 @@ import {
const windowsTarget = 'x86_64-pc-windows-msvc';
const universalTarget = 'universal-apple-darwin';
test('native sidecar builds reject universal targets and accept each macOS architecture', () => {
assert.throws(() => buildTauriBuildArguments([], universalTarget), /单架构/);
assert.throws(
() => buildTauriBuildArguments(['--target=universal-apple-darwin']),
/单架构/,
);
for (const target of ['aarch64-apple-darwin', 'x86_64-apple-darwin']) {
assert.deepEqual(buildTauriBuildArguments([], target), [
'build',
'--target',
target,
]);
}
});
function withEnv(overrides, run) {
const previous = new Map();
for (const [key, value] of Object.entries(overrides)) {
@@ -132,9 +152,12 @@ test('channel manifest URL and build-time endpoint follow the channel', () => {
});
});
test('universal macOS builds publish one artifact under both platform keys', () => {
assert.deepEqual(resolveManifestPlatformKeys(universalTarget), [
test('macOS manifests only advertise the architecture actually built', () => {
assert.throws(() => resolveManifestPlatformKeys(universalTarget), /单架构/);
assert.deepEqual(resolveManifestPlatformKeys('aarch64-apple-darwin'), [
'darwin-aarch64',
]);
assert.deepEqual(resolveManifestPlatformKeys('x86_64-apple-darwin'), [
'darwin-x86_64',
]);
assert.deepEqual(resolveManifestPlatformKeys(windowsTarget), [
@@ -142,6 +165,218 @@ test('universal macOS builds publish one artifact under both platform keys', ()
]);
});
test('release context resolves explicit targets before environment/default and fails closed', () => {
for (const args of [
['--target', 'aarch64-apple-darwin'],
['--target=aarch64-apple-darwin'],
['-t', 'aarch64-apple-darwin'],
]) {
for (const env of [{}, { AGC_BUILD_TARGET: windowsTarget }]) {
const context = resolveReleaseContext(args, env);
assert.equal(context.target, 'aarch64-apple-darwin');
assert.equal(context.channel, 'dev-mac');
assert.match(
context.bundleRoot.replaceAll('\\', '/'),
/target\/aarch64-apple-darwin\/release\/bundle$/,
);
assert.ok(Object.isFrozen(context));
}
assert.throws(
() => resolveReleaseContext(args, { AGC_UPDATE_CHANNEL: 'dev-win' }),
/只能用于 windows/,
);
}
assert.equal(resolveReleaseContext([], {}).target, windowsTarget);
assert.equal(
resolveReleaseContext([], { AGC_BUILD_TARGET: 'x86_64-apple-darwin' })
.channel,
'dev-mac',
);
for (const args of [
['--target'],
['--target='],
['--target', '--no-bundle'],
['--target', windowsTarget, '--target=aarch64-apple-darwin'],
['--target', universalTarget],
['--target', 'unknown'],
])
assert.throws(() => resolveReleaseContext(args, {}));
});
test('explicit macOS target drives version lookup, Tauri endpoint, artifact and manifest together', async () => {
const calls = [];
const seenContexts = [];
await withStubbedFetch(
(url) => {
calls.push(url);
assert.match(url, /\/dev-mac\/latest\.json$/);
return jsonResponse({ version: '0.1.67' });
},
() =>
withEnv(
{ AGC_BUILD_TARGET: undefined, AGC_UPDATE_CHANNEL: undefined },
() =>
buildRelease(['--target', 'aarch64-apple-darwin'], {
prepareVersion: async (context) => {
seenContexts.push(context);
assert.equal(
await resolveRemoteHighWaterVersion(context.channel),
'0.1.67',
);
},
build: (args, context) => {
seenContexts.push(context);
runTauriBuild(args, context, {
spawn: (_binary, command) => {
const configIndex = command.lastIndexOf('--config');
const config = JSON.parse(
readFileSync(command[configIndex + 1], 'utf8'),
);
assert.match(
config.plugins.updater.endpoints[0],
/\/dev-mac\/latest\.json$/,
);
assert.ok(command.includes('aarch64-apple-darwin'));
assert.ok(
!command.includes('--features=cocos-editor-execute'),
);
return { status: 0 };
},
});
},
generateManifest: (context) => {
seenContexts.push(context);
withSignedArtifact('陶泥儿.app.tar.gz', (artifact) => {
assert.equal(
selectReleaseArtifact(
['/tmp/win.exe', artifact, '/tmp/mac.dmg'],
context.target,
),
artifact,
);
const manifest = createUpdateManifest(artifact, context);
assert.deepEqual(Object.keys(manifest.platforms), [
'darwin-aarch64',
]);
assert.match(
manifest.platforms['darwin-aarch64'].url,
/\/dev-mac\//,
);
});
},
}),
),
);
assert.equal(calls.length, 1, 'Mac 不应读取 Windows 迁移指针');
assert.equal(seenContexts.length, 3);
assert.ok(seenContexts.every((context) => context === seenContexts[0]));
});
test('real manifest writer uses the resolved bundle root and does not emit Windows artifacts', async () => {
const root = mkdtempSync(path.join(os.tmpdir(), 'agc-mac-manifest-'));
try {
const artifact = path.join(root, '陶泥儿.app.tar.gz');
writeFileSync(artifact, 'mac package');
writeFileSync(`${artifact}.sig`, 'mac signature');
writeFileSync(path.join(root, 'windows.exe'), 'wrong platform');
const context = {
...resolveReleaseContext(['--target=x86_64-apple-darwin'], {}),
bundleRoot: root,
};
const result = await withStubbedFetch(
(url) => {
assert.match(url, /\/dev-mac\/latest\.json$/);
return jsonResponse({}, 404);
},
() => generateUpdateManifest(context),
);
assert.equal(result.artifact, artifact);
assert.equal(result.manifestPath, path.join(root, 'latest.json'));
assert.equal(result.legacyManifestPath, null);
assert.deepEqual(Object.keys(result.manifest.platforms), ['darwin-x86_64']);
assert.match(result.manifest.platforms['darwin-x86_64'].url, /\/dev-mac\//);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test('invalid target or mismatched channel fails before any release side effect', async () => {
let touched = false;
const sideEffects = {
prepareVersion: () => {
touched = true;
},
build: () => {
touched = true;
},
generateManifest: () => {
touched = true;
},
};
await assert.rejects(
() => buildRelease(['--target', universalTarget], sideEffects),
/单架构/,
);
await withEnv({ AGC_UPDATE_CHANNEL: 'dev-win' }, () =>
assert.rejects(
() => buildRelease(['--target=aarch64-apple-darwin'], sideEffects),
/只能用于 windows/,
),
);
assert.equal(touched, false);
});
test('Windows remains the default and explicit Windows overrides macOS environment', () => {
const files = ['/tmp/mac.app.tar.gz', '/tmp/windows.exe', '/tmp/mac.dmg'];
for (const context of [
resolveReleaseContext([], {}),
resolveReleaseContext(['--target', windowsTarget], {
AGC_BUILD_TARGET: 'aarch64-apple-darwin',
}),
]) {
assert.equal(context.channel, 'dev-win');
assert.equal(
selectReleaseArtifact(files, context.target),
'/tmp/windows.exe',
);
runTauriBuild(
['--target', windowsTarget, '--config', 'user-config.json'],
context,
{
spawn: (_binary, command) => {
assert.ok(command.includes('--features=cocos-editor-execute'));
assert.ok(command.includes('user-config.json'));
const configIndex = command.lastIndexOf('--config');
const config = JSON.parse(
readFileSync(command[configIndex + 1], 'utf8'),
);
assert.match(
config.plugins.updater.endpoints[0],
/\/dev-win\/latest\.json$/,
);
return { status: 0 };
},
},
);
}
});
test('no-bundle smoke skips version writes and manifest generation', async () => {
const steps = [];
await buildRelease(['--no-bundle', '--target=aarch64-apple-darwin'], {
prepareVersion: () => {
steps.push('version');
},
build: (_args, context) => {
steps.push(context.channel);
},
generateManifest: () => {
steps.push('manifest');
},
});
assert.deepEqual(steps, ['dev-mac']);
});
test('channel manifest carries version, platform keys and signature', () => {
withSignedArtifact('陶泥儿_0.1.48_x64-setup.exe', (artifact) => {
withEnv({ AGC_UPDATE_RELEASE_NOTES: '修复与改进' }, () => {
@@ -345,6 +580,7 @@ test('release upload forces overwrite for artifact, signature and channel pointe
);
assert.match(source, /agc\/\$\{channel\}\/latest\.json/u);
assert.match(source, /agc\/latest\.json/u);
assert.match(source, /await buildRelease\(process\.argv\.slice\(2\)\)/u);
});
test('release notes list client commits with short sha and bound their size', () => {
@@ -35,6 +35,12 @@ const windowsTauriConfig = JSON.parse(
'utf8',
),
);
const macosTauriConfig = JSON.parse(
fs.readFileSync(
new URL('../src-tauri/tauri.macos.conf.json', import.meta.url),
'utf8',
),
);
const cargoManifestSource = fs.readFileSync(
new URL('../src-tauri/Cargo.toml', import.meta.url),
'utf8',
@@ -1358,6 +1364,37 @@ if (windowsTauriConfig.bundle?.useLocalToolsDir !== true) {
'AI game creator shell Windows Tauri config must cache bundling tools in the project target directory',
);
}
assert.deepEqual(
macosTauriConfig.bundle?.resources,
Object.fromEntries([
...[
'bin/codex',
'bin/codex-code-mode-host',
'codex-path/rg',
'codex-resources/zsh/bin/zsh',
'codex-package.json',
'NOTICE.md',
'manifest.json',
].map((file) => [
`resources/codex/mac-native/${file}`,
`coding-agent/mac-native/${file}`,
]),
['resources/plugins', 'plugins'],
]),
'macOS must bundle the complete native Codex layout and plugin workspace',
);
assert.deepEqual(
macosTauriConfig.plugins?.updater?.endpoints,
[
'https://agc-dev.oss-rg-china-mainland.aliyuncs.com/agc/dev-mac/latest.json',
],
'macOS local builds must not use the Windows update channel',
);
assert.equal(
macosTauriConfig.bundle?.macOS?.minimumSystemVersion,
'15.0',
'macOS deployment baseline must cover the bundled native zsh requirement',
);
if (tauriConfig.app?.withGlobalTauri !== true) {
throw new Error(
@@ -1722,7 +1759,7 @@ for (const snippet of [
'fn append_local_permission_log_at(',
'"command.auto"',
'GameCreationAppPermission::Auto',
'GAME_CREATOR_BUNDLED_CODEX_CLI_RELATIVE_PATH',
'fn game_creator_bundled_codex_cli_path',
'validate_game_creator_bundled_codex_cli',
'内置 Codex CLI 完整性校验失败',
]) {
@@ -0,0 +1,222 @@
import assert from 'node:assert/strict';
import { spawn, spawnSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
// 只操作临时复制品;不启动 GUI、不读取开发机凭据、不访问 Provider。
assert.equal(process.platform, 'darwin', '此验证必须在 macOS 执行');
const source = path.resolve(process.argv[2] || '');
assert.ok(
source.endsWith('.app') && fs.statSync(source).isDirectory(),
'请传入 .app 绝对路径',
);
const root = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'agc-macos-bundle-')),
);
const app = path.join(root, '陶泥儿 隔离测试.app');
const home = path.join(root, 'home');
const config = path.join(root, 'config');
const tmp = path.join(root, 'tmp');
const codexHome = path.join(root, 'codex-home');
for (const directory of [home, config, tmp, codexHome]) {
fs.mkdirSync(directory, { mode: 0o700 });
}
const env = {
HOME: home,
PATH: '/usr/bin:/bin',
TMPDIR: tmp,
CODEX_HOME: codexHome,
};
function run(command, args) {
const result = spawnSync(command, args, {
cwd: root,
env,
encoding: 'utf8',
timeout: 30_000,
maxBuffer: 1024 * 1024,
});
assert.ifError(result.error);
return result;
}
async function hashFile(file) {
const hash = createHash('sha256');
for await (const chunk of fs.createReadStream(file)) hash.update(chunk);
return hash.digest('hex');
}
async function handshake(executable) {
const child = spawn(executable, ['app-server'], {
cwd: root,
env,
stdio: ['pipe', 'pipe', 'pipe'],
});
let buffered = '';
let stderrBytes = 0;
try {
await new Promise((resolve, reject) => {
const timer = setTimeout(
() => reject(new Error('app-server 初始化超时')),
15_000,
);
const finish = (error) => {
clearTimeout(timer);
if (error) reject(error);
else resolve();
};
child.on('error', finish);
child.on('exit', (code) =>
finish(new Error(`app-server 提前退出 ${code}`)),
);
child.stderr.on('data', (chunk) => {
stderrBytes += chunk.length;
if (stderrBytes > 1024 * 1024)
finish(new Error('app-server stderr 超限'));
});
child.stdout.on('data', (chunk) => {
buffered += chunk.toString('utf8');
if (buffered.length > 1024 * 1024)
return finish(new Error('app-server stdout 超限'));
let end;
while ((end = buffered.indexOf('\n')) >= 0) {
const line = buffered.slice(0, end);
buffered = buffered.slice(end + 1);
try {
const message = JSON.parse(line);
if (message.id !== 1) continue;
assert.ok(message.result?.userAgent, '初始化必须返回真实服务身份');
assert.equal(message.error, undefined);
child.stdin.write(`${JSON.stringify({ method: 'initialized' })}\n`);
finish();
} catch (error) {
finish(error);
}
}
});
child.stdin.on('error', finish);
child.stdin.write(
`${JSON.stringify({
id: 1,
method: 'initialize',
params: {
clientInfo: {
name: 'agc_bundle_smoke',
title: 'AGC bundle smoke',
version: '1',
},
capabilities: { experimentalApi: true },
},
})}\n`,
);
});
} finally {
if (child.exitCode === null && child.signalCode === null) {
await new Promise((resolve) => {
const timer = setTimeout(() => child.kill('SIGKILL'), 3000);
child.once('exit', () => {
clearTimeout(timer);
resolve();
});
child.kill('SIGTERM');
});
}
}
}
try {
fs.cpSync(source, app, { recursive: true });
const resources = path.join(app, 'Contents/Resources');
const bundle = path.join(resources, 'coding-agent/mac-native');
const executable = path.join(bundle, 'bin/codex');
const main = path.join(
app,
'Contents/MacOS/genarrative-ai-game-creator-shell',
);
const manifest = JSON.parse(
fs.readFileSync(path.join(bundle, 'manifest.json'), 'utf8'),
);
assert.equal(manifest.schemaVersion, 'genarrative-codex-sidecar.v2');
assert.equal(
manifest.platform,
process.arch === 'arm64' ? 'darwin-arm64' : 'darwin-x64',
);
assert.equal(manifest.version, 'codex-cli 0.147.0');
const components = [
'bin/codex',
'bin/codex-code-mode-host',
'codex-path/rg',
'codex-resources/zsh/bin/zsh',
'codex-package.json',
];
assert.deepEqual(Object.keys(manifest.files).sort(), [...components].sort());
for (const component of components) {
const file = path.join(bundle, component);
assert.equal(await hashFile(file), manifest.files[component], component);
if (component !== 'codex-package.json') {
fs.accessSync(file, fs.constants.X_OK);
const arch = run('/usr/bin/lipo', ['-archs', file]);
assert.equal(arch.status, 0, component);
assert.equal(
arch.stdout.trim(),
process.arch === 'arm64' ? 'arm64' : 'x86_64',
component,
);
}
}
assert.ok(fs.existsSync(path.join(bundle, 'NOTICE.md')));
const plugin = path.join(resources, 'plugins/agc-cocos-editor');
for (const file of [
'plugin.json',
'src/entry.mjs',
'panels/cocos-editor.html',
]) {
assert.ok(fs.existsSync(path.join(plugin, file)), file);
}
const packageFiles = fs.readdirSync(resources, { recursive: true });
assert.ok(
!packageFiles.some((file) =>
/(^|\/)(\.env[^/]*|auth\.json|node_modules|target|\.git)(\/|$)|\.(exe|dll)$/.test(
file,
),
),
);
assert.equal(run(executable, ['--version']).stdout.trim(), manifest.version);
assert.equal(
run(path.join(bundle, 'codex-path/rg'), ['--version']).status,
0,
);
assert.equal(
run(path.join(bundle, 'codex-resources/zsh/bin/zsh'), ['--version']).status,
0,
);
// 使用正式 AGC 查找/校验入口,而非只证明 sidecar 可以独立执行。
const status = run(main, ['--config-dir', config, '--llm-status']);
const statusText = `${status.stdout}\n${status.stderr}`;
assert.ok(!statusText.includes('Codex CLI 未安装'), statusText);
assert.ok(
statusText.includes('authentication-required'),
'隔离账号应仅被登录门禁拒绝',
);
await handshake(executable);
// 临时复制品缺少辅助程序时,正式入口必须拒绝内置程序;PATH 无全局 Codex 可兜底。
fs.renameSync(
path.join(bundle, 'bin/codex-code-mode-host'),
path.join(root, 'saved-code-mode-host'),
);
const broken = run(main, ['--config-dir', config, '--llm-status']);
assert.notEqual(broken.status, 0);
assert.match(`${broken.stdout}\n${broken.stderr}`, /Codex CLI 未安装/);
console.log(
'PASS: 隔离安装包资源、架构、摘要、权限、正式 Codex 查找、app-server 握手及缺组件拒绝',
);
console.log(
'未验证:GUI、真实登录/Provider 对话、Cocos macOS 原生桥接;插件 Node 仍为外部前提',
);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
@@ -12,8 +12,7 @@ if (!/^[a-z0-9][a-z0-9.-]{1,62}$/u.test(bucket) || /[\r\n\0]/u.test(endpoint)) {
process.env.AGC_UPDATE_OSS_BASE_URL ||= `https://${bucket}.${endpoint}/agc`;
const dryRun = readReleaseDryRun();
const { generateUpdateManifest, prepareReleaseVersion, runTauriBuild } =
await import('./build-release.mjs');
const { buildRelease } = await import('./build-release.mjs');
function runOssutil(args) {
const binary = process.env.OSSUTIL_BIN?.trim() || 'ossutil';
@@ -51,10 +50,8 @@ function runOssutil(args) {
if (result.status !== 0) process.exit(result.status ?? 1);
}
await prepareReleaseVersion();
runTauriBuild([]);
const { artifact, channel, legacyManifestPath, manifest, manifestPath } =
await generateUpdateManifest();
await buildRelease(process.argv.slice(2));
const artifactKey = `agc/${channel}/${manifest.version}/${path.basename(artifact)}`;
// Jenkins/ossutil 默认会在目标对象已存在时交互询问并按默认值跳过;
// 发布清单是固定的 latest 指针,必须显式覆盖,否则流水线会误报成功但远端仍保留旧版本。
+1 -1
View File
@@ -1745,7 +1745,7 @@ dependencies = [
[[package]]
name = "genarrative-ai-game-creator-shell"
version = "0.1.47"
version = "0.1.67"
dependencies = [
"agent-runtime-core",
"axum",
@@ -1,6 +1,6 @@
[package]
name = "genarrative-ai-game-creator-shell"
version = "0.1.47"
version = "0.1.67"
edition = "2021"
publish = false
+82 -48
View File
@@ -1,31 +1,18 @@
#[path = "build_support/codex_bundle.rs"]
mod codex_bundle;
#[path = "build_support/frontend_dist_guard.rs"]
mod frontend_dist_guard;
#[path = "build_support/runtime_prompt_bundle.rs"]
mod runtime_prompt_bundle;
#[cfg(windows)]
use sha2::{Digest, Sha256};
use std::collections::BTreeSet;
use std::env;
use std::fs;
use std::path::PathBuf;
#[cfg(windows)]
use std::io::{BufReader, Read};
const BUNDLED_CODEX_CLI_VERSION: &str = "codex-cli 0.147.0";
#[cfg(windows)]
const BUNDLED_CODEX_FILES: [&str; 6] = [
"bin/codex.exe",
"bin/codex-code-mode-host.exe",
"codex-path/rg.exe",
"codex-resources/codex-command-runner.exe",
"codex-resources/codex-windows-sandbox-setup.exe",
"codex-package.json",
];
#[cfg(windows)]
fn sha256_file(path: &std::path::Path) -> Result<String, std::io::Error> {
let file = fs::File::open(path)?;
let mut reader = BufReader::new(file);
@@ -42,7 +29,15 @@ fn sha256_file(path: &std::path::Path) -> Result<String, std::io::Error> {
}
fn stage_bundled_codex_cli(manifest_dir: &std::path::Path) {
#[cfg(windows)]
let target = env::var("TARGET").expect("Cargo TARGET");
println!("cargo:rustc-env=AGC_BUILD_TARGET={target}");
let Some(layout) = codex_bundle::for_target(&target) else {
assert!(
!target.contains("windows") && !target.contains("apple-darwin"),
"不支持的 Codex 随包目标:{target}"
);
return;
};
{
let app_root = manifest_dir
.parent()
@@ -51,24 +46,23 @@ fn stage_bundled_codex_cli(manifest_dir: &std::path::Path) {
.parent()
.and_then(|apps_dir| apps_dir.parent())
.expect("AI 游戏创作应用必须位于仓库 apps 目录下");
let source_candidates = [
app_root.join(
"node_modules/@openai/codex-win32-x64/vendor/x86_64-pc-windows-msvc",
),
app_root.join(
"node_modules/@openai/codex/node_modules/@openai/codex-win32-x64/vendor/x86_64-pc-windows-msvc",
),
repo_root.join(
"node_modules/@openai/codex-win32-x64/vendor/x86_64-pc-windows-msvc",
),
repo_root.join(
"node_modules/@openai/codex/node_modules/@openai/codex-win32-x64/vendor/x86_64-pc-windows-msvc",
),
];
let package = layout.npm_package;
let source_candidates = [app_root, repo_root]
.into_iter()
.flat_map(|root| {
[
root.join(format!("node_modules/@openai/{package}/vendor/{target}")),
root.join(format!(
"node_modules/@openai/codex/node_modules/@openai/{package}/vendor/{target}"
)),
]
})
.collect::<Vec<_>>();
let source = source_candidates
.iter()
.find(|path| {
BUNDLED_CODEX_FILES
layout
.files
.iter()
.all(|relative| path.join(relative).is_file())
})
@@ -83,14 +77,26 @@ fn stage_bundled_codex_cli(manifest_dir: &std::path::Path) {
.join("")
)
});
let target_dir = manifest_dir.join("resources/codex/win-x64");
let metadata: serde_json::Value = serde_json::from_slice(
&fs::read(source.join("codex-package.json")).expect("读取 Codex 原生包元数据失败"),
)
.expect("Codex 原生包元数据无效");
codex_bundle::validate_package_metadata(&metadata, &target, layout)
.unwrap_or_else(|error| panic!("{error}"));
let target_dir = manifest_dir.join("resources/codex").join(layout.directory);
let notice = target_dir.join("NOTICE.md");
if target.contains("apple-darwin") {
let source_notice =
manifest_dir.join("resources/codex/【声明】Mac内置Codex组件-2026-09-18.md");
stage_plugin_file(&source_notice, &notice);
println!("cargo:rerun-if-changed={}", source_notice.display());
}
if !notice.is_file() {
panic!("内置 Codex CLI 第三方声明缺失:{}", notice.display());
}
fs::create_dir_all(&target_dir).expect("创建内置 Codex CLI 资源目录失败");
let mut file_hashes = serde_json::Map::new();
for relative in BUNDLED_CODEX_FILES {
for relative in layout.files {
let source_path = source.join(relative);
let target_path = target_dir.join(relative);
if let Some(parent) = target_path.parent() {
@@ -104,15 +110,23 @@ fn stage_bundled_codex_cli(manifest_dir: &std::path::Path) {
if !target_matches_source {
fs::copy(&source_path, &target_path).expect("复制内置 Codex CLI 资源失败");
}
// 内容相同但曾被错误 chmod 的 staging 文件也必须恢复执行权限。
fs::set_permissions(
&target_path,
fs::metadata(&source_path)
.expect("读取组件权限失败")
.permissions(),
)
.expect("保留内置 Codex CLI 组件权限失败");
file_hashes.insert(
relative.to_string(),
serde_json::Value::String(source_sha256),
);
}
let manifest = serde_json::json!({
"schemaVersion": "genarrative-codex-sidecar.v2",
"platform": "win32-x64",
"version": BUNDLED_CODEX_CLI_VERSION,
"schemaVersion": codex_bundle::SCHEMA,
"platform": layout.platform,
"version": codex_bundle::CLI_VERSION,
"files": file_hashes,
});
let manifest_path = target_dir.join("manifest.json");
@@ -126,7 +140,7 @@ fn stage_bundled_codex_cli(manifest_dir: &std::path::Path) {
{
fs::write(&manifest_path, manifest_payload).expect("写入内置 Codex CLI 清单失败");
}
for relative in BUNDLED_CODEX_FILES {
for relative in layout.files {
println!("cargo:rerun-if-changed={}", source.join(relative).display());
}
println!("cargo:rerun-if-changed={}", notice.display());
@@ -256,8 +270,11 @@ fn stage_cocos_editor_payload(_manifest_dir: &std::path::Path) {}
///
/// 只复制插件运行需要的清单、入口、面板和 native payload,不复制 native 源码、
/// Cargo target 目录或 node_modules。
#[cfg(windows)]
fn stage_plugin_workspace(manifest_dir: &std::path::Path) {
let target = env::var("TARGET").expect("Cargo TARGET");
if !target.contains("windows") && !target.contains("apple-darwin") {
return;
}
let repo_root = manifest_dir
.parent()
.and_then(|app_root| app_root.parent())
@@ -266,6 +283,10 @@ fn stage_plugin_workspace(manifest_dir: &std::path::Path) {
.to_path_buf();
let workspace = repo_root.join("plugins");
let destination_root = manifest_dir.join("resources/plugins");
// staging 是专用生成目录;重建清除跨目标 payload 与已删除插件的残留。
if destination_root.exists() {
std::fs::remove_dir_all(&destination_root).expect("清理插件 staging 失败");
}
std::fs::create_dir_all(&destination_root).expect("创建插件资源目录失败");
let entries = match std::fs::read_dir(&workspace) {
Ok(entries) => entries,
@@ -273,6 +294,13 @@ fn stage_plugin_workspace(manifest_dir: &std::path::Path) {
};
for entry in entries.flatten() {
let plugin_root = entry.path();
assert!(
!entry
.file_type()
.expect("读取插件目录类型失败")
.is_symlink(),
"插件工作区不允许符号链接"
);
if !plugin_root.is_dir() || !plugin_root.join("plugin.json").is_file() {
continue;
}
@@ -287,17 +315,18 @@ fn stage_plugin_workspace(manifest_dir: &std::path::Path) {
std::path::PathBuf::from("panels"),
std::path::PathBuf::from("native/payload"),
] {
if relative == std::path::Path::new("native/payload") && !target.contains("windows") {
continue;
}
copy_plugin_tree(&plugin_root.join(&relative), &destination.join(&relative));
}
println!("cargo:rerun-if-changed={}", plugin_root.display());
}
}
#[cfg(windows)]
fn stage_plugin_file(source: &std::path::Path, destination: &std::path::Path) {
let Ok(bytes) = std::fs::read(source) else {
return;
};
let bytes = std::fs::read(source)
.unwrap_or_else(|error| panic!("读取随包资源失败 {}{error}", source.display()));
if std::fs::read(destination).is_ok_and(|existing| existing == bytes) {
return;
}
@@ -307,7 +336,6 @@ fn stage_plugin_file(source: &std::path::Path, destination: &std::path::Path) {
std::fs::write(destination, bytes).expect("复制插件资源失败");
}
#[cfg(windows)]
fn copy_plugin_tree(source: &std::path::Path, destination: &std::path::Path) {
let entries = match std::fs::read_dir(source) {
Ok(entries) => entries,
@@ -316,10 +344,17 @@ fn copy_plugin_tree(source: &std::path::Path, destination: &std::path::Path) {
for entry in entries.flatten() {
let target = destination.join(entry.file_name());
let path = entry.path();
assert!(
!entry
.file_type()
.expect("读取插件文件类型失败")
.is_symlink(),
"插件资源不允许符号链接"
);
if path.is_dir() {
let name = entry.file_name();
let name = name.to_string_lossy();
if matches!(name.as_ref(), "target" | "node_modules" | ".git") {
if name.starts_with('.') || matches!(name.as_ref(), "target" | "node_modules") {
continue;
}
std::fs::create_dir_all(&target).expect("创建插件资源目录失败");
@@ -331,12 +366,14 @@ fn copy_plugin_tree(source: &std::path::Path, destination: &std::path::Path) {
if name.contains(".test.") {
continue;
}
if name.starts_with('.') {
continue;
}
stage_plugin_file(&path, &target);
}
}
}
#[cfg(windows)]
fn copy_plugin_file(source: &std::path::Path, destination: &std::path::Path) {
if !source.is_file() {
return;
@@ -345,6 +382,3 @@ fn copy_plugin_file(source: &std::path::Path, destination: &std::path::Path) {
.expect("创建插件资源目录失败");
std::fs::copy(source, destination).expect("复制插件资源失败");
}
#[cfg(not(windows))]
fn stage_plugin_workspace(_manifest_dir: &std::path::Path) {}
@@ -0,0 +1,133 @@
//! 构建与运行共用的平台布局;只允许分发锁定原生包里的明确组件。
pub const VERSION: &str = "0.147.0";
pub const CLI_VERSION: &str = "codex-cli 0.147.0";
pub const SCHEMA: &str = "genarrative-codex-sidecar.v2";
#[derive(Clone, Copy, Debug)]
pub struct Layout {
pub platform: &'static str,
pub npm_package: &'static str,
pub directory: &'static str,
pub executable: &'static str,
pub files: &'static [&'static str],
}
const WINDOWS_FILES: &[&str] = &[
"bin/codex.exe",
"bin/codex-code-mode-host.exe",
"codex-path/rg.exe",
"codex-resources/codex-command-runner.exe",
"codex-resources/codex-windows-sandbox-setup.exe",
"codex-package.json",
];
const MAC_FILES: &[&str] = &[
"bin/codex",
"bin/codex-code-mode-host",
"codex-path/rg",
"codex-resources/zsh/bin/zsh",
"codex-package.json",
];
pub fn for_target(target: &str) -> Option<Layout> {
match target {
"x86_64-pc-windows-msvc" => Some(Layout {
platform: "win32-x64",
npm_package: "codex-win32-x64",
directory: "win-x64",
executable: "bin/codex.exe",
files: WINDOWS_FILES,
}),
"aarch64-apple-darwin" | "x86_64-apple-darwin" => Some(Layout {
platform: if target.starts_with("aarch64") {
"darwin-arm64"
} else {
"darwin-x64"
},
npm_package: if target.starts_with("aarch64") {
"codex-darwin-arm64"
} else {
"codex-darwin-x64"
},
directory: "mac-native",
executable: "bin/codex",
files: MAC_FILES,
}),
_ => None,
}
}
pub fn validate_package_metadata(
metadata: &serde_json::Value,
target: &str,
layout: Layout,
) -> Result<(), String> {
if metadata["layoutVersion"] == 1
&& metadata["version"] == VERSION
&& metadata["target"] == target
&& metadata["entrypoint"] == layout.executable
&& metadata["resourcesDir"] == "codex-resources"
&& metadata["pathDir"] == "codex-path"
{
Ok(())
} else {
Err(format!("Codex 原生包版本、布局或架构不匹配目标 {target}"))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn platform_layouts_are_explicit_and_preserve_upstream_components() {
let mac = for_target("aarch64-apple-darwin").unwrap();
assert_eq!(mac.platform, "darwin-arm64");
assert_eq!(mac.npm_package, "codex-darwin-arm64");
assert!(mac.files.contains(&"codex-resources/zsh/bin/zsh"));
assert!(mac.files.contains(&"bin/codex-code-mode-host"));
assert!(!mac.files.iter().any(|file| file.ends_with(".exe")));
let intel = for_target("x86_64-apple-darwin").unwrap();
assert_eq!(intel.platform, "darwin-x64");
assert_eq!(intel.npm_package, "codex-darwin-x64");
let windows = for_target("x86_64-pc-windows-msvc").unwrap();
assert_eq!(windows.directory, "win-x64");
assert_eq!(windows.files.len(), 6);
assert!(windows
.files
.contains(&"codex-resources/codex-windows-sandbox-setup.exe"));
assert!(for_target("universal-apple-darwin").is_none());
assert!(for_target("aarch64-pc-windows-msvc").is_none());
assert!(for_target("x86_64-unknown-linux-gnu").is_none());
}
#[test]
fn metadata_rejects_version_architecture_and_layout_drift() {
let target = "aarch64-apple-darwin";
let layout = for_target(target).unwrap();
let valid = serde_json::json!({
"layoutVersion": 1,
"version": VERSION,
"target": target,
"entrypoint": "bin/codex",
"resourcesDir": "codex-resources",
"pathDir": "codex-path",
});
assert!(validate_package_metadata(&valid, target, layout).is_ok());
for (key, value) in [
("layoutVersion", serde_json::json!(2)),
("version", serde_json::json!("0.0.0")),
("target", serde_json::json!("x86_64-apple-darwin")),
("entrypoint", serde_json::json!("bin/codex.exe")),
("resourcesDir", serde_json::json!("../private")),
("pathDir", serde_json::json!(null)),
] {
let mut invalid = valid.clone();
invalid[key] = value;
assert!(
validate_package_metadata(&invalid, target, layout).is_err(),
"{key}"
);
}
}
}
@@ -14,7 +14,7 @@ Let the client derive projections from real disk changes and trusted tool result
3. Keep read scopes separate: `asset.list` is the current project manifest, `asset.library.list` is the signed-in account library, and the web project's canvas resource read model is the authoritative canvas list. The account library is not the complete canvas list.
4. Use `canvas.asset_import` for safe account/canvas asset IDs or project-relative local paths. The client rechecks ownership and validates bytes; host absolute paths require native UI file-picker authorization.
5. When the user explicitly asks to create or derive video, character animation, sound effect, or background music, call `agc_create_or_derive_resource`. Use `create` only for video/audio without a source and `derive` with a registered `sourceLocalAssetId`; character animation is always derived from an image. Keep `prompt` inside the per-kind limit that the client really enforces: background music at most 140 characters, sound effect at most 1900, video and character animation at most 4000. A longer prompt is rejected before submission, so write the short version first instead of retrying the same text.
6. When the user explicitly asks to remove an image background, call `agc_remove_background` with a registered image `sourceLocalAssetId` and `assetName`. Optional `backgroundMode` is `complex` (semantic foreground segmentation; default) or `flat` (solid-colour background removal). Prefer `flat` when the background is known to be solid. Only `flat` accepts optional `screenColor`: `auto`, `#RRGGBB`, or omitted for automatic detection by the service. Do not select a colour on behalf of `auto`. The client requires the signed-in account, owns canvas/folder context and task identity, and returns only bounded queue state.
6. When the user explicitly asks to remove an image background, call `agc_remove_background` with a registered image `sourceLocalAssetId` and `assetName`. Optional `backgroundMode` is `complex` (semantic foreground segmentation; default) or `flat` (solid-colour background removal). Prefer `flat` when the background is known to be solid. Only `flat` accepts optional `screenColor`: `auto`, `#RRGGBB`, or omitted for automatic detection by the service. Do not select a colour on behalf of `auto`. The client requires the signed-in account, owns canvas/folder context and task identity, waits for the accepted operation, downloads and registers the completed local asset, and preserves the operation for recovery when the remote result is not yet known.
7. Preserve existing relative paths when a small edit is sufficient so client resource identities remain stable.
8. Do not edit `.agent/manifest.json`, revision counters, version records, resource IDs, canvas identities, source provenance, generation ledgers, or browser receipts by hand.
9. Do not create a version when no game file changed. The client compares content fingerprints and advances revision only after an actual source change.
@@ -16,4 +16,6 @@ Read scopes remain separate: `asset.list` is the current project's local manifes
`prompt` limits are per kind and are enforced before any paid submission: background music accepts 1-140 characters, sound effect 1-1900, video and character animation 1-4000, and image editing (`agc_edit_image`) 1-32000. The client composes the submitted request from a fixed prefix plus your prompt, so an over-limit prompt fails locally with the exact limit; shorten the text rather than resubmitting the same value. `agc_edit_image` remains the image path; this tool never generates or edits still images.
`agc_remove_background` accepts a registered image `sourceLocalAssetId`, `assetName`, and optional `backgroundMode` and `screenColor`. `complex` uses semantic segmentation to identify the foreground; `flat` removes a solid-colour background. Prefer `flat` when the background is known to be solid; omitting the mode selects `complex`. Only `flat` accepts a colour: `auto`, `#RRGGBB`, or omitted for automatic service detection. Never infer a concrete colour for `auto`. Empty or invalid values and colour without `flat` are rejected. The client resolves the formal source resource, canvas/folder context, stable operation identity, idempotency key, and authenticated External v1 `/api/external/v1/editor/images/background-removals` call. Mode and colour are part of request identity. Its result is bounded queue state; Codex must not poll internal workers, construct source URLs, or retry with a new identity after an uncertain response.
`agc_remove_background` accepts a registered image `sourceLocalAssetId`, `assetName`, and optional `backgroundMode` and `screenColor`. `complex` uses semantic segmentation to identify the foreground; `flat` removes a solid-colour background. Prefer `flat` when the background is known to be solid; omitting the mode selects `complex`. Only `flat` accepts a colour: `auto`, `#RRGGBB`, or omitted for automatic service detection. Never infer a concrete colour for `auto`. Empty or invalid values and colour without `flat` are rejected. The client resolves the formal source resource, canvas/folder context, stable operation identity, idempotency key, and authenticated request. Ordinary account mode maps the External v1 shaped route to `/api/editor/images/background-removals`; ExternalDeveloper mode uses `/api/external/v1/editor/images/background-removals`. Mode and colour are part of request identity. After acceptance, the client polls the authenticated generation status route, downloads the completed media, and commits it to the local manifest. If completion is unknown, it retains the same local operation for recovery; it never retries with a new identity or exposes internal worker details.
After an interrupted call, inspect `agc_list_registered_assets.pendingOperations`. Calling `agc_remove_background` again with the same source, name, mode, and colour resumes the matching pending operation. A submission marked `reconciliation-required` needs client-side reconciliation and cannot be automatically resumed. Do not change parameters to bypass a pending task. A queued receipt, fixed progress value, or absent local file does not establish that the background-removal provider is waiting in a queue; report only the observed state.
@@ -14,6 +14,9 @@ Implement the user's actual game request in the current project as an npm-manage
3. Build with the project's npm script before previewing. The playable entry is the package directory's `dist/index.html`; never report an unbuilt bare-module page as playable. Import assets or configure public assets so all runtime media is included in dist; preview and exports cannot read outside it.
4. Build a complete playable loop: visible objective, responsive input, meaningful state changes, success or failure feedback, and a reliable restart path where the game needs one.
5. Fit the active game scene to desktop and mobile viewports without accidental page scrollbars. Reserve deliberate safe space for HUD elements instead of covering interactive content.
- **画布居中只能由一处负责。** 使用 `Phaser.Scale.FIT``autoCenter: Phaser.Scale.CENTER_BOTH` 时,canvas 的直接父容器应使用尺寸明确的普通块布局,不再对同一 canvas 叠加 Grid/Flex 居中、`place-items: center`、自动外边距或居中 transform。Phaser 自动计算的 margin 与 CSS 居中叠加会使竖屏画面向右偏移。
- 若决定由 CSS 居中,则显式使用 `autoCenter: Phaser.Scale.NO_CENTER`,由 CSS 独立完成定位;外围页面可以继续使用 Grid/Flex,限制只针对同一 canvas 的重复定位。
- 出现偏移先检查游戏自身的 CSS 与 Phaser scale 配置,不添加 AGC 预览容器固定偏移补偿。修改布局后重新构建 dist,在桌面、移动及窗口 resize 后检查 canvas 相对游戏父容器居中(误差不超过 1 CSS px)、画面完整且无意外滚动条;不能仅凭 build 成功宣称布局通过。
6. Invoke `taonier-art-assets` for every new game brief that needs visual assets. First reuse suitable registered Taonier art; when the brief's required visual elements are missing or unsuitable, call the reviewed `agc_tools` generation/edit workflow in the same task. After the tool returns, wire its relative paths into the game and verify the rendered result. A game with unused generated assets or placeholder emoji/CSS where requested art should appear is not complete. Load media defensively only for genuinely optional effects, and never relabel a local placeholder as platform art.
7. Let Phaser own the render loop and input dispatch. Avoid duplicate scenes, stale event listeners, and state that survives restart unintentionally.
8. After a meaningful game change, use the browser playtest Skill and fix issues shown by real evidence before reporting completion.
@@ -7,5 +7,7 @@
- Score, steps, health, timer, or other core state updates consistently.
- Restart restores all state and does not duplicate timers, animation loops, or event listeners.
- Desktop and mobile layouts keep the core scene visible without accidental document scrolling.
- 画布的缩放与居中由 Phaser 或 CSS 中的一方独立负责。`FIT + CENTER_BOTH` 不与同一 canvas 父容器的 Grid/Flex 居中、自动外边距或居中 transform 叠加;使用 CSS 居中时关闭 Phaser 自动居中(`NO_CENTER`)。
- 在构建后的实际页面检查桌面、移动和 resize:比较 canvas 与游戏父容器的中心,预期居中时水平/垂直误差不超过 1 CSS px,并检查画面没有溢出或意外滚动条。偏移先修游戏 CSS/scale 配置,不用修改 AGC 预览位置掩盖。
- HUD and overlays reserve space and do not cover essential interactive content.
- Requested Taonier art is visibly integrated into the core experience when available.
@@ -1,6 +1,6 @@
{
"schemaVersion": "agc-skill-pack.v1",
"version": "2026-08-26.20",
"version": "2026-08-26.25",
"skills": [
{
"name": "agc-game-production-workflow",
@@ -63,7 +63,7 @@
"agents/openai.yaml",
"references/platform-art-contract.md"
],
"sha256": "c6329c6a3cbd17a237d042349d7fd8adcf240287ef56d23b49329923e976d534"
"sha256": "47ac742d9b88e5d6cd9833484ab212152578e58ae27f7add312fd1d78183385c"
},
{
"name": "agc-web-game-development",
@@ -80,7 +80,7 @@
"agents/openai.yaml",
"references/game-quality-checklist.md"
],
"sha256": "05b5cfbf7a40fd303717491f5cea84ff339a73359c9678b283fd54d2b5c45efd"
"sha256": "e122d8f3a6d986b594b95c971754d68197bf7896912fa8267d44a7aa129a57ba"
},
{
"name": "agc-browser-playtest",
@@ -123,7 +123,7 @@
"agents/openai.yaml",
"references/projection-contract.md"
],
"sha256": "93210c0eeb73b279d35aa85c201c226139b0bdf041f3300ac2c6e2c1bdd63afe"
"sha256": "247787975944ce8b21d7c879c39c60ec13608056cff9426ac374c9299937d475"
}
]
}
@@ -19,6 +19,12 @@ image, UI design image, or publication material; use `agc_edit_image` for an
edit of an existing registered image; use `taonier_prepare_game_art` only for
the complete game-art package and its canonical slices.
With `agc_generate_image`, `kind="character"` and `kind="art-spritesheet"`
generate the subject on a solid-colour background and automatically matte it
away afterwards, producing transparent-background results; write the prompt
for the subject only, never for a scene. `kind="image"` keeps the rendered
frame without extra processing.
When `agc_generate_image` is used with `kind="art-spritesheet"`, `sliceMode` is
required and has no default, so decide it explicitly:
@@ -0,0 +1,14 @@
# 内置 Codex CLI
本安装包包含锁定版本 Codex CLI 0.147.0 的 macOS 原生组件。
Codex CLI 按 Apache License 2.0 分发,源码与许可证见
https://github.com/openai/codex。
组件来自项目锁定的 `@openai/codex` 原生 npm 依赖,保留上游的
`bin/codex``bin/codex-code-mode-host``codex-path/rg`
`codex-resources/zsh/bin/zsh``codex-package.json` 相对布局。
原生依赖中的 ripgrep 与 zsh 按各自上游许可证分发:
https://github.com/BurntSushi/ripgrep 和 https://www.zsh.org/。
安装包不包含 API Key、登录状态、用户配置或项目数据。
@@ -21,6 +21,7 @@ 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;
@@ -40,7 +41,8 @@ use codex_app_server::*;
pub(crate) use codex_app_server::{
cancel_direct_codex_turn_at,
direct_codex_canonical_project_identity_for_commands as direct_codex_canonical_project_identity,
direct_game_creator_codex_chat_at, direct_game_creator_home_codex_chat, DirectTurnCancelView,
direct_game_creator_codex_chat_at, direct_game_creator_home_codex_chat,
direct_thread_id_for_project, DirectTurnCancelView,
};
use codex_cli::*;
pub(crate) use codex_cli::{
@@ -55,6 +57,7 @@ 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::*;
@@ -20,6 +20,29 @@ pub(crate) fn direct_codex_canonical_project_identity(
))
}
/// 项目根目录在 Thread Manager 里的线程身份。
///
/// 订阅入口、回合事件写入和"回合被兜底释放"三处必须算出同一个字符串,否则前端会订阅到
/// 一个永不产生事件的空线程。这个字符串**只取决于路径**:能归一就用 canonical 路径,只有
/// 归一本身失败(路径不存在 / 不是目录 / 无法安全解析)才退回调用方给的字符串。
///
/// 这里刻意不读 `.agent/manifest.json`:那次读取是"项目权威身份"(连接池摘要,见
/// `direct_codex_canonical_project_identity`)的要求,而线程 id 只是一个路径 key。把
/// manifest 的瞬时抖动混进线程 id,会让同一项目在"订阅那一刻"与"跑回合那一刻"算出两个
/// 字符串(例如调用方给的是符号链接路径),订阅就绑到一条永远不会有事件的空线程上。
pub(crate) fn direct_thread_id_for_project(root: &std::path::Path) -> String {
let Ok((canonical_root, _)) = resolve_direct_codex_project_authority(root) else {
return root.to_string_lossy().into_owned();
};
canonical_root
.to_str()
.and_then(|value| value.strip_prefix(r"\\?\"))
.map(std::path::Path::new)
.unwrap_or(canonical_root.as_path())
.to_string_lossy()
.into_owned()
}
pub(super) fn direct_codex_os_path_identity_bytes(path: &std::path::Path) -> Vec<u8> {
#[cfg(unix)]
{
File diff suppressed because it is too large Load Diff
@@ -5,18 +5,10 @@ use std::process::Stdio;
use sha2::{Digest, Sha256};
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt};
#[path = "../../build_support/codex_bundle.rs"]
mod codex_bundle;
const GAME_CREATOR_CODEX_CLI_EXECUTABLE: &str = "codex";
const GAME_CREATOR_BUNDLED_CODEX_CLI_RELATIVE_PATH: &str = "coding-agent/win-x64/bin/codex.exe";
const GAME_CREATOR_BUNDLED_CODEX_CLI_MANIFEST_RELATIVE_PATH: &str =
"coding-agent/win-x64/manifest.json";
const GAME_CREATOR_BUNDLED_CODEX_CLI_REQUIRED_FILES: [&str; 6] = [
"bin/codex.exe",
"bin/codex-code-mode-host.exe",
"codex-path/rg.exe",
"codex-resources/codex-command-runner.exe",
"codex-resources/codex-windows-sandbox-setup.exe",
"codex-package.json",
];
const GAME_CREATOR_CODEX_CLI_PROMPT_MAX_BYTES: usize = 4 * 1024 * 1024;
const GAME_CREATOR_CODEX_CLI_STDOUT_MAX_BYTES: usize = 4 * 1024 * 1024;
const GAME_CREATOR_CODEX_CLI_STDERR_MAX_BYTES: usize = 256 * 1024;
@@ -38,11 +30,11 @@ fn game_creator_codex_cli_executable_candidates_for(
path: Option<&std::ffi::OsStr>,
) -> Vec<PathBuf> {
let mut candidates = Vec::new();
if let Some(bundled) = game_creator_bundled_codex_cli_path(resource_dir) {
candidates.push(bundled);
}
#[cfg(windows)]
{
if let Some(resource_dir) = resource_dir {
candidates.push(resource_dir.join(GAME_CREATOR_BUNDLED_CODEX_CLI_RELATIVE_PATH));
}
fn append_native_npm_candidates(candidates: &mut Vec<PathBuf>, npm_root: &Path) {
let vendor_root = npm_root
.join("node_modules")
@@ -109,52 +101,71 @@ fn game_creator_codex_cli_executable_candidates() -> Vec<PathBuf> {
}
fn game_creator_bundled_resource_dir() -> Option<PathBuf> {
let executable = std::env::current_exe().ok()?;
game_creator_bundled_resource_dir_for(&executable)
}
fn game_creator_bundled_resource_dir_for(executable: &Path) -> Option<PathBuf> {
#[cfg(windows)]
{
std::env::current_exe()
.ok()
.and_then(|path| path.parent().map(Path::to_path_buf))
executable.parent().map(Path::to_path_buf)
}
#[cfg(not(windows))]
#[cfg(target_os = "macos")]
{
let macos = executable.parent()?;
let contents = macos.parent()?;
// 只接受真正的 app bundle 结构,开发态不从任意相邻目录加载程序。
if macos.file_name()? != "MacOS"
|| contents.file_name()? != "Contents"
|| contents.parent()?.extension()? != "app"
{
return None;
}
Some(contents.join("Resources"))
}
#[cfg(not(any(windows, target_os = "macos")))]
{
let _ = executable;
None
}
}
fn game_creator_bundled_codex_cli_path(resource_dir: Option<&Path>) -> Option<PathBuf> {
resource_dir.map(|resource_dir| resource_dir.join(GAME_CREATOR_BUNDLED_CODEX_CLI_RELATIVE_PATH))
let layout = codex_bundle::for_target(env!("AGC_BUILD_TARGET"))?;
Some(
resource_dir?
.join("coding-agent")
.join(layout.directory)
.join(layout.executable),
)
}
fn validate_game_creator_bundled_codex_cli(executable: &Path) -> Result<String, String> {
let layout = codex_bundle::for_target(env!("AGC_BUILD_TARGET"))
.ok_or_else(|| "当前平台不支持内置 Codex CLI".to_string())?;
let bundle_root = executable
.parent()
.and_then(Path::parent)
.ok_or_else(|| "内置 Codex CLI 路径无效".to_string())?;
let manifest_path = bundle_root.join(
Path::new(GAME_CREATOR_BUNDLED_CODEX_CLI_MANIFEST_RELATIVE_PATH)
.file_name()
.expect("bundled Codex manifest file name"),
);
let manifest_path = bundle_root.join("manifest.json");
let manifest = std::fs::read_to_string(&manifest_path)
.map_err(|_| "内置 Codex CLI 缺少完整性清单".to_string())
.and_then(|value| {
serde_json::from_str::<GameCreatorBundledCodexCliManifest>(&value)
.map_err(|_| "内置 Codex CLI 完整性清单无效".to_string())
})?;
if manifest.schema_version != "genarrative-codex-sidecar.v2"
|| manifest.platform != "win32-x64"
if manifest.schema_version != codex_bundle::SCHEMA
|| manifest.platform != layout.platform
|| manifest.version.trim().is_empty()
|| GAME_CREATOR_BUNDLED_CODEX_CLI_REQUIRED_FILES
.iter()
.any(|relative| {
manifest.files.get(*relative).map_or(true, |hash| {
hash.len() != 64 || !hash.bytes().all(|byte| byte.is_ascii_hexdigit())
})
|| layout.files.iter().any(|relative| {
manifest.files.get(*relative).map_or(true, |hash| {
hash.len() != 64 || !hash.bytes().all(|byte| byte.is_ascii_hexdigit())
})
})
{
return Err("内置 Codex CLI 完整性清单不受支持".to_string());
}
for relative in GAME_CREATOR_BUNDLED_CODEX_CLI_REQUIRED_FILES {
for relative in layout.files {
let path = bundle_root.join(relative);
let bytes = std::fs::read(&path).map_err(|_| {
format!(
@@ -163,7 +174,7 @@ fn validate_game_creator_bundled_codex_cli(executable: &Path) -> Result<String,
)
})?;
let actual = format!("{:x}", Sha256::digest(bytes));
if !actual.eq_ignore_ascii_case(manifest.files.get(relative).expect("validated hash")) {
if !actual.eq_ignore_ascii_case(manifest.files.get(*relative).expect("validated hash")) {
return Err(format!("内置 Codex CLI 完整性校验失败:组件 {relative}"));
}
}
@@ -899,7 +910,7 @@ mod tests {
fn codex_cli_candidates_prefer_bundled_sidecar_before_npm_and_path() {
let temp = tempfile::tempdir().expect("temp dir");
let resources = temp.path().join("resources");
let bundled = resources.join(GAME_CREATOR_BUNDLED_CODEX_CLI_RELATIVE_PATH);
let bundled = game_creator_bundled_codex_cli_path(Some(&resources)).unwrap();
std::fs::create_dir_all(bundled.parent().expect("bundled parent")).expect("bundled dir");
let app_data = temp.path().join("app-data");
let native = app_data
@@ -918,20 +929,19 @@ mod tests {
assert_eq!(candidates[1], native);
}
#[cfg(windows)]
#[cfg(any(windows, target_os = "macos"))]
#[test]
fn bundled_codex_cli_requires_matching_manifest_hash() {
let temp = tempfile::tempdir().expect("temp dir");
let executable = temp
.path()
.join(GAME_CREATOR_BUNDLED_CODEX_CLI_RELATIVE_PATH);
let executable = game_creator_bundled_codex_cli_path(Some(temp.path())).unwrap();
let layout = codex_bundle::for_target(env!("AGC_BUILD_TARGET")).unwrap();
std::fs::create_dir_all(executable.parent().expect("sidecar parent")).expect("sidecar dir");
let bundle_root = executable
.parent()
.and_then(Path::parent)
.expect("bundle root");
let mut hashes = std::collections::BTreeMap::new();
for relative in GAME_CREATOR_BUNDLED_CODEX_CLI_REQUIRED_FILES {
for relative in layout.files {
let path = bundle_root.join(relative);
std::fs::create_dir_all(path.parent().expect("component parent"))
.expect("component dir");
@@ -939,14 +949,14 @@ mod tests {
std::fs::write(&path, bytes.as_bytes()).expect("component bytes");
hashes.insert(relative, format!("{:x}", Sha256::digest(bytes.as_bytes())));
}
let files = serde_json::to_string(&hashes).expect("component hashes");
std::fs::write(
bundle_root.join("manifest.json"),
format!(
r#"{{"schemaVersion":"genarrative-codex-sidecar.v2","platform":"win32-x64","version":"codex-cli test","files":{files}}}"#
),
)
.expect("sidecar manifest");
let manifest = serde_json::json!({
"schemaVersion": codex_bundle::SCHEMA,
"platform": layout.platform,
"version": "codex-cli test",
"files": hashes,
});
let manifest_path = bundle_root.join("manifest.json");
std::fs::write(&manifest_path, manifest.to_string()).expect("sidecar manifest");
assert_eq!(
validate_game_creator_bundled_codex_cli(&executable).expect("trusted sidecar"),
@@ -956,15 +966,31 @@ mod tests {
assert!(validate_game_creator_bundled_codex_cli(&executable)
.expect_err("tampered sidecar must be rejected")
.contains("完整性校验失败"));
std::fs::write(&executable, format!("trusted {}", layout.executable)).unwrap();
let mut wrong_platform = manifest.clone();
wrong_platform["platform"] = serde_json::json!("wrong-platform");
std::fs::write(&manifest_path, wrong_platform.to_string()).unwrap();
assert!(validate_game_creator_bundled_codex_cli(&executable)
.expect_err("wrong platform must be rejected")
.contains("完整性清单不受支持"));
std::fs::write(&manifest_path, manifest.to_string()).unwrap();
let helper = bundle_root.join(layout.files[1]);
std::fs::write(&helper, b"tampered helper").unwrap();
assert!(validate_game_creator_bundled_codex_cli(&executable)
.expect_err("tampered helper must be rejected")
.contains("完整性校验失败"));
std::fs::remove_file(&helper).unwrap();
assert!(validate_game_creator_bundled_codex_cli(&executable)
.expect_err("missing helper must be rejected")
.contains("缺少必需组件"));
}
#[cfg(windows)]
#[cfg(any(windows, target_os = "macos"))]
#[test]
fn bundled_codex_cli_rejects_a_manifest_without_code_mode_host() {
let temp = tempfile::tempdir().expect("temp dir");
let executable = temp
.path()
.join(GAME_CREATOR_BUNDLED_CODEX_CLI_RELATIVE_PATH);
let executable = game_creator_bundled_codex_cli_path(Some(temp.path())).unwrap();
let layout = codex_bundle::for_target(env!("AGC_BUILD_TARGET")).unwrap();
std::fs::create_dir_all(executable.parent().expect("sidecar parent")).expect("sidecar dir");
let bundle_root = executable
.parent()
@@ -975,9 +1001,13 @@ mod tests {
let hash = format!("{:x}", Sha256::digest(bytes));
std::fs::write(
bundle_root.join("manifest.json"),
format!(
r#"{{"schemaVersion":"genarrative-codex-sidecar.v2","platform":"win32-x64","version":"codex-cli test","files":{{"bin/codex.exe":"{hash}"}}}}"#
),
serde_json::json!({
"schemaVersion": codex_bundle::SCHEMA,
"platform": layout.platform,
"version": "codex-cli test",
"files": { layout.executable: hash },
})
.to_string(),
)
.expect("sidecar manifest");
@@ -986,6 +1016,38 @@ mod tests {
.contains("完整性清单不受支持"));
}
#[cfg(target_os = "macos")]
#[test]
fn macos_bundle_resources_and_candidates_do_not_require_a_development_path() {
let executable = Path::new("/Applications/陶泥儿 测试.app/Contents/MacOS/taonier");
let resources = game_creator_bundled_resource_dir_for(executable).unwrap();
assert_eq!(
resources,
Path::new("/Applications/陶泥儿 测试.app/Contents/Resources")
);
let bundled = game_creator_bundled_codex_cli_path(Some(&resources)).unwrap();
let candidates = game_creator_codex_cli_executable_candidates_for(
Some(&resources),
None,
None,
None,
None,
);
assert_eq!(candidates, vec![bundled, PathBuf::from("codex")]);
assert!(
game_creator_bundled_resource_dir_for(Path::new("/tmp/target/release/taonier"))
.is_none()
);
assert!(
game_creator_bundled_resource_dir_for(Path::new("/tmp/Contents/MacOS/taonier"))
.is_none()
);
assert_eq!(
game_creator_codex_cli_executable_candidates_for(None, None, None, None, None),
vec![PathBuf::from("codex")],
);
}
#[cfg(windows)]
#[test]
fn codex_cli_resolver_finds_current_native_install() {
@@ -550,6 +550,8 @@ fn extract_mcp_arguments(root: &Path, tool: &str, arguments: &Value) -> Value {
"agc_generate_image" => {
copy_string(object, "kind", &mut out);
copy_string(object, "sliceMode", &mut out);
copy_number(object, "sliceCount", &mut out);
copy_string(object, "screenColor", &mut out);
copy_string(object, "aspectRatio", &mut out);
copy_string(object, "imageSize", &mut out);
copy_string(object, "assetName", &mut out);
@@ -1194,7 +1196,13 @@ mod tests {
"item": {
"type": "mcpToolCall",
"tool": "agc_generate_image",
"arguments": { "prompt": prompt, "kind": "icon-spec" }
"arguments": {
"prompt": prompt,
"kind": "art-spritesheet",
"sliceMode": "connected-components",
"sliceCount": 8,
"screenColor": "#CFEFFF"
}
}
}));
audit.finish(true);
@@ -1205,6 +1213,8 @@ mod tests {
let stored = item["arguments"]["prompt"].as_str().expect("prompt");
assert_eq!(stored.chars().count(), DIRECT_CODEX_AUDIT_BRIEF_CHARS);
assert_eq!(item["arguments"]["promptChars"], json!(5000));
assert_eq!(item["arguments"]["sliceCount"], json!(8));
assert_eq!(item["arguments"]["screenColor"], json!("#CFEFFF"));
assert_eq!(
item["arguments"]["promptSha256"],
json!(sha256_hex("".repeat(5000).as_bytes()))
@@ -5,8 +5,8 @@ mod validation;
mod wire;
pub(crate) use model::{
DirectCodexUserContentPart, DirectCodexUserItem, DirectCodexUserMessageEnvelope,
DirectCodexUserMessageItem, DirectCodexUserRole, DirectCodexUserRuntimeRegionPart,
DirectCodexUserContentPart, DirectCodexUserItem, DirectCodexUserMessageItem,
DirectCodexUserRole, DirectCodexUserRuntimeRegionPart,
};
pub(crate) use validation::validate_direct_codex_user_item;
pub(crate) use wire::{

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