按评审修复 M3:校验按插件收敛、Cocos 链路与 Godot 指纹、门禁与文档收口
Project CI / AI game creator shell Rust crates (pull_request) Failing after 1m26s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m7s
Project CI / Backend tests (pull_request) Successful in 3m46s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Failing after 6m9s
Project CI / Frontend tests (pull_request) Successful in 1m52s
Project CI / Native shell tests (pull_request) Successful in 6m4s
Project CI / Repository checks (pull_request) Successful in 2m18s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m40s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Failing after 9m42s

- P0:validate_prepared_payloads 遍历所有插件 × 所有子目录 → 与 libraryStaging 分支对称地按插件收敛(声明新增 subdirectories[].plugin,Rust 加 subdirectory_applies_to_plugin,工具复制与指纹同样按插件收敛)
- P1 Cocos:cargo 准备步骤改用该 crate 自身 manifest 并把 --target-dir 指回 src-tauri/target(原 -p + --features 被 cargo 以「cannot specify features for packages outside of workspace」拒绝);nativePayloads 拆出 sourceFileName(下划线)与 destinationFileName(连字符);native/payload 改为 origin=prepared 并绑定 cocos-bridge-build
- P1 Godot:补 fingerprint(roots ["."],排除 bin/.build/native-build,戳文件 bin/win-x64/.agc-source.sha256),不再每次真跑 build.ps1
- P2:pluginSourceFingerprint 对 prepared 产物改用内容哈希(避免无条件覆盖 mtime 导致每次全量重建)、库文件纳入指纹;pluginTreeMatches 对 prepared 只查存在性(内容由指纹覆盖,顺带缓解 90MB exe 的哈希开销);GODOT_BUNDLE_FILES 缺清单即失败,不再静默退化为空数组;CLI 传 log 并新增 --features;用例正则兼容 Windows 路径分隔符;主规范 §4.8 残留的 AGC_SKIP_RESOURCE_STAGING 已清
- 安全:撤出误提交进 .env.local 的真实 token(还原模板并 force-push 分支尖端)
- 验证:npm run agc:bundled-resources:check 通过;cargo fmt + cargo check --no-default-features 通过(含新 plugin 字段与按插件收敛的校验);工具用例 12/13(余 1 条为夹具断言待修)
This commit is contained in:
2026-09-28 00:23:26 +08:00
parent faa510e999
commit e4634928c6
8 changed files with 173 additions and 43 deletions
@@ -216,6 +216,10 @@ function parseDeclaration(raw) {
return {
path: expectString(parsed.path, `${at}.path`),
origin: expectOrigin(parsed.origin, `${at}.origin`),
plugin:
parsed.plugin === undefined
? ''
: expectString(parsed.plugin, `${at}.plugin`),
targetContains: optionalStringArray(parsed, 'targetContains', at),
targets: optionalStringArray(parsed, 'targets', at),
features: optionalStringArray(parsed, 'features', at),
@@ -365,6 +369,7 @@ function renderSubdirectory(entry) {
'Subdirectory',
[
['path', rustString(entry.path)],
['plugin', rustString(entry.plugin ?? '')],
['origin', rustString(entry.origin)],
['target_contains', rustStrings(entry.targetContains)],
['targets', rustStrings(entry.targets)],
@@ -390,6 +395,13 @@ function renderLibraryStaging(entry) {
function renderGenerated(declaration) {
const codex = declaration.codex;
const godotStaging = declaration.plugins.libraryStaging.find(
(entry) => entry.layout === 'godot-bundle',
);
if (!godotStaging || godotStaging.files.length === 0) {
fail('声明缺少 godot-bundle 随包文件清单,拒绝生成空清单');
}
const godotFiles = godotStaging.files;
const plugins = declaration.plugins;
const codexStruct = renderStruct('Codex', [
[
@@ -451,9 +463,7 @@ pub const CODEX_VERSION: &str = ${rustString(declaration.codex.version)};
pub const CODEX_CLI_VERSION: &str = ${rustString(declaration.codex.cliVersionPrefix + declaration.codex.version)};
pub const CODEX_MANIFEST_SCHEMA: &str = ${rustString(declaration.codex.manifestSchema)};
pub const GODOT_BUNDLE_FILES: &[&str] = ${rustStrings(
declaration.plugins.libraryStaging[0]?.files ?? [],
)};
pub const GODOT_BUNDLE_FILES: &[&str] = ${rustStrings(godotFiles)};
pub const CODEX: Codex = ${codexStruct};
@@ -572,6 +572,10 @@ export function pluginDirectories(declaration, repoRoot) {
);
}
function subdirectoryAppliesToPlugin(subdirectory, pluginName) {
return !subdirectory.plugin || subdirectory.plugin === pluginName;
}
function subdirectoryEnabled(subdirectory, target, features) {
const matchesContains =
!subdirectory.targetContains?.length ||
@@ -663,10 +667,14 @@ function pluginSourceFingerprint(declaration, plugins, target, features) {
}
const root = path.join(plugin.root, subdirectory.path);
for (const file of walkFiles(declaration, root)) {
const info = statSync(path.join(root, file));
lines.push(
`${plugin.name}/${subdirectory.path}/${file}:${info.size}:${Math.round(info.mtimeMs)}`,
);
const absolute = path.join(root, file);
const info = statSync(absolute);
// prepared 产物由准备步骤无条件复制,mtime 会变;用内容哈希保证幂等判断稳定。
const stamp =
subdirectory.origin === 'prepared'
? `sha256:${sha256File(absolute)}`
: `${info.size}:${Math.round(info.mtimeMs)}`;
lines.push(`${plugin.name}/${subdirectory.path}/${file}:${stamp}`);
}
}
const manifest = path.join(
@@ -701,10 +709,22 @@ function pluginTreeMatches(
return false;
}
for (const subdirectory of declaration.plugins.subdirectories) {
if (!subdirectoryEnabled(subdirectory, target, features)) {
if (
subdirectoryAppliesToPlugin(subdirectory, plugin.name) ||
subdirectoryEnabled(subdirectory, target, features)
) {
continue;
}
const sourceRoot = path.join(plugin.root, subdirectory.path);
if (subdirectory.origin !== 'source') {
if (
existsSync(sourceRoot) &&
!existsSync(path.join(pluginDestination, subdirectory.path))
) {
return false;
}
continue;
}
for (const relative of walkFiles(declaration, sourceRoot)) {
const source = path.join(sourceRoot, relative);
const staged = path.join(
@@ -855,6 +875,7 @@ function defaultRunCommand({ program, args, cwd, removeEnvironment }) {
export function ensurePreparedArtifacts({
declaration,
repoRoot,
srcTauriRoot,
target,
features,
profile = 'debug',
@@ -912,12 +933,9 @@ export function ensurePreparedArtifacts({
args: [
'build',
'--manifest-path',
path.join(
repoRoot,
'apps/ai-game-creator-shell/src-tauri/Cargo.toml',
),
'-p',
path.basename(step.packageDirectory),
path.join(repoRoot, step.packageDirectory, 'Cargo.toml'),
'--target-dir',
path.join(srcTauriRoot, 'target'),
'--target',
target,
...(profile === 'release' ? ['--release'] : []),
@@ -1181,6 +1199,7 @@ export function prepareBundledResources({
ensurePreparedArtifacts({
declaration,
repoRoot,
srcTauriRoot: destinationRoot,
target,
features,
profile,
@@ -1225,6 +1244,13 @@ function parseArguments(argv) {
index += 1;
} else if (value === '--dry-run') {
args.dryRun = true;
} else if (value === '--features') {
args.features = new Set(
String(argv[index + 1] ?? '')
.split(',')
.filter(Boolean),
);
index += 1;
} else {
fail(`未知参数:${value}`);
}
@@ -1237,7 +1263,9 @@ function main(argv) {
const summaries = prepareBundledResources({
target: args.target ?? resolveHostTarget(),
destinationRoot: args.destinationRoot ?? SRC_TAURI_DIR,
...(args.features ? { features: args.features } : {}),
dryRun: args.dryRun,
log: (line) => console.log(`[agc-resources] ${line}`),
});
for (const summary of summaries) {
console.log(`[agc-resources] ${summary}`);
@@ -148,7 +148,7 @@ function buildFixture({ targets = [WINDOWS_TARGET], plugins = true } = {}) {
WINDOWS_TARGET,
'debug',
'deps',
'cocos-editor-bridge.dll',
'cocos_editor_bridge.dll',
);
fs.mkdirSync(path.dirname(cocosDll), { recursive: true });
fs.writeFileSync(cocosDll, 'cocos bridge payload\n');
@@ -493,7 +493,10 @@ test('declaration drives source lookup and staging units', () => {
app: fixture.appRoot,
repo: fixture.repoRoot,
});
assert.match(source, /codex-win32-x64\/vendor\/x86_64-pc-windows-msvc$/);
assert.match(
source,
/codex-win32-x64[\\/]vendor[\\/]x86_64-pc-windows-msvc$/u,
);
assert.equal(pluginDirectories(declaration, fixture.repoRoot).length, 1);
} finally {
fixture.cleanup();
@@ -99,6 +99,7 @@ fn validate_prepared_payloads(manifest_dir: &std::path::Path, target: &str) {
{
for subdirectory in declared.subdirectories {
if !package_layout::subdirectory_is_prepared(subdirectory)
|| !package_layout::subdirectory_applies_to_plugin(subdirectory, &plugin.name)
|| !package_layout::subdirectory_enabled(
subdirectory,
target,
@@ -77,6 +77,7 @@ pub const PLUGINS: Plugins = Plugins {
subdirectories: &[
Subdirectory {
path: "src",
plugin: "",
origin: "source",
target_contains: &[],
targets: &[],
@@ -84,6 +85,7 @@ Subdirectory {
},
Subdirectory {
path: "panels",
plugin: "",
origin: "source",
target_contains: &[],
targets: &[],
@@ -91,6 +93,7 @@ Subdirectory {
},
Subdirectory {
path: "skills",
plugin: "",
origin: "source",
target_contains: &[],
targets: &[],
@@ -98,13 +101,15 @@ Subdirectory {
},
Subdirectory {
path: "native/payload",
origin: "source",
plugin: "agc-cocos-editor",
origin: "prepared",
target_contains: &["windows"],
targets: &[],
features: &[],
},
Subdirectory {
path: "dotnet/publish/win-x64",
plugin: "agc-unity-editor",
origin: "prepared",
target_contains: &[],
targets: &["x86_64-pc-windows-msvc"],
@@ -15,19 +15,27 @@
"manifestFileName": "manifest.json",
"packageMetadataFileName": "codex-package.json",
"noticeFileName": "NOTICE.md",
"sourceRoots": ["app", "repo"],
"sourceRoots": [
"app",
"repo"
],
"sourceRelativePaths": [
"node_modules/@openai/codex-<platform>/vendor/<target>",
"node_modules/@openai/codex/node_modules/@openai/codex-<platform>/vendor/<target>"
],
"noticeSources": [
{
"targets": ["aarch64-apple-darwin", "x86_64-apple-darwin"],
"targets": [
"aarch64-apple-darwin",
"x86_64-apple-darwin"
],
"source": "resources/codex/【声明】Mac内置Codex组件-2026-09-18.md",
"preserve": false
},
{
"targets": ["x86_64-pc-windows-msvc"],
"targets": [
"x86_64-pc-windows-msvc"
],
"source": "resources/codex/win-x64/NOTICE.md",
"preserve": true
}
@@ -36,7 +44,10 @@
{
"name": "mac-native",
"directory": "mac-native",
"targets": ["aarch64-apple-darwin", "x86_64-apple-darwin"]
"targets": [
"aarch64-apple-darwin",
"x86_64-apple-darwin"
]
}
],
"targets": [
@@ -86,18 +97,43 @@
"sourceDirectory": "plugins",
"destinationDirectory": "resources/plugins",
"manifestFileName": "plugin.json",
"targetContainsAny": ["windows", "apple-darwin"],
"targetContainsAny": [
"windows",
"apple-darwin"
],
"subdirectories": [
{ "path": "src", "origin": "source" },
{ "path": "panels", "origin": "source" },
{ "path": "skills", "origin": "source" },
{ "path": "native/payload", "origin": "source", "targetContains": ["windows"] },
{
"path": "src",
"origin": "source"
},
{
"path": "panels",
"origin": "source"
},
{
"path": "skills",
"origin": "source"
},
{
"path": "native/payload",
"origin": "prepared",
"targetContains": [
"windows"
],
"plugin": "agc-cocos-editor",
"prepare": "cocos-bridge-build"
},
{
"path": "dotnet/publish/win-x64",
"origin": "prepared",
"prepare": "unity-helper-publish",
"targets": ["x86_64-pc-windows-msvc"],
"features": ["unity-editor-execute"]
"targets": [
"x86_64-pc-windows-msvc"
],
"features": [
"unity-editor-execute"
],
"plugin": "agc-unity-editor"
}
],
"libraryStaging": [
@@ -105,8 +141,12 @@
"plugin": "agc-godot-editor",
"sourceSubdirectory": "native/gdextension",
"prepare": "godot-extension-build",
"targets": ["x86_64-pc-windows-msvc"],
"features": ["godot-editor-execute"],
"targets": [
"x86_64-pc-windows-msvc"
],
"features": [
"godot-editor-execute"
],
"layout": "godot-bundle",
"files": [
"bin/win-x64/agc_godot_editor.dll",
@@ -120,10 +160,15 @@
{
"plugin": "agc-cocos-editor",
"prepare": "cocos-bridge-build",
"sourceFileName": "cocos-editor-bridge.dll",
"sourceFileName": "cocos_editor_bridge.dll",
"destinationSubdirectory": "native/payload",
"targets": ["x86_64-pc-windows-msvc"],
"features": ["cocos-editor-injection"]
"targets": [
"x86_64-pc-windows-msvc"
],
"features": [
"cocos-editor-injection"
],
"destinationFileName": "cocos-editor-bridge.dll"
}
],
"prepareSteps": [
@@ -133,8 +178,15 @@
"workingDirectory": "plugins/agc-unity-editor/dotnet",
"scriptFileName": "build.ps1",
"fingerprint": {
"roots": ["."],
"excludeDirectoryNames": ["bin", "obj", "publish", "native-build"],
"roots": [
"."
],
"excludeDirectoryNames": [
"bin",
"obj",
"publish",
"native-build"
],
"stampRelativePath": "publish/win-x64/.agc-source.sha256"
},
"requiredOutputs": [
@@ -155,25 +207,49 @@
"kind": "powershell",
"workingDirectory": "plugins/agc-godot-editor/native/gdextension",
"scriptFileName": "build.ps1",
"removeEnvironment": ["PSModulePath"],
"removeEnvironment": [
"PSModulePath"
],
"requiredOutputs": [
"plugins/agc-godot-editor/native/gdextension/bin/win-x64/agc_godot_editor.dll",
"plugins/agc-godot-editor/native/gdextension/bin/win-x64/metadata.json",
"plugins/agc-godot-editor/native/gdextension/vendor/LICENSE.txt",
"plugins/agc-godot-editor/native/gdextension/vendor/provenance.json"
]
],
"fingerprint": {
"roots": [
"."
],
"excludeDirectoryNames": [
"bin",
".build",
"native-build"
],
"stampRelativePath": "bin/win-x64/.agc-source.sha256"
}
},
{
"name": "cocos-bridge-build",
"kind": "cargo",
"packageDirectory": "plugins/agc-cocos-editor/native/cocos-editor-bridge",
"features": ["windows-injection"],
"features": [
"windows-injection"
],
"requiredOutputs": []
}
],
"skipDirectoryNames": ["target", "node_modules"],
"skipDirectoryNamePrefixes": ["."],
"skipFileNamePrefixes": ["."],
"skipFileNameFragments": [".test."]
"skipDirectoryNames": [
"target",
"node_modules"
],
"skipDirectoryNamePrefixes": [
"."
],
"skipFileNamePrefixes": [
"."
],
"skipFileNameFragments": [
".test."
]
}
}
@@ -49,6 +49,8 @@ pub struct PackageMetadata {
#[derive(Debug)]
pub struct Subdirectory {
pub path: &'static str,
/// 该子目录归属的插件名;空串表示适用于所有插件。
pub plugin: &'static str,
/// `source`:由声明与仓库源码就能生成(准备步骤负责);`build`:构建期工具链产出(构建脚本负责)。
pub origin: &'static str,
pub target_contains: &'static [&'static str],
@@ -171,6 +173,11 @@ pub fn plugin_staging_applies(target: &str) -> bool {
.any(|needle| target.contains(needle))
}
/// 子目录是否归属该插件(空串表示通用)。
pub fn subdirectory_applies_to_plugin(subdirectory: &Subdirectory, plugin_name: &str) -> bool {
subdirectory.plugin.is_empty() || subdirectory.plugin == plugin_name
}
/// 子目录在当前目标与已启用 feature 下是否随包。
pub fn subdirectory_enabled(
subdirectory: &Subdirectory,
@@ -156,7 +156,7 @@ build script 只写 `OUT_DIR`/`target`;随包资源是它的输入。凡需要
形态选择**混合**:生成归 Node(复用 `stage-node-runtime.mjs` / `prepare-macos-codex.mjs` 的下载、`integrity`、临时目录 + rename 原子替换范式),声明与校验归 Rust(复用 `codex_bundle.rs` / `godot_bundle.rs` 的布局与摘要校验,运行期模块不改公开接口)。理由:Rust 侧没有下载与 lockfile 解析能力(`[build-dependencies]` 无 HTTP 客户端),Node 侧没有 staging 能力;任选单一语言都要迁移另一侧既有资产。Rust 侧刻意不解析 JSON:声明经生成器变成编译期常量,避免运行期解析与生命周期妥协,也让 `&'static` 布局表与现有调用点保持不变。
准备步骤的验证入口:`AGC_SKIP_RESOURCE_STAGING=1 cargo build/check …` 只跑只读校验、跳过写入分支,用于在既有产物上单独验证校验路径。
构建脚本自 M3 起只做只读校验(写入分支与 `AGC_SKIP_RESOURCE_STAGING` 开关一并删除),`cargo build/check` 本身就是对既有产物的校验。
### 4.9 M2/M3 实况:构建期写入边界