Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bd94db73c9 |
@@ -5,7 +5,6 @@
|
||||
## 目录约定
|
||||
|
||||
- `.codex/skills/` 是项目专属 skill 根目录。每个 skill 以目录中的 `SKILL.md` 为入口,配套的参考资料和脚本放在同一目录下。
|
||||
- `spec-driven-development` 负责跨模块、公开契约、SpacetimeDB、AGC/Runtime 和复杂 UI 任务的规范先行与逐里程碑验收;具体规则以 `docs/` 下的 SDD 工作流和模板为准。
|
||||
- `.codex/plugins/` 保存随仓库分发的项目插件资源及其参考资料。当前的 `game-studio` 插件提供浏览器游戏设计、原型、2D/3D 技术栈、素材管线和 playtest 工作流;是否启用遵循当前 Codex 的插件加载机制,不依赖旧工具的环境变量或个人配置脚本。
|
||||
- `.codex/hooks/`、`.codex/environments/` 等目录保存项目工具链所需的 hooks 和环境模板;它们不替代项目代码中的运行时配置。
|
||||
- 长期有效的产品、架构、接口、排障和协作知识统一放在 `docs/` 与 `docs/project-memory/`,不复制到本目录。
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
---
|
||||
name: spec-driven-development
|
||||
description: 在 Genarrative 中处理跨模块功能、公开 API/DTO、SpacetimeDB schema、AGC/Runtime 或复杂 UI 状态链路时,按主规范、里程碑规范、单里程碑实现计划和验收证据推进;小型局部修复不触发。
|
||||
license: MIT
|
||||
metadata:
|
||||
codex:
|
||||
tags: [SDD, 规范驱动开发, 主规范, 里程碑, 验收]
|
||||
---
|
||||
|
||||
# SDD 规范驱动开发
|
||||
|
||||
本 skill 把 Genarrative 的复杂任务路由到规范先行、单里程碑交付的工作流。权威规则和模板分别见:
|
||||
|
||||
- `docs/【协作规范】规范驱动开发工作流-2026-09-12.md`
|
||||
- `docs/project-memory/shared-memory/【模板】规范驱动开发主规范与里程碑模板-2026-09-12.md`
|
||||
|
||||
## 触发条件
|
||||
|
||||
使用本 skill 的任务包括:
|
||||
|
||||
- 跨前端、`api-server`、`platform-*`、`spacetime-client`、`spacetime-module` 或 `module-*` 的功能。
|
||||
- `/api/external/v1`、共享 DTO、OpenAPI、SpacetimeDB schema、迁移或跨版本重放合同变化。
|
||||
- AGC / DirectProject、Agent Runtime、工具白名单、审批、持久化恢复或资源工作流变化。
|
||||
- 复杂 UI 状态链路、入口/页面生命周期变化,或需要多个独立验收面的功能。
|
||||
|
||||
文案、单文件局部修复、无行为变化重构和一次性诊断继续使用轻量流程;执行中一旦触及公开行为或跨模块合同,切换到本 skill。
|
||||
|
||||
## 执行要求
|
||||
|
||||
1. 检查工作树,读取当前专题、源码、测试、契约和历史决策;写清交付结果、验收判据和不做项。
|
||||
2. 找到或更新唯一主规范。主规范描述行为合同、边界、非目标、兼容/迁移和证据,不绑定实现细节。
|
||||
3. 在 `docs/project-memory/plans/` 创建一个或多个 `【里程碑】中文标题-YYYY-MM-DD.md`,填写 `Version`、`Status`、`Date`、`Parent Spec`、范围、依赖和验收标准;评审通过前不写业务代码。
|
||||
4. 每次只选择一个已评审里程碑,创建对应的 `【实施计划】中文标题-YYYY-MM-DD.md`,明确修改边界、顺序、验证命令、风险和回滚点。
|
||||
5. 发现行为需要变化时,严格按 `主规范 → 尚未实现的里程碑规范 → 当前实现计划 → 代码与测试` 更新。
|
||||
6. 完成一个里程碑后,提供主规范/里程碑逐条对照、自动化验证、必要运行时 smoke、边界验证和未验证项;未验收不得推进下一个里程碑。
|
||||
7. 全部验收通过后,将持久结论合并回主规范,删除已完成的临时计划,并检查文档、契约、测试和提交边界一致。
|
||||
|
||||
## 约束
|
||||
|
||||
- 里程碑和实现计划是开发期协调文件,完成或取消后删除;阶段编号和临时名称不得进入产品代码、用户文档、测试名称或提交标题。
|
||||
- 活动计划可以提交以便团队同步,但不得把一次性实现步骤写进长期规范。
|
||||
- 继续遵守现有 API、SpacetimeDB、AGC、编码和中文文档约束;本 skill 不替代专题门禁。
|
||||
- 文档任务至少运行 `npm run check:doc-index`、`npm run check:encoding` 和 `git diff --check`;代码任务再运行范围匹配的测试、类型检查、schema/OpenAPI 或运行时 smoke。
|
||||
+54
-336
@@ -27,28 +27,9 @@ env:
|
||||
RUSTC_WRAPPER: ''
|
||||
CARGO_BUILD_RUSTC_WRAPPER: ''
|
||||
|
||||
# job 声明顺序就是 runner 领取顺序,因此把最长尾的客户端 Rust 门禁排在前面,
|
||||
# 让它在最少的等待下占用并发槽位;其余 job 按时长递减排列。
|
||||
#
|
||||
# 客户端(微信壳 / Expo 移动壳 / Tauri 桌面壳 / AI 游戏创作壳)门禁原先全部串在
|
||||
# `Native shell tests` 一个 job 里,实测 18 分 37 秒。现在按门禁组拆成一个分组一个 job:
|
||||
# `Native shell tests`(契约 + H5 / 微信 / 移动 / 桌面壳门禁 + 发布构建 smoke)、
|
||||
# `AI game creator shell web tests`(typecheck + 壳内测试)、`AI game creator shell
|
||||
# Rust shard 1/4` 到 `4/4`(AGC 壳 bin 单测按名单分 4 片)、`AI game creator shell
|
||||
# Rust smoke`(agent-run smoke)与 `AI game creator shell Rust crates`(AGC 壳依赖的
|
||||
# 共享 / 平台 crate 测试)。各自的命令与拆分前逐一对应,本地
|
||||
# `npm run check:native-shells` 仍是同一条串行序列。
|
||||
#
|
||||
# AGC 壳的 bin 单测(2466 条)按名单分 4 片、一片一个 job:片内保持 `--test-threads=1`
|
||||
# (当年线程并行会互相干扰的是进程内后台锁与异步终态),片与片之间靠 job 级并发摊开。
|
||||
# 不要改回「一个 job 里多进程并行这几片」:同一容器内它们会争抢共享 HOME、target 与固定
|
||||
# 临时路径,实测比整套串行还慢。每个分片 job 都会自校验「片并集等于全集且互斥」。
|
||||
jobs:
|
||||
# AGC 壳自身的 Rust bin 单测分片,4 片各自独立 job 并发执行、片内仍保持
|
||||
# `--test-threads=1`。这里不装 npm 依赖:壳 Rust 门禁只用 cargo 与 node 内建模块,
|
||||
# 也只需要 AGC 壳自己那份锁定依赖。
|
||||
ai-game-creator-shell-rust-shard-1:
|
||||
name: AI game creator shell Rust shard 1/4
|
||||
repository-checks:
|
||||
name: Repository checks
|
||||
runs-on: genarrative-ci
|
||||
steps:
|
||||
- name: Checkout full history from Gitea
|
||||
@@ -60,228 +41,76 @@ jobs:
|
||||
- name: Validate preinstalled CI job image and sandbox
|
||||
run: GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 bash scripts/check-gitea-ci-job-image.sh
|
||||
|
||||
- name: Prepare AI game creator shell Rust dependencies
|
||||
- name: Resolve comparison base
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# AGC 壳有独立 Cargo.lock,其 path 依赖已含 platform-llm、platform-agent、
|
||||
# agent-runtime-core 与 shared-contracts,因此只锁这一份 manifest 就能覆盖壳测试
|
||||
# 与 smoke 的全部第三方依赖;server-rs 那次预热归 crate 级 job,不在这里重复。
|
||||
for attempt in $(seq 1 5); do
|
||||
if cargo fetch --locked \
|
||||
--target x86_64-unknown-linux-gnu \
|
||||
--manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml; then
|
||||
break
|
||||
fi
|
||||
if [[ "${attempt}" -eq 5 ]]; then
|
||||
echo 'AI game creator shell Cargo dependency fetch failed after 5 attempts.' >&2
|
||||
base_ref="$(node -e '
|
||||
const fs = require("node:fs");
|
||||
const event = JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH, "utf8"));
|
||||
process.stdout.write(event.pull_request?.base?.sha ?? event.before ?? "");
|
||||
')"
|
||||
if [[ -n "${base_ref}" && ! "${base_ref}" =~ ^0+$ ]]; then
|
||||
git cat-file -e "${base_ref}^{commit}" 2>/dev/null || {
|
||||
echo "comparison base commit is unavailable: ${base_ref}" >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep $((attempt * 2))
|
||||
done
|
||||
}
|
||||
else
|
||||
base_ref="$(git merge-base HEAD origin/master 2>/dev/null || git rev-parse HEAD)"
|
||||
fi
|
||||
resolved_base_ref="$(git rev-parse --verify "${base_ref}^{commit}" 2>/dev/null || true)"
|
||||
head_ref="$(git rev-parse HEAD)"
|
||||
if [[ "${resolved_base_ref}" == "${head_ref}" ]]; then
|
||||
resolved_base_ref="$(git rev-parse --verify HEAD^ 2>/dev/null || true)"
|
||||
fi
|
||||
if [[ -z "${resolved_base_ref}" ]]; then
|
||||
echo 'comparison base must resolve to a commit distinct from HEAD.' >&2
|
||||
exit 1
|
||||
fi
|
||||
base_ref="${resolved_base_ref}"
|
||||
if [[ "${GITHUB_EVENT_NAME:-}" == 'pull_request' ]] \
|
||||
&& ! git merge-base --is-ancestor "${base_ref}" HEAD; then
|
||||
echo 'pull request head does not contain the latest base commit; update the branch and rerun CI.' >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "SPACETIME_SCHEMA_BASE_REF=${base_ref}" >> "${GITHUB_ENV}"
|
||||
|
||||
- name: Run AI game creator shell Rust shard 1/4
|
||||
run: npm run check:native-shells:agc-rust-shard-1
|
||||
- name: Install npm dependencies
|
||||
run: bash scripts/ci-npm-ci-with-retry.sh
|
||||
|
||||
ai-game-creator-shell-rust-shard-2:
|
||||
name: AI game creator shell Rust shard 2/4
|
||||
- name: Run repository checks
|
||||
run: npm run check:repository-ci
|
||||
|
||||
frontend-tests:
|
||||
name: Frontend tests
|
||||
runs-on: genarrative-ci
|
||||
steps:
|
||||
- name: Checkout full history from Gitea
|
||||
- name: Checkout source from Gitea
|
||||
env:
|
||||
GENARRATIVE_GITEA_FETCH_DEPTH: '0'
|
||||
GENARRATIVE_GITEA_FETCH_DEPTH: '1'
|
||||
GENARRATIVE_GITEA_TOKEN: ${{ github.token }}
|
||||
run: genarrative-gitea-checkout
|
||||
|
||||
- name: Validate preinstalled CI job image and sandbox
|
||||
run: GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 bash scripts/check-gitea-ci-job-image.sh
|
||||
|
||||
- name: Prepare AI game creator shell Rust dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for attempt in $(seq 1 5); do
|
||||
if cargo fetch --locked \
|
||||
--target x86_64-unknown-linux-gnu \
|
||||
--manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml; then
|
||||
break
|
||||
fi
|
||||
if [[ "${attempt}" -eq 5 ]]; then
|
||||
echo 'AI game creator shell Cargo dependency fetch failed after 5 attempts.' >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep $((attempt * 2))
|
||||
done
|
||||
- name: Install npm dependencies
|
||||
run: bash scripts/ci-npm-ci-with-retry.sh
|
||||
|
||||
- name: Run AI game creator shell Rust shard 2/4
|
||||
run: npm run check:native-shells:agc-rust-shard-2
|
||||
- name: Run frontend and script tests
|
||||
run: npm run test
|
||||
|
||||
ai-game-creator-shell-rust-shard-3:
|
||||
name: AI game creator shell Rust shard 3/4
|
||||
runs-on: genarrative-ci
|
||||
steps:
|
||||
- name: Checkout full history from Gitea
|
||||
env:
|
||||
GENARRATIVE_GITEA_FETCH_DEPTH: '0'
|
||||
GENARRATIVE_GITEA_TOKEN: ${{ github.token }}
|
||||
run: genarrative-gitea-checkout
|
||||
- name: Run BgFilter worker smoke harness tests
|
||||
run: npm run bgfilter-worker:smoke-test
|
||||
|
||||
- name: Validate preinstalled CI job image and sandbox
|
||||
run: GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 bash scripts/check-gitea-ci-job-image.sh
|
||||
- name: Validate production health patrol behavior
|
||||
run: npm run check:production-health-patrol
|
||||
|
||||
- name: Prepare AI game creator shell Rust dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for attempt in $(seq 1 5); do
|
||||
if cargo fetch --locked \
|
||||
--target x86_64-unknown-linux-gnu \
|
||||
--manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml; then
|
||||
break
|
||||
fi
|
||||
if [[ "${attempt}" -eq 5 ]]; then
|
||||
echo 'AI game creator shell Cargo dependency fetch failed after 5 attempts.' >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep $((attempt * 2))
|
||||
done
|
||||
- name: Validate production API release behavior
|
||||
run: npm run check:production-api-release
|
||||
|
||||
- name: Run AI game creator shell Rust shard 3/4
|
||||
run: npm run check:native-shells:agc-rust-shard-3
|
||||
|
||||
ai-game-creator-shell-rust-shard-4:
|
||||
name: AI game creator shell Rust shard 4/4
|
||||
runs-on: genarrative-ci
|
||||
steps:
|
||||
- name: Checkout full history from Gitea
|
||||
env:
|
||||
GENARRATIVE_GITEA_FETCH_DEPTH: '0'
|
||||
GENARRATIVE_GITEA_TOKEN: ${{ github.token }}
|
||||
run: genarrative-gitea-checkout
|
||||
|
||||
- name: Validate preinstalled CI job image and sandbox
|
||||
run: GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 bash scripts/check-gitea-ci-job-image.sh
|
||||
|
||||
- name: Prepare AI game creator shell Rust dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for attempt in $(seq 1 5); do
|
||||
if cargo fetch --locked \
|
||||
--target x86_64-unknown-linux-gnu \
|
||||
--manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml; then
|
||||
break
|
||||
fi
|
||||
if [[ "${attempt}" -eq 5 ]]; then
|
||||
echo 'AI game creator shell Cargo dependency fetch failed after 5 attempts.' >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep $((attempt * 2))
|
||||
done
|
||||
|
||||
- name: Run AI game creator shell Rust shard 4/4
|
||||
run: npm run check:native-shells:agc-rust-shard-4
|
||||
|
||||
# agent-run smoke 会 spawn `cargo run`(走壳自己的 manifest),同样不装 npm 依赖,
|
||||
# 单独一个 job,免得把已经压到 4 分钟级的片 job 拖长。
|
||||
ai-game-creator-shell-rust-smoke:
|
||||
name: AI game creator shell Rust smoke
|
||||
runs-on: genarrative-ci
|
||||
steps:
|
||||
- name: Checkout full history from Gitea
|
||||
env:
|
||||
GENARRATIVE_GITEA_FETCH_DEPTH: '0'
|
||||
GENARRATIVE_GITEA_TOKEN: ${{ github.token }}
|
||||
run: genarrative-gitea-checkout
|
||||
|
||||
- name: Validate preinstalled CI job image and sandbox
|
||||
run: GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 bash scripts/check-gitea-ci-job-image.sh
|
||||
|
||||
- name: Prepare AI game creator shell Rust dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for attempt in $(seq 1 5); do
|
||||
if cargo fetch --locked \
|
||||
--target x86_64-unknown-linux-gnu \
|
||||
--manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml; then
|
||||
break
|
||||
fi
|
||||
if [[ "${attempt}" -eq 5 ]]; then
|
||||
echo 'AI game creator shell Cargo dependency fetch failed after 5 attempts.' >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep $((attempt * 2))
|
||||
done
|
||||
|
||||
- name: Run AI game creator shell agent-run smoke
|
||||
run: npm run check:native-shells:agc-rust-smoke
|
||||
|
||||
# AGC 壳依赖的共享 / 平台 crate 测试用的是 server-rs workspace 与两个无锁独立 crate
|
||||
# 的 manifest,属另一套依赖图,因此单独一个 job 预热、单独跑。
|
||||
ai-game-creator-shell-rust-crates:
|
||||
name: AI game creator shell Rust crates
|
||||
runs-on: genarrative-ci
|
||||
steps:
|
||||
- name: Checkout full history from Gitea
|
||||
env:
|
||||
GENARRATIVE_GITEA_FETCH_DEPTH: '0'
|
||||
GENARRATIVE_GITEA_TOKEN: ${{ github.token }}
|
||||
run: genarrative-gitea-checkout
|
||||
|
||||
- name: Validate preinstalled CI job image and sandbox
|
||||
run: GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 bash scripts/check-gitea-ci-job-image.sh
|
||||
|
||||
- name: Prepare server-rs Rust dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for attempt in $(seq 1 5); do
|
||||
if cargo fetch --locked \
|
||||
--target x86_64-unknown-linux-gnu \
|
||||
--manifest-path server-rs/Cargo.toml; then
|
||||
break
|
||||
fi
|
||||
if [[ "${attempt}" -eq 5 ]]; then
|
||||
echo 'server-rs Cargo dependency fetch failed after 5 attempts.' >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep $((attempt * 2))
|
||||
done
|
||||
|
||||
- name: Prepare standalone Rust crate dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# agent-runtime-core / agent-runtime-orchestration 被 server-rs/Cargo.toml 的
|
||||
# exclude 排除,不参与上面的 workspace 锁文件,因此上面那次锁定 fetch 覆盖不到它们;
|
||||
# 而 `npm run ai-game-creator-shell:check:rust:crates` 会用
|
||||
# `cargo test --manifest-path` 单独跑这两个 crate。不在这里预热的话,这两条测试
|
||||
# 会在测试阶段自己 `Updating crates.io index`,crates.io 一抖动整条 job 就红
|
||||
# (见 #327 / PR #316 run 1950)。
|
||||
# 两个 crate 都没有提交 Cargo.lock,所以这里只能做不带锁标志的 fetch:
|
||||
# 加锁标志会因为缺少锁文件直接失败。生成的 Cargo.lock 落在两个 crate 目录内,
|
||||
# 已被各自的 .gitignore 忽略,只留在容器里;随后的测试阶段因此能用锁定版本
|
||||
# 解析,不再触碰 registry index。
|
||||
for manifest_path in \
|
||||
server-rs/crates/agent-runtime-core/Cargo.toml \
|
||||
server-rs/crates/agent-runtime-orchestration/Cargo.toml; do
|
||||
for attempt in $(seq 1 5); do
|
||||
if cargo fetch \
|
||||
--target x86_64-unknown-linux-gnu \
|
||||
--manifest-path "${manifest_path}"; then
|
||||
break
|
||||
fi
|
||||
if [[ "${attempt}" -eq 5 ]]; then
|
||||
echo "standalone crate dependency fetch failed after 5 attempts: ${manifest_path}" >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep $((attempt * 2))
|
||||
done
|
||||
done
|
||||
|
||||
- name: Run AI game creator shell shared crate gates
|
||||
run: npm run check:native-shells:agc-rust-crates
|
||||
- name: Validate production API deploy behavior
|
||||
run: npm run check:production-api-deploy
|
||||
|
||||
backend-tests:
|
||||
name: Backend tests
|
||||
@@ -365,8 +194,6 @@ jobs:
|
||||
- name: Check SpacetimeDB module
|
||||
run: cargo check --locked -p spacetime-module --manifest-path server-rs/Cargo.toml
|
||||
|
||||
# 客户端的壳级与契约门禁:静态契约断言、H5 / 微信 / 移动 / 桌面壳运行时门禁,
|
||||
# 以及依赖发布产物的构建 smoke。
|
||||
native-shell-tests:
|
||||
name: Native shell tests
|
||||
runs-on: genarrative-ci
|
||||
@@ -388,6 +215,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for manifest_path in \
|
||||
server-rs/Cargo.toml \
|
||||
apps/desktop-shell/src-tauri/Cargo.toml \
|
||||
apps/ai-game-creator-shell/src-tauri/Cargo.toml; do
|
||||
for attempt in $(seq 1 5); do
|
||||
@@ -404,118 +232,8 @@ jobs:
|
||||
done
|
||||
done
|
||||
|
||||
- name: Run native shell contract gates
|
||||
run: npm run check:native-shells:contract
|
||||
|
||||
- name: Run native shell gates
|
||||
run: npm run check:native-shells:shells
|
||||
|
||||
- name: Run native shell release build smoke
|
||||
run: npm run check:native-shells:release
|
||||
run: npm run check:native-shells
|
||||
|
||||
- name: Ensure native lockfiles are unchanged
|
||||
run: git diff --exit-code -- apps/desktop-shell/src-tauri/Cargo.lock apps/ai-game-creator-shell/src-tauri/Cargo.lock
|
||||
|
||||
frontend-tests:
|
||||
name: Frontend tests
|
||||
runs-on: genarrative-ci
|
||||
steps:
|
||||
- name: Checkout source from Gitea
|
||||
env:
|
||||
GENARRATIVE_GITEA_FETCH_DEPTH: '1'
|
||||
GENARRATIVE_GITEA_TOKEN: ${{ github.token }}
|
||||
run: genarrative-gitea-checkout
|
||||
|
||||
- name: Validate preinstalled CI job image and sandbox
|
||||
run: GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 bash scripts/check-gitea-ci-job-image.sh
|
||||
|
||||
- name: Install npm dependencies
|
||||
run: bash scripts/ci-npm-ci-with-retry.sh
|
||||
|
||||
- name: Run frontend and script tests
|
||||
run: npm run test
|
||||
|
||||
- name: Run BgFilter worker smoke harness tests
|
||||
run: npm run bgfilter-worker:smoke-test
|
||||
|
||||
- name: Validate production health patrol behavior
|
||||
run: npm run check:production-health-patrol
|
||||
|
||||
- name: Validate production API release behavior
|
||||
run: npm run check:production-api-release
|
||||
|
||||
- name: Validate production API deploy behavior
|
||||
run: npm run check:production-api-deploy
|
||||
|
||||
repository-checks:
|
||||
name: Repository checks
|
||||
runs-on: genarrative-ci
|
||||
steps:
|
||||
- name: Checkout full history from Gitea
|
||||
env:
|
||||
GENARRATIVE_GITEA_FETCH_DEPTH: '0'
|
||||
GENARRATIVE_GITEA_TOKEN: ${{ github.token }}
|
||||
run: genarrative-gitea-checkout
|
||||
|
||||
- name: Validate preinstalled CI job image and sandbox
|
||||
run: GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 bash scripts/check-gitea-ci-job-image.sh
|
||||
|
||||
- name: Resolve comparison base
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
base_ref="$(node -e '
|
||||
const fs = require("node:fs");
|
||||
const event = JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH, "utf8"));
|
||||
process.stdout.write(event.pull_request?.base?.sha ?? event.before ?? "");
|
||||
')"
|
||||
if [[ -n "${base_ref}" && ! "${base_ref}" =~ ^0+$ ]]; then
|
||||
git cat-file -e "${base_ref}^{commit}" 2>/dev/null || {
|
||||
echo "comparison base commit is unavailable: ${base_ref}" >&2
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
base_ref="$(git merge-base HEAD origin/master 2>/dev/null || git rev-parse HEAD)"
|
||||
fi
|
||||
resolved_base_ref="$(git rev-parse --verify "${base_ref}^{commit}" 2>/dev/null || true)"
|
||||
head_ref="$(git rev-parse HEAD)"
|
||||
if [[ "${resolved_base_ref}" == "${head_ref}" ]]; then
|
||||
resolved_base_ref="$(git rev-parse --verify HEAD^ 2>/dev/null || true)"
|
||||
fi
|
||||
if [[ -z "${resolved_base_ref}" ]]; then
|
||||
echo 'comparison base must resolve to a commit distinct from HEAD.' >&2
|
||||
exit 1
|
||||
fi
|
||||
base_ref="${resolved_base_ref}"
|
||||
if [[ "${GITHUB_EVENT_NAME:-}" == 'pull_request' ]] \
|
||||
&& ! git merge-base --is-ancestor "${base_ref}" HEAD; then
|
||||
echo 'pull request head does not contain the latest base commit; update the branch and rerun CI.' >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "SPACETIME_SCHEMA_BASE_REF=${base_ref}" >> "${GITHUB_ENV}"
|
||||
|
||||
- name: Install npm dependencies
|
||||
run: bash scripts/ci-npm-ci-with-retry.sh
|
||||
|
||||
- name: Run repository checks
|
||||
run: npm run check:repository-ci
|
||||
|
||||
# 客户端的 AI 游戏创作壳前端门禁:typecheck 与壳内测试,不触碰 Cargo。
|
||||
ai-game-creator-shell-web-tests:
|
||||
name: AI game creator shell web tests
|
||||
runs-on: genarrative-ci
|
||||
steps:
|
||||
- name: Checkout full history from Gitea
|
||||
env:
|
||||
GENARRATIVE_GITEA_FETCH_DEPTH: '0'
|
||||
GENARRATIVE_GITEA_TOKEN: ${{ github.token }}
|
||||
run: genarrative-gitea-checkout
|
||||
|
||||
- name: Validate preinstalled CI job image and sandbox
|
||||
run: GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 bash scripts/check-gitea-ci-job-image.sh
|
||||
|
||||
- name: Install npm dependencies
|
||||
run: bash scripts/ci-npm-ci-with-retry.sh
|
||||
|
||||
- name: Run AI game creator shell web gates
|
||||
run: npm run check:native-shells:agc-web
|
||||
|
||||
@@ -40,8 +40,6 @@ temp*build*/
|
||||
/apps/ai-game-creator-shell/src-tauri/resources/codex/win-x64/codex-path/
|
||||
/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/
|
||||
/plugins/agc-cocos-editor/native/payload/
|
||||
/apps/ai-game-creator-shell/logs/
|
||||
/apps/ai-game-creator-shell/.llm-drafts/
|
||||
/apps/ai-game-creator-shell/game-creator.config.local.json
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
3. [`docs/project-memory/README.md`](docs/project-memory/README.md)、[`project-overview.md`](docs/project-memory/shared-memory/project-overview.md)、[`team-conventions.md`](docs/project-memory/shared-memory/team-conventions.md)、[`development-workflow.md`](docs/project-memory/shared-memory/development-workflow.md)。
|
||||
4. 与任务相关的 [`decision-log.md`](docs/project-memory/shared-memory/decision-log.md)、[`pitfalls.md`](docs/project-memory/shared-memory/pitfalls.md)、[`docs/README.md`](docs/README.md) 和当前专题文档。
|
||||
- 落地工程修改前,先确认是否已有足够具体的 PRD、技术方案或当前融合文档;文档仍存在编码级歧义时,先补文档再编码。
|
||||
- 跨模块功能、公开 API/DTO、SpacetimeDB schema、AGC/Runtime 或复杂 UI 状态链路必须按 [`docs/【协作规范】规范驱动开发工作流-2026-09-12.md`](docs/【协作规范】规范驱动开发工作流-2026-09-12.md) 先完成主规范、里程碑规范和单里程碑实现计划;评审与验收门禁未通过前不得进入下一里程碑。局部修复、文案和无行为变化重构继续走轻量流程。
|
||||
- 本仓库的本地 RAG 位于 [`scripts/rag/`](scripts/rag/);RAG 只作为候选上下文,不替代打开源文件核对。默认不安装 RAG 运行时依赖,需要启用时必须先询问用户,并只安装到 gitignored 的 `.rag/runtime/`。
|
||||
|
||||
## 绝对约束
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
# resources/plugins 由 build.rs 从 plugins/ 复制生成,属于构建产物。
|
||||
# 它在 dev 监听范围内,重新生成会让 Tauri dev 误判为源码改动而触发
|
||||
# “构建 -> 监听 -> 再构建”的自触发循环。
|
||||
resources/plugins/
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@genarrative/ai-game-creator-shell",
|
||||
"private": true,
|
||||
"version": "0.1.29",
|
||||
"version": "0.1.12",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "node scripts/start-tauri-dev.mjs",
|
||||
|
||||
@@ -371,15 +371,7 @@ async function runMissingConfigWizard(setActiveChild, explicitConfigDir) {
|
||||
}
|
||||
}
|
||||
|
||||
async function secureWindowsPrivateRuntimePath(
|
||||
targetPath,
|
||||
options,
|
||||
secureWindowsPath = null,
|
||||
) {
|
||||
if (secureWindowsPath) {
|
||||
await secureWindowsPath(targetPath, options);
|
||||
return;
|
||||
}
|
||||
async function secureWindowsPrivateRuntimePath(targetPath, options) {
|
||||
const { secureWindowsGameCreatorPathForCurrentUser } = await import(
|
||||
'./game-creator-config-wizard.mjs'
|
||||
);
|
||||
@@ -391,7 +383,6 @@ async function copyPrivateRuntimeConfigEntry(
|
||||
runtimeConfigDir,
|
||||
fileName,
|
||||
required,
|
||||
secureWindowsPath = null,
|
||||
) {
|
||||
const sourcePath = path.join(sourceConfigDir, fileName);
|
||||
const sourceMetadata = await lstat(sourcePath).catch((error) => {
|
||||
@@ -411,11 +402,9 @@ async function copyPrivateRuntimeConfigEntry(
|
||||
const sourceBytes = await readFile(sourcePath);
|
||||
const destinationFile = await open(destinationPath, 'wx', 0o600);
|
||||
try {
|
||||
await secureWindowsPrivateRuntimePath(
|
||||
destinationPath,
|
||||
{ isDirectory: false },
|
||||
secureWindowsPath,
|
||||
);
|
||||
await secureWindowsPrivateRuntimePath(destinationPath, {
|
||||
isDirectory: false,
|
||||
});
|
||||
await destinationFile.writeFile(sourceBytes);
|
||||
await destinationFile.sync();
|
||||
} finally {
|
||||
@@ -448,11 +437,7 @@ async function copyPrivateRuntimeConfigEntry(
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function prepareSwarmTestRuntimeConfig(
|
||||
sourceConfigDir,
|
||||
tempRoot,
|
||||
{ secureWindowsPath = null } = {},
|
||||
) {
|
||||
export async function prepareSwarmTestRuntimeConfig(sourceConfigDir, tempRoot) {
|
||||
if (!path.isAbsolute(sourceConfigDir)) {
|
||||
throw new Error('配置来源目录必须是绝对路径');
|
||||
}
|
||||
@@ -472,11 +457,9 @@ export async function prepareSwarmTestRuntimeConfig(
|
||||
);
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
await secureWindowsPrivateRuntimePath(
|
||||
runtimeConfigDir,
|
||||
{ isDirectory: true },
|
||||
secureWindowsPath,
|
||||
);
|
||||
await secureWindowsPrivateRuntimePath(runtimeConfigDir, {
|
||||
isDirectory: true,
|
||||
});
|
||||
} else {
|
||||
await chmod(runtimeConfigDir, 0o700);
|
||||
}
|
||||
@@ -494,14 +477,12 @@ export async function prepareSwarmTestRuntimeConfig(
|
||||
runtimeConfigDir,
|
||||
configFileName,
|
||||
true,
|
||||
secureWindowsPath,
|
||||
);
|
||||
await copyPrivateRuntimeConfigEntry(
|
||||
canonicalSourceConfigDir,
|
||||
runtimeConfigDir,
|
||||
localConfigFileName,
|
||||
false,
|
||||
secureWindowsPath,
|
||||
);
|
||||
return {
|
||||
path: await realpath(runtimeConfigDir),
|
||||
|
||||
@@ -4,11 +4,6 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import {
|
||||
defaultEditorFeatures,
|
||||
withDefaultCargoFeatures,
|
||||
} from './cargo-features.mjs';
|
||||
|
||||
const appRoot = fileURLToPath(new URL('..', import.meta.url));
|
||||
const defaultReleaseTarget = 'x86_64-pc-windows-msvc';
|
||||
const releaseTarget =
|
||||
@@ -164,30 +159,10 @@ export async function prepareReleaseVersion() {
|
||||
return nextVersion;
|
||||
}
|
||||
|
||||
export function buildTauriBuildArguments(
|
||||
args = [],
|
||||
target = releaseTarget,
|
||||
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 targetArgs = noBundle || explicitTarget ? [] : ['--target', target];
|
||||
const features = defaultEditorFeatures(
|
||||
explicitTarget || (noBundle ? platform : target),
|
||||
);
|
||||
return [
|
||||
'build',
|
||||
...withDefaultCargoFeatures([...targetArgs, ...args], features),
|
||||
];
|
||||
}
|
||||
|
||||
export function runTauriBuild(args = []) {
|
||||
const noBundle = args.includes('--no-bundle');
|
||||
const hasTarget = args.includes('--target');
|
||||
const targetArgs = noBundle || hasTarget ? [] : ['--target', releaseTarget];
|
||||
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
||||
const result = spawnSync(
|
||||
npmCommand,
|
||||
@@ -197,7 +172,9 @@ export function runTauriBuild(args = []) {
|
||||
'exec',
|
||||
'tauri',
|
||||
'--',
|
||||
...buildTauriBuildArguments(args),
|
||||
'build',
|
||||
...targetArgs,
|
||||
...args,
|
||||
],
|
||||
{ cwd: appRoot, stdio: 'inherit', shell: process.platform === 'win32' },
|
||||
);
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
/** 默认桌面能力;显式 feature 参数优先,不把应用参数当 Cargo 参数。 */
|
||||
export function withDefaultCargoFeatures(argv, features) {
|
||||
const separator = argv.indexOf('--');
|
||||
const cargoArgs = separator < 0 ? argv : argv.slice(0, separator);
|
||||
if (
|
||||
!features.length ||
|
||||
cargoArgs.some(
|
||||
(value) =>
|
||||
value === '--features' ||
|
||||
value === '-f' ||
|
||||
value.startsWith('--features=') ||
|
||||
/^-f.+/u.test(value),
|
||||
)
|
||||
) {
|
||||
return argv;
|
||||
}
|
||||
return [`--features=${features.join(',')}`, ...argv];
|
||||
}
|
||||
|
||||
export function defaultEditorFeatures(target) {
|
||||
return target === 'win32' || target.includes('windows')
|
||||
? ['cocos-editor-execute']
|
||||
: [];
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { test } from 'node:test';
|
||||
|
||||
import { buildTauriBuildArguments } from './build-release.mjs';
|
||||
import { withDefaultCargoFeatures } from './cargo-features.mjs';
|
||||
|
||||
test('Windows release includes the same editor feature as development', () => {
|
||||
assert.deepEqual(
|
||||
buildTauriBuildArguments([], 'x86_64-pc-windows-msvc', 'win32'),
|
||||
[
|
||||
'build',
|
||||
'--features=cocos-editor-execute',
|
||||
'--target',
|
||||
'x86_64-pc-windows-msvc',
|
||||
],
|
||||
);
|
||||
assert.deepEqual(
|
||||
buildTauriBuildArguments(
|
||||
['--no-bundle'],
|
||||
'x86_64-pc-windows-msvc',
|
||||
'linux',
|
||||
),
|
||||
['build', '--no-bundle'],
|
||||
);
|
||||
assert.deepEqual(
|
||||
buildTauriBuildArguments(['--target=aarch64-apple-darwin']),
|
||||
['build', '--target=aarch64-apple-darwin'],
|
||||
);
|
||||
});
|
||||
test('explicit Cargo features override defaults in every supported spelling', () => {
|
||||
for (const args of [
|
||||
['--features', 'custom'],
|
||||
['--features=custom'],
|
||||
['-f', 'custom'],
|
||||
['-fcustom'],
|
||||
]) {
|
||||
assert.deepEqual(
|
||||
withDefaultCargoFeatures(args, ['cocos-editor-execute']),
|
||||
args,
|
||||
);
|
||||
}
|
||||
assert.deepEqual(
|
||||
withDefaultCargoFeatures(
|
||||
['--', '--features=app'],
|
||||
['cocos-editor-execute'],
|
||||
),
|
||||
['--features=cocos-editor-execute', '--', '--features=app'],
|
||||
);
|
||||
});
|
||||
@@ -108,33 +108,15 @@ const rustSharedContractSource = fs.readFileSync(
|
||||
);
|
||||
const allowedUncalledTauriCommands = [
|
||||
'append_direct_project_conversation_message',
|
||||
// TODO: Remove the retired binding command after the legacy runtime path is removed.
|
||||
'bind_components',
|
||||
'chat_with_game_creator_agent',
|
||||
'check_ui_editor_font_glyph_coverage',
|
||||
'create_ui_design_resource',
|
||||
// 图片类生成的同步变体:GUI 已改为 `start_local_project_asset_generation` + 项目内任务账本
|
||||
// (提交即返回、后台生成)。这条命令**没有生产调用方**,只有 Rust 集成测试
|
||||
// (`src/tests/project.rs`)与 `commands.rs` 单测在调;待后续批次删除,或改为转调
|
||||
// `start_local_project_asset_generation`。
|
||||
'generate_local_project_asset',
|
||||
'open_game_creator_launcher_window',
|
||||
'open_game_creator_workspace_window',
|
||||
'read_direct_project_conversation',
|
||||
'reset_design_agent_session',
|
||||
'stop_local_game_preview_if_matches',
|
||||
'start_game_creator_external_mcp',
|
||||
'stop_game_creator_external_mcp',
|
||||
'list_agc_plugins',
|
||||
'list_agc_extensions',
|
||||
'refresh_agc_plugins',
|
||||
'start_agc_plugin',
|
||||
'stop_agc_plugin',
|
||||
'reload_agc_plugin',
|
||||
'call_agc_plugin',
|
||||
'read_agc_plugin_panel',
|
||||
'set_agc_plugin_project_path',
|
||||
'set_agc_plugin_enabled',
|
||||
];
|
||||
const sourceExtensions = new Set([
|
||||
'.json',
|
||||
@@ -1303,10 +1285,7 @@ if (tauriConfig.identifier !== 'world.genarrative.ai-game-creator') {
|
||||
throw new Error('AI game creator shell identifier drifted');
|
||||
}
|
||||
|
||||
const expectedBundledDesignAgentResources = {
|
||||
'design-agent': 'design-agent',
|
||||
};
|
||||
const expectedBundledWindowsResources = {
|
||||
const expectedBundledCodexResources = {
|
||||
'resources/codex/win-x64/bin/codex.exe': 'codex/win-x64/bin/codex.exe',
|
||||
'resources/codex/win-x64/bin/codex-code-mode-host.exe':
|
||||
'codex/win-x64/bin/codex-code-mode-host.exe',
|
||||
@@ -1320,35 +1299,17 @@ const expectedBundledWindowsResources = {
|
||||
'codex/win-x64/codex-package.json',
|
||||
'resources/codex/win-x64/NOTICE.md': 'codex/win-x64/NOTICE.md',
|
||||
'resources/codex/win-x64/manifest.json': 'codex/win-x64/manifest.json',
|
||||
'resources/plugins': 'plugins',
|
||||
};
|
||||
assert.deepEqual(
|
||||
tauriConfig.bundle?.resources,
|
||||
expectedBundledDesignAgentResources,
|
||||
'AI game creator shell base Tauri config must bundle the design-agent resource pack',
|
||||
);
|
||||
for (const key of Object.keys(tauriConfig.bundle?.resources ?? {})) {
|
||||
if (String(key).includes('codex')) {
|
||||
throw new Error(
|
||||
'AI game creator shell base Tauri config must not require Windows-only Codex resources',
|
||||
);
|
||||
}
|
||||
if (tauriConfig.bundle?.resources !== undefined) {
|
||||
throw new Error(
|
||||
'AI game creator shell base Tauri config must not require Windows-only Codex resources',
|
||||
);
|
||||
}
|
||||
assert.deepEqual(
|
||||
windowsTauriConfig.bundle?.resources,
|
||||
expectedBundledWindowsResources,
|
||||
'AI game creator shell Windows Tauri config must bundle the complete pinned Codex resource set and Cocos bridge payload directory',
|
||||
expectedBundledCodexResources,
|
||||
'AI game creator shell Windows Tauri config must bundle the complete pinned Codex resource set',
|
||||
);
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(
|
||||
windowsTauriConfig.bundle?.resources ?? {},
|
||||
'design-agent',
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
'design-agent resource pack must not be mixed into the Windows Codex sidecar bundle',
|
||||
);
|
||||
}
|
||||
if (windowsTauriConfig.bundle?.useLocalToolsDir !== true) {
|
||||
throw new Error(
|
||||
'AI game creator shell Windows Tauri config must cache bundling tools in the project target directory',
|
||||
|
||||
@@ -1,461 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
// AGC 壳 Rust 套件的分片运行器。
|
||||
//
|
||||
// 背景:AGC 壳的 Rust 单测集中在 src/main.rs 的 bin target(实测 2466 条),仓库口径用
|
||||
// `--test-threads=1` 跑,理由是并行调度会让共享 Agent Runtime 后台锁与异步终态的用例在
|
||||
// **同一进程内**互相干扰(见 development-workflow 的 Tauri suite 单线程口径)。代价是
|
||||
// 整套用例串行跑满 507 秒,占掉 CI 上 `AI game creator shell Rust tests` job 的大头。
|
||||
//
|
||||
// 这里保留「片内串行」的既有口径,只把用例集合切成 N 片:
|
||||
// - CI 用 `--shard-index=<i>` 让**每个 job 只跑一片**,靠多个 job 并发把整套用例摊开;
|
||||
// - 本地不传 `--shard-index` 时把 N 片放进 N 个**独立进程**并行(--concurrency 可调),
|
||||
// 保留一条命令跑全量的入口。
|
||||
// 片并集必须等于全集、且不得重复,数量不符即失败,防止分片规则改动后静默漏跑;该校验
|
||||
// 与「只跑一片」无关,因此在每个 job 上都会执行。
|
||||
//
|
||||
// 注意:同一容器内多进程并行这套用例(共享 HOME、target、固定临时路径)实测会互相拖慢,
|
||||
// 比串行还慢,所以 CI 走「一个 job 一片」而不是单 job 内并行。
|
||||
//
|
||||
// 用法:
|
||||
// node scripts/run-rust-shell-test-shards.mjs --shards=4 --shard-index=2 # CI:只跑第 2 片
|
||||
// node scripts/run-rust-shell-test-shards.mjs [--shards=4] [--concurrency=4] # 本地:全量
|
||||
// node scripts/run-rust-shell-test-shards.mjs --manifest=<Cargo.toml> --target-kind=lib --no-locked
|
||||
//
|
||||
// 参数:
|
||||
// --shards=<n> 分片数,默认 4
|
||||
// --shard-index=<i> 只跑第 i 片(1..shards);不传则跑全部分片
|
||||
// --concurrency=<n> 同时运行的片数,默认等于分片数;--shard-index 时恒为 1
|
||||
// --manifest=<path> Cargo.toml,默认 ../src-tauri/Cargo.toml(相对本脚本)
|
||||
// --target-kind=<k> bin | lib,默认 bin(本地自测小 crate 时用 lib)
|
||||
// --bin=<name> bin target 名,默认 genarrative-ai-game-creator-shell
|
||||
// --package=<name> target-kind=lib 时要跑的包名(配合 lib 目标使用)
|
||||
// --no-locked 传给 cargo 时不带 --locked(只对没有提交 Cargo.lock 的 crate 需要)
|
||||
// --shard-tmp-root=<path> 片专属 TMPDIR 的父目录,默认 <系统临时目录>/agc-rust-shards
|
||||
|
||||
import { spawn } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
|
||||
const shellRoot = path.resolve(scriptDirectory, '..');
|
||||
|
||||
function fail(message) {
|
||||
console.error(`[rust-shards] ${message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function parsePositiveInteger(name, rawValue) {
|
||||
if (!/^[1-9][0-9]*$/.test(rawValue)) {
|
||||
fail(`${name} must be a positive integer, received: ${rawValue}`);
|
||||
}
|
||||
|
||||
return Number(rawValue);
|
||||
}
|
||||
|
||||
const options = {
|
||||
shards: 4,
|
||||
shardIndex: undefined,
|
||||
concurrency: undefined,
|
||||
manifestPath: path.join(shellRoot, 'src-tauri', 'Cargo.toml'),
|
||||
targetKind: 'bin',
|
||||
binName: 'genarrative-ai-game-creator-shell',
|
||||
packageName: undefined,
|
||||
locked: true,
|
||||
shardTmpRoot: path.join(os.tmpdir(), 'agc-rust-shards'),
|
||||
};
|
||||
|
||||
for (const rawArgument of process.argv.slice(2)) {
|
||||
if (rawArgument === '--no-locked') {
|
||||
options.locked = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
const separatorIndex = rawArgument.indexOf('=');
|
||||
if (!rawArgument.startsWith('--') || separatorIndex === -1) {
|
||||
fail(`unexpected argument: ${rawArgument}`);
|
||||
}
|
||||
|
||||
const name = rawArgument.slice(2, separatorIndex);
|
||||
const value = rawArgument.slice(separatorIndex + 1);
|
||||
switch (name) {
|
||||
case 'shards':
|
||||
options.shards = parsePositiveInteger('--shards', value);
|
||||
break;
|
||||
case 'shard-index':
|
||||
options.shardIndex = parsePositiveInteger('--shard-index', value);
|
||||
break;
|
||||
case 'concurrency':
|
||||
options.concurrency = parsePositiveInteger('--concurrency', value);
|
||||
break;
|
||||
case 'manifest':
|
||||
options.manifestPath = path.resolve(process.cwd(), value);
|
||||
break;
|
||||
case 'target-kind':
|
||||
if (value !== 'bin' && value !== 'lib') {
|
||||
fail(`--target-kind must be bin or lib, received: ${value}`);
|
||||
}
|
||||
options.targetKind = value;
|
||||
break;
|
||||
case 'bin':
|
||||
options.binName = value;
|
||||
break;
|
||||
case 'package':
|
||||
options.packageName = value;
|
||||
break;
|
||||
case 'shard-tmp-root':
|
||||
options.shardTmpRoot = path.resolve(process.cwd(), value);
|
||||
break;
|
||||
default:
|
||||
fail(`unexpected argument: ${rawArgument}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!fs.existsSync(options.manifestPath)) {
|
||||
fail(`manifest does not exist: ${options.manifestPath}`);
|
||||
}
|
||||
|
||||
if (options.shardIndex !== undefined && options.shardIndex > options.shards) {
|
||||
fail(
|
||||
`--shard-index (${options.shardIndex}) must be within --shards (${options.shards})`,
|
||||
);
|
||||
}
|
||||
|
||||
const concurrency = options.concurrency ?? options.shards;
|
||||
const crateRoot = path.dirname(options.manifestPath);
|
||||
|
||||
function buildCargoArguments(target) {
|
||||
const cargoArguments = ['test'];
|
||||
if (options.locked) {
|
||||
cargoArguments.push('--locked');
|
||||
}
|
||||
cargoArguments.push('--manifest-path', options.manifestPath);
|
||||
if (target.kind === 'lib') {
|
||||
if (options.packageName !== undefined) {
|
||||
cargoArguments.push('-p', options.packageName);
|
||||
}
|
||||
cargoArguments.push('--lib');
|
||||
return cargoArguments;
|
||||
}
|
||||
cargoArguments.push('--bin', target.name);
|
||||
return cargoArguments;
|
||||
}
|
||||
|
||||
function formatDuration(milliseconds) {
|
||||
return `${(milliseconds / 1000).toFixed(1)}s`;
|
||||
}
|
||||
|
||||
// 编译一次,直接拿到测试可执行文件:后续每片都运行同一个二进制,不再各自调用 cargo,
|
||||
// 免得 N 个 cargo 去争 package cache 与 target 目录锁。
|
||||
function resolveTestExecutable() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const cargoArguments = buildCargoArguments({
|
||||
kind: options.targetKind,
|
||||
name: options.binName,
|
||||
});
|
||||
cargoArguments.push('--no-run', '--message-format=json');
|
||||
console.log(`[rust-shards] cargo ${cargoArguments.join(' ')}`);
|
||||
|
||||
const child = spawn('cargo', cargoArguments, {
|
||||
cwd: crateRoot,
|
||||
env: process.env,
|
||||
stdio: ['ignore', 'pipe', 'inherit'],
|
||||
});
|
||||
|
||||
let buffered = '';
|
||||
const executables = [];
|
||||
child.stdout.setEncoding('utf8');
|
||||
child.stdout.on('data', (chunk) => {
|
||||
buffered += chunk;
|
||||
const lines = buffered.split('\n');
|
||||
buffered = lines.pop() ?? '';
|
||||
for (const line of lines) {
|
||||
if (!line.startsWith('{')) {
|
||||
continue;
|
||||
}
|
||||
let message;
|
||||
try {
|
||||
message = JSON.parse(line);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
message.reason === 'compiler-artifact' &&
|
||||
typeof message.executable === 'string'
|
||||
) {
|
||||
executables.push(message.executable);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
reject(new Error(`unable to start cargo: ${error.message}`));
|
||||
});
|
||||
child.on('close', (code) => {
|
||||
if (code !== 0) {
|
||||
reject(
|
||||
new Error(
|
||||
`cargo ${cargoArguments.join(' ')} failed with exit code ${code}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const uniqueExecutables = [...new Set(executables)];
|
||||
if (uniqueExecutables.length !== 1) {
|
||||
reject(
|
||||
new Error(
|
||||
`expected exactly one test executable for the ${options.targetKind} target, found ${uniqueExecutables.length}: ${uniqueExecutables.join(', ')}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
resolve(uniqueExecutables[0]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function listTestNames(executable) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(executable, ['--list'], {
|
||||
cwd: crateRoot,
|
||||
env: process.env,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
child.stdout.setEncoding('utf8');
|
||||
child.stderr.setEncoding('utf8');
|
||||
child.stdout.on('data', (chunk) => {
|
||||
stdout += chunk;
|
||||
});
|
||||
child.stderr.on('data', (chunk) => {
|
||||
stderr += chunk;
|
||||
});
|
||||
child.on('error', (error) => {
|
||||
reject(new Error(`unable to list tests: ${error.message}`));
|
||||
});
|
||||
child.on('close', (code) => {
|
||||
if (code !== 0) {
|
||||
reject(
|
||||
new Error(
|
||||
`listing tests failed with exit code ${code}: ${stderr.trim()}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const names = [];
|
||||
for (const line of stdout.split('\n')) {
|
||||
const match = /^(.*): test$/.exec(line.trim());
|
||||
if (match !== null && match[1].length > 0) {
|
||||
names.push(match[1]);
|
||||
}
|
||||
}
|
||||
resolve(names);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function splitTestNames(testNames, shardCount) {
|
||||
const sortedNames = [...testNames].sort();
|
||||
const shards = Array.from({ length: shardCount }, () => []);
|
||||
sortedNames.forEach((testName, index) => {
|
||||
shards[index % shardCount].push(testName);
|
||||
});
|
||||
return shards;
|
||||
}
|
||||
|
||||
function assertShardsCoverEveryTest(testNames, shards) {
|
||||
const flattened = shards.flat();
|
||||
if (flattened.length !== testNames.length) {
|
||||
fail(
|
||||
`shard split covered ${flattened.length} of ${testNames.length} tests; the split rule must be exhaustive`,
|
||||
);
|
||||
}
|
||||
if (new Set(flattened).size !== flattened.length) {
|
||||
fail(
|
||||
'shard split selected the same test more than once; the split rule must be disjoint',
|
||||
);
|
||||
}
|
||||
const missing = testNames.filter((testName) => !flattened.includes(testName));
|
||||
if (missing.length > 0) {
|
||||
fail(
|
||||
`shard split is missing tests, for example: ${missing.slice(0, 5).join(', ')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function runShard(executable, shardIndex, shardCount, shardTestNames) {
|
||||
const label = `shard ${shardIndex + 1}/${shardCount}`;
|
||||
const shardTmpDirectory = path.join(
|
||||
options.shardTmpRoot,
|
||||
`shard-${shardIndex + 1}`,
|
||||
);
|
||||
fs.rmSync(shardTmpDirectory, { recursive: true, force: true });
|
||||
fs.mkdirSync(shardTmpDirectory, { recursive: true });
|
||||
const startedAt = Date.now();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn(
|
||||
executable,
|
||||
['--exact', ...shardTestNames, '--test-threads=1'],
|
||||
{
|
||||
cwd: crateRoot,
|
||||
env: {
|
||||
...process.env,
|
||||
TMPDIR: shardTmpDirectory,
|
||||
TMP: shardTmpDirectory,
|
||||
TEMP: shardTmpDirectory,
|
||||
},
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
},
|
||||
);
|
||||
|
||||
const failureLines = [];
|
||||
let inFailureList = false;
|
||||
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);
|
||||
}
|
||||
});
|
||||
child.stderr.on('data', (chunk) => {
|
||||
stderr += chunk;
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
resolve({
|
||||
label,
|
||||
ok: false,
|
||||
durationMs: Date.now() - startedAt,
|
||||
testCount: shardTestNames.length,
|
||||
failures: [`unable to start test binary: ${error.message}`],
|
||||
stderr,
|
||||
});
|
||||
});
|
||||
child.on('close', (code) => {
|
||||
resolve({
|
||||
label,
|
||||
ok: code === 0,
|
||||
durationMs: Date.now() - startedAt,
|
||||
testCount: shardTestNames.length,
|
||||
failures: failureLines,
|
||||
stderr,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function runWithConcurrency(shards, runner) {
|
||||
const results = new Array(shards.length);
|
||||
let nextIndex = 0;
|
||||
const workers = Array.from(
|
||||
{ length: Math.min(concurrency, shards.length) },
|
||||
async () => {
|
||||
while (nextIndex < shards.length) {
|
||||
const index = nextIndex;
|
||||
nextIndex += 1;
|
||||
results[index] = await runner(shards[index], index);
|
||||
}
|
||||
},
|
||||
);
|
||||
await Promise.all(workers);
|
||||
return results;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const executable = await resolveTestExecutable();
|
||||
const testNames = await listTestNames(executable);
|
||||
if (testNames.length === 0) {
|
||||
fail(
|
||||
`no tests discovered in ${options.manifestPath} (${options.targetKind})`,
|
||||
);
|
||||
}
|
||||
|
||||
const shards = splitTestNames(testNames, options.shards);
|
||||
assertShardsCoverEveryTest(testNames, shards);
|
||||
|
||||
const selectedShards =
|
||||
options.shardIndex === undefined
|
||||
? shards.map((shardTestNames, index) => ({ index, shardTestNames }))
|
||||
: [
|
||||
{
|
||||
index: options.shardIndex - 1,
|
||||
shardTestNames: shards[options.shardIndex - 1],
|
||||
},
|
||||
];
|
||||
|
||||
if (options.shardIndex === undefined) {
|
||||
console.log(
|
||||
`[rust-shards] ${testNames.length} tests, ${shards.length} shard(s), concurrency ${Math.min(concurrency, shards.length)}`,
|
||||
);
|
||||
for (const { index, shardTestNames } of selectedShards) {
|
||||
console.log(
|
||||
`[rust-shards] shard ${index + 1}/${shards.length}: ${shardTestNames.length} test(s)`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
`[rust-shards] ${testNames.length} tests, ${shards.length} shard(s), running shard ${options.shardIndex}/${shards.length} (${selectedShards[0].shardTestNames.length} test(s))`,
|
||||
);
|
||||
}
|
||||
|
||||
const results = await runWithConcurrency(
|
||||
selectedShards,
|
||||
({ index, shardTestNames }) =>
|
||||
runShard(executable, index, shards.length, shardTestNames),
|
||||
);
|
||||
|
||||
let failed = false;
|
||||
for (const result of results) {
|
||||
if (result.ok) {
|
||||
console.log(
|
||||
`[rust-shards] ${result.label} ok: ${result.testCount} test(s) in ${formatDuration(result.durationMs)}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
failed = true;
|
||||
console.error(
|
||||
`[rust-shards] ${result.label} FAILED: ${result.testCount} test(s) in ${formatDuration(result.durationMs)}`,
|
||||
);
|
||||
for (const failure of result.failures) {
|
||||
console.error(`[rust-shards] ${failure}`);
|
||||
}
|
||||
if (result.stderr.trim().length > 0) {
|
||||
console.error(
|
||||
`[rust-shards] stderr: ${result.stderr.trim().split('\n').slice(-20).join('\n[rust-shards] ')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (failed) {
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('[rust-shards] OK');
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
fail(error instanceof Error ? error.message : String(error));
|
||||
});
|
||||
@@ -8,7 +8,6 @@ export const SKILL_PACK_SCHEMA_VERSION = 'agc-skill-pack.v1';
|
||||
export const EXPECTED_SKILL_NAMES = Object.freeze([
|
||||
'agc-browser-playtest',
|
||||
'agc-client-projection',
|
||||
'agc-game-production-workflow',
|
||||
'agc-project-structure',
|
||||
'agc-web-game-development',
|
||||
'taonier-art-assets',
|
||||
|
||||
@@ -84,7 +84,6 @@ function resolveBackendTargetsFromState(
|
||||
requireAgcBackend = false,
|
||||
expectedDatabase = backendDatabase,
|
||||
expectedSpacetimeDataDir = backendSpacetimeDataDir,
|
||||
expectedRepoRoot = repoRoot,
|
||||
fallbackApiTarget = defaultApiTarget,
|
||||
} = {},
|
||||
) {
|
||||
@@ -102,25 +101,7 @@ function resolveBackendTargetsFromState(
|
||||
const hasMatchingDataDir =
|
||||
Boolean(spacetimeDataDir) &&
|
||||
spacetimeDataDir === resolve(expectedSpacetimeDataDir);
|
||||
const instanceId =
|
||||
typeof state?.instanceId === 'string' ? state.instanceId.trim() : '';
|
||||
const hasMatchingRepoRoot =
|
||||
typeof state?.repoRoot === 'string' &&
|
||||
resolve(state.repoRoot) === resolve(expectedRepoRoot);
|
||||
const hasMatchingInstance =
|
||||
Boolean(instanceId) &&
|
||||
[apiServer, spacetime, bgfilterWorker]
|
||||
.filter(Boolean)
|
||||
.every(
|
||||
(service) =>
|
||||
service.repoRoot &&
|
||||
resolve(service.repoRoot) === resolve(expectedRepoRoot) &&
|
||||
service.instanceId === instanceId,
|
||||
);
|
||||
const hasMatchingBackend =
|
||||
hasMatchingDatabase &&
|
||||
hasMatchingDataDir &&
|
||||
(!requireAgcBackend || (hasMatchingRepoRoot && hasMatchingInstance));
|
||||
const hasMatchingBackend = hasMatchingDatabase && hasMatchingDataDir;
|
||||
const canReuseState = !requireAgcBackend || hasMatchingBackend;
|
||||
const apiUrl =
|
||||
canReuseState && isActive(apiServer) && apiServer.url
|
||||
@@ -146,8 +127,6 @@ function resolveBackendTargetsFromState(
|
||||
spacetimeDataDir,
|
||||
hasMatchingDatabase,
|
||||
hasMatchingDataDir,
|
||||
hasMatchingRepoRoot,
|
||||
hasMatchingInstance,
|
||||
hasMatchingBackend,
|
||||
};
|
||||
}
|
||||
@@ -457,10 +436,6 @@ function spawnChild(command, args, options, spawnImpl = spawn) {
|
||||
const child = spawnImpl(command, args, {
|
||||
...options,
|
||||
shell: useShell,
|
||||
// npm.cmd and the Windows shell otherwise create a visible console for
|
||||
// every service in the dev stack. Their stdout/stderr is already inherited
|
||||
// by the launcher, so no separate terminal window is useful.
|
||||
windowsHide: process.platform === 'win32' ? true : options.windowsHide,
|
||||
// POSIX 下让每个长驻服务拥有独立进程组,退出时可以连同 npm、
|
||||
// Node、Cargo 及其子进程一起清理,避免残留订阅任务持续刷日志。
|
||||
detached: isPosix,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { withDefaultCargoFeatures } from './cargo-features.mjs';
|
||||
import {
|
||||
readAgcDevEndpoint,
|
||||
resolveAgcDevEndpoint,
|
||||
@@ -21,10 +20,6 @@ import {
|
||||
const appRoot = fileURLToPath(new URL('..', import.meta.url));
|
||||
const repoRoot = resolve(appRoot, '../..');
|
||||
const tauriCliPath = resolve(repoRoot, 'node_modules/@tauri-apps/cli/tauri.js');
|
||||
const AGC_DESIGN_DEBUG_ENV = 'GENARRATIVE_AGC_DESIGN_DEBUG';
|
||||
const AGC_DESIGN_DEBUG_VITE_ENV = 'VITE_GENARRATIVE_AGC_DESIGN_DEBUG';
|
||||
const designDebugEnabled =
|
||||
process.env[AGC_DESIGN_DEBUG_ENV]?.trim() === '0' ? '0' : '1';
|
||||
|
||||
function buildTauriArguments(argv, devUrl = readAgcDevEndpoint().url) {
|
||||
const args = [...argv];
|
||||
@@ -48,24 +43,6 @@ function buildTauriArguments(argv, devUrl = readAgcDevEndpoint().url) {
|
||||
];
|
||||
}
|
||||
|
||||
// `agc_cocos_execute` 与 Cocos 编辑器适配器只在 `cocos-editor-execute` feature 下
|
||||
// 注册。开发构建默认在 Windows 打开它,否则 Agent 的工具清单里根本没有该工具,
|
||||
// 只能退化成改写脚本。可用 AGC_DEV_CARGO_FEATURES(逗号分隔)覆盖,传空串即关闭。
|
||||
function readDevCargoFeatures(env = process.env) {
|
||||
const override = env.AGC_DEV_CARGO_FEATURES;
|
||||
if (override !== undefined) {
|
||||
return override
|
||||
.split(',')
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
return process.platform === 'win32' ? ['cocos-editor-execute'] : [];
|
||||
}
|
||||
|
||||
function withDevCargoFeatures(argv, features = readDevCargoFeatures()) {
|
||||
return withDefaultCargoFeatures(argv, features);
|
||||
}
|
||||
|
||||
function spawnTauriCli(argv, { env = process.env } = {}) {
|
||||
return spawnChild(process.execPath, [tauriCliPath, ...argv], {
|
||||
cwd: appRoot,
|
||||
@@ -129,16 +106,9 @@ async function runTauriDev(
|
||||
shutdownRequested.then(() => false),
|
||||
]);
|
||||
if (!prepared || shutdownSignal) return 1;
|
||||
const tauriArguments = buildTauriArguments(
|
||||
withDevCargoFeatures(argv),
|
||||
endpoint.url,
|
||||
);
|
||||
const tauriArguments = buildTauriArguments(argv, endpoint.url);
|
||||
child = spawnCli(tauriArguments, {
|
||||
env: {
|
||||
...withAgcDevEndpointEnv(endpoint),
|
||||
[AGC_DESIGN_DEBUG_ENV]: designDebugEnabled,
|
||||
[AGC_DESIGN_DEBUG_VITE_ENV]: designDebugEnabled,
|
||||
},
|
||||
env: withAgcDevEndpointEnv(endpoint),
|
||||
});
|
||||
const childResult = waitForCli(child);
|
||||
const outcome = await Promise.race([
|
||||
@@ -187,13 +157,7 @@ async function prepareFrontendDev(endpoint, { onChild, signal }) {
|
||||
const frontend = spawnChild(
|
||||
process.platform === 'win32' ? 'npm.cmd' : 'npm',
|
||||
['run', 'agc:serve'],
|
||||
{
|
||||
cwd: repoRoot,
|
||||
env: {
|
||||
...withAgcDevEndpointEnv(endpoint),
|
||||
[AGC_DESIGN_DEBUG_VITE_ENV]: designDebugEnabled,
|
||||
},
|
||||
},
|
||||
{ cwd: repoRoot, env: withAgcDevEndpointEnv(endpoint) },
|
||||
);
|
||||
onChild(frontend);
|
||||
console.log(
|
||||
@@ -229,7 +193,6 @@ export {
|
||||
isDirectModuleExecution,
|
||||
runTauriDev,
|
||||
spawnTauriCli,
|
||||
withDevCargoFeatures,
|
||||
};
|
||||
|
||||
if (isDirectModuleExecution()) {
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
# resources/plugins 由 build.rs 从 plugins/ 复制生成,属于构建产物。
|
||||
# 它在 dev 监听范围内,重新生成会让 Tauri dev 误判为源码改动而触发
|
||||
# “构建 -> 监听 -> 再构建”的自触发循环。
|
||||
resources/plugins/
|
||||
+1
-26
@@ -733,20 +733,6 @@ dependencies = [
|
||||
"error-code",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cocos-editor-bridge"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"editor-adapter-api",
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"tungstenite",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.7"
|
||||
@@ -1219,14 +1205,6 @@ version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
||||
|
||||
[[package]]
|
||||
name = "editor-adapter-api"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.16.0"
|
||||
@@ -1725,14 +1703,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "genarrative-ai-game-creator-shell"
|
||||
version = "0.1.29"
|
||||
version = "0.1.12"
|
||||
dependencies = [
|
||||
"agent-runtime-core",
|
||||
"axum",
|
||||
"base64 0.22.1",
|
||||
"chromiumoxide",
|
||||
"cocos-editor-bridge",
|
||||
"editor-adapter-api",
|
||||
"futures",
|
||||
"getrandom 0.3.4",
|
||||
"http",
|
||||
@@ -4305,7 +4281,6 @@ dependencies = [
|
||||
"cookie",
|
||||
"cookie_store",
|
||||
"encoding_rs",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
[package]
|
||||
name = "genarrative-ai-game-creator-shell"
|
||||
version = "0.1.29"
|
||||
version = "0.1.12"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[features]
|
||||
default = []
|
||||
cocos-editor = ["cocos-editor-bridge/process-discovery"]
|
||||
cocos-editor-execute = ["cocos-editor", "cocos-editor-bridge/windows-bootstrap"]
|
||||
cocos-editor-injection = ["cocos-editor-execute", "cocos-editor-bridge/windows-injection"]
|
||||
|
||||
[build-dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
@@ -22,8 +19,6 @@ ts-rs = "12.0.1"
|
||||
typed_floats = { version = "1.0.7", features = ["serde"] }
|
||||
nalgebra = { version = "0.35.0", features = ["serde-serialize"] }
|
||||
agent-runtime-core = { path = "../../../server-rs/crates/agent-runtime-core" }
|
||||
cocos-editor-bridge = { path = "../../../plugins/agc-cocos-editor/native/cocos-editor-bridge", default-features = false }
|
||||
editor-adapter-api = { path = "../../../server-rs/crates/editor-adapter-api" }
|
||||
base64 = "0.22"
|
||||
axum = "0.8"
|
||||
chromiumoxide = "0.9.1"
|
||||
|
||||
@@ -182,8 +182,6 @@ fn main() {
|
||||
);
|
||||
let manifest_path = manifest_dir.join("prompts/runtime/manifest.json");
|
||||
stage_bundled_codex_cli(&manifest_dir);
|
||||
stage_plugin_workspace(&manifest_dir);
|
||||
stage_cocos_editor_payload(&manifest_dir);
|
||||
let compiled = runtime_prompt_bundle::compile_manifest(&manifest_path)
|
||||
.unwrap_or_else(|error| panic!("Prompt Bundle 编译失败:{error}"));
|
||||
validate_seed_task_catalog(&compiled);
|
||||
@@ -205,146 +203,3 @@ fn main() {
|
||||
}
|
||||
tauri_build::build()
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn stage_cocos_editor_payload(manifest_dir: &std::path::Path) {
|
||||
if std::env::var_os("CARGO_FEATURE_COCOS_EDITOR_INJECTION").is_none() {
|
||||
return;
|
||||
}
|
||||
let out_dir = std::path::PathBuf::from(std::env::var_os("OUT_DIR").expect("OUT_DIR"));
|
||||
let profile_dir = out_dir
|
||||
.ancestors()
|
||||
.find(|path| path.file_name().is_some_and(|name| name == "build"))
|
||||
.and_then(|build_dir| build_dir.parent())
|
||||
.expect("AGC Cargo profile directory not found");
|
||||
let candidates = [
|
||||
profile_dir.join("deps/cocos_editor_bridge.dll"),
|
||||
profile_dir.join("cocos_editor_bridge.dll"),
|
||||
];
|
||||
let source = candidates
|
||||
.iter()
|
||||
.find(|path| path.is_file())
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"Cocos bridge native payload 未构建:{}",
|
||||
candidates
|
||||
.iter()
|
||||
.map(|p| p.display().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(";")
|
||||
)
|
||||
});
|
||||
for destination in [
|
||||
// 插件工作区里的 payload 是开发态与打包态的唯一真源。
|
||||
manifest_dir
|
||||
.join("../../../plugins/agc-cocos-editor/native/payload/cocos-editor-bridge.dll"),
|
||||
// 随包资源目录与 tauri.windows.conf.json 的 `resources/plugins` 映射保持一致。
|
||||
manifest_dir
|
||||
.join("resources/plugins/agc-cocos-editor/native/payload/cocos-editor-bridge.dll"),
|
||||
] {
|
||||
std::fs::create_dir_all(destination.parent().expect("payload resource parent"))
|
||||
.expect("创建 Cocos bridge payload 目录失败");
|
||||
std::fs::copy(source, &destination).expect("复制 Cocos bridge native payload 失败");
|
||||
}
|
||||
println!("cargo:rerun-if-changed={}", source.display());
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn stage_cocos_editor_payload(_manifest_dir: &std::path::Path) {}
|
||||
|
||||
/// 把 `plugins/` 工作区里的插件包随包映射到应用资源目录。
|
||||
///
|
||||
/// 只复制插件运行需要的清单、入口、面板和 native payload,不复制 native 源码、
|
||||
/// Cargo target 目录或 node_modules。
|
||||
#[cfg(windows)]
|
||||
fn stage_plugin_workspace(manifest_dir: &std::path::Path) {
|
||||
let repo_root = manifest_dir
|
||||
.parent()
|
||||
.and_then(|app_root| app_root.parent())
|
||||
.and_then(|apps_dir| apps_dir.parent())
|
||||
.expect("AGC 应用必须位于仓库 apps 目录下")
|
||||
.to_path_buf();
|
||||
let workspace = repo_root.join("plugins");
|
||||
let destination_root = manifest_dir.join("resources/plugins");
|
||||
std::fs::create_dir_all(&destination_root).expect("创建插件资源目录失败");
|
||||
let entries = match std::fs::read_dir(&workspace) {
|
||||
Ok(entries) => entries,
|
||||
Err(_) => return,
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let plugin_root = entry.path();
|
||||
if !plugin_root.is_dir() || !plugin_root.join("plugin.json").is_file() {
|
||||
continue;
|
||||
}
|
||||
let name = entry.file_name();
|
||||
let destination = destination_root.join(&name);
|
||||
copy_plugin_file(
|
||||
&plugin_root.join("plugin.json"),
|
||||
&destination.join("plugin.json"),
|
||||
);
|
||||
for relative in [
|
||||
std::path::PathBuf::from("src"),
|
||||
std::path::PathBuf::from("panels"),
|
||||
std::path::PathBuf::from("native/payload"),
|
||||
] {
|
||||
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;
|
||||
};
|
||||
if std::fs::read(destination).is_ok_and(|existing| existing == bytes) {
|
||||
return;
|
||||
}
|
||||
if let Some(parent) = destination.parent() {
|
||||
std::fs::create_dir_all(parent).expect("创建插件资源目录失败");
|
||||
}
|
||||
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,
|
||||
Err(_) => return,
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let target = destination.join(entry.file_name());
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
let name = entry.file_name();
|
||||
let name = name.to_string_lossy();
|
||||
if matches!(name.as_ref(), "target" | "node_modules" | ".git") {
|
||||
continue;
|
||||
}
|
||||
std::fs::create_dir_all(&target).expect("创建插件资源目录失败");
|
||||
copy_plugin_tree(&path, &target);
|
||||
} else {
|
||||
// 测试文件不随包分发。
|
||||
let name = entry.file_name();
|
||||
let name = name.to_string_lossy();
|
||||
if name.contains(".test.") {
|
||||
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;
|
||||
}
|
||||
std::fs::create_dir_all(destination.parent().expect("插件资源父目录"))
|
||||
.expect("创建插件资源目录失败");
|
||||
std::fs::copy(source, destination).expect("复制插件资源失败");
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn stage_plugin_workspace(_manifest_dir: &std::path::Path) {}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "main",
|
||||
"description": "AI 游戏创作主窗口允许读取系统剪贴板图片,用于粘贴素材附件;允许弹出原生打开/保存对话框用于素材上传与导出。",
|
||||
"description": "AI 游戏创作主窗口允许读取系统剪贴板图片,用于粘贴素材附件。",
|
||||
"windows": ["client"],
|
||||
"permissions": [
|
||||
"clipboard-manager:allow-read-image",
|
||||
@@ -21,7 +21,6 @@
|
||||
]
|
||||
},
|
||||
"opener:default",
|
||||
"dialog:allow-open",
|
||||
"dialog:allow-save"
|
||||
"dialog:allow-open"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
当前阶段:系统架构。明确系统清单、职责边界、依赖和数据归属。
|
||||
@@ -1,6 +0,0 @@
|
||||
共享过程文件(如需维护,请使用这些相对路径):
|
||||
- project/analysis.md
|
||||
- project/决策台账.md
|
||||
- project/dialog.md
|
||||
不要把正式产物写在工作区根目录,也不要等审批失败后再迁移。
|
||||
阶段审批工具:当你判断本阶段必需产物已完成时,必须提交阶段审批。用户批准后 Runtime 自动进入下一阶段;你不能自行切换阶段。
|
||||
@@ -1 +0,0 @@
|
||||
当前阶段:概念设计。明确游戏是什么、不是什么,并形成概念设计产物。
|
||||
@@ -1,4 +0,0 @@
|
||||
顾问阶段不需要继续自主推动项目或主动安排下一步;遵照用户的具体指示行动。
|
||||
根据用户指示回答问题、读取相关文档、修改工作区文件,并说明改动可能影响的已有产物。
|
||||
涉及方向性变化或多个可行方案时,先向用户说明影响并等待用户决定;不要替用户做决定。
|
||||
顾问阶段没有下一层,也不需要提交阶段审批。
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user