From 3fba76fd6ed33684983930c545d04911dc4c94cd Mon Sep 17 00:00:00 2001 From: suzmii Date: Mon, 28 Sep 2026 10:54:29 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8C=89=E5=AE=A1=E8=AE=A1=E7=BB=93=E8=AE=BA?= =?UTF-8?q?=E6=94=B6=E5=8F=A3=20M3=EF=BC=9ACI=20=E6=8E=A5=E7=BA=BF?= =?UTF-8?q?=E3=80=81=E5=B9=B6=E5=8F=91=E5=8F=A3=E5=BE=84=E3=80=81feature?= =?UTF-8?q?=20=E5=8D=95=E4=B8=80=E6=8E=A8=E5=AF=BC=E4=B8=8E=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E5=AF=B9=E9=BD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CI:把 bundled-resources:test(13 条,跨平台、注入假执行器)接进 ai-game-creator-shell:check:web,Linux CI 即可覆盖准备步骤逻辑 - 并发:按「无实际场景不做并发支持」定论——不加锁,但替换失败改为可读错误并保留已生成的 staging;技术方案 §4.3 与风险表同步改写 - feature:新增 cargo-features.resolveEditorFeatures 作为唯一推导入口(env > 命令行 --features > 平台默认),dev 与发布入口的 cargo 参数、随包资源准备步骤共用同一集合;--no-bundle 也执行 staging(app 构建本身需要随包资源),只跳过总号发布 - 文档:pitfalls 的校验收口限定到 source 派生内容;里程碑验收清单按实况修正(命令 --、prepared 只查存在性、Windows 侧已复跑) - 验证:准备步骤 13/13、发布入口 39/39、dev 入口 12/12、声明门禁一致 --- .../scripts/build-release.mjs | 9 ++-- .../scripts/build-release.test.mjs | 14 ++++-- .../scripts/cargo-features.mjs | 45 +++++++++++++++++++ .../scripts/prepare-bundled-resources.mjs | 9 +++- .../scripts/start-tauri-dev.mjs | 15 ++----- .../build_support/package-layout.generated.rs | 2 +- ...¼–辑器分支产物归位与症状层补丁清理-2026-09-26.md | 6 +-- docs/project-memory/shared-memory/pitfalls.md | 2 +- package.json | 2 +- 9 files changed, 80 insertions(+), 24 deletions(-) diff --git a/apps/ai-game-creator-shell/scripts/build-release.mjs b/apps/ai-game-creator-shell/scripts/build-release.mjs index a10f95ee8..632356a94 100644 --- a/apps/ai-game-creator-shell/scripts/build-release.mjs +++ b/apps/ai-game-creator-shell/scripts/build-release.mjs @@ -11,6 +11,7 @@ import { } from './agc-global-version.mjs'; import { defaultEditorFeatures, + resolveEditorFeatures, withDefaultCargoFeatures, } from './cargo-features.mjs'; import { @@ -435,11 +436,11 @@ function writeChannelConfigFile(channel, target, includeNodeRuntime = false) { /// 随包资源必须在打包工具之前生成:构建脚本只做只读校验,不再生成。 export function stageBundledResources( target, - { prepare = prepareBundledResources } = {}, + { prepare = prepareBundledResources, features } = {}, ) { const summaries = prepare({ target, - features: new Set(defaultEditorFeatures(target)), + features: new Set(features ?? defaultEditorFeatures(target)), profile: 'release', log: (line) => console.log(`[ai-game-creator-shell] ${line}`), }); @@ -465,10 +466,12 @@ export function runTauriBuild( } const tauriArguments = buildTauriBuildArguments(args, context.target); const { channel, target } = context; + const features = resolveEditorFeatures({ argv: args, target }); if (!args.includes('--no-bundle')) { stageRuntime(target); - stageBundled(target); } + // app 构建本身就需要随包资源,因此 --no-bundle 也要 staging;只有总号发布是打包专属。 + stageBundled(target, { features }); const configPath = writeChannelConfigFile( channel, target, diff --git a/apps/ai-game-creator-shell/scripts/build-release.test.mjs b/apps/ai-game-creator-shell/scripts/build-release.test.mjs index 3d046d9d8..50525dd34 100644 --- a/apps/ai-game-creator-shell/scripts/build-release.test.mjs +++ b/apps/ai-game-creator-shell/scripts/build-release.test.mjs @@ -839,12 +839,20 @@ test('release stages Node before Tauri and injects its resource mapping only for }, }); assert.deepEqual(events, ['stage', 'bundled', 'build']); + events.length = 0; runTauriBuild(['--no-bundle', '--target', windowsTarget], context, { stageRuntime() { - assert.fail('no-bundle must not stage resources'); + assert.fail('no-bundle must not stage node runtime'); }, - stageBundled() { - assert.fail('no-bundle must not stage bundled resources'); + stageBundled(target, options) { + // app 构建本身就需要随包资源,因此 --no-bundle 也要 staging, + // 且必须拿到与 cargo 相同的 feature 集(避免「cargo 开、staging 没开」)。 + assert.equal(target, windowsTarget); + assert.ok( + options?.features?.length > 0, + 'bundled staging 必须收到 feature 集', + ); + events.push('bundled'); }, spawn(_binary, args) { const config = JSON.parse( diff --git a/apps/ai-game-creator-shell/scripts/cargo-features.mjs b/apps/ai-game-creator-shell/scripts/cargo-features.mjs index 515432f5b..c8303d42b 100644 --- a/apps/ai-game-creator-shell/scripts/cargo-features.mjs +++ b/apps/ai-game-creator-shell/scripts/cargo-features.mjs @@ -22,3 +22,48 @@ export function defaultEditorFeatures(target) { ? ['cocos-editor-execute', 'unity-editor-execute', 'godot-editor-execute'] : []; } + +/// 单一声明式解析:本次构建实际生效的编辑器 feature 集。 +/// 解析顺序:环境变量 `AGC_DEV_CARGO_FEATURES` > 命令行 `--features` > 目标平台默认值。 +/// dev 入口、发布入口的 cargo 参数与随包资源准备步骤都必须用这里的返回值, +/// 否则会出现「cargo 开了 feature、staging 没开(或反之)」的窗口。 +export function resolveEditorFeatures({ + argv = [], + target, + env = process.env, +} = {}) { + // 顺序:环境变量(dev 的显式覆盖)> 命令行 --features > 目标平台默认值。 + const fromEnv = String(env.AGC_DEV_CARGO_FEATURES ?? '').trim(); + if (fromEnv) { + return fromEnv + .split(',') + .map((name) => name.trim()) + .filter(Boolean); + } + const fromArgv = featuresFromArgv(argv); + if (fromArgv) { + return fromArgv; + } + return defaultEditorFeatures(target); +} + +/// 从 argv 里解析 `--features a,b` / `--features=a,b`;没有则返回 null。 +export function featuresFromArgv(argv = []) { + for (let index = 0; index < argv.length; index += 1) { + const value = String(argv[index]); + if (value.startsWith('--features=')) { + return value + .slice('--features='.length) + .split(',') + .map((name) => name.trim()) + .filter(Boolean); + } + if (value === '--features' && argv[index + 1] !== undefined) { + return String(argv[index + 1]) + .split(',') + .map((name) => name.trim()) + .filter(Boolean); + } + } + return null; +} diff --git a/apps/ai-game-creator-shell/scripts/prepare-bundled-resources.mjs b/apps/ai-game-creator-shell/scripts/prepare-bundled-resources.mjs index 674e668e4..44a83c92e 100755 --- a/apps/ai-game-creator-shell/scripts/prepare-bundled-resources.mjs +++ b/apps/ai-game-creator-shell/scripts/prepare-bundled-resources.mjs @@ -498,7 +498,14 @@ function stageAtomically(unitPath, builder) { throw error; } rmSync(unitPath, { recursive: true, force: true }); - renameSync(stagingPath, unitPath); + try { + renameSync(stagingPath, unitPath); + } catch (error) { + // 并发调用未做加锁(见技术方案 §4.3):这里只保证失败可读、且不丢已经生成好的 staging。 + fail( + `替换 ${unitPath} 失败(${error.code ?? error.message}):同一资源目录可能正被另一个准备步骤进程写入;staging 已保留在 ${stagingPath},确认没有并发进程后重试`, + ); + } } function prepareCodex({ diff --git a/apps/ai-game-creator-shell/scripts/start-tauri-dev.mjs b/apps/ai-game-creator-shell/scripts/start-tauri-dev.mjs index 47a6eaeb3..6c82e54cb 100644 --- a/apps/ai-game-creator-shell/scripts/start-tauri-dev.mjs +++ b/apps/ai-game-creator-shell/scripts/start-tauri-dev.mjs @@ -3,7 +3,7 @@ import { fileURLToPath } from 'node:url'; import { buildLocalRustProcessEnv } from '../../../scripts/dev.mjs'; import { - defaultEditorFeatures, + resolveEditorFeatures, withDefaultCargoFeatures, } from './cargo-features.mjs'; import { @@ -57,18 +57,11 @@ function buildTauriArguments(argv, devUrl = readAgcDevEndpoint().url) { // 开发和发行构建使用同一平台编辑器 feature 集合。 // 可用 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 defaultEditorFeatures(process.platform); +function readDevCargoFeatures(argv = []) { + return resolveEditorFeatures({ argv, target: process.platform }); } -function withDevCargoFeatures(argv, features = readDevCargoFeatures()) { +function withDevCargoFeatures(argv, features = readDevCargoFeatures(argv)) { return withDefaultCargoFeatures(argv, features); } diff --git a/apps/ai-game-creator-shell/src-tauri/build_support/package-layout.generated.rs b/apps/ai-game-creator-shell/src-tauri/build_support/package-layout.generated.rs index 636ba82be..e614b8567 100644 --- a/apps/ai-game-creator-shell/src-tauri/build_support/package-layout.generated.rs +++ b/apps/ai-game-creator-shell/src-tauri/build_support/package-layout.generated.rs @@ -1,7 +1,7 @@ // @generated by apps/ai-game-creator-shell/scripts/check-package-layout.mjs // 来源:build_support/package-layout.json。不要手工编辑本文件。 // 修改随包资源布局请编辑声明文件,然后运行 -// npm run agc:package-layout:sync(在仓库根目录) +// npm run agc:bundled-resources:sync(在仓库根目录) // 门禁会校验两者一致(npm run agc:typecheck 链内含 check-package-layout.mjs)。 pub const DECLARATION_SCHEMA: &str = "agc-package-layout.v1"; diff --git a/docs/project-memory/plans/【里程碑】AGC编辑器分支产物归位与症状层补丁清理-2026-09-26.md b/docs/project-memory/plans/【里程碑】AGC编辑器分支产物归位与症状层补丁清理-2026-09-26.md index 8c0ca00b1..30f2b39e4 100644 --- a/docs/project-memory/plans/【里程碑】AGC编辑器分支产物归位与症状层补丁清理-2026-09-26.md +++ b/docs/project-memory/plans/【里程碑】AGC编辑器分支产物归位与症状层补丁清理-2026-09-26.md @@ -13,7 +13,7 @@ - 准备步骤(`scripts/prepare-bundled-resources.mjs`)按声明执行 `powershell.exe -File build.ps1` 与 `cargo build -p … --target …`,用内容指纹跳过未变化的步骤,校验必需产物齐全后才复制;命中指纹且产物齐全时零写入。 - 构建脚本删除了三处产物生成与整棵树复制(`prepare_unity_editor_helper`、`prepare_godot_editor_extension`、`stage_cocos_editor_payload`、`stage_build_generated_plugin_payloads` 及其辅助函数,共减少约 220 行),只保留只读校验:源码派生内容逐文件比对、已准备产物存在性、Godot 随包库沿用既有深度校验(`godot_bundle::validate`)。Godot 随包文件清单改由声明提供(单一来源)。 - `.taurignore` 的两份 staging 条目已删除(构建期不再写 `resources/plugins`,无需忽略)。 -- 准备步骤的 Windows 侧行为**尚未在真机验证**:本机 macOS 只能跑通编译、声明门禁与用例(命令执行器在用例中注入假实现)。 +- 准备步骤的 Windows 侧命令路径已随系统性审计在真机复跑(powershell/cargo 两条路径、产物归位与幂等均已验证)。 ## 目标 @@ -57,13 +57,13 @@ 2. **构建期不再写随包资源** - 取 `src-tauri/resources` 全量快照(相对路径/大小/mtime/sha256)→ `touch apps/ai-game-creator-shell/src-tauri/build.rs` → 再 `cargo build` → 两次快照必须逐项一致。 3. **构建新鲜度**:源码不变时连续两次 `cargo build --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml`,第二次应为秒级 `Finished`,且不再出现 `Compiling genarrative-ai-game-creator-shell`。 -4. **打包一致性**:出一次 Windows 安装包,核对包内 `plugins/` 下三种编辑器分支产物的路径与 sha256 与迁移前一致;`cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --no-run`(会触发构建脚本的只读校验)必须通过。 +4. **打包一致性**:出一次 Windows 安装包,核对包内 `plugins/` 下三种编辑器分支产物的路径与 sha256 与迁移前一致;`cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --no-run` 必须通过(会触发只读校验,所以要先跑过准备步骤)。 5. **客户端启动**:进入 Unity/Godot/Cocos 编辑器分支各一次,确认对应 helper/扩展被加载,没有「缺少组件」类提示。 6. **边界(负例)** - 删掉 `plugins/agc-unity-editor/dotnet/publish/` 且让工具链不可用后打包:准备步骤必须给出明确失败原因(缺工具链/缺产物),而不是静默产出缺组件的包。 - 删掉 `target/agc-resource-staging.json` 再跑准备步骤:预期重新生成,产物内容不变(丢缓存只多一次哈希)。 - 删掉 `plugins/agc-unity-editor/dotnet/publish/win-x64/.agc-source.sha256` 后重跑准备步骤:预期重新执行 dotnet publish,而不是复用旧产物。 - - 手工改 `resources/**` 一个字节后 `cargo build`:只读校验必须失败(源码派生内容逐文件比对)。 + - 手工改 `resources/**` 一个字节后 `cargo build`:只读校验必须失败(该规则覆盖 source 派生内容;prepared 产物只查存在性,见排障经验)。 记录:把每步命令、关键输出与结论贴回本里程碑或对应 PR;未通过项回到主规范 §9 记为未决问题。 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 9a319d877..ab6dd2640 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -15,7 +15,7 @@ ## 2026-09-27 随包资源的写入方按产物来源分界:源码派生直接复制,需工具链的先由准备步骤产出 - **写法**:新增随包内容先判断来源——能从仓库源码复制就写进 `build_support/package-layout.json` 的 `subdirectories`(`origin: source`);需要外部工具链或同一次 cargo 构建才能产出的,写成 `origin: prepared` / `libraryStaging` / `nativePayloads`,并在 `plugins.prepareSteps` 里声明要跑的程序、工作目录、指纹与必需产物——**不要写进构建脚本**(构建脚本自 M3 起只做只读校验,不再生成任何随包资源)。 -- **校验口径**:`origin: source` 的内容在构建期会与仓库源码逐文件比对(插件清单 + 逐文件 sha256 + 整树符号链接),手改 `resources/**` 会被 `cargo build` 直接拒绝;`prepared` 只查存在性,Godot 随包库额外跑 `godot_bundle::validate` 的深度校验。 +- **校验口径**:`origin: source` 的内容在构建期会与仓库源码逐文件比对(插件清单 + 逐文件 sha256 + 整树符号链接),手改这部分会被 `cargo build` 直接拒绝;`origin: prepared` 只查存在性(Godot 随包库额外跑 `godot_bundle::validate`),手改 prepared 产物不会被拒,要改就改准备步骤的来源或声明。 - **准备步骤指纹**:声明了指纹的步骤(Unity)命中后不会重跑工具链,改 `plugins/**` 源码即失效;指纹戳文件(`publish/win-x64/.agc-source.sha256`)删掉只会多跑一次构建。`resources/plugins` 由准备步骤拥有,不要手工往里放文件。 ## 2026-09-27 AGC 随包资源的布局只能改声明文件,生成物由门禁锁死 diff --git a/package.json b/package.json index 250192b8f..e2b600db2 100644 --- a/package.json +++ b/package.json @@ -195,7 +195,7 @@ "agent-runtime-core:check": "cargo test --manifest-path server-rs/crates/agent-runtime-core/Cargo.toml", "agent-runtime-orchestration:check": "cargo test --manifest-path server-rs/crates/agent-runtime-orchestration/Cargo.toml", "ai-game-creator-shell:typecheck": "npm --prefix apps/ai-game-creator-shell run typecheck", - "ai-game-creator-shell:check:web": "npm run ai-game-creator-shell:typecheck && npm run test -- apps/ai-game-creator-shell/tests", + "ai-game-creator-shell:check:web": "npm run ai-game-creator-shell:typecheck && npm --prefix apps/ai-game-creator-shell run bundled-resources:test && npm run test -- apps/ai-game-creator-shell/tests", "ai-game-creator-shell:check:rust:crates": "npm run agent-runtime-core:check && npm run agent-runtime-orchestration:check && cargo test --locked -p platform-llm --manifest-path server-rs/Cargo.toml && cargo test --locked -p shared-contracts --manifest-path server-rs/Cargo.toml game_creation_app", "ai-game-creator-shell:check:rust:shell": "node apps/ai-game-creator-shell/scripts/run-rust-shell-test-shards.mjs --shards=4", "ai-game-creator-shell:check:rust": "npm run ai-game-creator-shell:check:rust:crates && npm run ai-game-creator-shell:check:rust:shell",