Compare commits

...

23 Commits

Author SHA1 Message Date
kdletters fdc48aa573 将 Godot 原生引导迁移到官方 C++ 绑定
Project CI / AI game creator shell Rust shard 1/4 (push) Failing after 17s
Project CI / AI game creator shell Rust shard 4/4 (push) Failing after 18s
Project CI / AI game creator shell Rust shard 3/4 (push) Failing after 18s
Project CI / AI game creator shell Rust shard 2/4 (push) Failing after 18s
Project CI / Backend tests (push) Failing after 17s
Project CI / AI game creator shell Rust smoke (push) Failing after 19s
Project CI / Native shell tests (push) Failing after 18s
Project CI / AI game creator shell Rust crates (push) Failing after 18s
Project CI / Frontend tests (push) Failing after 6s
Project CI / AI game creator shell web tests (push) Failing after 11s
Project CI / Repository checks (push) Failing after 11s
使用固定版本 godot-cpp 替换手写 C ABI 引导并保留执行协议
采用 MSVC 与 CMake 构建,校验依赖归档和源码缓存
修复动态卸载时的实例绑定与单例包装生命周期
补充构建缓存测试、实机验收结果及分发文档
2026-09-20 22:00:44 +08:00
kdletters 429f991bde 回滚误入 master 的游戏分发 WIP(夹带在 OSS v1 签名提交里)
Project CI / AI game creator shell Rust shard 2/4 (push) Failing after 19s
Project CI / AI game creator shell Rust shard 3/4 (push) Failing after 20s
Project CI / AI game creator shell Rust shard 1/4 (push) Failing after 20s
Project CI / AI game creator shell Rust shard 4/4 (push) Failing after 20s
Project CI / Backend tests (push) Failing after 17s
Project CI / AI game creator shell Rust crates (push) Failing after 17s
Project CI / AI game creator shell Rust smoke (push) Failing after 19s
Project CI / Native shell tests (push) Failing after 18s
Project CI / Frontend tests (push) Failing after 8s
Project CI / AI game creator shell web tests (push) Failing after 15s
Project CI / Repository checks (push) Failing after 17s
- 撤销 39aed2e48 夹带的游戏分发前后端、SpacetimeDB 绑定、主站路由、后台审核页、文档与 nginx 改动,master 恢复可编译状态
- 保留该提交自身的 AGC 发号改动:agc-global-version.mjs 与其参数构造单测
- 恢复被误删的 output 产物,避免把无关清理混进该提交
- 补回两篇技术文档在 docs/README.md 的索引(它们属于后续两个补提交文档,不属于游戏分发)
- 游戏分发完整实现保留在 codex/game-distribution 分支继续开发
2026-09-20 20:52:24 +08:00
lhk229 a2acfa9a6f 修复 Godot 原生插件的 MSVC 编译兼容性
Project CI / Frontend tests (push) Failing after 6s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 17s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 18s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 18s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 19s
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 17s
Project CI / AI game creator shell Rust crates (pull_request) Failing after 16s
Project CI / Native shell tests (pull_request) Failing after 15s
Project CI / Backend tests (pull_request) Failing after 16s
Project CI / Frontend tests (pull_request) Failing after 6s
Project CI / AI game creator shell web tests (pull_request) Failing after 12s
Project CI / Repository checks (pull_request) Failing after 12s
Project CI / AI game creator shell Rust shard 1/4 (push) Failing after 19s
Project CI / AI game creator shell Rust shard 3/4 (push) Failing after 19s
Project CI / AI game creator shell Rust shard 4/4 (push) Failing after 19s
Project CI / AI game creator shell Rust shard 2/4 (push) Failing after 19s
Project CI / AI game creator shell Rust crates (push) Failing after 20s
Project CI / AI game creator shell Rust smoke (push) Failing after 20s
Project CI / Backend tests (push) Failing after 20s
Project CI / Native shell tests (push) Failing after 19s
Project CI / AI game creator shell web tests (push) Failing after 12s
Project CI / Repository checks (push) Failing after 12s
将 ABI 存储改为 C11 显式 16 字节对齐并保留 128 字节容量
补充 Windows 编译环境要求与 MSVC 对齐问题排障说明
2026-09-20 19:39:12 +08:00
kdletters cde34428f9 补提交 AGC 后端框架与演进路线文档
Project CI / AI game creator shell Rust shard 4/4 (push) Failing after 18s
Project CI / AI game creator shell Rust shard 2/4 (push) Failing after 18s
Project CI / AI game creator shell Rust shard 3/4 (push) Failing after 18s
Project CI / AI game creator shell Rust shard 1/4 (push) Failing after 18s
Project CI / AI game creator shell Rust smoke (push) Failing after 18s
Project CI / AI game creator shell Rust crates (push) Failing after 19s
Project CI / Native shell tests (push) Failing after 19s
Project CI / Backend tests (push) Failing after 19s
Project CI / Frontend tests (push) Failing after 7s
Project CI / Repository checks (push) Failing after 11s
Project CI / AI game creator shell web tests (push) Failing after 12s
docs/README.md 已引用但文件未入库,是文档索引门禁失败的第二处缺失\n补齐后 check:doc-index 可在干净检出上通过
2026-09-20 18:53:20 +08:00
kdletters 07dc1e9add 补提交 Responses API 与 Agents SDK 迁移评估文档
docs/README.md 已引用该评估文档但文件未入库,导致 check:doc-index 与 pre-push 对所有人生效失败
本次把文档正文补进仓库,恢复文档索引门禁
2026-09-20 18:52:59 +08:00
kdletters 39aed2e486 OSS 写入显式使用 v1 签名
Project CI / AI game creator shell Rust shard 1/4 (push) Failing after 18s
Project CI / AI game creator shell Rust shard 4/4 (push) Failing after 18s
Project CI / AI game creator shell Rust shard 2/4 (push) Failing after 19s
Project CI / AI game creator shell Rust shard 3/4 (push) Failing after 19s
Project CI / AI game creator shell Rust crates (push) Failing after 18s
Project CI / Native shell tests (push) Failing after 18s
Project CI / Backend tests (push) Failing after 18s
Project CI / AI game creator shell Rust smoke (push) Failing after 19s
Project CI / Frontend tests (push) Failing after 7s
Project CI / AI game creator shell web tests (push) Failing after 11s
Project CI / Repository checks (push) Failing after 11s
ossutil v2 默认 v4 签名,缺 region 会直接失败\n总号写入与回读统一走 buildOssutilArgs,默认 --sign-version v1,可切 v4 并配合 AGC_OSS_REGION\n补充参数构造单测
2026-09-20 18:33:02 +08:00
kdletters 752116fe41 改用可执行性校验确认 ossutil 就绪
Project CI / AI game creator shell Rust shard 2/4 (push) Failing after 19s
Project CI / AI game creator shell Rust shard 4/4 (push) Failing after 19s
Project CI / AI game creator shell Rust shard 3/4 (push) Failing after 20s
Project CI / AI game creator shell Rust shard 1/4 (push) Failing after 20s
Project CI / AI game creator shell Rust smoke (push) Failing after 18s
Project CI / Native shell tests (push) Failing after 18s
Project CI / Backend tests (push) Failing after 18s
Project CI / AI game creator shell Rust crates (push) Failing after 19s
Project CI / Frontend tests (push) Failing after 7s
Project CI / AI game creator shell web tests (push) Failing after 11s
Project CI / Repository checks (push) Failing after 11s
ossutil v2 不支持 --version,版本打印会误判为失败
引导阶段只校验二进制存在且可执行,具体命令错误仍由发号步骤暴露
2026-09-20 18:24:42 +08:00
kdletters d64ce64056 修复 AGC 发号 Job 的 ossutil 引导路径
Project CI / AI game creator shell Rust shard 4/4 (push) Failing after 18s
Project CI / AI game creator shell Rust shard 1/4 (push) Failing after 19s
Project CI / AI game creator shell Rust shard 2/4 (push) Failing after 19s
Project CI / AI game creator shell Rust shard 3/4 (push) Failing after 19s
Project CI / Native shell tests (push) Failing after 19s
Project CI / AI game creator shell Rust crates (push) Failing after 19s
Project CI / Backend tests (push) Failing after 19s
Project CI / AI game creator shell Rust smoke (push) Failing after 19s
Project CI / Frontend tests (push) Failing after 6s
Project CI / AI game creator shell web tests (push) Failing after 11s
Project CI / Repository checks (push) Failing after 12s
官方 ossutil v2 zip 解压后带版本目录,先定位二进制再落到工作区固定路径
避免 chmod 找不到文件导致发号 Job 在引导阶段失败
2026-09-20 18:20:58 +08:00
kdletters 9e0cbc7041 AGC 客户端版本号收敛为单一发号源
Project CI / AI game creator shell Rust shard 2/4 (push) Failing after 18s
Project CI / AI game creator shell Rust shard 4/4 (push) Failing after 18s
Project CI / AI game creator shell Rust shard 1/4 (push) Failing after 18s
Project CI / AI game creator shell Rust shard 3/4 (push) Failing after 18s
Project CI / Backend tests (push) Failing after 18s
Project CI / Native shell tests (push) Failing after 18s
Project CI / AI game creator shell Rust crates (push) Failing after 18s
Project CI / AI game creator shell Rust smoke (push) Failing after 19s
Project CI / Frontend tests (push) Failing after 7s
Project CI / AI game creator shell web tests (push) Failing after 12s
Project CI / Repository checks (push) Failing after 12s
新增 OSS agc/global-version.json 发号模块与发号入口脚本\n构建侧优先采用发号 Job 下发的总号,未传入时本地兜底发号\n渠道高水位降级为回退断言,请求号低于渠道清单版本即失败关闭\n新增 Genarrative-Agc-Global-Version-Issue 发号 Job 与 job config\n调度管线与手动管线先发号再透传 AGC_RELEASE_VERSION\n补充单元测试、生产运维门禁、主规范与里程碑文档
2026-09-20 18:13:45 +08:00
lhk229 4864e92e30 修复策划智能体提示词边界与资源引用 (#433)
Project CI / AI game creator shell Rust shard 1/4 (push) Failing after 18s
Project CI / AI game creator shell Rust shard 4/4 (push) Failing after 18s
Project CI / AI game creator shell Rust shard 3/4 (push) Failing after 18s
Project CI / AI game creator shell Rust shard 2/4 (push) Failing after 18s
Project CI / AI game creator shell Rust smoke (push) Failing after 19s
Project CI / Native shell tests (push) Failing after 19s
Project CI / Backend tests (push) Failing after 19s
Project CI / AI game creator shell Rust crates (push) Failing after 19s
Project CI / Frontend tests (push) Failing after 6s
Project CI / AI game creator shell web tests (push) Failing after 12s
Project CI / Repository checks (push) Failing after 12s
恢复顾问态不自主推进、不安排下一步和不提交阶段审批的约束
恢复过程文档轻量记录原则并明确审批与问询的职责边界
补回顶层设计的排除方向及星露谷示例定位
统一星露谷分析示例的五处旧路径引用
同步策划专题文档与团队共享约定

Reviewed-on: #433
Co-authored-by: Linghong <ink29535@proton.me>
Co-committed-by: Linghong <ink29535@proton.me>
2026-09-20 18:07:37 +08:00
kdletters 288e1d4f4a 同步最新主干并修复 Godot 插件启动竞态
Project CI / AI game creator shell Rust shard 1/4 (push) Failing after 13s
Project CI / AI game creator shell Rust shard 2/4 (push) Failing after 12s
Project CI / AI game creator shell Rust shard 3/4 (push) Failing after 12s
Project CI / AI game creator shell Rust shard 4/4 (push) Failing after 13s
Project CI / AI game creator shell Rust smoke (push) Failing after 12s
Project CI / AI game creator shell Rust crates (push) Failing after 20s
Project CI / Backend tests (push) Failing after 23s
Project CI / Native shell tests (push) Failing after 12s
Project CI / Frontend tests (push) Failing after 5s
Project CI / Repository checks (push) Failing after 11s
Project CI / AI game creator shell web tests (push) Failing after 13s
合入主干 Rust 1.98.1 工具链及对应 CI 文档更新
Godot 插件先接收受控项目快照再注册命令和连接能力
补充启动竞态回归、失败回执诊断及插件启动合同
2026-09-20 17:46:53 +08:00
kdletters 5822b64d7c 合并 Godot 编辑器插件与常用操作指导到主分支
接入 Godot 原生桥、受控执行、Runner 回执与编辑器操作指南
保留主分支 Cocos 和 Unity 跨工程能力及外置提示词结构
解决插件生命周期、工具目录、前端启动和文档合并冲突
2026-09-20 17:38:48 +08:00
suzmii e230d9dc32 Merge pull request '升级 Rust toolchain 至 1.98.1' (#432) from chore/rust-toolchain-1-98 into master
Project CI / AI game creator shell Rust shard 1/4 (push) Failing after 12s
Project CI / AI game creator shell Rust shard 2/4 (push) Failing after 11s
Project CI / AI game creator shell Rust shard 3/4 (push) Failing after 11s
Project CI / AI game creator shell Rust shard 4/4 (push) Failing after 13s
Project CI / AI game creator shell Rust smoke (push) Failing after 20s
Project CI / AI game creator shell Rust crates (push) Failing after 28s
Project CI / Backend tests (push) Failing after 26s
Project CI / Frontend tests (push) Failing after 9s
Project CI / Native shell tests (push) Failing after 20s
Project CI / Repository checks (push) Failing after 22s
Project CI / AI game creator shell web tests (push) Failing after 25s
Reviewed-on: #432
2026-09-20 17:35:17 +08:00
suzmii b10a2d5d79 Merge branch 'master' into chore/rust-toolchain-1-98
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-20 17:35:09 +08:00
suzmii a35cdcc68c 同步 CI 镜像 tag 与文档口径,并补上工具链一致性护栏
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 17s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 11s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 14s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 12s
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 11s
Project CI / AI game creator shell Rust crates (pull_request) Failing after 10s
Project CI / Backend tests (pull_request) Failing after 10s
Project CI / Native shell tests (pull_request) Failing after 10s
Project CI / Frontend tests (pull_request) Failing after 5s
Project CI / Repository checks (pull_request) Failing after 11s
Project CI / AI game creator shell web tests (pull_request) Failing after 11s
- 将 CI job 镜像默认 tag 从 genarrative/gitea-project-ci:20260807.1 升为 20260920.1
- 将 Dockerfile 两处 org.opencontainers.image.version 统一为 2026.09.20.1(原先一处 2026.07.23.1、一处 2026.08.07.1,后写的覆盖先写的)
- 同步 deploy/container/README.md 与开发运维文档中的镜像归档示例名
- 订正 pitfalls.md 中 CI 镜像的 Rust 版本,并记录 number_of_links 在 1.98.1 上实测仍未稳定
- 在 decision-log.md 追加本次 Rust 工具链升级到 1.98.1 的决策条目
- 在 scripts/project-ci-workflow.test.ts 新增一致性用例:镜像版本段、digest 与 tag 必须与 rust-toolchain.toml 及两份运维文档一致
2026-09-20 17:06:49 +08:00
kdletters 37189c9252 记录项目主模型使用历史并补录旧项目
Project CI / AI game creator shell Rust shard 3/4 (push) Failing after 7m38s
Project CI / AI game creator shell Rust shard 2/4 (push) Failing after 7m40s
Project CI / AI game creator shell Rust shard 4/4 (push) Failing after 7m45s
Project CI / AI game creator shell Rust shard 1/4 (push) Failing after 8m27s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m24s
Project CI / AI game creator shell Rust crates (push) Successful in 3m8s
Project CI / Repository checks (push) Successful in 7m53s
Project CI / Frontend tests (push) Successful in 8m48s
Project CI / Native shell tests (push) Successful in 11m14s
Project CI / Backend tests (push) Successful in 12m54s
Project CI / AI game creator shell web tests (push) Successful in 5m53s
在项目 .agent 中保存请求模型与响应确认型号,区分目录标识和真实模型
为旧项目补录带来源标记的模型信息,保留历史并确保重复打开幂等
在 Direct Codex 代理中提取模型元数据,异步落盘且不阻塞响应转发
补充模型追溯边界测试并同步技术方案与团队约定
2026-09-20 17:02:44 +08:00
suzmii e1c8edd6bf Merge remote-tracking branch 'origin/master' into fix/rust-toolchain-1-98
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 19s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 19s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 19s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 19s
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 17s
Project CI / Backend tests (pull_request) Failing after 18s
Project CI / Native shell tests (pull_request) Failing after 18s
Project CI / AI game creator shell Rust crates (pull_request) Failing after 18s
Project CI / Frontend tests (pull_request) Failing after 7s
Project CI / AI game creator shell web tests (pull_request) Failing after 11s
Project CI / Repository checks (pull_request) Failing after 12s
2026-09-20 15:59:17 +08:00
suzmii 8a3daff55b 升级 Rust 工具链到 1.98.1
- 将 rust-toolchain.toml 的 channel 从 1.96.0 提升到 1.98.1,保留 rustfmt 组件要求
- 将 CI job 镜像 Rust stage 从 rust:1.96-bookworm 更新为 rust:1.98-bookworm,并同步新的固定镜像 digest
- 同步 deploy/container/README.md 中 Rust 版本与 digest 的说明
- 同步开发运维文档中 CI 环境与 rust-toolchain.toml 的版本口径为 1.98.1
2026-09-20 15:59:07 +08:00
kdletters 97d8a6c39a 清理并统一外置智能体提示词
Project CI / AI game creator shell Rust shard 4/4 (push) Failing after 7m34s
Project CI / AI game creator shell Rust shard 2/4 (push) Failing after 7m42s
Project CI / AI game creator shell Rust shard 3/4 (push) Failing after 7m43s
Project CI / AI game creator shell Rust shard 1/4 (push) Failing after 8m1s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m58s
Project CI / AI game creator shell Rust crates (push) Successful in 3m9s
Project CI / Backend tests (push) Successful in 12m4s
Project CI / Frontend tests (push) Successful in 11m37s
Project CI / Native shell tests (push) Successful in 15m37s
Project CI / Repository checks (push) Successful in 13m51s
Project CI / AI game creator shell web tests (push) Successful in 6m52s
将智能体系统指令、工具说明和恢复模板集中到外置提示词目录
精简重复否定、历史对照与无关实现说明,保留必要执行约束
扩展提示词构建校验和源码边界检查并接入现有本地与持续集成门禁
同步内置技能、协作规范、技术文档与对应验证断言
2026-09-20 15:32:48 +08:00
lhk229 70271b408e 删除策划agent v2,与退役功能解耦 (#355)
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 7m19s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 7m35s
Project CI / AI game creator shell Rust shard 4/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 smoke (push) Successful in 2m13s
Project CI / AI game creator shell Rust crates (push) Successful in 3m18s
Project CI / Backend tests (push) Successful in 9m16s
Project CI / Repository checks (push) Successful in 8m11s
Project CI / Native shell tests (push) Successful in 11m56s
Project CI / Frontend tests (push) Successful in 11m49s
Project CI / AI game creator shell web tests (push) Successful in 6m37s
Reviewed-on: #355
Co-authored-by: Linghong <ink29535@proton.me>
Co-committed-by: Linghong <ink29535@proton.me>
2026-09-20 14:49:57 +08:00
kdletters 623e007fae 完善客户端发布渠道并接入模板库灰度
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 7m36s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 7m37s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 7m42s
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 7m51s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m55s
Project CI / AI game creator shell Rust crates (push) Successful in 4m15s
Project CI / Repository checks (push) Successful in 4m45s
Project CI / Frontend tests (push) Successful in 7m47s
Project CI / Native shell tests (push) Successful in 9m33s
Project CI / Backend tests (push) Successful in 10m9s
Project CI / AI game creator shell web tests (push) Successful in 4m38s
区分发布渠道与系统,支持 dev、release 和自定义渠道
允许网站通过服务端配置选择客户端下载检测渠道
接入模板库灰度权限并阻断退出和切号后的异步操作
补齐发布、下载、灰度与会话竞态测试及当前规范
2026-09-20 12:12:49 +08:00
kdletters fc0ce4ee5f 补齐跨工程插件启动的界面测试响应
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 7m29s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 7m36s
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 7m42s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m28s
Project CI / AI game creator shell Rust crates (push) Successful in 2m47s
Project CI / Backend tests (push) Successful in 8m49s
Project CI / Repository checks (push) Successful in 8m40s
Project CI / Frontend tests (push) Successful in 9m41s
Project CI / Native shell tests (push) Successful in 12m29s
Project CI / AI game creator shell web tests (push) Failing after 4m51s
为五个项目打开与对话恢复测试提供插件项目绑定及空插件列表响应

保留原有业务状态断言和产品错误提示,修复完整界面门禁暴露的测试桩缺口
2026-09-20 10:58:10 +08:00
kdletters c1d26f11df 移除 Cocos 和 Unity 插件的工程类型限制
统一插件列表、启动、面板和 Agent 工具的可用性判断,保留开关与平台约束

调整前端自动启动并修复 Cocos 项目切换的旧连接与订阅快照竞态

补充插件宿主、工具目录和前端回归测试,同步插件技术规范与共享决策
2026-09-20 10:53:16 +08:00
313 changed files with 14928 additions and 20573 deletions
+4
View File
@@ -235,6 +235,10 @@ VITE_DEBUG_MODE=""
# This is read by api-server and exposed through /api/runtime/frontend-config.
GENARRATIVE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR="false"
# 官网客户端下载检测渠道:dev、release 或自定义渠道;修改后重启 API 服务。
# Windows/macOS 是系统维度,不填写 dev-win/dev-mac。
GENARRATIVE_CLIENT_DOWNLOAD_CHANNEL="dev"
# Optional: official VikingDB credentials for regenerating build-tag similarities
# with the Python embedding script. The script auto-loads `.env.local` and uses
# the fixed `bge-large-zh` embedding model.
+16 -19
View File
@@ -4,12 +4,13 @@
- 本文件只保留 Agent 进入仓库后必须立即遵守的最高优先级规则;完整执行细则见 [`docs/【协作规范】Agent工作入口与执行准则-2026-06-22.md`](docs/【协作规范】Agent工作入口与执行准则-2026-06-22.md)。
- 团队级长期项目记忆位于 [`docs/project-memory/`](docs/project-memory/),供 3 名开发人员和各自本地 Agent 通过 Git 同步。
- [`.codex/`](.codex/) 保存仓库级 Codex 工具资源,例如 skills、plugins、hooks 和配置模板;长期项目知识不要写入 `.codex/`
- [`.codex/`](.codex/) 保存仓库级 Codex 工具资源,例如 skills、plugins、hooks 和配置模板;长期项目知识写入 `docs/``docs/project-memory/`
-`docs/project-memory/shared-memory/` 与当前代码或最新 `docs/` 冲突,以代码和最新 `docs/` 为准,并同步修正过期共享记忆。
## 开始任务前
- 先写清一句话交付结果、验收判据和不做项,再按“必须项 / 风险项 / 可选项”排序;优先完成修改、定向验证和边界检查组成的最小闭环。设置时间盒和检查点,新增发现只有在影响交付判据时才扩大范围,否则记录为后续事项;工具探测、历史整理或验证便利不能自行改变任务目标
- 先写清一句话交付结果、验收判据和修改范围,再按“必须项 / 风险项 / 可选项”排序;优先完成修改、定向验证和边界检查组成的最小闭环。设置时间盒和检查点,新增发现只有在影响交付判据时才扩大范围,其余记录为后续事项。
- Agent 可见内容直接描述当前任务、输入和成功条件,细节按调用需要提供。
- 简单自包含任务可以直接执行;复杂开发、跨模块修改、后端 / UI / 文档体系调整前,按顺序读取:
1. 本文件。
2. [`docs/【协作规范】Agent工作入口与执行准则-2026-06-22.md`](docs/【协作规范】Agent工作入口与执行准则-2026-06-22.md)。
@@ -23,24 +24,20 @@
- 禁止提交个人 `~/.codex` 配置、`.env`、API Key、Token、Cookie、会话记录、认证文件、本地私密路径、构建产物、日志、缓存和数据库 dump。
- 不要在 `.gitignore` 中新增 `.env.local`
- 不要擅自把现有中文文案、注释、剧情文档改写成英文;看到中文乱码时先确认真实编码,不要沿用乱码或用英文替换
- 修改包含中文的文件时优先局部补丁,避免整文件重写;修改后优先运行仓库编码检查。
- 后续新增 Markdown 文档文件名必须以分类标签开头,格式为 `【标签名】中文标题-日期.md`;历史文档不要求批量重命名,除非本次任务明确涉及
- 现有中文文案、注释、剧情文档保持中文;看到乱码时先确认真实编码并恢复正确中文
- 修改包含中文的文件时优先局部补丁;修改后优先运行仓库编码检查。
- 新增 Markdown 文档文件名必须以分类标签开头,格式为 `【标签名】中文标题-日期.md`
- 工程修改要同步更新对应 `docs/` 文档;产生长期有效的架构约定、接口变化、排障经验、开发流程或协作规则时,同步更新 `docs/project-memory/shared-memory/`
- 默认保持系统简洁:优先复用、修改、扩展现有系统、页面和公共组件,不新建平行系统或平行页面
- UI 开发优先复用现有公共组件;发现跨页面或跨端重复的视觉/交互模式时,先抽取到 `packages/shared` 共享组件库并让现有页面迁移使用,禁止在业务页复制同类 UI。共享组件承载通用表现与交互,不下沉领域规则、后端副作用正式业务状态。
- 对已明确退役且不存在现役调用方、公开契约、持久化数据、活跃实例或迁移要求的对象,坚持“四不写”:
1. 不写历史兼容代码
2. 不写用于维持退役行为的防御性兼容测试
3. 不写仅说明其曾存在或已删除的墓碑注释
4. 不写仅记录其已删除的墓碑文档;直接将权威文档更新为当前状态。
- 公开 API、持久化数据、SpacetimeDB schema、跨版本重放、活跃实例和正式迁移不适用“四不写”;必要兼容应最小化、白名单化并配套契约或迁移测试,迁移完成后同步删除兼容实现与对应测试。
- UI 面板中不要默认写功能说明、规则描述或开发解释文案;移动端优先,同时保证网页端可正常显示和操作。
- 点击按钮弹出独立面板的设计,不要实现成在当前面板下面追加内容。
- 默认保持系统简洁:优先复用、修改、扩展现有系统、页面和公共组件。
- UI 开发优先复用现有公共组件;发现跨页面或跨端重复的视觉/交互模式时,先抽取到 `packages/shared` 共享组件库并让现有页面迁移使用。共享组件承载通用表现与交互,领域规则、后端副作用正式业务状态由后端负责
- 对已明确退役且不存在现役调用方、公开契约、持久化数据、活跃实例或迁移要求的对象,直接清理实现、专属测试和说明,将权威文档更新为当前状态;历史由 Git 保存。
- 公开 API、持久化数据、SpacetimeDB schema、跨版本重放、活跃实例和正式迁移按实际需求保留最小、白名单化的兼容,并配套契约或迁移测试;迁移完成后同步删除兼容实现与对应测试
- UI 面板优先呈现任务内容与操作,说明按当前操作需要提供;移动端优先,同时保证网页端可正常显示和操作
- 点击按钮弹出的独立面板使用弹窗、抽屉、popover 或页面级 portal
## 任务路由
- Issue 使用自托管 Gitea;优先用 Gitea UI/API 或 `tea` CLI,不使用 GitHub `gh` 或 GitLab `glab`,除非仓库已迁移。默认 triage 标签:`needs-triage``needs-info``ready-for-agent``ready-for-human``wontfix`
- Issue 使用自托管 Gitea;优先用 Gitea UI/API 或 `tea` CLI。默认 triage 标签:`needs-triage``needs-info``ready-for-agent``ready-for-human``wontfix`
- 需要仓库级 Codex skills/plugins 时,再读取 [`.codex/README.md`](.codex/README.md)。
- 涉及 AI 游戏创作独立 App、多智能体 Runtime、本地项目产物或本地 HTTP 预览时,先读取 [`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`](docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md)。
- 新增、补齐、迁移或重构玩法入口、玩法类型、创作工作台、生成页、结果页、发布、运行态、作品架、广场或公开 read model 前,必须读取并按 [`genarrative-play-type-integration`](.codex/skills/genarrative-play-type-integration/SKILL.md) 执行。
@@ -50,10 +47,10 @@
## 后端红线
- 后端最新技术约束以 [`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`](docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md) 为准。
- 后端路线固定为 `server-rs + Axum + SpacetimeDB`;旧 `server-node`、Express、PostgreSQL、Go 服务端、`maincloud` / `Maincloud` / `MAINCLOUD` 只作为历史残留,不作为兼容目标
- 后端路线固定为 `server-rs + Axum + SpacetimeDB`
- DDD 分层边界按总纲执行:领域规则沉到 `module-*`SpacetimeDB 表和事务编排留在 `spacetime-module`,后端访问 SpacetimeDB 统一经 `spacetime-client` facadeHTTP/SSE/BFF 留在 `api-server`,外部副作用留在 `platform-*`,前后端 DTO 留在 `shared-contracts`
- 前端只做表现、交互和临时 UI 状态,不承接正式业务真相,不绕过后端投影后端 API 直接实现业务规则
- 契约、路由、DTO 去留和 breaking change 以当前后端架构文档、`server-rs/crates/api-server/src/app.rs``shared-contracts``packages/shared` 为准;不得在前端、`api-server` 或临时兼容层中重新发明旧接口
- 前端负责表现、交互和临时 UI 状态;正式业务状态与规则以后端投影后端 API 为准
- 契约、路由、DTO 去留和 breaking change 以当前后端架构文档、`server-rs/crates/api-server/src/app.rs``shared-contracts``packages/shared` 为准。
- 凡修改 `/api/external/v1` 的路由、HTTP 方法、请求 / 响应 DTO、请求头、状态码、鉴权或异步语义,必须在同一次变更中同步更新权威契约 [`docs/openapi/genarrative-external-v1.openapi.json`](docs/openapi/genarrative-external-v1.openapi.json) 及对应契约测试;Rust 实现与 OpenAPI 未保持一致时任务不得视为完成。
- SpacetimeDB 已有表新增字段时,字段必须放在 Rust 表结构体最后,并设置明确默认值;需要删除、改名、重排或改类型时,必须先询问用户并确认迁移计划。
- 修改 SpacetimeDB schema 后必须同步 `migration.rs`、表目录和生成绑定,并运行 `npm run check:spacetime-schema`
@@ -154,6 +154,27 @@ test('灰度发布页可通过功能入口生成画布 Agent Gate Key', async ()
);
});
test('灰度发布页可选择模板库并默认启用零比例灰度', async () => {
const user = userEvent.setup();
render(
<AdminGrayReleaseConfigPage token="admin-token" onUnauthorized={vi.fn()} />,
);
await screen.findByRole('button', { name: 'editor.new-toolbar' });
await user.selectOptions(screen.getByLabelText('Gate Key 前缀'), ['agc']);
expect((screen.getByLabelText('Gate Key') as HTMLInputElement).value).toBe(
'agc:template-library',
);
expect(
(screen.getByLabelText('Gate Key 目标') as HTMLSelectElement).value,
).toBe('template-library');
expect((screen.getByLabelText('启用') as HTMLInputElement).checked).toBe(
true,
);
expect((screen.getByLabelText('灰度比例') as HTMLInputElement).value).toBe(
'0',
);
});
test('灰度发布页保存时转换数组和百分比', async () => {
const user = userEvent.setup();
vi.mocked(upsertAdminFeatureGateConfig).mockResolvedValueOnce({
@@ -27,9 +27,17 @@ interface GateTargetOption {
const GATE_PREFIX_LABELS: Record<string, string> = {
'image-editor': '画布',
agc: '客户端',
};
const FIXED_GATE_TARGETS: GateTargetOption[] = [
{
prefix: 'agc',
suffix: 'template-library',
key: 'agc:template-library',
label: '模板库',
description: '客户端模板库灰度',
},
{
prefix: 'image-editor',
suffix: 'agent-sidebar',
@@ -180,7 +188,7 @@ export function AdminGrayReleaseConfigPage({
setSelectedGateKey('');
setGatePrefix(option.prefix);
setGateKey(option.key);
setEnabled(false);
setEnabled(option.key === 'agc:template-library');
setRolloutPercent('0');
setAllowUserIds('');
setAllowUserTags('');
@@ -0,0 +1,330 @@
/**
* AGC 总版本号(发号源)。
*
* 唯一事实源是 OSS 对象 `agc/global-version.json`;渠道清单只写各自本次拿到的号。
* 仓库里的 5 个版本文件仍由构建改写,但只作构建输入参考,不作为事实源。
*
* 发号顺序固定为「先写总号 → 再构建 → 再发渠道清单」:任何一步失败都不回滚,
* 只烧号。这样渠道之间不会复用同一个号,代价是可能出现空洞。
*/
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
export const AGC_GLOBAL_VERSION_OBJECT_KEY = 'agc/global-version.json';
const defaultOssBaseUrl =
'https://agc-dev.oss-rg-china-mainland.aliyuncs.com/agc';
const versionPattern = /^\d+\.\d+\.\d+$/u;
function trimTrailingSlashes(value) {
return value.replace(/\/+$/u, '');
}
export function ossBaseUrl(env = process.env) {
return trimTrailingSlashes(
env.AGC_UPDATE_OSS_BASE_URL?.trim() || defaultOssBaseUrl,
);
}
export function globalVersionUrl(env = process.env) {
return `${ossBaseUrl(env)}/global-version.json`;
}
export function parseVersion(value, label) {
if (typeof value !== 'string' || !versionPattern.test(value.trim())) {
throw new Error(`${label} 不是有效的三段版本号:${String(value)}`);
}
return value.trim();
}
export function compareVersions(left, right) {
const leftParts = parseVersion(left, '左版本').split('.').map(Number);
const rightParts = parseVersion(right, '右版本').split('.').map(Number);
for (let index = 0; index < 3; index += 1) {
if (leftParts[index] !== rightParts[index]) {
return leftParts[index] > rightParts[index] ? 1 : -1;
}
}
return 0;
}
/** 取较大版本;任一为空时返回另一个。 */
export function maxVersion(...versions) {
return versions
.filter((value) => typeof value === 'string' && value.trim())
.map((value) => parseVersion(value, '候选版本'))
.reduce(
(best, current) =>
best == null || compareVersions(current, best) > 0 ? current : best,
null,
);
}
export function nextVersion(current) {
const [major, minor, patch] = parseVersion(current, '总版本')
.split('.')
.map(Number);
if (patch === Number.MAX_SAFE_INTEGER) {
throw new Error(`版本号 patch 已达到上限:${current}`);
}
return `${major}.${minor}.${patch + 1}`;
}
export function readReleaseDryRun(env = process.env) {
const value = env.AGC_RELEASE_DRY_RUN?.trim().toLowerCase();
return value === '1' || value === 'true';
}
async function fetchJson(url, label, { fetchImpl = fetch } = {}) {
let response;
try {
response = await fetchImpl(url, {
headers: { Accept: 'application/json' },
});
} catch (error) {
throw new Error(`读取 ${label} 失败:${error.message}`);
}
if (response.status === 404) return null;
if (!response.ok) {
throw new Error(`读取 ${label} 失败:HTTP ${response.status}`);
}
try {
return await response.json();
} catch (error) {
throw new Error(`${label} 不是有效 JSON${error.message}`);
}
}
/** 渠道清单版本;缺失或 404 时返回 null(首次启用渠道)。 */
export async function readChannelVersion(channel, options = {}) {
const payload = await fetchJson(
`${ossBaseUrl(options.env)}/${channel}/latest.json`,
`${channel} 渠道清单`,
options,
);
if (payload == null) return null;
const version = typeof payload.version === 'string' ? payload.version : '';
if (!version) {
throw new Error(`${channel} 渠道清单缺少 version 字段`);
}
return parseVersion(version, `${channel} 渠道清单 version`);
}
/** 旧协议迁移指针 `agc/latest.json`;只在迁移窗口内存在,仅参与播种。 */
export async function readLegacyPointerVersion(options = {}) {
const payload = await fetchJson(
`${ossBaseUrl(options.env)}/latest.json`,
'OSS 迁移指针',
options,
);
if (payload == null) return null;
const version = typeof payload.version === 'string' ? payload.version : '';
return version ? parseVersion(version, 'OSS 迁移指针 version') : null;
}
export async function readGlobalVersion(options = {}) {
const payload = await fetchJson(
globalVersionUrl(options.env),
'AGC 总版本号',
options,
);
if (payload == null) return null;
const version = typeof payload.version === 'string' ? payload.version : '';
if (!version) {
throw new Error('AGC 总版本号对象缺少 version 字段');
}
return {
...payload,
version: parseVersion(version, 'AGC 总版本号 version'),
};
}
/**
* 一次性播种基线:仓库当前版本、渠道清单与旧迁移指针里的最大值。
* 基线本身不发给客户端,首个发放号是 baseline + 1。
*/
export async function resolveSeedBaseline({
channels = ['dev-win', 'dev-mac'],
repoVersion = null,
env = process.env,
fetchImpl = fetch,
} = {}) {
const candidates = [];
if (repoVersion) candidates.push(parseVersion(repoVersion, '仓库当前版本'));
for (const channel of channels) {
const version = await readChannelVersion(channel, { env, fetchImpl });
if (version) candidates.push(version);
}
const legacy = await readLegacyPointerVersion({ env, fetchImpl });
if (legacy) candidates.push(legacy);
const baseline = maxVersion(...candidates);
if (!baseline) {
throw new Error('无法确定总版本号播种基线:仓库版本与渠道清单都不可用');
}
return baseline;
}
/**
* 组装 ossutil 参数。
*
* 该桶与凭据按 v1 签名使用(ossutil v2 默认 v4,缺 region 会直接失败),
* 因此默认显式传 `--sign-version v1`;需要 v4 时用 `AGC_OSS_SIGN_VERSION=v4`
* 并同时给 `AGC_OSS_REGION`。
*/
export function buildOssutilArgs({
args,
endpoint,
accessKeyId,
accessKeySecret,
env = process.env,
}) {
const finalArgs = [...args, '--endpoint', endpoint];
const region = env.AGC_OSS_REGION?.trim();
if (region) finalArgs.push('--region', region);
finalArgs.push('--sign-version', env.AGC_OSS_SIGN_VERSION?.trim() || 'v1');
if (accessKeyId) {
finalArgs.push(
'--access-key-id',
accessKeyId,
'--access-key-secret',
accessKeySecret,
);
}
return finalArgs;
}
function runOssutil(args, { env = process.env } = {}) {
const binary = env.OSSUTIL_BIN?.trim() || 'ossutil';
const endpoint =
env.AGC_OSS_ENDPOINT?.trim() || 'oss-rg-china-mainland.aliyuncs.com';
const accessKeyId = env.AGC_OSS_ACCESS_KEY_ID?.trim();
const accessKeySecret = env.AGC_OSS_ACCESS_KEY_SECRET;
if (Boolean(accessKeyId) !== Boolean(accessKeySecret)) {
throw new Error('OSS AccessKey ID 和 Secret 必须同时提供');
}
const result = spawnSync(
binary,
buildOssutilArgs({
args,
endpoint,
accessKeyId,
accessKeySecret,
env,
}),
{ stdio: 'inherit', shell: false, env },
);
if (result.error) {
throw new Error(`无法执行 ${binary},请先安装并配置 ossutil`);
}
if (result.status !== 0) {
throw new Error(`${binary} 执行失败,退出码 ${result.status}`);
}
}
/** 写入总版本号对象;dry-run 下只打印将要执行的上传。 */
export function writeGlobalVersion(payload, options = {}) {
const { env = process.env, dryRun = readReleaseDryRun(env) } = options;
const bucket = env.AGC_OSS_BUCKET?.trim() || 'agc-dev';
const body = Buffer.from(`${JSON.stringify(payload, null, 2)}\n`, 'utf8');
const tempDirectory = fs.mkdtempSync(
path.join(os.tmpdir(), 'agc-global-version-'),
);
const tempPath = path.join(tempDirectory, 'global-version.json');
try {
fs.writeFileSync(tempPath, body);
const ossUrl = `oss://${bucket}/${AGC_GLOBAL_VERSION_OBJECT_KEY}`;
if (dryRun) {
console.log(
`[dry-run] 不写总版本号:${ossUrl} <- ${JSON.stringify(payload)}`,
);
return { written: false, objectUrl: ossUrl, payload };
}
runOssutil(['cp', '--force', tempPath, ossUrl], { env });
return { written: true, objectUrl: ossUrl, payload };
} finally {
fs.rmSync(tempDirectory, { force: true, recursive: true });
}
}
/**
* 发一次号并写回总版本号对象。
*
* 写后回读校验:若远端值与自己写下的不一致,说明有并发发号,按失败关闭处理
* (号已烧,不重试、不回滚),由人工确认后再发。
*/
export async function issueGlobalVersion({
channel,
commit = null,
buildId = null,
repoVersion = null,
env = process.env,
fetchImpl = fetch,
now = () => new Date().toISOString(),
writeImpl = writeGlobalVersion,
} = {}) {
if (!channel) throw new Error('发号必须显式指定 channel');
const dryRun = readReleaseDryRun(env);
const current = await readGlobalVersion({ env, fetchImpl });
let baseline = current?.version ?? null;
let seeded = false;
if (!baseline) {
baseline = await resolveSeedBaseline({ repoVersion, env, fetchImpl });
seeded = true;
}
const issued = nextVersion(baseline);
const payload = {
version: issued,
updatedAt: now(),
channel,
commit,
buildId,
};
console.log(
`[agc-global-version] ${
seeded ? `按播种基线 ${baseline} 首发` : `总号 ${baseline}`
} -> ${issued}channel=${channel} dry-run=${dryRun}`,
);
writeImpl(payload, { env, dryRun });
if (!dryRun) {
const stored = await readGlobalVersion({ env, fetchImpl });
if (!stored || stored.version !== issued) {
throw new Error(
`总版本号写后回读不一致:期望 ${issued},远端 ${
stored?.version ?? '不存在'
};可能存在并发发号,本次构建失败关闭`,
);
}
}
return issued;
}
/** 渠道高水位断言:请求号低于本渠道清单版本即失败关闭。 */
export function assertRequestedVersionNotBelowChannel({
requested,
channelVersion,
channel,
}) {
const requestedVersion = parseVersion(requested, '请求版本');
if (channelVersion == null) return requestedVersion;
const current = parseVersion(channelVersion, `${channel} 渠道版本`);
if (compareVersions(requestedVersion, current) < 0) {
throw new Error(
`请求版本 ${requestedVersion} 低于 ${channel} 渠道当前清单版本 ${current};拒绝回退发布`,
);
}
return requestedVersion;
}
/** 只读预览:不写回、不烧号。 */
export async function previewNextGlobalVersion(options = {}) {
const current = await readGlobalVersion(options);
const baseline =
current?.version ??
(await resolveSeedBaseline({
repoVersion: options.repoVersion,
env: options.env,
fetchImpl: options.fetchImpl,
}));
return nextVersion(baseline);
}
@@ -0,0 +1,206 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import {
assertRequestedVersionNotBelowChannel,
buildOssutilArgs,
issueGlobalVersion,
maxVersion,
nextVersion,
previewNextGlobalVersion,
resolveSeedBaseline,
} from './agc-global-version.mjs';
function jsonResponse(payload, status = 200) {
return {
ok: status >= 200 && status < 300,
status,
json: async () => payload,
};
}
/** 以 URL 为键的假 OSS:只读 fetch + 记录写入。 */
function createFakeOss({ objects = {} } = {}) {
const state = { ...objects };
const writes = [];
return {
state,
writes,
fetchImpl: async (url) => {
const key = String(url).replace(/^https?:\/\/[^/]+\//u, '');
if (!(key in state)) return jsonResponse(null, 404);
return jsonResponse(state[key]);
},
writeImpl: (payload, options = {}) => {
writes.push({ payload, dryRun: Boolean(options.dryRun) });
if (!options.dryRun) state['agc/global-version.json'] = payload;
return { written: !options.dryRun, payload };
},
};
}
test('播种基线取仓库版本、各渠道清单与旧迁移指针的最大值', async () => {
const oss = createFakeOss({
objects: {
'agc/dev-win/latest.json': { version: '0.1.57' },
'agc/dev-mac/latest.json': { version: '0.1.12' },
'agc/latest.json': { version: '0.1.60' },
},
});
assert.equal(
await resolveSeedBaseline({
repoVersion: '0.1.48',
env: {},
fetchImpl: oss.fetchImpl,
}),
'0.1.60',
);
assert.equal(maxVersion('0.1.9', '0.1.10', null), '0.1.10');
});
test('无总号时按播种基线发首号,并把总号写回唯一事实源', async () => {
const oss = createFakeOss({
objects: {
'agc/dev-win/latest.json': { version: '0.1.57' },
'agc/dev-mac/latest.json': { version: '0.1.12' },
},
});
const issued = await issueGlobalVersion({
channel: 'dev-win',
commit: 'a'.repeat(40),
buildId: '319',
repoVersion: '0.1.48',
env: {},
fetchImpl: oss.fetchImpl,
writeImpl: oss.writeImpl,
now: () => '2026-09-20T00:00:00.000Z',
});
assert.equal(issued, '0.1.58');
assert.deepEqual(
oss.writes.map((entry) => entry.payload.version),
['0.1.58'],
);
assert.equal(oss.state['agc/global-version.json'].version, '0.1.58');
assert.equal(oss.state['agc/global-version.json'].channel, 'dev-win');
assert.equal(oss.state['agc/global-version.json'].buildId, '319');
});
test('已有总号时只递增,不再回看渠道清单', async () => {
const oss = createFakeOss({
objects: {
'agc/global-version.json': { version: '0.2.7' },
// 渠道清单被手工改小也不能把总号拉回去。
'agc/dev-win/latest.json': { version: '0.1.10' },
},
});
const issued = await issueGlobalVersion({
channel: 'dev-mac',
env: {},
fetchImpl: oss.fetchImpl,
writeImpl: oss.writeImpl,
});
assert.equal(issued, '0.2.8');
assert.equal(oss.state['agc/global-version.json'].version, '0.2.8');
});
test('dry-run 只预览下一位,不写回、不烧号', async () => {
const oss = createFakeOss({
objects: { 'agc/global-version.json': { version: '0.3.4' } },
});
const preview = await previewNextGlobalVersion({
env: {},
fetchImpl: oss.fetchImpl,
});
assert.equal(preview, '0.3.5');
assert.equal(oss.writes.length, 0);
const issued = await issueGlobalVersion({
channel: 'dev-win',
env: { AGC_RELEASE_DRY_RUN: '1' },
fetchImpl: oss.fetchImpl,
writeImpl: oss.writeImpl,
});
assert.equal(issued, '0.3.5');
assert.deepEqual(
oss.writes.map((entry) => entry.dryRun),
[true],
);
assert.equal(oss.state['agc/global-version.json'].version, '0.3.4');
});
test('传入低于本渠道清单的号时失败关闭', () => {
assert.equal(
assertRequestedVersionNotBelowChannel({
requested: '0.1.60',
channelVersion: '0.1.60',
channel: 'dev-win',
}),
'0.1.60',
);
assert.throws(
() =>
assertRequestedVersionNotBelowChannel({
requested: '0.1.59',
channelVersion: '0.1.60',
channel: 'dev-win',
}),
/低于 dev-win 渠道当前清单版本/u,
);
assert.equal(
assertRequestedVersionNotBelowChannel({
requested: '0.1.1',
channelVersion: null,
channel: 'dev-mac',
}),
'0.1.1',
);
});
test('写后回读不一致(并发发号)时失败关闭', async () => {
const oss = createFakeOss({
objects: { 'agc/global-version.json': { version: '0.5.1' } },
});
await assert.rejects(
issueGlobalVersion({
channel: 'dev-win',
env: {},
// 模拟另一个发号进程在写入后覆盖了总号。
writeImpl: (payload, options) => {
const result = oss.writeImpl(payload, options);
// 另一个发号进程紧随其后覆盖总号。
oss.state['agc/global-version.json'] = { version: '0.5.9' };
return result;
},
fetchImpl: oss.fetchImpl,
}),
/写后回读不一致/u,
);
});
test('nextVersion 只在 patch 位递增', () => {
assert.equal(nextVersion('0.1.9'), '0.1.10');
assert.equal(nextVersion('1.0.0'), '1.0.1');
assert.throws(() => nextVersion('0.1'), /不是有效的三段版本号/u);
});
test('ossutil 参数默认使用 v1 签名,并可按需带 region 与 v4', () => {
const base = {
args: ['cp', '--force', '/tmp/a.json', 'oss://agc-dev/agc/global-version.json'],
endpoint: 'oss-rg-china-mainland.aliyuncs.com',
accessKeyId: 'id',
accessKeySecret: 'secret',
env: {},
};
const v1 = buildOssutilArgs(base);
assert.equal(v1[v1.indexOf('--sign-version') + 1], 'v1');
assert.ok(!v1.includes('--region'));
assert.equal(v1[v1.indexOf('--access-key-id') + 1], 'id');
assert.equal(v1[v1.indexOf('--access-key-secret') + 1], 'secret');
const v4 = buildOssutilArgs({
...base,
env: { AGC_OSS_SIGN_VERSION: 'v4', AGC_OSS_REGION: 'cn-beijing' },
});
assert.equal(v4[v4.indexOf('--region') + 1], 'cn-beijing');
assert.equal(v4[v4.indexOf('--sign-version') + 1], 'v4');
});
@@ -957,9 +957,6 @@ async function runInteractiveCargo(cliArguments, setActiveChild) {
return result;
}
// 立项策划跑 standard 档,`agent.delegate` 这类动作按项目权限策略必须逐个确认,
// 而确认和问询都只从 CLI 的 stdin 读。自主构建档没有这一步,所以只有 --plan 需要
// 一个把「人坐在终端前敲 approve」自动化掉的应答器;判据本身仍然走后端确认命令。
const swarmConfirmationPromptPattern = /输入 approve 或 reject$/u;
const swarmUserInputPromptPattern = /请选择 1-\d+,或直接输入其他答案:$/u;
@@ -5,6 +5,10 @@ import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
assertRequestedVersionNotBelowChannel,
issueGlobalVersion as issueAgcGlobalVersion,
} from './agc-global-version.mjs';
import {
defaultEditorFeatures,
withDefaultCargoFeatures,
@@ -68,7 +72,7 @@ export function resolveReleaseContext(args = [], env = process.env) {
);
return Object.freeze({
target,
channel: resolveReleaseChannel(env, target),
channel: resolveReleaseChannel(env),
bundleRoot: path.join(
appRoot,
'src-tauri',
@@ -87,14 +91,14 @@ const cargoLockPath = path.join(appRoot, 'src-tauri', 'Cargo.lock');
const defaultOssBaseUrl =
'https://agc-dev.oss-rg-china-mainland.aliyuncs.com/agc';
/**
* 发布渠道 → 目标平台。渠道名会进入 OSS 路径并烘焙进客户端端点,
* 一旦发布就不能改名(改名等于已发布客户端再也找不到更新)。
*/
const releaseChannels = {
'dev-win': 'windows',
'dev-mac': 'darwin',
};
const reservedChannelNames = new Set([
'win',
'mac',
'windows',
'macos',
'darwin',
'linux',
]);
/**
* 影响 Windows 客户端产物的路径。调度管线的发布范围判定与这里的提交摘要必须
@@ -163,39 +167,36 @@ export function resolveReleasePlatform(target = defaultTarget()) {
throw new Error(`不支持的发布目标:${target}`);
}
export function resolveReleaseChannel(
env = process.env,
target = defaultTarget(),
) {
const platform = resolveReleasePlatform(target);
const requested = env.AGC_UPDATE_CHANNEL?.trim();
if (requested) {
const channelPlatform = releaseChannels[requested];
if (!channelPlatform) {
throw new Error(
`未知发布渠道 ${requested};当前支持:${Object.keys(releaseChannels).join('、')}`,
);
}
if (channelPlatform !== platform) {
throw new Error(
`渠道 ${requested} 只能用于 ${channelPlatform} 目标,当前构建目标为 ${target}`,
);
}
return requested;
}
const defaultChannel = Object.entries(releaseChannels).find(
([, channelPlatform]) => channelPlatform === platform,
)?.[0];
if (!defaultChannel) {
export function resolveReleaseChannel(env = process.env) {
const channel = env.AGC_UPDATE_CHANNEL?.trim() ?? 'dev';
if (
!/^[a-z][a-z0-9-]{0,31}$/u.test(channel) ||
channel.endsWith('-') ||
reservedChannelNames.has(channel) ||
/-(win|mac)$/u.test(channel)
) {
throw new Error(
`目标 ${target} 没有默认发布渠道,请显式设置 AGC_UPDATE_CHANNEL`,
'发布渠道无效:请使用 dev、release 或最多 32 位的小写字母、数字和连字符名称,系统名称不属于渠道',
);
}
return defaultChannel;
return channel;
}
export function updateManifestUrl(channel = resolveReleaseChannel()) {
return `${ossBaseUrl()}/${channel}/latest.json`;
/** 系统分区延续已发布客户端端点,渠道本身不包含系统。 */
export function resolveReleasePartition(
channel = resolveReleaseChannel(),
target = defaultTarget(),
) {
channel = resolveReleaseChannel({ AGC_UPDATE_CHANNEL: channel });
validateReleaseTarget(target);
return `${channel}-${resolveReleasePlatform(target) === 'windows' ? 'win' : 'mac'}`;
}
export function updateManifestUrl(
channel = resolveReleaseChannel(),
target = defaultTarget(),
) {
return `${ossBaseUrl()}/${resolveReleasePartition(channel, target)}/latest.json`;
}
/**
@@ -246,8 +247,8 @@ async function readManifestVersion(manifestUrl, label) {
}
/** 上一次发布的渠道清单:拿版本做高水位、拿 commit 生成自动更新摘要。 */
async function readRemoteChannelManifest(channel = resolveReleaseChannel()) {
return fetchManifest(updateManifestUrl(channel), 'OSS 渠道清单');
async function readRemoteChannelManifest(channel, target) {
return fetchManifest(updateManifestUrl(channel, target), 'OSS 渠道清单');
}
/**
@@ -259,14 +260,17 @@ async function readRemoteChannelManifest(channel = resolveReleaseChannel()) {
*/
export async function resolvePreviousReleaseCommit(
channel = resolveReleaseChannel(),
{ override = process.env.AGC_UPDATE_PREVIOUS_COMMIT } = {},
{
override = process.env.AGC_UPDATE_PREVIOUS_COMMIT,
target = defaultTarget(),
} = {},
) {
const explicit = override?.trim();
if (explicit && /^[0-9a-f]{7,40}$/u.test(explicit)) {
return explicit;
}
try {
const manifest = await readRemoteChannelManifest(channel);
const manifest = await readRemoteChannelManifest(channel, target);
const commit =
typeof manifest?.commit === 'string' ? manifest.commit.trim() : '';
return /^[0-9a-f]{7,40}$/u.test(commit) ? commit : null;
@@ -288,12 +292,14 @@ export async function resolvePreviousReleaseCommit(
*/
export async function resolveRemoteHighWaterVersion(
channel = resolveReleaseChannel(),
target = defaultTarget(),
) {
const channelVersion = await readManifestVersion(
updateManifestUrl(channel),
updateManifestUrl(channel, target),
'OSS 渠道清单',
);
if (channel !== 'dev-win') return channelVersion;
if (channel !== 'dev' || resolveReleasePlatform(target) !== 'windows')
return channelVersion;
const legacyVersion = await readManifestVersion(
legacyBridgeManifestUrl(),
'OSS 迁移指针',
@@ -310,14 +316,35 @@ function replaceVersionLine(source, version, pattern, label) {
return source.replace(pattern, `$1${version}$3`);
}
/**
* 版本来源固定为 OSS 总版本号(`agc/global-version.json`):
* - CI 统一构建由发号 Job 先发号,再通过 AGC_RELEASE_VERSION 透传给各渠道;
* - 未传入时(本地手工兜底)由本函数现场发号并写回总号;
* - 渠道高水位只做断言:传入号低于本渠道清单版本即失败关闭。
*/
export async function prepareReleaseVersion(context = resolveReleaseContext()) {
const { channel } = context;
const { channel, target } = context;
const localVersion = parseVersion(readPackageJson().version, '本地版本');
const remoteVersion = await resolveRemoteHighWaterVersion(channel);
const remoteVersion = await resolveRemoteHighWaterVersion(channel, target);
const requestedVersion = process.env.AGC_RELEASE_VERSION?.trim();
const nextVersion = requestedVersion
? parseVersion(requestedVersion, '指定版本')
: nextPatchVersion(localVersion, remoteVersion);
? assertRequestedVersionNotBelowChannel({
requested: requestedVersion,
channelVersion: remoteVersion,
channel,
})
: await issueAgcGlobalVersion({
channel,
commit:
process.env.COMMIT_HASH?.trim() ||
process.env.GIT_COMMIT?.trim() ||
null,
buildId:
process.env.BUILD_NUMBER?.trim() ||
process.env.AGC_BUILD_ID?.trim() ||
null,
repoVersion: localVersion,
});
const packageSource = fs.readFileSync(packageJsonPath, 'utf8');
fs.writeFileSync(
@@ -376,8 +403,8 @@ export async function prepareReleaseVersion(context = resolveReleaseContext()) {
console.log(
requestedVersion
? `[ai-game-creator-shell] 渠道 ${channel} 使用指定版本 ${nextVersion}(本${localVersion} / OSS ${remoteVersion ?? '不存在'}`
: `[ai-game-creator-shell] 渠道 ${channel} ${localVersion} / OSS ${remoteVersion ?? '不存在'} -> ${nextVersion}`,
? `[ai-game-creator-shell] 渠道 ${channel} 使用发号 Job 下发的总号 ${nextVersion}(本渠道清单 ${remoteVersion ?? '不存在'} / 仓库 ${localVersion}`
: `[ai-game-creator-shell] 渠道 ${channel}地兜底发号 ${nextVersion}(本渠道清单 ${remoteVersion ?? '不存在'} / 仓库 ${localVersion}`,
);
return nextVersion;
}
@@ -402,24 +429,27 @@ export function buildTauriBuildArguments(
}
/** 渠道端点必须由构建期注入:官方更新插件的端点配置不支持运行期改渠道。 */
export function createChannelConfig(channel = resolveReleaseChannel()) {
export function createChannelConfig(
channel = resolveReleaseChannel(),
target = defaultTarget(),
) {
return {
plugins: {
updater: {
endpoints: [updateManifestUrl(channel)],
endpoints: [updateManifestUrl(channel, target)],
},
},
};
}
function writeChannelConfigFile(channel) {
function writeChannelConfigFile(channel, target) {
const configPath = path.join(
os.tmpdir(),
`agc-tauri-channel-${channel}.json`,
`agc-tauri-channel-${channel}-${target}.json`,
);
fs.writeFileSync(
configPath,
`${JSON.stringify(createChannelConfig(channel), null, 2)}\n`,
`${JSON.stringify(createChannelConfig(channel, target), null, 2)}\n`,
);
return configPath;
}
@@ -436,8 +466,8 @@ export function runTauriBuild(
throw new Error('构建参数与发布上下文目标不一致');
}
const tauriArguments = buildTauriBuildArguments(args, context.target);
const { channel } = context;
const configPath = writeChannelConfigFile(channel);
const { channel, target } = context;
const configPath = writeChannelConfigFile(channel, target);
console.log(
`[ai-game-creator-shell] 渠道 ${channel} 端点配置:${configPath}`,
);
@@ -553,7 +583,7 @@ export function createUpdateManifest(
artifactPath,
{
target = defaultTarget(),
channel = resolveReleaseChannel(process.env, target),
channel = resolveReleaseChannel(),
publishedAt = new Date().toISOString(),
notes = readReleaseNotes(),
commit = readHeadCommit(),
@@ -561,7 +591,7 @@ export function createUpdateManifest(
} = {},
) {
validateReleaseTarget(target);
resolveReleaseChannel({ AGC_UPDATE_CHANNEL: channel }, target);
const partition = resolveReleasePartition(channel, target);
const signature = readUpdaterSignature(artifactPath);
const version = readPackageJson().version;
const firstInstallArtifact = selectFirstInstallArtifact(
@@ -569,8 +599,8 @@ export function createUpdateManifest(
{ target, version, artifact: artifactPath },
);
const fileName = path.basename(artifactPath);
const url = `${ossBaseUrl()}/${channel}/${encodeURIComponent(version)}/${encodeURIComponent(fileName)}`;
const downloadUrl = `${ossBaseUrl()}/${channel}/${encodeURIComponent(version)}/${encodeURIComponent(path.basename(firstInstallArtifact))}`;
const url = `${ossBaseUrl()}/${partition}/${encodeURIComponent(version)}/${encodeURIComponent(fileName)}`;
const downloadUrl = `${ossBaseUrl()}/${partition}/${encodeURIComponent(version)}/${encodeURIComponent(path.basename(firstInstallArtifact))}`;
const platforms = {};
const downloads = {};
for (const key of resolveManifestPlatformKeys(target)) {
@@ -703,14 +733,22 @@ export function formatRecentReleaseNotes(commits) {
/** 旧协议(sha256)清单:只用于把已发布客户端带到新渠道协议,一个版本周期后整条删除。 */
export function createLegacyUpdateManifest(
artifactPath,
{ channel = resolveReleaseChannel(), notes = readReleaseNotes() } = {},
{
channel = resolveReleaseChannel(),
target = defaultTarget(),
notes = readReleaseNotes(),
} = {},
) {
const partition = resolveReleasePartition(channel, target);
if (partition !== 'dev-win') {
throw new Error('旧协议迁移清单只属于 dev 渠道的 Windows 系统');
}
const bytes = fs.readFileSync(artifactPath);
const version = readPackageJson().version;
const fileName = path.basename(artifactPath);
return {
version,
downloadUrl: `${ossBaseUrl()}/${channel}/${encodeURIComponent(version)}/${encodeURIComponent(fileName)}`,
downloadUrl: `${ossBaseUrl()}/${partition}/${encodeURIComponent(version)}/${encodeURIComponent(fileName)}`,
sha256: createHash('sha256').update(bytes).digest('hex'),
size: bytes.length,
...(notes ? { releaseNotes: notes } : {}),
@@ -732,7 +770,9 @@ export async function generateUpdateManifest(
artifact,
});
const manualNotes = readReleaseNotes();
const previousCommit = await resolvePreviousReleaseCommit(channel);
const previousCommit = await resolvePreviousReleaseCommit(channel, {
target,
});
const commits = collectReleaseCommits(previousCommit);
const recentCommits = previousCommit ? null : collectRecentReleaseCommits();
const notes =
@@ -758,8 +798,8 @@ export async function generateUpdateManifest(
notes ? `${notes}\n` : '(本次没有可用的更新摘要)\n',
);
const legacyManifest =
channel === 'dev-win'
? createLegacyUpdateManifest(artifact, { channel, notes })
channel === 'dev' && resolveReleasePlatform(target) === 'windows'
? createLegacyUpdateManifest(artifact, { channel, target, notes })
: null;
const legacyManifestPath = legacyManifest
? path.join(bundleRoot, 'legacy-latest.json')
@@ -790,6 +830,7 @@ export async function generateUpdateManifest(
}
return {
channel,
target,
artifact,
downloadArtifact,
manifest,
@@ -30,6 +30,7 @@ import {
resolvePreviousReleaseCommit,
resolveReleaseChannel,
resolveReleaseContext,
resolveReleasePartition,
resolveRemoteHighWaterVersion,
runTauriBuild,
selectFirstInstallArtifact,
@@ -126,32 +127,60 @@ test('does not select unsupported files', () => {
);
});
test('resolves the channel from the target platform and rejects mismatches', () => {
assert.equal(resolveReleaseChannel({}, windowsTarget), 'dev-win');
assert.equal(resolveReleaseChannel({}, universalTarget), 'dev-mac');
test('channels are independent of platform and accept release and custom names', () => {
assert.equal(resolveReleaseChannel({}), 'dev');
for (const channel of ['dev', 'release', 'beta-2', 'a'.repeat(32)]) {
assert.equal(
resolveReleaseChannel({ AGC_UPDATE_CHANNEL: channel }),
channel,
);
assert.equal(
resolveReleasePartition(channel, windowsTarget),
`${channel}-win`,
);
assert.equal(
resolveReleasePartition(channel, 'aarch64-apple-darwin'),
`${channel}-mac`,
);
}
assert.equal(
resolveReleaseChannel({ AGC_UPDATE_CHANNEL: 'dev-mac' }, universalTarget),
resolveReleaseChannel({ AGC_UPDATE_CHANNEL: ' release ' }),
'release',
);
for (const channel of [
'',
' ',
'win',
'mac',
'windows',
'macos',
'darwin',
'linux',
'dev-win',
'dev-mac',
);
assert.throws(
() =>
resolveReleaseChannel({ AGC_UPDATE_CHANNEL: 'dev-mac' }, windowsTarget),
/只能用于 darwin 目标/u,
);
assert.throws(
() =>
resolveReleaseChannel({ AGC_UPDATE_CHANNEL: 'beta-win' }, windowsTarget),
/未知发布渠道/u,
);
'Release',
'../dev',
'a/b',
'a_b',
'-beta',
'beta-',
'1beta',
'a'.repeat(33),
]) {
assert.throws(
() => resolveReleaseChannel({ AGC_UPDATE_CHANNEL: channel }),
/发布渠道无效/u,
);
}
});
test('channel manifest URL and build-time endpoint follow the channel', () => {
withEnv({ AGC_UPDATE_OSS_BASE_URL: undefined }, () => {
assert.equal(
updateManifestUrl('dev-win'),
updateManifestUrl('dev', windowsTarget),
'https://agc-dev.oss-rg-china-mainland.aliyuncs.com/agc/dev-win/latest.json',
);
assert.deepEqual(createChannelConfig('dev-mac'), {
assert.deepEqual(createChannelConfig('dev', 'aarch64-apple-darwin'), {
plugins: {
updater: {
endpoints: [
@@ -160,6 +189,15 @@ test('channel manifest URL and build-time endpoint follow the channel', () => {
},
},
});
assert.equal(
updateManifestUrl('release', windowsTarget),
'https://agc-dev.oss-rg-china-mainland.aliyuncs.com/agc/release-win/latest.json',
);
assert.equal(
createChannelConfig('beta-2', 'x86_64-apple-darwin').plugins.updater
.endpoints[0],
'https://agc-dev.oss-rg-china-mainland.aliyuncs.com/agc/beta-2-mac/latest.json',
);
});
});
@@ -185,7 +223,7 @@ test('release context resolves explicit targets before environment/default and f
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.equal(context.channel, 'dev');
assert.match(
context.bundleRoot.replaceAll('\\', '/'),
/target\/aarch64-apple-darwin\/release\/bundle$/,
@@ -194,14 +232,14 @@ test('release context resolves explicit targets before environment/default and f
}
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',
'dev',
);
for (const args of [
['--target'],
@@ -231,7 +269,10 @@ test('explicit macOS target drives version lookup, Tauri endpoint, artifact and
prepareVersion: async (context) => {
seenContexts.push(context);
assert.equal(
await resolveRemoteHighWaterVersion(context.channel),
await resolveRemoteHighWaterVersion(
context.channel,
context.target,
),
'0.1.67',
);
},
@@ -406,7 +447,7 @@ test('manifest writer refuses to create latest when the current Mac DMG is missi
}
});
test('invalid target or mismatched channel fails before any release side effect', async () => {
test('invalid target or platform used as channel fails before any release side effect', async () => {
let touched = false;
const sideEffects = {
prepareVersion: () => {
@@ -426,7 +467,7 @@ test('invalid target or mismatched channel fails before any release side effect'
await withEnv({ AGC_UPDATE_CHANNEL: 'dev-win' }, () =>
assert.rejects(
() => buildRelease(['--target=aarch64-apple-darwin'], sideEffects),
/只能用于 windows/,
/发布渠道无效/,
),
);
assert.equal(touched, false);
@@ -440,7 +481,7 @@ test('Windows remains the default and explicit Windows overrides macOS environme
AGC_BUILD_TARGET: 'aarch64-apple-darwin',
}),
]) {
assert.equal(context.channel, 'dev-win');
assert.equal(context.channel, 'dev');
assert.equal(
selectReleaseArtifact(files, context.target),
'/tmp/windows.exe',
@@ -484,14 +525,14 @@ test('no-bundle smoke skips version writes and manifest generation', async () =>
steps.push('manifest');
},
});
assert.deepEqual(steps, ['dev-mac']);
assert.deepEqual(steps, ['dev']);
});
test('channel manifest carries version, platform keys and signature', () => {
withSignedArtifact('陶泥儿_0.1.48_x64-setup.exe', (artifact) => {
withEnv({ AGC_UPDATE_RELEASE_NOTES: '修复与改进' }, () => {
const manifest = createUpdateManifest(artifact, {
channel: 'dev-win',
channel: 'dev',
target: windowsTarget,
publishedAt: '2026-09-17T00:00:00.000Z',
});
@@ -522,7 +563,7 @@ test('missing signature fails the channel manifest closed', () => {
assert.throws(
() =>
createUpdateManifest(artifact, {
channel: 'dev-win',
channel: 'dev',
target: windowsTarget,
}),
/缺少更新包签名/u,
@@ -535,7 +576,7 @@ test('missing signature fails the channel manifest closed', () => {
test('legacy manifest keeps the sha256 contract of published clients', () => {
withSignedArtifact('陶泥儿_0.1.48_x64-setup.exe', (artifact) => {
const legacy = createLegacyUpdateManifest(artifact, {
channel: 'dev-win',
channel: 'dev',
});
assert.match(legacy.version, /^\d+\.\d+\.\d+$/u);
assert.equal(legacy.sha256.length, 64);
@@ -551,6 +592,86 @@ test('next release version follows the higher local or channel version', () => {
assert.equal(nextPatchVersion('0.1.12', null), '0.1.13');
});
for (const channel of ['release', 'beta-2']) {
for (const target of [windowsTarget, 'aarch64-apple-darwin']) {
test(`${channel} ${target} freezes its endpoint, version source and published objects`, async () => {
const root = mkdtempSync(path.join(os.tmpdir(), 'agc-channel-manifest-'));
try {
const windows = target === windowsTarget;
const partition = `${channel}-${windows ? 'win' : 'mac'}`;
const artifact = path.join(
root,
windows ? '陶泥儿_x64-setup.exe' : '陶泥儿.app.tar.gz',
);
writeFileSync(artifact, 'updater package');
writeFileSync(`${artifact}.sig`, 'updater signature');
if (!windows) createDmgFixture(root, target);
const context = {
...resolveReleaseContext([`--target=${target}`], {
AGC_UPDATE_CHANNEL: channel,
}),
bundleRoot: root,
};
const requests = [];
const result = await withStubbedFetch(
(url) => {
requests.push(url);
assert.ok(url.endsWith(`/agc/${partition}/latest.json`));
return jsonResponse({
version: '2.3.4',
commit: 'abcdef1234567890',
});
},
async () => {
assert.equal(
await resolveRemoteHighWaterVersion(
context.channel,
context.target,
),
'2.3.4',
);
runTauriBuild([`--target=${target}`], context, {
spawn: (_binary, command) => {
const config = JSON.parse(
readFileSync(
command[command.lastIndexOf('--config') + 1],
'utf8',
),
);
assert.ok(
config.plugins.updater.endpoints[0].endsWith(
`/agc/${partition}/latest.json`,
),
);
return { status: 0 };
},
});
return generateUpdateManifest(context);
},
);
assert.equal(result.channel, channel);
assert.equal(result.target, target);
assert.equal(result.manifest.version, packageVersion);
assert.equal(result.legacyManifestPath, null);
assert.equal(result.legacyManifest, null);
assert.equal(requests.length, 2);
for (const entry of [
...Object.values(result.manifest.platforms),
...Object.values(result.manifest.downloads),
]) {
assert.ok(entry.url.includes(`/agc/${partition}/${packageVersion}/`));
}
assert.throws(
() => createLegacyUpdateManifest(artifact, { channel, target }),
/只属于 dev 渠道/u,
);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
}
}
test('version high water keeps the legacy pointer during the migration window', async () => {
await withStubbedFetch(
(url) =>
@@ -558,7 +679,10 @@ test('version high water keeps the legacy pointer during the migration window',
? jsonResponse({}, 404)
: jsonResponse({ version: '0.1.57' }),
async () => {
assert.equal(await resolveRemoteHighWaterVersion('dev-win'), '0.1.57');
assert.equal(
await resolveRemoteHighWaterVersion('dev', windowsTarget),
'0.1.57',
);
// 旧指针 0.1.57 已是高水位,下一次发布必须是 0.1.58,不能退回渠道本地版本。
assert.equal(nextPatchVersion('0.1.47', '0.1.57'), '0.1.58');
},
@@ -572,7 +696,10 @@ test('version high water takes the higher of channel and legacy pointer', async
? jsonResponse({ version: '0.1.60' })
: jsonResponse({ version: '0.1.57' }),
async () => {
assert.equal(await resolveRemoteHighWaterVersion('dev-win'), '0.1.60');
assert.equal(
await resolveRemoteHighWaterVersion('dev', windowsTarget),
'0.1.60',
);
},
);
});
@@ -587,7 +714,16 @@ test('version high water ignores the windows migration pointer for other channel
return jsonResponse({ version: '0.1.12' });
},
async () => {
assert.equal(await resolveRemoteHighWaterVersion('dev-mac'), '0.1.12');
for (const [channel, target] of [
['dev', 'aarch64-apple-darwin'],
['release', windowsTarget],
['beta-2', windowsTarget],
]) {
assert.equal(
await resolveRemoteHighWaterVersion(channel, target),
'0.1.12',
);
}
},
);
});
@@ -597,20 +733,20 @@ test('release notes anchor prefers the explicit commit and falls back to the man
() => jsonResponse({ version: '0.1.61', commit: 'abcdef1234567890' }),
async () => {
assert.equal(
await resolvePreviousReleaseCommit('dev-win', {
await resolvePreviousReleaseCommit('dev', {
override: '6017d46088c04199e99cf89f347b12d67591475e',
}),
'6017d46088c04199e99cf89f347b12d67591475e',
);
// 覆盖值非法时忽略,继续用清单里的 commit。
assert.equal(
await resolvePreviousReleaseCommit('dev-win', {
await resolvePreviousReleaseCommit('dev', {
override: 'not-a-sha',
}),
'abcdef1234567890',
);
assert.equal(
await resolvePreviousReleaseCommit('dev-win', { override: ' ' }),
await resolvePreviousReleaseCommit('dev', { override: ' ' }),
'abcdef1234567890',
);
},
@@ -620,7 +756,7 @@ test('release notes anchor prefers the explicit commit and falls back to the man
() => jsonResponse({ version: '0.1.61' }),
async () => {
assert.equal(
await resolvePreviousReleaseCommit('dev-win', { override: undefined }),
await resolvePreviousReleaseCommit('dev', { override: undefined }),
null,
);
},
@@ -634,7 +770,7 @@ test('release notes anchor degrades to null when the manifest cannot be read', a
};
try {
assert.equal(
await resolvePreviousReleaseCommit('dev-win', { override: undefined }),
await resolvePreviousReleaseCommit('dev', { override: undefined }),
null,
);
} finally {
@@ -0,0 +1,121 @@
/**
* AGC 总版本号发号入口(CI 发号 Job 与本地手工兜底共用)。
*
* 用法:
* node scripts/issue-global-version.mjs --channel dev-win [--commit <sha>] [--build-id <id>] [--out <file>]
* node scripts/issue-global-version.mjs --seed-only
* node scripts/issue-global-version.mjs --dry-run --channel dev-win # 只预览,不写回、不烧号
*
* 输出固定为一行 `AGC_GLOBAL_VERSION=<version>`,便于 Jenkins 直接读取。
*/
import fs from 'node:fs';
import {
issueGlobalVersion,
previewNextGlobalVersion,
readGlobalVersion,
readReleaseDryRun,
resolveSeedBaseline,
writeGlobalVersion,
} from './agc-global-version.mjs';
function parseArgs(argv) {
const options = {
channel: '',
commit: '',
buildId: '',
repoVersion: '',
out: '',
seedOnly: false,
dryRun: readReleaseDryRun(),
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
const readValue = (label) => {
const value = argv[index + 1];
if (value == null || value.startsWith('--')) {
throw new Error(`${label} 缺少取值`);
}
index += 1;
return value;
};
switch (arg) {
case '--channel':
options.channel = readValue('--channel');
break;
case '--commit':
options.commit = readValue('--commit');
break;
case '--build-id':
options.buildId = readValue('--build-id');
break;
case '--repo-version':
options.repoVersion = readValue('--repo-version');
break;
case '--out':
options.out = readValue('--out');
break;
case '--seed-only':
options.seedOnly = true;
break;
case '--dry-run':
options.dryRun = true;
break;
default:
throw new Error(`未知参数:${arg}`);
}
}
return options;
}
function emit(version, options) {
console.log(`AGC_GLOBAL_VERSION=${version}`);
if (options.out) {
fs.writeFileSync(options.out, `${version}\n`, 'utf8');
}
}
const options = parseArgs(process.argv.slice(2));
process.env.AGC_RELEASE_DRY_RUN = options.dryRun ? '1' : '0';
if (options.seedOnly) {
const current = await readGlobalVersion();
if (current) {
console.log(
`[agc-global-version] 总号已存在(${current.version}),播种跳过;需要重新播种请先人工确认`,
);
emit(current.version, options);
} else {
const baseline = await resolveSeedBaseline({
repoVersion: options.repoVersion || null,
});
const payload = {
version: baseline,
updatedAt: new Date().toISOString(),
channel: options.channel || 'seed',
commit: options.commit || null,
buildId: options.buildId || null,
};
writeGlobalVersion(payload, { dryRun: options.dryRun });
console.log(
`[agc-global-version] 播种基线 ${baseline}(尚未发号,首发为下一位)`,
);
emit(baseline, options);
}
} else if (options.dryRun) {
const preview = await previewNextGlobalVersion({
repoVersion: options.repoVersion || null,
});
console.log(
`[agc-global-version] 预览下一个总号 ${preview}dry-run 不写回、不烧号)`,
);
emit(preview, options);
} else {
const issued = await issueGlobalVersion({
channel: options.channel || 'manual',
commit: options.commit || null,
buildId: options.buildId || null,
repoVersion: options.repoVersion || null,
});
emit(issued, options);
}
@@ -1,6 +1,8 @@
import { spawnSync } from 'node:child_process';
import path from 'node:path';
import { resolveReleasePartition } from './build-release.mjs';
/**
* 发布上传的 OSS 命令行整理:把 ossutil 参数与凭据整理成可执行或可打印的形式,
* 便于在 dry-run 下核对将要执行的上传,同时保证任何输出都不回显凭据明文。
@@ -34,16 +36,28 @@ export function createReleaseUploadPlan(
artifact,
downloadArtifact,
channel,
target,
manifest,
manifestPath,
legacyManifestPath,
},
bucket,
) {
if (!artifact || !downloadArtifact || !manifestPath || !manifest?.version) {
throw new Error('发布结果缺少更新包、首装包或清单');
if (
!artifact ||
!downloadArtifact ||
!manifestPath ||
!manifest?.version ||
!channel ||
!target
) {
throw new Error('发布结果缺少渠道、构建目标、更新包、首装包或清单');
}
const prefix = `oss://${bucket}/agc/${channel}`;
const partition = resolveReleasePartition(channel, target);
if (legacyManifestPath && partition !== 'dev-win') {
throw new Error('旧协议迁移清单只属于 dev 渠道的 Windows 系统');
}
const prefix = `oss://${bucket}/agc/${partition}`;
const artifacts = [
...new Set(
[artifact, `${artifact}.sig`, downloadArtifact].map((file) =>
@@ -40,22 +40,24 @@ test('printed upload command keeps arguments and hides credentials', () => {
);
});
function withReleaseFixture(channel, architecture, run) {
function withReleaseFixture(channel, architecture, run, platform = 'macos') {
const root = mkdtempSync(path.join(os.tmpdir(), 'agc-upload-plan-'));
try {
const artifact = path.join(
root,
channel === 'dev-win'
platform === 'windows'
? '陶泥儿_1.2.3_x64-setup.exe'
: '陶泥儿.app.tar.gz',
);
const downloadArtifact =
channel === 'dev-win'
platform === 'windows'
? artifact
: path.join(root, `陶泥儿_1.2.3_${architecture}.dmg`);
const manifestPath = path.join(root, 'latest.json');
const legacyManifestPath =
channel === 'dev-win' ? path.join(root, 'legacy-latest.json') : null;
channel === 'dev' && platform === 'windows'
? path.join(root, 'legacy-latest.json')
: null;
for (const file of [
artifact,
`${artifact}.sig`,
@@ -69,6 +71,10 @@ function withReleaseFixture(channel, architecture, run) {
artifact,
downloadArtifact,
channel,
target:
platform === 'windows'
? 'x86_64-pc-windows-msvc'
: `${architecture === 'aarch64' ? 'aarch64' : 'x86_64'}-apple-darwin`,
manifest: { version: '1.2.3' },
manifestPath,
legacyManifestPath,
@@ -86,7 +92,7 @@ const uploadOptions = {
for (const architecture of ['aarch64', 'x64']) {
test(`uploads every ${architecture} Mac object before the channel pointer`, () => {
withReleaseFixture('dev-mac', architecture, (release) => {
withReleaseFixture('dev', architecture, (release) => {
const calls = [];
uploadReleaseArtifacts(release, {
...uploadOptions,
@@ -120,37 +126,42 @@ for (const architecture of ['aarch64', 'x64']) {
}
test('Windows uploads the shared installer once and publishes migration metadata last', () => {
withReleaseFixture('dev-win', 'x64', (release) => {
const plan = createReleaseUploadPlan(release, 'agc-dev');
assert.deepEqual(
plan.map(({ source }) => source),
[
release.artifact,
`${release.artifact}.sig`,
release.manifestPath,
release.legacyManifestPath,
],
);
assert.equal(plan.at(-1).destination, 'oss://agc-dev/agc/latest.json');
const calls = [];
uploadReleaseArtifacts(release, {
...uploadOptions,
spawn: (_binary, args) => {
assert.deepEqual(args.slice(0, 2), ['cp', '--force']);
calls.push(args[3]);
return { status: 0 };
},
});
assert.deepEqual(
calls,
plan.map(({ destination }) => destination),
);
});
withReleaseFixture(
'dev',
'x64',
(release) => {
const plan = createReleaseUploadPlan(release, 'agc-dev');
assert.deepEqual(
plan.map(({ source }) => source),
[
release.artifact,
`${release.artifact}.sig`,
release.manifestPath,
release.legacyManifestPath,
],
);
assert.equal(plan.at(-1).destination, 'oss://agc-dev/agc/latest.json');
const calls = [];
uploadReleaseArtifacts(release, {
...uploadOptions,
spawn: (_binary, args) => {
assert.deepEqual(args.slice(0, 2), ['cp', '--force']);
calls.push(args[3]);
return { status: 0 };
},
});
assert.deepEqual(
calls,
plan.map(({ destination }) => destination),
);
},
'windows',
);
});
for (const failedArtifactIndex of [0, 1, 2]) {
test(`failed Mac object ${failedArtifactIndex} prevents both later objects and latest publication`, () => {
withReleaseFixture('dev-mac', 'aarch64', (release) => {
withReleaseFixture('dev', 'aarch64', (release) => {
const destinations = [];
assert.throws(
() =>
@@ -176,7 +187,7 @@ for (const failedArtifactIndex of [0, 1, 2]) {
}
test('dry run prints the complete plan without spawning uploads or exposing credentials', () => {
withReleaseFixture('dev-mac', 'aarch64', (release) => {
withReleaseFixture('dev', 'aarch64', (release) => {
const output = [];
uploadReleaseArtifacts(release, {
...uploadOptions,
@@ -195,3 +206,32 @@ test('dry run prints the complete plan without spawning uploads or exposing cred
assert.doesNotMatch(output.join('\n'), /fixture-id|fixture-secret|已上传/u);
});
});
for (const channel of ['release', 'beta-2']) {
for (const platform of ['windows', 'macos']) {
test(`${channel} ${platform} uploads only its own partition and cannot write the dev bridge`, () => {
withReleaseFixture(
channel,
'x64',
(release) => {
const plan = createReleaseUploadPlan(release, 'agc-dev');
const suffix = platform === 'windows' ? 'win' : 'mac';
const prefix = `oss://agc-dev/agc/${channel}-${suffix}/`;
assert.ok(
plan.every(({ destination }) => destination.startsWith(prefix)),
);
assert.equal(plan.at(-1).destination, `${prefix}latest.json`);
assert.throws(
() =>
createReleaseUploadPlan(
{ ...release, legacyManifestPath: release.manifestPath },
'agc-dev',
),
/只属于 dev 渠道/u,
);
},
platform,
);
});
}
}
@@ -312,34 +312,15 @@ function runShard(executable, shardIndex, shardCount, shardTestNames) {
},
);
const failureLines = [];
let inFailureList = false;
// Rust 的首个 failures: 后有空行,不能按空行结束采集,否则会丢掉 panic 详情。
// 只保存有界尾部;成功时不输出,失败时优先输出完整失败段。
let stdoutTail = '';
let stderr = '';
const consumeLine = (rawLine) => {
const line = rawLine.replace(/\r$/, '');
if (line.includes('failures:')) {
inFailureList = true;
return;
}
if (inFailureList) {
if (line.trim().length === 0) {
inFailureList = false;
return;
}
failureLines.push(line.trim());
}
};
child.stdout.setEncoding('utf8');
child.stderr.setEncoding('utf8');
let stdoutBuffer = '';
child.stdout.on('data', (chunk) => {
stdoutBuffer += chunk;
const lines = stdoutBuffer.split('\n');
stdoutBuffer = lines.pop() ?? '';
for (const line of lines) {
consumeLine(line);
}
stdoutTail = (stdoutTail + chunk).slice(-64_000);
});
child.stderr.on('data', (chunk) => {
stderr += chunk;
@@ -356,12 +337,17 @@ function runShard(executable, shardIndex, shardCount, shardTestNames) {
});
});
child.on('close', (code) => {
const output = stdoutTail.replace(/\r\n/g, '\n');
const failureStart = output.indexOf('failures:\n');
resolve({
label,
ok: code === 0,
durationMs: Date.now() - startedAt,
testCount: shardTestNames.length,
failures: failureLines,
failures: output
.slice(failureStart < 0 ? 0 : failureStart)
.trim()
.split('\n'),
stderr,
});
});
@@ -438,7 +424,7 @@ async function main() {
}
failed = true;
console.error(
`[rust-shards] ${result.label} FAILED: ${result.testCount} test(s) in ${formatDuration(result.durationMs)}`,
`[rust-shards] ${result.label} FAILED (selected ${result.testCount} test(s)) in ${formatDuration(result.durationMs)}`,
);
for (const failure of result.failures) {
console.error(`[rust-shards] ${failure}`);
@@ -0,0 +1,70 @@
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
const runner = fileURLToPath(
new URL('./run-rust-shell-test-shards.mjs', import.meta.url),
);
function runFixture(t, source) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agc-shard-output-'));
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
fs.mkdirSync(path.join(root, 'src'));
fs.writeFileSync(
path.join(root, 'Cargo.toml'),
'[package]\nname = "shard-output-fixture"\nversion = "0.1.0"\nedition = "2021"\n',
);
fs.writeFileSync(path.join(root, 'src/lib.rs'), source);
const result = spawnSync(
process.execPath,
[
runner,
`--manifest=${path.join(root, 'Cargo.toml')}`,
'--target-kind=lib',
'--no-locked',
'--shards=1',
`--shard-tmp-root=${path.join(root, 'tmp')}`,
],
{
encoding: 'utf8',
timeout: 60_000,
windowsHide: true,
env: { ...process.env, CARGO_TARGET_DIR: path.join(root, 'target') },
},
);
assert.ifError(result.error);
return { status: result.status, output: result.stdout + result.stderr };
}
test('failed shard retains panic details and separates selected count from failures', (t) => {
const result = runFixture(
t,
`
#[test]
fn passing_case() {}
#[test]
fn failing_case() {
assert_eq!(1, 2, "shard panic evidence");
}
`,
);
assert.equal(result.status, 1, result.output);
assert.match(result.output, /FAILED \(selected 2 test\(s\)\)/);
assert.match(result.output, /failing_case/);
assert.match(result.output, /panicked at src[\\/]lib\.rs:/);
assert.match(result.output, /shard panic evidence/);
assert.match(result.output, /left: 1/);
assert.match(result.output, /right: 2/);
assert.match(result.output, /1 passed; 1 failed/);
});
test('successful shard keeps its compact summary', (t) => {
const result = runFixture(t, '#[test]\nfn passing_case() {}\n');
assert.equal(result.status, 0, result.output);
assert.match(result.output, /shard 1\/1 ok: 1 test\(s\)/);
assert.doesNotMatch(result.output, /test passing_case \.\.\. ok/);
});
@@ -1192,7 +1192,7 @@ async function main() {
function isDirectModuleExecution() {
return Boolean(
process.argv[1] &&
resolve(process.argv[1]) === fileURLToPath(import.meta.url),
resolve(process.argv[1]) === fileURLToPath(import.meta.url),
);
}
@@ -26,6 +26,8 @@ struct PromptBundleManifest {
version: String,
#[serde(deserialize_with = "deserialize_unique_string_map")]
sections: BTreeMap<String, String>,
#[serde(default, deserialize_with = "deserialize_unique_string_map")]
text_catalogs: BTreeMap<String, String>,
compositions: PromptCompositions,
variants: PromptVariants,
#[serde(default)]
@@ -121,6 +123,11 @@ struct AgentRole {
brief_path_name: String,
}
#[derive(Deserialize)]
struct TextCatalog(
#[serde(deserialize_with = "deserialize_unique_string_map")] BTreeMap<String, String>,
);
fn deserialize_unique_string_map<'de, D>(
deserializer: D,
) -> Result<BTreeMap<String, String>, D::Error>
@@ -212,6 +219,13 @@ pub fn compile_manifest(manifest_path: &Path) -> Result<CompiledPromptBundle, St
dependencies.insert(path);
sections.insert(section_id.clone(), content);
}
let texts = read_text_catalogs(
&manifest.text_catalogs,
base,
&canonical_base,
&mut section_paths,
&mut dependencies,
)?;
validate_registered_markdown_files(base, &section_paths, &mut dependencies)?;
validate_composition(
@@ -365,7 +379,8 @@ pub fn compile_manifest(manifest_path: &Path) -> Result<CompiledPromptBundle, St
));
}
}
let rust_source = render_rust(&manifest, &sections);
let mut rust_source = render_rust(&manifest, &sections);
rust_source.push_str(&render_text_macro(&texts));
let specialist_nodes = manifest
.agent_catalog
.groups
@@ -554,9 +569,13 @@ fn validate_section_reference(
}
fn validate_section_path(path: &str) -> Result<(), String> {
validate_prompt_path(path, ".md")
}
fn validate_prompt_path(path: &str, extension: &str) -> Result<(), String> {
if path.is_empty()
|| path.contains('\\')
|| !path.ends_with(".md")
|| !path.ends_with(extension)
|| path.split('/').any(|segment| {
segment.is_empty()
|| !segment.chars().all(|character| {
@@ -577,6 +596,61 @@ fn validate_section_path(path: &str) -> Result<(), String> {
Ok(())
}
fn read_text_catalogs(
catalogs: &BTreeMap<String, String>,
base: &Path,
canonical_base: &Path,
registered_paths: &mut BTreeSet<String>,
dependencies: &mut BTreeSet<PathBuf>,
) -> Result<BTreeMap<String, String>, String> {
let mut texts = BTreeMap::new();
for (catalog_id, relative_path) in catalogs {
validate_identifier(catalog_id, "text catalog id", false)?;
validate_prompt_path(relative_path, ".json")?;
if relative_path == "manifest.json"
|| !registered_paths.insert(portable_section_path_key(relative_path))
{
return Err(format!("Prompt 文本目录路径重复:{relative_path}"));
}
validate_no_symlink_components(base, relative_path)?;
let path = base.join(relative_path);
let canonical_path = fs::canonicalize(&path)
.map_err(|error| format!("解析 Prompt 文本目录路径失败 {relative_path}{error}"))?;
if !canonical_path.starts_with(canonical_base) || !path.is_file() {
return Err(format!(
"Prompt 文本目录必须是 Bundle 内的普通文件:{relative_path}"
));
}
let content = fs::read_to_string(&path)
.map_err(|error| format!("读取 Prompt 文本目录失败 {relative_path}{error}"))?;
let TextCatalog(entries) = serde_json::from_str(&content)
.map_err(|error| format!("解析 Prompt 文本目录失败 {relative_path}{error}"))?;
if entries.is_empty() {
return Err(format!("Prompt 文本目录不能为空:{catalog_id}"));
}
for (key, text) in entries {
validate_identifier(&key, "text key", true)?;
validate_nonempty(&text, "prompt text")?;
texts.insert(format!("{catalog_id}.{key}"), text);
}
dependencies.insert(path);
}
Ok(texts)
}
fn render_text_macro(texts: &BTreeMap<String, String>) -> String {
let mut output = String::from("\n#[allow(unused_macros)]\nmacro_rules! prompt_text {\n");
for (key, text) in texts {
output.push_str(&format!(
" ({}) => {{ {} }};\n",
rust_literal(key),
rust_literal(text),
));
}
output.push_str("}\n");
output
}
fn portable_section_path_key(path: &str) -> String {
path.to_ascii_lowercase()
}
@@ -590,9 +664,12 @@ fn validate_registered_markdown_files(
collect_markdown_files(base, base, &mut discovered_paths, dependencies)?;
for path in discovered_paths {
if !registered_paths.contains(&path) {
return Err(format!(
"Prompt Bundle 存在未登记的 Markdown section{path}"
));
let kind = if path.ends_with(".json") {
"文本目录"
} else {
"Markdown section"
};
return Err(format!("Prompt Bundle 存在未登记的 {kind}{path}"));
}
}
Ok(())
@@ -626,7 +703,10 @@ fn collect_markdown_files(
|| !path
.extension()
.and_then(|extension| extension.to_str())
.is_some_and(|extension| extension.eq_ignore_ascii_case("md"))
.is_some_and(|extension| {
extension.eq_ignore_ascii_case("md") || extension.eq_ignore_ascii_case("json")
})
|| path == base.join("manifest.json")
{
continue;
}
@@ -647,7 +727,12 @@ fn collect_markdown_files(
})
.collect::<Result<Vec<_>, _>>()?
.join("/");
validate_section_path(&relative)?;
let extension = if relative.ends_with(".json") {
".json"
} else {
".md"
};
validate_prompt_path(&relative, extension)?;
discovered_paths.insert(portable_section_path_key(&relative));
}
Ok(())
@@ -2,5 +2,5 @@
- project/analysis.md
- project/决策台账.md
- project/dialog.md
不要把正式产物写在工作区根目录,也不要等审批失败后再迁移
阶段审批工具:当你判断阶段必需产物已完成时,必须提交阶段审批。用户批准后 Runtime 自动进入下一阶段;你不能自行切换阶段
正式产物使用当前阶段指定的相对路径
五个策划阶段审批:当你判断当前策划阶段必需产物已完成时,必须提交阶段审批。用户批准后 Runtime 自动进入下一阶段。
@@ -1,4 +1,3 @@
顾问阶段不需要继续自主推项目或主动安排下一步;遵照用户的具体指示行动
顾问阶段遵照用户的具体指示行动,不自主推项目或主动安排下一步,不提交阶段审批
根据用户指示回答问题、读取相关文档、修改工作区文件,并说明改动可能影响的已有产物。
涉及方向性变化或多个可行方案时,先向用户说明影响并等待用户决定;不要替用户做决定
顾问阶段没有下一层,也不需要提交阶段审批。
涉及方向性变化或多个可行方案时,先向用户说明影响并等待用户决定。
@@ -1,4 +1,4 @@
概念阶段定稿时,创建或更新 `project/速览卡.md`。下面是速览卡的参考结构;根据游戏类型、项目规模和用户要求选择字段,同类内容可以合并,项目不需要的字段可以省略,复杂项目可以增加必要字段。表格和列表中的示例行按实际对象逐行扩展,不代表数量上限
概念阶段定稿时,创建或更新 `project/速览卡.md`。下面是速览卡的参考结构;根据游戏类型、项目规模和用户要求选择适用字段,同类内容可以合并,复杂项目可以增加必要字段。表格和列表中的示例行按实际对象逐行扩展:
# 速览卡:《游戏名》
File diff suppressed because one or more lines are too long
@@ -3,7 +3,7 @@
版本:v3 | 规则:台账放活队列——design 只放结论、分析只放论证、决定与开放问题住这里。编号连续不复用;被推翻的行标 overturned 挂新行,不删行。
状态六态:`confirmed`(用户亲口/亲选)/ `auto_decided`(技术类代决,必带理由+推翻条件,用户一键可翻)/ `default_pending`(默认建议兜底,用户未点头)/ `prototype_pending`(待原型验证)/ `pending_user`(等用户拍板)/ `overturned`(被推翻,挂旧行编号)。
> 编号口径:D-01~D-13 与 exemplars/stardew-analysis.md 台账节选一致(D-04~D-06、D-08~D-10、D-12 原为"就地小权衡,直接登记未开条目",此处按登记口径展开);D-14 起为技术文档期新增,与 stardew-tdd-tech.md 开放问题回执互引。
> 编号口径:D-01~D-13 与 templates/stardew-analysis.md 台账节选一致(D-04~D-06、D-08~D-10、D-12 原为"就地小权衡,直接登记未开条目",此处按登记口径展开);D-14 起为技术文档期新增,与 stardew-tdd-tech.md 开放问题回执互引。
## 当前待办(活队列)
@@ -18,7 +18,7 @@
- 规划掌控:时间、体力、季节和资金构成可理解的取舍,提前准备会让未来更高效。
- 探索成长:探索区域、战斗和资源发现提供变化与风险,并将成果转化为农场与角色的长期改善。
三者的关系不是并列小游戏,而是互相供给:农场提供稳定资源与恢复空间,探索提供稀有资源和发现,社交与社区目标提供方向和情感回报。
三者互相供给:农场提供稳定资源与恢复空间,探索提供稀有资源和发现,社交与社区目标提供方向和情感回报。
## 核心推动力
玩家每天拥有有限的时间与体力,但可以在一天结束后保留成果,并把收益投入到工具、设施、种子、装备和关系中。短期的"今天做什么"决策,持续转化为长期的"我的生活变得怎样"。
@@ -5,7 +5,7 @@ name: game-gdd-architecture
description: 写游戏策划案(GDD)系统架构时使用。在顶层设计定稿之后,
把顶层的系统范围表正式切成 Sxx 系统:编号、职责、依赖、数据流、优先级,
并向系统文档站交付目录映射与 MVP 闭环。配套:templates/architecture.md、
templates/analysis.md(全局一份)、exemplars/stardew-architecture.md、exemplars/stardew-analysis.md(全局一份)。
templates/analysis.md(全局一份)、exemplars/stardew-architecture.md、templates/stardew-analysis.md(全局一份)。
---
# 系统架构写法(策划 agent · 系统架构分册)
@@ -15,11 +15,11 @@ description: 写游戏策划案(GDD)系统架构时使用。在顶层设计
## 〇、结构适配原则
本分册的章节、字段和数量是参考结构,不是固定清单。先根据游戏类型、项目规模、用户要求和顶层设计判断适用项:适用项写入,同类项可合并,若某项对本项目没意义则省略;复杂项目可以拆分补充,简单项目可以压缩为最小可用架构。
根据游戏类型、项目规模、用户要求和顶层设计选取本分册的适用内容,同类项可合并;复杂项目可以拆分补充,简单项目可以压缩为最小可用架构。
## 一、这一层的判断立场
你是架构师,切系统的刀在你手里。在这个层里你相信:
- 切分是为了**职责清晰、可独立讨论**,不是为了凑数量。只有确实需要独立职责、状态或数据边界的部分才拆成系统;每个实际拆出的系统应能说明删除后的影响。
- 为需要独立职责、状态或数据边界的部分拆分系统,使其**职责清晰、可独立讨论**;每个实际拆出的系统应能说明删除后的影响。
- **数据所有权唯一**:同一事实只由一个系统维护,其他系统只引用稳定 ID,
不复制主数据。两个系统管同一件事 = 架构事故。
- **依赖无环**是硬要求;信息呈现层只读状态、只经行动入口写入。
@@ -139,11 +139,10 @@ P1/P2 可用能力表(能力/说明)控制颗粒度。
## 五、分析文档(全局一份,按层分节)
**全局唯一一份《分析.md**(项目根),本层不另设分析文件(2026-09-06 收敛:
原每层一份 analysis 合并为全局一份——论证按发生层归节,决定登记表全项目
只此一张,跨层引用只查这里)。模板与例子:资源 `templates/analysis.md`
`exemplars/stardew-analysis.md`。状态池(灵感池/代决/待原型等活队列)在决策台账
不放分析文档——本文件只放已决论证与登记。
**全局共用 `project/analysis.md`**:论证按发生层归节,决定登记表全项目共用,
跨层引用查这里。模板与例子:资源 `templates/analysis.md`
`templates/stardew-analysis.md`。已决论证与登记写入分析文档,
灵感池代决待原型等活队列写入决策台账
- 条目格式:`## 问题:<一句话>` 状态(agent_proposal / user_confirmed /
superseded,登记 D-__)+ 广度分析(牵动面+候选 ≥2)+ 深度分析
@@ -158,7 +157,7 @@ P1/P2 可用能力表(能力/说明)控制颗粒度。
推翻时新增行挂旧行编号,旧行不删。
## 六、写完自查参考,不是闸门)
## 六、自查参考
- 每个 Sxx 都能一句话答"删了它什么塌"吗?
- 顶层的循环环节全覆盖、无重复认领吗?
- 依赖图无环?主数据无一物两管?
@@ -5,7 +5,7 @@ name: game-gdd-concept
description: 写游戏策划案(GDD)概念层时使用。把一句话游戏想法写成一份
"一次写对、之后不动"的立项概念文档——它是后续所有设计争议的仲裁依据。
任何游戏类型通用。配套:templates/concept-design.md、templates/analysis.md(全局一份)、
exemplars/stardew-concept.md、exemplars/stardew-analysis.md(全局一份)。
exemplars/stardew-concept.md、templates/stardew-analysis.md(全局一份)。
---
# 概念层写法(策划 agent · 概念层分册)
@@ -15,7 +15,7 @@ description: 写游戏策划案(GDD)概念层时使用。把一句话游戏
## 〇、结构适配原则
本分册的章节、字段和数量是参考结构,不是固定清单。先根据游戏类型、项目规模、用户要求和上层已定范围判断适用项:适用项写入,同类项可合并,若某项对本项目没意义则省略;复杂项目可以拆分补充,简单项目可以压缩为最小可用规格。
根据游戏类型、项目规模、用户要求和上层已定范围选取本分册的适用内容,同类项可合并;复杂项目可以拆分补充,简单项目可以压缩为最小可用规格。
## 一、这一层的判断立场
你是资深游戏策划,看过上千份概念案,清楚绝大多数死在"什么都说、什么都不尖"。
@@ -133,11 +133,10 @@ description: 写游戏策划案(GDD)概念层时使用。把一句话游戏
## 五、分析文档(全局一份,按层分节)
**全局唯一一份《分析.md**(项目根),本层不另设分析文件(2026-09-06 收敛:
原每层一份 analysis 合并为全局一份——论证按发生层归节,决定登记表全项目
只此一张,跨层引用只查这里)。模板与例子:资源 `templates/analysis.md`
`exemplars/stardew-analysis.md`。状态池(灵感池/代决/待原型等活队列)在决策台账
不放分析文档——本文件只放已决论证与登记。
**全局共用 `project/analysis.md`**:论证按发生层归节,决定登记表全项目共用,
跨层引用查这里。模板与例子:资源 `templates/analysis.md`
`templates/stardew-analysis.md`。已决论证与登记写入分析文档,
灵感池代决待原型等活队列写入决策台账
- 条目格式:`## 问题:<一句话>` 状态(agent_proposal / user_confirmed /
superseded,登记 D-__)+ 广度分析(牵动面+候选 ≥2)+ 深度分析
@@ -152,7 +151,7 @@ description: 写游戏策划案(GDD)概念层时使用。把一句话游戏
推翻时新增行挂旧行编号,旧行不删。
## 六、写完自查参考,不是闸门)
## 六、自查参考
- 卖点唯一吗?念头句立得住吗?
- 随便挑一个后续设计问题,锚点六项之一能当裁判吗?
- "不是什么"表封死了最可能的误会方向吗?
@@ -14,7 +14,7 @@ description: 写单个系统的设计文档(Sxx)时的总纲——通用纪
## 〇、结构适配原则
本分册的章节、字段和数量是参考结构,不是固定清单。先根据系统类型、实际复杂度、用户要求和架构职责判断适用项:适用项写入,同类项可合并,若某项对本系统没意义则省略;复杂系统可以拆分补充,简单系统可以压缩为最小可执行规格。
根据系统类型、实际复杂度、用户要求和架构职责选取本分册的适用内容,同类项可合并;复杂系统可以拆分补充,简单系统可以压缩为最小可执行规格。
## 一、这一层的判断立场
你是写单个系统的策划。在这个层里你相信:
@@ -74,11 +74,10 @@ description: 写单个系统的设计文档(Sxx)时的总纲——通用纪
## 五、分析文档(全局一份,按层分节)
**全局唯一一份《分析.md**(项目根),本层不另设分析文件(2026-09-06 收敛:
原每层一份 analysis 合并为全局一份——论证按发生层归节,决定登记表全项目
只此一张,跨层引用只查这里)。模板与例子:资源 `templates/analysis.md`
`exemplars/stardew-analysis.md`。状态池(灵感池/代决/待原型等活队列)在决策台账
不放分析文档——本文件只放已决论证与登记。
**全局共用 `project/analysis.md`**:论证按发生层归节,决定登记表全项目共用,
跨层引用查这里。模板与例子:资源 `templates/analysis.md`
`templates/stardew-analysis.md`。已决论证与登记写入分析文档,
灵感池代决待原型等活队列写入决策台账
- 条目格式:`## 问题:<一句话>` 状态(agent_proposal / user_confirmed /
superseded,登记 D-__)+ 广度分析(牵动面+候选 ≥2)+ 深度分析
@@ -93,7 +92,7 @@ description: 写单个系统的设计文档(Sxx)时的总纲——通用纪
推翻时新增行挂旧行编号,旧行不删。
## 六、写完自查参考,不是闸门)
## 六、自查参考
- 目的一句话成立吗?边界节和架构职责表逐行对齐吗?
- 输入输出和依赖图逐边对上吗?有没有泛称漏网?
- 状态是枚举还是散文?失败路径给了原因和恢复吗?

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