Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b19df467e | |||
| 96b44d8660 | |||
| 33308a93e8 | |||
| f6dad1950f | |||
| a4b103a1b0 | |||
| 05d455d4ef | |||
| d8b37e0da9 | |||
| 21c3bd7a2b | |||
| 756297d2df | |||
| e02811588c | |||
| e1780b21b0 |
@@ -30,7 +30,6 @@ The hosted MCP offers the following tools. Choose the task tool when its action
|
||||
| `PATCH /api/external/v1/editor/assets/{assetId}` | `organize_asset_library` (`update_asset`) | `update_editor_asset` |
|
||||
| `DELETE /api/external/v1/editor/assets/{assetId}` | `delete_resources` (`delete_asset`) | `delete_editor_asset` |
|
||||
| `POST /api/external/v1/editor/images/generations` | `generate_image`, `modify_image` (`variation`, fixed `kind="quick-edit"`) | `generate_external_editor_image` |
|
||||
| `POST /api/external/v1/editor/scenes/generations` | structured game-scene generation (no hosted MCP tool yet) | `generate_external_editor_scene` |
|
||||
| `POST /api/external/v1/editor/images/edits` | `modify_image` (`edit`) | `edit_external_editor_image` |
|
||||
| `POST /api/external/v1/editor/images/background-removals` | `modify_image` (`remove_background`) | `remove_external_editor_image_background` |
|
||||
| `POST /api/external/v1/editor/icon-spritesheets/generations` | `generate_icon_spritesheet` | `generate_external_editor_icon_spritesheet` |
|
||||
@@ -90,7 +89,6 @@ Every generation row requires a stable `Idempotency-Key` header and returns HTTP
|
||||
| Capability | POST path | Required body fields | Common optional body fields |
|
||||
| ------------------- | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Image generation | `/api/external/v1/editor/images/generations` | `prompt` | `kind`, `style`, `model`, `aspectRatio`, `imageSize`, `size`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` |
|
||||
| Game scene | `/api/external/v1/editor/scenes/generations` | `sceneContent`, `stylePreset` | `customStyle` (required when `stylePreset="custom"`), `model`, `aspectRatio`, `imageSize`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` |
|
||||
| Image edit/redraw | `/api/external/v1/editor/images/edits` | `prompt`, `sourceReferenceId` | `referenceImageSrcs`, `model`, `size`, `projectId`, `assetFolderId`, `assetLabel`, `targetLayerId`, `canvasCompletion` |
|
||||
| Background removal | `/api/external/v1/editor/images/background-removals` | `sourceImageSrc` | `projectId`, `sourceResourceId`, `targetLayerId`, static-image `assetKind`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` |
|
||||
| Icon spritesheet | `/api/external/v1/editor/icon-spritesheets/generations` | `referenceId`, `iconDescriptions`, `sliceMode` | `gridX`, `gridY`, `sliceCount`, `style`, `referenceImageSrcs`, `screenColor`, `model`, `aspectRatio`, `imageSize`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
|
||||
@@ -100,7 +98,7 @@ Every generation row requires a stable `Idempotency-Key` header and returns HTTP
|
||||
| Sound effect | `/api/external/v1/editor/audios/sound-effects/generations` | `prompt` | `model`, `duration`, `loop`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` |
|
||||
| Background music | `/api/external/v1/editor/audios/background-music/generations` | `gptDescriptionPrompt`, `makeInstrumental` | `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` |
|
||||
|
||||
Poll all ten through:
|
||||
Poll all nine through:
|
||||
|
||||
```text
|
||||
GET /api/external/v1/generations/{operationId}
|
||||
@@ -142,7 +140,7 @@ The icon-spritesheet primary `referenceId` is intentionally stricter than ordina
|
||||
Use OpenAPI as the final authority; these common values are a routing aid:
|
||||
|
||||
- Image `kind`: `spec`, `character`, `quick-edit`, `ui-design`, `publication-material`; ordinary image generation may omit it.
|
||||
- Game scenes must use the dedicated structured route `POST /api/external/v1/editor/scenes/generations` (`sceneContent` + `stylePreset`; `customStyle` required for `custom`). Do not send `kind: "scene"` or `assetKind: "scene"` through generic image generation; the server rejects both before queueing. The scene route assembles the full provider prompt server-side and never accepts a caller-assembled `prompt`.
|
||||
- External v1 currently has no structured game-scene generation operation. Do not send `kind: "scene"` or `assetKind: "scene"` through generic image generation; the server rejects both before queueing.
|
||||
- Image `model`: `gpt-image-2`, `gemini-3.1-flash-image-preview`, `nanobanana2`, `nano-banana`.
|
||||
- Image `aspectRatio`: `1:1`, `2:3`, `3:2`, `9:16`, `16:9`.
|
||||
- Image `imageSize`: `0.5K`, `1K`, `2K`.
|
||||
|
||||
@@ -148,7 +148,7 @@ For the lower-level asset/resource creation endpoints, `generationInputs` is rep
|
||||
|
||||
## Art Spec and Image Request
|
||||
|
||||
Game scenes have a dedicated structured route: `POST /api/external/v1/editor/scenes/generations` with `sceneContent` and `stylePreset` (`customStyle` required when `stylePreset` is `custom`). The server assembles the full provider prompt; a caller-assembled `prompt` is not accepted. `kind: "scene"` and `assetKind: "scene"` remain invalid on generic image generation and return HTTP `400` before any generation job is queued.
|
||||
Generic External v1 image generation does not expose the main-site structured game-scene contract. `kind: "scene"` and `assetKind: "scene"` are both invalid and return HTTP `400` before any generation job is queued. Do not replace the structured scene fields and server-owned prompt assembly with a generic image prompt.
|
||||
|
||||
When maintaining a reusable art spec, carry it in `generationInputs.artSpec` and reflect important constraints in the prompt. This is an example with both canvas and library destinations, not a requirement for every generation:
|
||||
|
||||
|
||||
@@ -57,6 +57,8 @@ temp*build*/
|
||||
/apps/ai-game-creator-shell/logs/
|
||||
/apps/ai-game-creator-shell/src-tauri/resources/node-runtime/
|
||||
/apps/ai-game-creator-shell/src-tauri/resources/node-runtime-staging-*/
|
||||
/apps/ai-game-creator-shell/src-tauri/resources/plugins-staging-*/
|
||||
/apps/ai-game-creator-shell/src-tauri/resources/codex/*-staging-*/
|
||||
/apps/ai-game-creator-shell/.llm-drafts/
|
||||
/apps/ai-game-creator-shell/game-creator.config.local.json
|
||||
/apps/mobile-shell/.expo/
|
||||
|
||||
@@ -13,6 +13,10 @@
|
||||
"skill-pack:check": "node scripts/check-skill-pack.mjs",
|
||||
"skill-pack:sync": "node scripts/check-skill-pack.mjs --write",
|
||||
"skill-pack:test": "node --test scripts/check-skill-pack.test.mjs",
|
||||
"bundled-resources:check": "node scripts/check-package-layout.mjs",
|
||||
"bundled-resources:sync": "node scripts/check-package-layout.mjs --write",
|
||||
"bundled-resources:prepare": "node scripts/prepare-bundled-resources.mjs",
|
||||
"bundled-resources:test": "node --test scripts/prepare-bundled-resources.test.mjs",
|
||||
"llm-status": "node scripts/run-cli-with-config.mjs --llm-status",
|
||||
"agent-task": "node scripts/run-cli-with-config.mjs --agent-task",
|
||||
"config": "node scripts/game-creator-config-wizard.mjs",
|
||||
@@ -24,7 +28,7 @@
|
||||
"agent-runtime:supervisor-swarm-tool-plan-handoff-runner-kill-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-tool-plan-handoff-runner-kill",
|
||||
"agent-runtime:steer-real-e2e": "node scripts/agent-runtime-steer-real-e2e.mjs",
|
||||
"agent-runtime:steer-runner-kill-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite steer-runner-kill",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit && npm run skill-pack:check && node scripts/check-config.mjs"
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit && npm run skill-pack:check && npm run bundled-resources:check && node scripts/check-config.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cubone/react-file-manager": "^1.35.0",
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
resolveReleaseChannel,
|
||||
} from './channel-identity.mjs';
|
||||
import { prepareNsisToolsetForRelease } from './nsis-toolset.mjs';
|
||||
import { prepareBundledResources } from './prepare-bundled-resources.mjs';
|
||||
import { stageNodeRuntime } from './stage-node-runtime.mjs';
|
||||
|
||||
const appRoot = fileURLToPath(new URL('..', import.meta.url));
|
||||
@@ -431,10 +432,29 @@ function writeChannelConfigFile(channel, target, includeNodeRuntime = false) {
|
||||
return configPath;
|
||||
}
|
||||
|
||||
/// 随包资源必须在打包工具之前生成:构建脚本只做只读校验,不再生成。
|
||||
export function stageBundledResources(
|
||||
target,
|
||||
{ prepare = prepareBundledResources } = {},
|
||||
) {
|
||||
const summaries = prepare({
|
||||
target,
|
||||
features: new Set(defaultEditorFeatures(target)),
|
||||
log: (line) => console.log(`[ai-game-creator-shell] ${line}`),
|
||||
});
|
||||
for (const summary of summaries) {
|
||||
console.log(`[ai-game-creator-shell] ${summary}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function runTauriBuild(
|
||||
args = [],
|
||||
context = resolveReleaseContext(args),
|
||||
{ spawn = spawnSync, stageRuntime = stageNodeRuntime } = {},
|
||||
{
|
||||
spawn = spawnSync,
|
||||
stageRuntime = stageNodeRuntime,
|
||||
stageBundled = stageBundledResources,
|
||||
} = {},
|
||||
) {
|
||||
if (
|
||||
explicitBuildTarget(args) &&
|
||||
@@ -444,7 +464,10 @@ export function runTauriBuild(
|
||||
}
|
||||
const tauriArguments = buildTauriBuildArguments(args, context.target);
|
||||
const { channel, target } = context;
|
||||
if (!args.includes('--no-bundle')) stageRuntime(target);
|
||||
if (!args.includes('--no-bundle')) {
|
||||
stageRuntime(target);
|
||||
stageBundled(target);
|
||||
}
|
||||
const configPath = writeChannelConfigFile(
|
||||
channel,
|
||||
target,
|
||||
|
||||
@@ -363,6 +363,8 @@ test('packaged renderer receives the same channel as the updater manifest', () =
|
||||
// 必须 stub:真实 staging 会用宿主平台(如 macOS 的 darwin/arm64)去对默认的
|
||||
// Windows 目标做一致性校验,在非 Windows 主机上直接失败——本用例只关心渠道注入。
|
||||
stageRuntime: () => {},
|
||||
// 同上:随包资源准备会读取真实上游包,本用例只关心渠道环境变量。
|
||||
stageBundled: () => {},
|
||||
spawn: (_binary, _args, options) => {
|
||||
spawnOptions = options;
|
||||
return { status: 0 };
|
||||
@@ -456,6 +458,8 @@ test('explicit macOS target drives version lookup, Tauri endpoint, artifact and
|
||||
seenContexts.push(context);
|
||||
runTauriBuild(args, context, {
|
||||
stageRuntime: () => {},
|
||||
// 必须 stub:随包资源准备会读取真实上游包与仓库插件工作区,本用例只关心参数。
|
||||
stageBundled: () => {},
|
||||
spawn: (_binary, command) => {
|
||||
const configIndex = command.lastIndexOf('--config');
|
||||
const config = JSON.parse(
|
||||
@@ -705,6 +709,7 @@ test('Windows remains the default and explicit Windows overrides macOS environme
|
||||
context,
|
||||
{
|
||||
stageRuntime: () => {},
|
||||
stageBundled: () => {},
|
||||
spawn: (_binary, command) => {
|
||||
assert.ok(
|
||||
command.includes(
|
||||
@@ -818,6 +823,10 @@ test('release stages Node before Tauri and injects its resource mapping only for
|
||||
assert.equal(target, windowsTarget);
|
||||
events.push('stage');
|
||||
},
|
||||
stageBundled(target) {
|
||||
assert.equal(target, windowsTarget);
|
||||
events.push('bundled');
|
||||
},
|
||||
spawn(_binary, args) {
|
||||
events.push('build');
|
||||
const config = JSON.parse(
|
||||
@@ -829,11 +838,14 @@ test('release stages Node before Tauri and injects its resource mapping only for
|
||||
return { status: 0 };
|
||||
},
|
||||
});
|
||||
assert.deepEqual(events, ['stage', 'build']);
|
||||
assert.deepEqual(events, ['stage', 'bundled', 'build']);
|
||||
runTauriBuild(['--no-bundle', '--target', windowsTarget], context, {
|
||||
stageRuntime() {
|
||||
assert.fail('no-bundle must not stage resources');
|
||||
},
|
||||
stageBundled() {
|
||||
assert.fail('no-bundle must not stage bundled resources');
|
||||
},
|
||||
spawn(_binary, args) {
|
||||
const config = JSON.parse(
|
||||
readFileSync(args[args.lastIndexOf('--config') + 1], 'utf8'),
|
||||
@@ -848,6 +860,9 @@ test('release stages Node before Tauri and injects its resource mapping only for
|
||||
stageRuntime() {
|
||||
throw new Error('missing runtime');
|
||||
},
|
||||
stageBundled() {
|
||||
assert.fail('invalid runtime must prevent bundled staging');
|
||||
},
|
||||
spawn() {
|
||||
assert.fail('invalid runtime must prevent build');
|
||||
},
|
||||
@@ -960,6 +975,8 @@ for (const channel of ['release', 'beta-2']) {
|
||||
);
|
||||
runTauriBuild([`--target=${target}`], context, {
|
||||
stageRuntime: () => {},
|
||||
// 必须 stub:随包资源准备会读取真实上游包与仓库插件工作区,本用例只关心参数。
|
||||
stageBundled: () => {},
|
||||
spawn: (_binary, command) => {
|
||||
const config = JSON.parse(
|
||||
readFileSync(
|
||||
|
||||
+512
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,460 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { createHash } from 'node:crypto';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { test } from 'node:test';
|
||||
|
||||
import {
|
||||
DECLARATION_PATH,
|
||||
findCodexSource,
|
||||
pluginDirectories,
|
||||
prepareBundledResources,
|
||||
readDeclaration,
|
||||
resolveHostTarget,
|
||||
stagingUnit,
|
||||
} from './prepare-bundled-resources.mjs';
|
||||
|
||||
const WINDOWS_TARGET = 'x86_64-pc-windows-msvc';
|
||||
const MAC_TARGET = 'aarch64-apple-darwin';
|
||||
|
||||
function sha256File(file) {
|
||||
return createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
||||
}
|
||||
|
||||
/// 造一个最小工作区:app(含 node_modules 上游包)、repo(含 plugins 工作区)、lockfile。
|
||||
function buildFixture({ targets = [WINDOWS_TARGET], plugins = true } = {}) {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agc-resources-'));
|
||||
const appRoot = path.join(root, 'app');
|
||||
const repoRoot = path.join(root, 'repo');
|
||||
const destinationRoot = path.join(appRoot, 'src-tauri');
|
||||
const declaration = readDeclaration(DECLARATION_PATH);
|
||||
const lockfile = { packages: {} };
|
||||
|
||||
for (const target of targets) {
|
||||
const layout = declaration.codex.targets.find(
|
||||
(entry) => entry.target === target,
|
||||
);
|
||||
assert.ok(layout, `声明缺少目标 ${target}`);
|
||||
const vendor = path.join(
|
||||
appRoot,
|
||||
`node_modules/@openai/codex-${layout.platform}/vendor/${target}`,
|
||||
);
|
||||
for (const relative of layout.files) {
|
||||
const file = path.join(vendor, relative);
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
file,
|
||||
relative === declaration.codex.packageMetadataFileName
|
||||
? `${JSON.stringify(
|
||||
{
|
||||
layoutVersion: declaration.codex.packageMetadata.layoutVersion,
|
||||
version: declaration.codex.version,
|
||||
target,
|
||||
entrypoint: layout.executable,
|
||||
resourcesDir: declaration.codex.packageMetadata.resourcesDir,
|
||||
pathDir: declaration.codex.packageMetadata.pathDir,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`
|
||||
: `component ${target} ${relative}\n`,
|
||||
);
|
||||
if (relative === layout.executable) {
|
||||
fs.chmodSync(file, 0o755);
|
||||
}
|
||||
}
|
||||
lockfile.packages[`node_modules/@openai/codex-${layout.platform}`] = {
|
||||
resolved: `https://registry.npmjs.org/@openai/codex-${layout.platform}/-/${layout.platform}.tgz`,
|
||||
integrity: `sha512-${target}`,
|
||||
};
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.join(destinationRoot, 'resources/codex'), {
|
||||
recursive: true,
|
||||
});
|
||||
for (const entry of declaration.codex.noticeSources) {
|
||||
if (entry.preserve) {
|
||||
continue;
|
||||
}
|
||||
const file = path.join(destinationRoot, entry.source);
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
fs.writeFileSync(file, 'mac codex notice\n');
|
||||
}
|
||||
if (targets.includes(WINDOWS_TARGET)) {
|
||||
const tracked = path.join(
|
||||
destinationRoot,
|
||||
'resources/codex/win-x64/NOTICE.md',
|
||||
);
|
||||
fs.mkdirSync(path.dirname(tracked), { recursive: true });
|
||||
fs.writeFileSync(tracked, 'windows codex notice\n');
|
||||
}
|
||||
|
||||
if (plugins) {
|
||||
const pluginRoot = path.join(repoRoot, 'plugins/agc-demo-editor');
|
||||
fs.mkdirSync(path.join(pluginRoot, 'src'), { recursive: true });
|
||||
fs.mkdirSync(path.join(pluginRoot, 'panels'), { recursive: true });
|
||||
fs.mkdirSync(path.join(pluginRoot, 'target'), { recursive: true });
|
||||
fs.mkdirSync(path.join(pluginRoot, '.git'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(pluginRoot, 'plugin.json'),
|
||||
'{"name":"agc-demo-editor"}\n',
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pluginRoot, 'src/entry.mjs'),
|
||||
'export const entry = 1;\n',
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pluginRoot, 'panels/panel.html'),
|
||||
'<html></html>\n',
|
||||
);
|
||||
fs.writeFileSync(path.join(pluginRoot, 'panels/panel.test.mjs'), 'test\n');
|
||||
fs.writeFileSync(path.join(pluginRoot, 'target/junk.rs'), 'junk\n');
|
||||
fs.writeFileSync(path.join(pluginRoot, '.git/HEAD'), 'ref\n');
|
||||
fs.writeFileSync(path.join(pluginRoot, '.env'), 'secret\n');
|
||||
}
|
||||
|
||||
const lockfilePath = path.join(root, 'package-lock.json');
|
||||
fs.writeFileSync(lockfilePath, JSON.stringify(lockfile, null, 2));
|
||||
return {
|
||||
root,
|
||||
appRoot,
|
||||
repoRoot,
|
||||
destinationRoot,
|
||||
lockfilePath,
|
||||
recordPath: path.join(root, 'record.json'),
|
||||
declaration,
|
||||
cleanup: () => fs.rmSync(root, { recursive: true, force: true }),
|
||||
};
|
||||
}
|
||||
|
||||
function snapshot(directory) {
|
||||
const entries = [];
|
||||
const stack = [['', directory]];
|
||||
while (stack.length > 0) {
|
||||
const [prefix, current] = stack.pop();
|
||||
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
||||
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
||||
const full = path.join(current, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
stack.push([relative, full]);
|
||||
} else {
|
||||
const info = fs.statSync(full);
|
||||
entries.push({
|
||||
relative,
|
||||
size: info.size,
|
||||
mtimeMs: info.mtimeMs,
|
||||
mode: info.mode & 0o777,
|
||||
sha256: sha256File(full),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return entries.sort((left, right) =>
|
||||
left.relative.localeCompare(right.relative),
|
||||
);
|
||||
}
|
||||
|
||||
function prepare(fixture, overrides = {}) {
|
||||
return prepareBundledResources({
|
||||
target: WINDOWS_TARGET,
|
||||
destinationRoot: fixture.destinationRoot,
|
||||
declarationPath: DECLARATION_PATH,
|
||||
recordPath: fixture.recordPath,
|
||||
lockfilePath: fixture.lockfilePath,
|
||||
repoRoot: fixture.repoRoot,
|
||||
appRoot: fixture.appRoot,
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
test('stages declared codex components with manifest and preserved notice', () => {
|
||||
const fixture = buildFixture();
|
||||
try {
|
||||
const summaries = prepare(fixture);
|
||||
assert.match(summaries[0], /codex x86_64-pc-windows-msvc 重新生成/);
|
||||
|
||||
const declaration = fixture.declaration;
|
||||
const layout = declaration.codex.targets.find(
|
||||
(entry) => entry.target === WINDOWS_TARGET,
|
||||
);
|
||||
const unit = path.join(
|
||||
fixture.destinationRoot,
|
||||
'resources/codex',
|
||||
layout.directory,
|
||||
);
|
||||
for (const relative of layout.files) {
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(unit, relative)),
|
||||
`缺少组件 ${relative}`,
|
||||
);
|
||||
}
|
||||
assert.equal(
|
||||
fs.readFileSync(path.join(unit, 'NOTICE.md'), 'utf8'),
|
||||
'windows codex notice\n',
|
||||
'受版本控制的第三方声明必须原地保留',
|
||||
);
|
||||
const manifest = JSON.parse(
|
||||
fs.readFileSync(path.join(unit, 'manifest.json'), 'utf8'),
|
||||
);
|
||||
assert.deepEqual(Object.keys(manifest), [
|
||||
'files',
|
||||
'platform',
|
||||
'schemaVersion',
|
||||
'version',
|
||||
]);
|
||||
assert.equal(manifest.platform, layout.platform);
|
||||
assert.equal(manifest.schemaVersion, declaration.codex.manifestSchema);
|
||||
assert.equal(
|
||||
manifest.version,
|
||||
`${declaration.codex.cliVersionPrefix}${declaration.codex.version}`,
|
||||
);
|
||||
assert.deepEqual(
|
||||
Object.keys(manifest.files).sort(),
|
||||
[...layout.files].sort(),
|
||||
'清单文件集合必须等于组件白名单',
|
||||
);
|
||||
for (const relative of layout.files) {
|
||||
assert.equal(
|
||||
manifest.files[relative],
|
||||
sha256File(path.join(unit, relative)),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
fixture.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test('second run is a no-op: identical content and timestamps', () => {
|
||||
const fixture = buildFixture();
|
||||
try {
|
||||
prepare(fixture);
|
||||
const unit = path.join(
|
||||
fixture.destinationRoot,
|
||||
'resources/codex',
|
||||
'win-x64',
|
||||
);
|
||||
const plugins = path.join(fixture.destinationRoot, 'resources/plugins');
|
||||
const before = { codex: snapshot(unit), plugins: snapshot(plugins) };
|
||||
const summaries = prepare(fixture);
|
||||
assert.match(summaries[0], /命中缓存/);
|
||||
assert.match(summaries[1], /命中缓存/);
|
||||
assert.deepEqual(
|
||||
snapshot(unit),
|
||||
before.codex,
|
||||
'codex 产物内容与时间戳必须不变',
|
||||
);
|
||||
assert.deepEqual(
|
||||
snapshot(plugins),
|
||||
before.plugins,
|
||||
'插件产物内容与时间戳必须不变',
|
||||
);
|
||||
} finally {
|
||||
fixture.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test('stages the macOS universal group with both architectures', () => {
|
||||
const fixture = buildFixture({
|
||||
targets: [MAC_TARGET, 'x86_64-apple-darwin'],
|
||||
});
|
||||
try {
|
||||
const summaries = prepare(fixture, { target: MAC_TARGET });
|
||||
assert.match(summaries[0], /mac-native/);
|
||||
const unit = path.join(
|
||||
fixture.destinationRoot,
|
||||
'resources/codex/mac-native',
|
||||
);
|
||||
for (const directory of ['darwin-arm64', 'darwin-x64']) {
|
||||
for (const file of ['bin/codex', 'manifest.json', 'NOTICE.md']) {
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(unit, directory, file)),
|
||||
`缺少 ${directory}/${file}`,
|
||||
);
|
||||
}
|
||||
assert.equal(
|
||||
fs.readFileSync(path.join(unit, directory, 'NOTICE.md'), 'utf8'),
|
||||
'mac codex notice\n',
|
||||
);
|
||||
}
|
||||
assert.deepEqual(fs.readdirSync(unit).sort(), [
|
||||
'darwin-arm64',
|
||||
'darwin-x64',
|
||||
]);
|
||||
} finally {
|
||||
fixture.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test('copies only whitelisted plugin subdirectories', () => {
|
||||
const fixture = buildFixture();
|
||||
try {
|
||||
prepare(fixture);
|
||||
const staged = path.join(
|
||||
fixture.destinationRoot,
|
||||
'resources/plugins/agc-demo-editor',
|
||||
);
|
||||
assert.ok(fs.existsSync(path.join(staged, 'plugin.json')));
|
||||
assert.ok(fs.existsSync(path.join(staged, 'src/entry.mjs')));
|
||||
assert.ok(fs.existsSync(path.join(staged, 'panels/panel.html')));
|
||||
assert.ok(
|
||||
!fs.existsSync(path.join(staged, 'panels/panel.test.mjs')),
|
||||
'测试文件不随包',
|
||||
);
|
||||
assert.ok(
|
||||
!fs.existsSync(path.join(staged, 'target')),
|
||||
'构建产物目录不随包',
|
||||
);
|
||||
assert.ok(!fs.existsSync(path.join(staged, '.git')), '隐藏目录不随包');
|
||||
assert.ok(!fs.existsSync(path.join(staged, '.env')), '隐藏文件不随包');
|
||||
} finally {
|
||||
fixture.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test('fails closed when the upstream package metadata drifts from the declaration', () => {
|
||||
const fixture = buildFixture();
|
||||
try {
|
||||
const declaration = fixture.declaration;
|
||||
const layout = declaration.codex.targets.find(
|
||||
(entry) => entry.target === WINDOWS_TARGET,
|
||||
);
|
||||
const metadataFile = path.join(
|
||||
fixture.appRoot,
|
||||
`node_modules/@openai/codex-${layout.platform}/vendor/${WINDOWS_TARGET}/codex-package.json`,
|
||||
);
|
||||
const metadata = JSON.parse(fs.readFileSync(metadataFile, 'utf8'));
|
||||
assert.equal(metadata.version, declaration.codex.version);
|
||||
for (const [key, value] of [
|
||||
['version', '0.0.0'],
|
||||
['layoutVersion', 2],
|
||||
['entrypoint', 'bin/other.exe'],
|
||||
['resourcesDir', '../private'],
|
||||
]) {
|
||||
fs.writeFileSync(
|
||||
metadataFile,
|
||||
`${JSON.stringify({ ...metadata, [key]: value }, null, 2)}\n`,
|
||||
);
|
||||
assert.throws(
|
||||
() => prepare(fixture),
|
||||
/上游包元数据与声明不一致/,
|
||||
`${key} 漂移必须被拒绝`,
|
||||
);
|
||||
}
|
||||
assert.ok(
|
||||
!fs.existsSync(
|
||||
path.join(
|
||||
fixture.destinationRoot,
|
||||
'resources/codex/win-x64/manifest.json',
|
||||
),
|
||||
),
|
||||
'拒绝时不得留下产物',
|
||||
);
|
||||
} finally {
|
||||
fixture.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test('fails closed when the upstream package is missing', () => {
|
||||
const fixture = buildFixture();
|
||||
try {
|
||||
fs.rmSync(path.join(fixture.appRoot, 'node_modules'), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
assert.throws(() => prepare(fixture), /npm ci/);
|
||||
assert.ok(
|
||||
!fs.existsSync(
|
||||
path.join(
|
||||
fixture.destinationRoot,
|
||||
'resources/codex/win-x64/manifest.json',
|
||||
),
|
||||
),
|
||||
'失败时不得留下半成品清单',
|
||||
);
|
||||
assert.ok(
|
||||
!fs.existsSync(path.join(fixture.destinationRoot, 'resources/plugins')),
|
||||
);
|
||||
} finally {
|
||||
fixture.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test('fails closed for unsupported targets', () => {
|
||||
const fixture = buildFixture();
|
||||
try {
|
||||
assert.throws(
|
||||
() => prepare(fixture, { target: 'x86_64-unknown-linux-gnu' }),
|
||||
/声明不含目标/,
|
||||
);
|
||||
assert.throws(() => resolveHostTarget('linux', 'x64'), /不支持的目标平台/);
|
||||
} finally {
|
||||
fixture.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test('fails closed when the destination is owned by something else', () => {
|
||||
const fixture = buildFixture();
|
||||
try {
|
||||
const unit = path.join(fixture.destinationRoot, 'resources/codex/win-x64');
|
||||
fs.writeFileSync(path.join(unit, 'foreign.bin'), 'foreign\n');
|
||||
assert.throws(() => prepare(fixture), /被非本工具内容占用/);
|
||||
|
||||
const plugins = path.join(fixture.destinationRoot, 'resources/plugins');
|
||||
fs.rmSync(path.join(unit, 'foreign.bin'), { force: true });
|
||||
fs.mkdirSync(path.join(plugins, 'someone-elses-plugin'), {
|
||||
recursive: true,
|
||||
});
|
||||
assert.throws(() => prepare(fixture), /插件随包目录被非本工具内容占用/);
|
||||
} finally {
|
||||
fixture.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test('dry run writes nothing', () => {
|
||||
const fixture = buildFixture();
|
||||
try {
|
||||
const summaries = prepare(fixture, { dryRun: true });
|
||||
assert.match(summaries[0], /需要重新生成(dry-run 未写入)/);
|
||||
const unit = path.join(fixture.destinationRoot, 'resources/codex/win-x64');
|
||||
assert.deepEqual(
|
||||
fs.readdirSync(unit),
|
||||
['NOTICE.md'],
|
||||
'dry-run 不得写入任何组件或清单',
|
||||
);
|
||||
assert.ok(
|
||||
!fs.existsSync(path.join(fixture.destinationRoot, 'resources/plugins')),
|
||||
);
|
||||
assert.ok(!fs.existsSync(fixture.recordPath));
|
||||
} finally {
|
||||
fixture.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test('declaration drives source lookup and staging units', () => {
|
||||
const declaration = readDeclaration(DECLARATION_PATH);
|
||||
const windows = stagingUnit(declaration, WINDOWS_TARGET);
|
||||
assert.equal(windows.directory, 'win-x64');
|
||||
assert.deepEqual(
|
||||
windows.targets.map((member) => member.target),
|
||||
[WINDOWS_TARGET],
|
||||
);
|
||||
const mac = stagingUnit(declaration, MAC_TARGET);
|
||||
assert.equal(mac.directory, 'mac-native');
|
||||
assert.deepEqual(
|
||||
mac.targets.map((member) => member.target),
|
||||
['aarch64-apple-darwin', 'x86_64-apple-darwin'],
|
||||
);
|
||||
|
||||
const fixture = buildFixture();
|
||||
try {
|
||||
const source = findCodexSource(declaration, WINDOWS_TARGET, {
|
||||
app: fixture.appRoot,
|
||||
repo: fixture.repoRoot,
|
||||
});
|
||||
assert.match(source, /codex-win32-x64\/vendor\/x86_64-pc-windows-msvc$/);
|
||||
assert.equal(pluginDirectories(declaration, fixture.repoRoot).length, 1);
|
||||
} finally {
|
||||
fixture.cleanup();
|
||||
}
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { buildLocalRustProcessEnv } from '../../../scripts/dev.mjs';
|
||||
import {
|
||||
defaultEditorFeatures,
|
||||
withDefaultCargoFeatures,
|
||||
@@ -10,6 +11,10 @@ import {
|
||||
resolveAgcDevEndpoint,
|
||||
withAgcDevEndpointEnv,
|
||||
} from './dev-port.mjs';
|
||||
import {
|
||||
prepareBundledResources,
|
||||
supportedHostTarget,
|
||||
} from './prepare-bundled-resources.mjs';
|
||||
import {
|
||||
isAiGameCreatorServer,
|
||||
preflightExistingVite,
|
||||
@@ -67,6 +72,29 @@ function withDevCargoFeatures(argv, features = readDevCargoFeatures()) {
|
||||
return withDefaultCargoFeatures(argv, features);
|
||||
}
|
||||
|
||||
/// 随包资源必须在 Tauri 之前生成:构建脚本只做只读校验,不再生成资源。
|
||||
/// 命中缓存的重复调用不写任何文件,因此每次 dev 启动都会先跑一次。
|
||||
function prepareBundledResourcesBeforeTauri(
|
||||
features = readDevCargoFeatures(),
|
||||
{ prepare = prepareBundledResources, log = console.log } = {},
|
||||
) {
|
||||
const target = supportedHostTarget();
|
||||
if (!target) {
|
||||
log(
|
||||
'[ai-game-creator-shell] 当前平台不受随包资源声明覆盖,跳过随包资源准备',
|
||||
);
|
||||
return;
|
||||
}
|
||||
const summaries = prepare({
|
||||
target,
|
||||
features: new Set(features),
|
||||
log: (line) => log(`[ai-game-creator-shell] ${line}`),
|
||||
});
|
||||
for (const summary of summaries) {
|
||||
log(`[ai-game-creator-shell] ${summary}`);
|
||||
}
|
||||
}
|
||||
|
||||
function spawnTauriCli(argv, { env = process.env } = {}) {
|
||||
return spawnChild(process.execPath, [tauriCliPath, ...argv], {
|
||||
cwd: appRoot,
|
||||
@@ -75,6 +103,17 @@ function spawnTauriCli(argv, { env = process.env } = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
/// Tauri dev 的 Cargo 直接继承启动器环境,用户级 / 仓库级 Cargo 配置里的
|
||||
/// `rustc-wrapper`(本地常见为 sccache)会在这里生效。本地 sccache daemon 状态
|
||||
/// 一旦损坏,`cargo` 的首次 rustc 探测就会失败并阻断整个 AGC 启动;因此这里复用
|
||||
/// `npm run dev` 的本地 Rust 环境规则,由脚本而不是本机 Cargo 配置决定 wrapper。
|
||||
function buildTauriDevProcessEnv(endpoint, env = process.env) {
|
||||
return buildLocalRustProcessEnv({
|
||||
...withAgcDevEndpointEnv(endpoint, env),
|
||||
[AGC_DESIGN_DEBUG_ENV]: designDebugEnabled,
|
||||
});
|
||||
}
|
||||
|
||||
async function runTauriDev(
|
||||
argv = process.argv.slice(2),
|
||||
{
|
||||
@@ -84,6 +123,7 @@ async function runTauriDev(
|
||||
spawnCli = spawnTauriCli,
|
||||
waitForCli = waitForChildTermination,
|
||||
terminateTree = terminateChildTree,
|
||||
prepareResources = prepareBundledResourcesBeforeTauri,
|
||||
} = {},
|
||||
) {
|
||||
const endpoint = await resolveDevEndpoint();
|
||||
@@ -130,15 +170,14 @@ async function runTauriDev(
|
||||
shutdownRequested.then(() => false),
|
||||
]);
|
||||
if (!prepared || shutdownSignal) return 1;
|
||||
const devFeatures = readDevCargoFeatures();
|
||||
prepareResources(devFeatures);
|
||||
const tauriArguments = buildTauriArguments(
|
||||
withDevCargoFeatures(argv),
|
||||
withDevCargoFeatures(argv, devFeatures),
|
||||
endpoint.url,
|
||||
);
|
||||
child = spawnCli(tauriArguments, {
|
||||
env: {
|
||||
...withAgcDevEndpointEnv(endpoint),
|
||||
[AGC_DESIGN_DEBUG_ENV]: designDebugEnabled,
|
||||
},
|
||||
env: buildTauriDevProcessEnv(endpoint),
|
||||
});
|
||||
const childResult = waitForCli(child);
|
||||
const outcome = await Promise.race([
|
||||
@@ -225,7 +264,9 @@ function isDirectModuleExecution() {
|
||||
|
||||
export {
|
||||
buildTauriArguments,
|
||||
buildTauriDevProcessEnv,
|
||||
isDirectModuleExecution,
|
||||
prepareBundledResourcesBeforeTauri,
|
||||
runTauriDev,
|
||||
spawnTauriCli,
|
||||
withDevCargoFeatures,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// 构建脚本只用布局里的目录与校验入口(写入分支已移交准备步骤),
|
||||
// 其余字段与常量供运行期使用,因此这里不报构建上下文里的 dead_code。
|
||||
#[allow(dead_code)]
|
||||
#[path = "build_support/codex_bundle.rs"]
|
||||
mod codex_bundle;
|
||||
#[path = "build_support/codex_package_metadata.rs"]
|
||||
mod codex_package_metadata;
|
||||
#[path = "build_support/frontend_dist_guard.rs"]
|
||||
mod frontend_dist_guard;
|
||||
#[path = "build_support/godot_bundle.rs"]
|
||||
@@ -15,164 +16,7 @@ use std::env;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use std::io::{BufReader, Read};
|
||||
|
||||
fn sha256_file(path: &std::path::Path) -> Result<String, std::io::Error> {
|
||||
let file = fs::File::open(path)?;
|
||||
let mut reader = BufReader::new(file);
|
||||
let mut hasher = Sha256::new();
|
||||
let mut buffer = [0_u8; 64 * 1024];
|
||||
loop {
|
||||
let read = reader.read(&mut buffer)?;
|
||||
if read == 0 {
|
||||
break;
|
||||
}
|
||||
hasher.update(&buffer[..read]);
|
||||
}
|
||||
Ok(format!("{:x}", hasher.finalize()))
|
||||
}
|
||||
|
||||
fn stage_bundled_codex_cli(manifest_dir: &std::path::Path) {
|
||||
let target = env::var("TARGET").expect("Cargo TARGET");
|
||||
println!("cargo:rustc-env=AGC_BUILD_TARGET={target}");
|
||||
if target.contains("apple-darwin") {
|
||||
// Tauri 的 universal 两次 Cargo 编译共用 resource staging,
|
||||
// 每次都生成完整双架构目录,最终 bundle 不取决于最后编译的切片。
|
||||
let staging = manifest_dir.join("resources/codex/mac-native");
|
||||
if staging.exists() {
|
||||
fs::remove_dir_all(&staging).expect("清理 macOS Codex staging 失败");
|
||||
}
|
||||
for target in ["aarch64-apple-darwin", "x86_64-apple-darwin"] {
|
||||
stage_codex_target(manifest_dir, target);
|
||||
}
|
||||
} else {
|
||||
stage_codex_target(manifest_dir, &target);
|
||||
}
|
||||
}
|
||||
|
||||
fn stage_codex_target(manifest_dir: &std::path::Path, target: &str) {
|
||||
let Some(layout) = codex_bundle::for_target(target) else {
|
||||
assert!(
|
||||
!target.contains("windows") && !target.contains("apple-darwin"),
|
||||
"不支持的 Codex 随包目标:{target}"
|
||||
);
|
||||
return;
|
||||
};
|
||||
{
|
||||
let app_root = manifest_dir
|
||||
.parent()
|
||||
.expect("AI 游戏创作 Tauri manifest 必须位于应用目录下");
|
||||
let repo_root = app_root
|
||||
.parent()
|
||||
.and_then(|apps_dir| apps_dir.parent())
|
||||
.expect("AI 游戏创作应用必须位于仓库 apps 目录下");
|
||||
let package = format!("codex-{}", layout.platform);
|
||||
let source_candidates = [app_root, repo_root]
|
||||
.into_iter()
|
||||
.flat_map(|root| {
|
||||
[
|
||||
root.join(format!("node_modules/@openai/{package}/vendor/{target}")),
|
||||
root.join(format!(
|
||||
"node_modules/@openai/codex/node_modules/@openai/{package}/vendor/{target}"
|
||||
)),
|
||||
]
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let source = source_candidates
|
||||
.iter()
|
||||
.find(|path| {
|
||||
layout
|
||||
.files
|
||||
.iter()
|
||||
.all(|relative| path.join(relative).is_file())
|
||||
})
|
||||
.cloned()
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"内置 Codex CLI 缺失;请先在仓库根目录执行 npm ci(已检查:{})",
|
||||
source_candidates
|
||||
.iter()
|
||||
.map(|path| path.display().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(";")
|
||||
)
|
||||
});
|
||||
let metadata: serde_json::Value = serde_json::from_slice(
|
||||
&fs::read(source.join("codex-package.json")).expect("读取 Codex 原生包元数据失败"),
|
||||
)
|
||||
.expect("Codex 原生包元数据无效");
|
||||
codex_package_metadata::validate_package_metadata(&metadata, target, layout)
|
||||
.unwrap_or_else(|error| panic!("{error}"));
|
||||
let target_dir = manifest_dir.join("resources/codex").join(layout.directory);
|
||||
let notice = target_dir.join("NOTICE.md");
|
||||
if target.contains("apple-darwin") {
|
||||
let source_notice =
|
||||
manifest_dir.join("resources/codex/【声明】Mac内置Codex组件-2026-09-18.md");
|
||||
stage_plugin_file(&source_notice, ¬ice);
|
||||
println!("cargo:rerun-if-changed={}", source_notice.display());
|
||||
}
|
||||
if !notice.is_file() {
|
||||
panic!("内置 Codex CLI 第三方声明缺失:{}", notice.display());
|
||||
}
|
||||
fs::create_dir_all(&target_dir).expect("创建内置 Codex CLI 资源目录失败");
|
||||
let mut file_hashes = serde_json::Map::new();
|
||||
for relative in layout.files {
|
||||
let source_path = source.join(relative);
|
||||
let target_path = target_dir.join(relative);
|
||||
if let Some(parent) = target_path.parent() {
|
||||
fs::create_dir_all(parent).expect("创建内置 Codex CLI 资源子目录失败");
|
||||
}
|
||||
let source_sha256 = sha256_file(&source_path).expect("读取内置 Codex CLI 资源失败");
|
||||
let target_matches_source = target_path.is_file()
|
||||
&& sha256_file(&target_path)
|
||||
.map(|target_sha256| target_sha256 == source_sha256)
|
||||
.unwrap_or(false);
|
||||
let source_permissions = fs::metadata(&source_path)
|
||||
.expect("读取组件权限失败")
|
||||
.permissions();
|
||||
if !target_matches_source {
|
||||
fs::copy(&source_path, &target_path).expect("复制内置 Codex CLI 资源失败");
|
||||
fs::set_permissions(&target_path, source_permissions.clone())
|
||||
.expect("保留内置 Codex CLI 组件权限失败");
|
||||
} else if fs::metadata(&target_path)
|
||||
.expect("读取内置 Codex CLI 资源失败")
|
||||
.permissions()
|
||||
!= source_permissions
|
||||
{
|
||||
// 内容相同但曾被错误 chmod 的 staging 文件也必须恢复执行权限。
|
||||
// 权限已一致时不再写元数据:Windows 上这次写入会更新 change time,
|
||||
// 让 tauri dev 的文件监听把每次构建都当成 staging 变更而无限重建。
|
||||
fs::set_permissions(&target_path, source_permissions)
|
||||
.expect("保留内置 Codex CLI 组件权限失败");
|
||||
}
|
||||
file_hashes.insert(
|
||||
relative.to_string(),
|
||||
serde_json::Value::String(source_sha256),
|
||||
);
|
||||
}
|
||||
let manifest = serde_json::json!({
|
||||
"schemaVersion": codex_bundle::SCHEMA,
|
||||
"platform": layout.platform,
|
||||
"version": codex_bundle::CLI_VERSION,
|
||||
"files": file_hashes,
|
||||
});
|
||||
let manifest_path = target_dir.join("manifest.json");
|
||||
let manifest_payload = format!(
|
||||
"{}\n",
|
||||
serde_json::to_string_pretty(&manifest).expect("序列化内置 Codex CLI 清单失败")
|
||||
);
|
||||
if fs::read_to_string(&manifest_path)
|
||||
.map(|current| current != manifest_payload)
|
||||
.unwrap_or(true)
|
||||
{
|
||||
fs::write(&manifest_path, manifest_payload).expect("写入内置 Codex CLI 清单失败");
|
||||
}
|
||||
for relative in layout.files {
|
||||
println!("cargo:rerun-if-changed={}", source.join(relative).display());
|
||||
}
|
||||
println!("cargo:rerun-if-changed={}", notice.display());
|
||||
}
|
||||
}
|
||||
use codex_bundle::package_layout;
|
||||
|
||||
fn seed_task_group_id(
|
||||
group: &shared_contracts::game_creation_app::GameCreationAppAgentGroup,
|
||||
@@ -217,16 +61,58 @@ fn validate_seed_task_catalog(compiled: &runtime_prompt_bundle::CompiledPromptBu
|
||||
}
|
||||
}
|
||||
|
||||
/// 只读校验:确认已经落盘的随包产物与声明一致。本函数不写任何文件。
|
||||
fn validate_staged_resources(manifest_dir: &std::path::Path) {
|
||||
let target = env::var("TARGET").expect("Cargo TARGET");
|
||||
for staged_target in package_layout::staged_targets(&target) {
|
||||
let Some(layout) = codex_bundle::for_target(staged_target) else {
|
||||
continue;
|
||||
};
|
||||
let target_dir = manifest_dir
|
||||
.join(package_layout::codex().resource_directory)
|
||||
.join(layout.directory);
|
||||
package_layout::validate_staged_codex_bundle(&target_dir, staged_target).unwrap_or_else(
|
||||
|error| panic!("内置 Codex CLI 随包资源校验失败({staged_target}):{error}"),
|
||||
);
|
||||
}
|
||||
validate_staged_plugin_workspace(manifest_dir, &target);
|
||||
}
|
||||
|
||||
/// 只读校验插件随包工作区:声明的源码派生内容必须与仓库源码逐文件一致,整树无符号链接。
|
||||
fn validate_staged_plugin_workspace(manifest_dir: &std::path::Path, target: &str) {
|
||||
let declared = package_layout::plugins();
|
||||
let repo_root = manifest_dir
|
||||
.parent()
|
||||
.and_then(|app_root| app_root.parent())
|
||||
.and_then(|apps_dir| apps_dir.parent())
|
||||
.expect("AGC 应用必须位于仓库 apps 目录下");
|
||||
package_layout::validate_staged_plugins(
|
||||
&repo_root.join(declared.source_directory),
|
||||
&manifest_dir.join(declared.destination_directory),
|
||||
target,
|
||||
package_layout::cargo_feature_enabled,
|
||||
)
|
||||
.unwrap_or_else(|error| panic!("插件随包资源校验失败:{error}"));
|
||||
}
|
||||
fn main() {
|
||||
let manifest_dir = PathBuf::from(
|
||||
env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be available"),
|
||||
);
|
||||
let manifest_path = manifest_dir.join("prompts/runtime/manifest.json");
|
||||
stage_bundled_codex_cli(&manifest_dir);
|
||||
prepare_unity_editor_helper(&manifest_dir);
|
||||
prepare_godot_editor_extension(&manifest_dir);
|
||||
stage_plugin_workspace(&manifest_dir);
|
||||
stage_cocos_editor_payload(&manifest_dir);
|
||||
// 运行期定位随包目录依赖该编译期常量,与是否跳过 staging 无关(见技术方案 §4.4)。
|
||||
println!(
|
||||
"cargo:rustc-env=AGC_BUILD_TARGET={}",
|
||||
env::var("TARGET").expect("Cargo TARGET")
|
||||
);
|
||||
// AGC_SKIP_RESOURCE_STAGING=1 只做只读校验(要求随包资源已由准备步骤生成),
|
||||
// 用于在既有产物上单独验证校验路径。
|
||||
if env::var_os("AGC_SKIP_RESOURCE_STAGING").is_none() {
|
||||
prepare_unity_editor_helper(&manifest_dir);
|
||||
prepare_godot_editor_extension(&manifest_dir);
|
||||
stage_build_generated_plugin_payloads(&manifest_dir);
|
||||
stage_cocos_editor_payload(&manifest_dir);
|
||||
}
|
||||
validate_staged_resources(&manifest_dir);
|
||||
let compiled = runtime_prompt_bundle::compile_manifest(&manifest_path)
|
||||
.unwrap_or_else(|error| panic!("Prompt Bundle 编译失败:{error}"));
|
||||
validate_seed_task_catalog(&compiled);
|
||||
@@ -429,134 +315,101 @@ fn prepare_godot_editor_extension(manifest_dir: &std::path::Path) {
|
||||
godot_bundle::validate(&root).unwrap_or_else(|error| panic!("{error}"));
|
||||
}
|
||||
|
||||
/// 把 `plugins/` 工作区里的插件包随包映射到应用资源目录。
|
||||
/// 构建期产物归位:只有构建过程才产出、因而无法由准备步骤生成的随包子目录。
|
||||
///
|
||||
/// 只复制插件运行需要的清单、入口、面板和 native payload,不复制 native 源码、
|
||||
/// Cargo target 目录或 node_modules。
|
||||
fn stage_plugin_workspace(manifest_dir: &std::path::Path) {
|
||||
/// 源码派生的子目录由准备步骤在 `tauri dev|build` 之前写入;这里只补构建期才存在的产物。
|
||||
/// 把这批产物也归位到准备步骤(连同编辑器分支产物)在后续里程碑完成。
|
||||
fn stage_build_generated_plugin_payloads(manifest_dir: &std::path::Path) {
|
||||
let target = env::var("TARGET").expect("Cargo TARGET");
|
||||
if !target.contains("windows") && !target.contains("apple-darwin") {
|
||||
if !package_layout::plugin_staging_applies(&target) {
|
||||
return;
|
||||
}
|
||||
let declared = package_layout::plugins();
|
||||
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");
|
||||
// staging 是专用生成目录;重建清除跨目标 payload 与已删除插件的残留。
|
||||
if destination_root.exists() {
|
||||
std::fs::remove_dir_all(&destination_root).expect("清理插件 staging 失败");
|
||||
}
|
||||
std::fs::create_dir_all(&destination_root).expect("创建插件资源目录失败");
|
||||
let entries = match std::fs::read_dir(&workspace) {
|
||||
Ok(entries) => entries,
|
||||
Err(_) => return,
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let plugin_root = entry.path();
|
||||
assert!(
|
||||
!entry
|
||||
.file_type()
|
||||
.expect("读取插件目录类型失败")
|
||||
.is_symlink(),
|
||||
"插件工作区不允许符号链接"
|
||||
);
|
||||
if !plugin_root.is_dir() || !plugin_root.join("plugin.json").is_file() {
|
||||
continue;
|
||||
}
|
||||
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("skills"),
|
||||
std::path::PathBuf::from("native/payload"),
|
||||
std::path::PathBuf::from("dotnet/publish/win-x64"),
|
||||
] {
|
||||
if (relative == std::path::Path::new("native/payload") && !target.contains("windows"))
|
||||
|| (relative == std::path::Path::new("dotnet/publish/win-x64")
|
||||
&& (target != "x86_64-pc-windows-msvc"
|
||||
|| env::var_os("CARGO_FEATURE_UNITY_EDITOR_EXECUTE").is_none()))
|
||||
.expect("AGC 应用必须位于仓库 apps 目录下");
|
||||
let destination_root = manifest_dir.join(declared.destination_directory);
|
||||
let plugins = package_layout::plugin_directories(
|
||||
&repo_root.join(declared.source_directory),
|
||||
declared.manifest_file_name,
|
||||
)
|
||||
.unwrap_or_else(|error| panic!("{error}"));
|
||||
for plugin in plugins {
|
||||
for subdirectory in declared.subdirectories {
|
||||
if !package_layout::subdirectory_is_build_derived(subdirectory)
|
||||
|| !package_layout::subdirectory_enabled(
|
||||
subdirectory,
|
||||
&target,
|
||||
package_layout::cargo_feature_enabled,
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
copy_plugin_tree(&plugin_root.join(&relative), &destination.join(&relative));
|
||||
let relative = package_layout::declared_relative_path(subdirectory.path);
|
||||
let source = plugin.path.join(&relative);
|
||||
if !source.is_dir() {
|
||||
continue;
|
||||
}
|
||||
copy_staged_tree(
|
||||
&source,
|
||||
&destination_root.join(&plugin.name).join(&relative),
|
||||
);
|
||||
}
|
||||
if name == "agc-godot-editor" {
|
||||
godot_bundle::stage(
|
||||
&plugin_root.join("native/gdextension"),
|
||||
&destination.join("native/gdextension"),
|
||||
&target,
|
||||
env::var_os("CARGO_FEATURE_GODOT_EDITOR_EXECUTE").is_some(),
|
||||
)
|
||||
.unwrap_or_else(|error| panic!("{error}"));
|
||||
for staging in declared.library_staging {
|
||||
if plugin.name != staging.plugin
|
||||
|| !package_layout::library_staging_enabled(
|
||||
staging,
|
||||
&target,
|
||||
package_layout::cargo_feature_enabled,
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let relative = package_layout::declared_relative_path(staging.source_subdirectory);
|
||||
match staging.layout {
|
||||
"godot-bundle" => godot_bundle::stage(
|
||||
&plugin.path.join(&relative),
|
||||
&destination_root.join(&plugin.name).join(&relative),
|
||||
&target,
|
||||
true,
|
||||
)
|
||||
.unwrap_or_else(|error| panic!("{error}")),
|
||||
other => panic!("未实现的随包库 staging 布局:{other}"),
|
||||
}
|
||||
}
|
||||
println!("cargo:rerun-if-changed={}", plugin_root.display());
|
||||
}
|
||||
}
|
||||
|
||||
fn stage_plugin_file(source: &std::path::Path, destination: &std::path::Path) {
|
||||
let bytes = std::fs::read(source)
|
||||
.unwrap_or_else(|error| panic!("读取随包资源失败 {}:{error}", source.display()));
|
||||
if std::fs::read(destination).is_ok_and(|existing| existing == bytes) {
|
||||
/// 复制一棵目录树(按声明跳过构建产物与测试文件);内容一致时不重写。
|
||||
fn copy_staged_tree(source: &std::path::Path, destination: &std::path::Path) {
|
||||
if !source.is_dir() {
|
||||
return;
|
||||
}
|
||||
if let Some(parent) = destination.parent() {
|
||||
std::fs::create_dir_all(parent).expect("创建插件资源目录失败");
|
||||
}
|
||||
std::fs::write(destination, bytes).expect("复制插件资源失败");
|
||||
}
|
||||
|
||||
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());
|
||||
fs::create_dir_all(destination).expect("创建插件资源目录失败");
|
||||
for entry in fs::read_dir(source).expect("读取插件资源失败").flatten() {
|
||||
let path = entry.path();
|
||||
let file_name = entry.file_name();
|
||||
let name = file_name.to_string_lossy().to_string();
|
||||
let target = destination.join(&file_name);
|
||||
assert!(
|
||||
!entry
|
||||
entry
|
||||
.file_type()
|
||||
.expect("读取插件文件类型失败")
|
||||
.is_symlink(),
|
||||
"插件资源不允许符号链接"
|
||||
);
|
||||
if path.is_dir() {
|
||||
let name = entry.file_name();
|
||||
let name = name.to_string_lossy();
|
||||
if name.starts_with('.') || matches!(name.as_ref(), "target" | "node_modules") {
|
||||
if !package_layout::skip_directory(&name) {
|
||||
copy_staged_tree(&path, &target);
|
||||
}
|
||||
} else if !package_layout::skip_file_name(&name) {
|
||||
let bytes = fs::read(&path).expect("读取插件资源失败");
|
||||
if fs::read(&target).is_ok_and(|existing| existing == bytes) {
|
||||
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;
|
||||
}
|
||||
if name.starts_with('.') {
|
||||
continue;
|
||||
}
|
||||
stage_plugin_file(&path, &target);
|
||||
fs::write(&target, bytes).expect("复制插件资源失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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("复制插件资源失败");
|
||||
}
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
//! 构建与运行共用的平台布局;只允许分发锁定原生包里的明确组件。
|
||||
//!
|
||||
//! 布局、组件白名单与版本常量来自唯一声明 `build_support/package-layout.json`
|
||||
//! (Rust 侧经 `build_support/package-layout.generated.rs` 取得编译期常量,
|
||||
//! 由 `scripts/check-package-layout.mjs` 生成并在门禁中校验一致)。
|
||||
//! 本模块只读声明,不写任何随包资源。
|
||||
|
||||
pub const VERSION: &str = "0.155.1";
|
||||
pub const CLI_VERSION: &str = "codex-cli 0.155.1";
|
||||
pub const SCHEMA: &str = "genarrative-codex-sidecar.v2";
|
||||
// 共享声明模块:构建脚本、运行期与测试各自只用到其中一部分,未用到的入口不算缺陷。
|
||||
#[allow(dead_code)]
|
||||
#[path = "package_layout.rs"]
|
||||
pub(crate) mod package_layout;
|
||||
|
||||
pub const VERSION: &str = package_layout::CODEX_VERSION;
|
||||
pub const CLI_VERSION: &str = package_layout::CODEX_CLI_VERSION;
|
||||
pub const SCHEMA: &str = package_layout::CODEX_MANIFEST_SCHEMA;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Layout {
|
||||
@@ -12,46 +22,13 @@ pub struct Layout {
|
||||
pub files: &'static [&'static str],
|
||||
}
|
||||
|
||||
const WINDOWS_FILES: &[&str] = &[
|
||||
"bin/codex.exe",
|
||||
"bin/codex-code-mode-host.exe",
|
||||
"codex-path/rg.exe",
|
||||
"codex-resources/codex-command-runner.exe",
|
||||
"codex-resources/codex-windows-sandbox-setup.exe",
|
||||
"codex-package.json",
|
||||
];
|
||||
const MAC_FILES: &[&str] = &[
|
||||
"bin/codex",
|
||||
"bin/codex-code-mode-host",
|
||||
"codex-path/rg",
|
||||
"codex-resources/zsh/bin/zsh",
|
||||
"codex-package.json",
|
||||
];
|
||||
|
||||
pub fn for_target(target: &str) -> Option<Layout> {
|
||||
match target {
|
||||
"x86_64-pc-windows-msvc" => Some(Layout {
|
||||
platform: "win32-x64",
|
||||
directory: "win-x64",
|
||||
executable: "bin/codex.exe",
|
||||
files: WINDOWS_FILES,
|
||||
}),
|
||||
"aarch64-apple-darwin" | "x86_64-apple-darwin" => Some(Layout {
|
||||
platform: if target.starts_with("aarch64") {
|
||||
"darwin-arm64"
|
||||
} else {
|
||||
"darwin-x64"
|
||||
},
|
||||
directory: if target.starts_with("aarch64") {
|
||||
"mac-native/darwin-arm64"
|
||||
} else {
|
||||
"mac-native/darwin-x64"
|
||||
},
|
||||
executable: "bin/codex",
|
||||
files: MAC_FILES,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
package_layout::codex_target(target).map(|declared| Layout {
|
||||
platform: declared.platform,
|
||||
directory: declared.directory,
|
||||
executable: declared.executable,
|
||||
files: declared.files,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -80,4 +57,11 @@ mod tests {
|
||||
assert!(for_target("aarch64-pc-windows-msvc").is_none());
|
||||
assert!(for_target("x86_64-unknown-linux-gnu").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constants_come_from_the_shared_declaration() {
|
||||
assert_eq!(VERSION, "0.155.1");
|
||||
assert_eq!(CLI_VERSION, format!("codex-cli {VERSION}"));
|
||||
assert_eq!(SCHEMA, "genarrative-codex-sidecar.v2");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
//! 随包阶段的原生包元数据校验,不进入运行时生产模块。
|
||||
|
||||
use super::codex_bundle::{Layout, VERSION};
|
||||
|
||||
pub fn validate_package_metadata(
|
||||
metadata: &serde_json::Value,
|
||||
target: &str,
|
||||
layout: Layout,
|
||||
) -> Result<(), String> {
|
||||
if metadata["layoutVersion"] == 1
|
||||
&& metadata["version"] == VERSION
|
||||
&& metadata["target"] == target
|
||||
&& metadata["entrypoint"] == layout.executable
|
||||
&& metadata["resourcesDir"] == "codex-resources"
|
||||
&& metadata["pathDir"] == "codex-path"
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("Codex 原生包版本、布局或架构不匹配目标 {target}"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::codex_bundle::for_target;
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn metadata_rejects_version_architecture_and_layout_drift() {
|
||||
let target = "aarch64-apple-darwin";
|
||||
let layout = for_target(target).unwrap();
|
||||
let valid = serde_json::json!({
|
||||
"layoutVersion": 1,
|
||||
"version": VERSION,
|
||||
"target": target,
|
||||
"entrypoint": "bin/codex",
|
||||
"resourcesDir": "codex-resources",
|
||||
"pathDir": "codex-path",
|
||||
});
|
||||
assert!(validate_package_metadata(&valid, target, layout).is_ok());
|
||||
for (key, value) in [
|
||||
("layoutVersion", serde_json::json!(2)),
|
||||
("version", serde_json::json!("0.0.0")),
|
||||
("target", serde_json::json!("x86_64-apple-darwin")),
|
||||
("entrypoint", serde_json::json!("bin/codex.exe")),
|
||||
("resourcesDir", serde_json::json!("../private")),
|
||||
("pathDir", serde_json::json!(null)),
|
||||
] {
|
||||
let mut invalid = valid.clone();
|
||||
invalid[key] = value;
|
||||
assert!(
|
||||
validate_package_metadata(&invalid, target, layout).is_err(),
|
||||
"{key}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
// @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:typecheck 链内含 check-package-layout.mjs)。
|
||||
|
||||
pub const DECLARATION_SCHEMA: &str = "agc-package-layout.v1";
|
||||
pub const LAYOUT_VERSION: u64 = 1;
|
||||
|
||||
pub const CODEX_VERSION: &str = "0.155.1";
|
||||
pub const CODEX_CLI_VERSION: &str = "codex-cli 0.155.1";
|
||||
pub const CODEX_MANIFEST_SCHEMA: &str = "genarrative-codex-sidecar.v2";
|
||||
|
||||
pub const CODEX: Codex = Codex {
|
||||
package_metadata: PackageMetadata {
|
||||
layout_version: 1,
|
||||
resources_dir: "codex-resources",
|
||||
path_dir: "codex-path",
|
||||
},
|
||||
resource_directory: "resources/codex",
|
||||
manifest_file_name: "manifest.json",
|
||||
package_metadata_file_name: "codex-package.json",
|
||||
notice_file_name: "NOTICE.md",
|
||||
source_roots: &["app", "repo"],
|
||||
source_relative_paths: &["node_modules/@openai/codex-<platform>/vendor/<target>", "node_modules/@openai/codex/node_modules/@openai/codex-<platform>/vendor/<target>"],
|
||||
notice_sources: &[
|
||||
NoticeSource {
|
||||
targets: &["aarch64-apple-darwin", "x86_64-apple-darwin"],
|
||||
source: "resources/codex/【声明】Mac内置Codex组件-2026-09-18.md",
|
||||
preserve: false,
|
||||
},
|
||||
NoticeSource {
|
||||
targets: &["x86_64-pc-windows-msvc"],
|
||||
source: "resources/codex/win-x64/NOTICE.md",
|
||||
preserve: true,
|
||||
}
|
||||
],
|
||||
universal_groups: &[
|
||||
UniversalGroup {
|
||||
name: "mac-native",
|
||||
directory: "mac-native",
|
||||
targets: &["aarch64-apple-darwin", "x86_64-apple-darwin"],
|
||||
}
|
||||
],
|
||||
targets: &[
|
||||
CodexTarget {
|
||||
target: "x86_64-pc-windows-msvc",
|
||||
platform: "win32-x64",
|
||||
directory: "win-x64",
|
||||
executable: "bin/codex.exe",
|
||||
files: &["bin/codex.exe", "bin/codex-code-mode-host.exe", "codex-path/rg.exe", "codex-resources/codex-command-runner.exe", "codex-resources/codex-windows-sandbox-setup.exe", "codex-package.json"],
|
||||
},
|
||||
CodexTarget {
|
||||
target: "aarch64-apple-darwin",
|
||||
platform: "darwin-arm64",
|
||||
directory: "mac-native/darwin-arm64",
|
||||
executable: "bin/codex",
|
||||
files: &["bin/codex", "bin/codex-code-mode-host", "codex-path/rg", "codex-resources/zsh/bin/zsh", "codex-package.json"],
|
||||
},
|
||||
CodexTarget {
|
||||
target: "x86_64-apple-darwin",
|
||||
platform: "darwin-x64",
|
||||
directory: "mac-native/darwin-x64",
|
||||
executable: "bin/codex",
|
||||
files: &["bin/codex", "bin/codex-code-mode-host", "codex-path/rg", "codex-resources/zsh/bin/zsh", "codex-package.json"],
|
||||
}
|
||||
],
|
||||
};
|
||||
|
||||
pub const PLUGINS: Plugins = Plugins {
|
||||
source_directory: "plugins",
|
||||
destination_directory: "resources/plugins",
|
||||
manifest_file_name: "plugin.json",
|
||||
target_contains_any: &["windows", "apple-darwin"],
|
||||
subdirectories: &[
|
||||
Subdirectory {
|
||||
path: "src",
|
||||
origin: "source",
|
||||
target_contains: &[],
|
||||
targets: &[],
|
||||
features: &[],
|
||||
},
|
||||
Subdirectory {
|
||||
path: "panels",
|
||||
origin: "source",
|
||||
target_contains: &[],
|
||||
targets: &[],
|
||||
features: &[],
|
||||
},
|
||||
Subdirectory {
|
||||
path: "skills",
|
||||
origin: "source",
|
||||
target_contains: &[],
|
||||
targets: &[],
|
||||
features: &[],
|
||||
},
|
||||
Subdirectory {
|
||||
path: "native/payload",
|
||||
origin: "source",
|
||||
target_contains: &["windows"],
|
||||
targets: &[],
|
||||
features: &[],
|
||||
},
|
||||
Subdirectory {
|
||||
path: "dotnet/publish/win-x64",
|
||||
origin: "build",
|
||||
target_contains: &[],
|
||||
targets: &["x86_64-pc-windows-msvc"],
|
||||
features: &["unity-editor-execute"],
|
||||
}
|
||||
],
|
||||
library_staging: &[
|
||||
LibraryStaging {
|
||||
plugin: "agc-godot-editor",
|
||||
source_subdirectory: "native/gdextension",
|
||||
targets: &["x86_64-pc-windows-msvc"],
|
||||
features: &["godot-editor-execute"],
|
||||
layout: "godot-bundle",
|
||||
}
|
||||
],
|
||||
skip_directory_names: &["target", "node_modules"],
|
||||
skip_directory_name_prefixes: &["."],
|
||||
skip_file_name_prefixes: &["."],
|
||||
skip_file_name_fragments: &[".test."],
|
||||
};
|
||||
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"schema": "agc-package-layout.v1",
|
||||
"layoutVersion": 1,
|
||||
"description": "AGC 随包资源布局与复制规则的唯一声明。Rust 侧构建期校验与 Node 侧准备步骤共用本文件,任何一侧都不得再写第二份布局或组件白名单。含 <platform>、<target> 占位符的字段由调用方按目标三元展开。修改布局时同步递增 layoutVersion(准备步骤的缓存 key 组成部分)。",
|
||||
"codex": {
|
||||
"version": "0.155.1",
|
||||
"cliVersionPrefix": "codex-cli ",
|
||||
"manifestSchema": "genarrative-codex-sidecar.v2",
|
||||
"packageMetadata": {
|
||||
"layoutVersion": 1,
|
||||
"resourcesDir": "codex-resources",
|
||||
"pathDir": "codex-path"
|
||||
},
|
||||
"resourceDirectory": "resources/codex",
|
||||
"manifestFileName": "manifest.json",
|
||||
"packageMetadataFileName": "codex-package.json",
|
||||
"noticeFileName": "NOTICE.md",
|
||||
"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"],
|
||||
"source": "resources/codex/【声明】Mac内置Codex组件-2026-09-18.md",
|
||||
"preserve": false
|
||||
},
|
||||
{
|
||||
"targets": ["x86_64-pc-windows-msvc"],
|
||||
"source": "resources/codex/win-x64/NOTICE.md",
|
||||
"preserve": true
|
||||
}
|
||||
],
|
||||
"universalGroups": [
|
||||
{
|
||||
"name": "mac-native",
|
||||
"directory": "mac-native",
|
||||
"targets": ["aarch64-apple-darwin", "x86_64-apple-darwin"]
|
||||
}
|
||||
],
|
||||
"targets": [
|
||||
{
|
||||
"target": "x86_64-pc-windows-msvc",
|
||||
"platform": "win32-x64",
|
||||
"directory": "win-x64",
|
||||
"executable": "bin/codex.exe",
|
||||
"files": [
|
||||
"bin/codex.exe",
|
||||
"bin/codex-code-mode-host.exe",
|
||||
"codex-path/rg.exe",
|
||||
"codex-resources/codex-command-runner.exe",
|
||||
"codex-resources/codex-windows-sandbox-setup.exe",
|
||||
"codex-package.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "aarch64-apple-darwin",
|
||||
"platform": "darwin-arm64",
|
||||
"directory": "mac-native/darwin-arm64",
|
||||
"executable": "bin/codex",
|
||||
"files": [
|
||||
"bin/codex",
|
||||
"bin/codex-code-mode-host",
|
||||
"codex-path/rg",
|
||||
"codex-resources/zsh/bin/zsh",
|
||||
"codex-package.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "x86_64-apple-darwin",
|
||||
"platform": "darwin-x64",
|
||||
"directory": "mac-native/darwin-x64",
|
||||
"executable": "bin/codex",
|
||||
"files": [
|
||||
"bin/codex",
|
||||
"bin/codex-code-mode-host",
|
||||
"codex-path/rg",
|
||||
"codex-resources/zsh/bin/zsh",
|
||||
"codex-package.json"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"plugins": {
|
||||
"sourceDirectory": "plugins",
|
||||
"destinationDirectory": "resources/plugins",
|
||||
"manifestFileName": "plugin.json",
|
||||
"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": "dotnet/publish/win-x64",
|
||||
"origin": "build",
|
||||
"targets": ["x86_64-pc-windows-msvc"],
|
||||
"features": ["unity-editor-execute"]
|
||||
}
|
||||
],
|
||||
"libraryStaging": [
|
||||
{
|
||||
"plugin": "agc-godot-editor",
|
||||
"sourceSubdirectory": "native/gdextension",
|
||||
"targets": ["x86_64-pc-windows-msvc"],
|
||||
"features": ["godot-editor-execute"],
|
||||
"layout": "godot-bundle"
|
||||
}
|
||||
],
|
||||
"skipDirectoryNames": ["target", "node_modules"],
|
||||
"skipDirectoryNamePrefixes": ["."],
|
||||
"skipFileNamePrefixes": ["."],
|
||||
"skipFileNameFragments": [".test."]
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,11 +8,6 @@ use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt};
|
||||
#[path = "../../build_support/codex_bundle.rs"]
|
||||
pub(crate) mod codex_bundle;
|
||||
|
||||
// 复用构建端校验的既有单测,生产运行时只编译共享布局。
|
||||
#[cfg(test)]
|
||||
#[path = "../../build_support/codex_package_metadata.rs"]
|
||||
mod codex_package_metadata;
|
||||
|
||||
const GAME_CREATOR_CODEX_CLI_EXECUTABLE: &str = "codex";
|
||||
const GAME_CREATOR_CODEX_CLI_PROMPT_MAX_BYTES: usize = 4 * 1024 * 1024;
|
||||
const GAME_CREATOR_CODEX_CLI_STDOUT_MAX_BYTES: usize = 4 * 1024 * 1024;
|
||||
|
||||
@@ -2326,30 +2326,6 @@ fn direct_taonier_reference_matches_local_source(
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn direct_taonier_background_asset_identity(
|
||||
root: &Path,
|
||||
expected_reference_source: Option<&DirectTaonierArtAssetIdentity>,
|
||||
) -> Option<DirectTaonierArtAssetIdentity> {
|
||||
direct_taonier_art_asset_identity(
|
||||
root,
|
||||
DIRECT_CODEX_BACKGROUND_ASSET_PATH,
|
||||
GameCreationAppAssetKind::Scene,
|
||||
"/api/external/v1/editor/scenes/generations",
|
||||
"scene",
|
||||
expected_reference_source,
|
||||
)
|
||||
.or_else(|| {
|
||||
direct_taonier_art_asset_identity(
|
||||
root,
|
||||
DIRECT_CODEX_BACKGROUND_ASSET_PATH,
|
||||
GameCreationAppAssetKind::Scene,
|
||||
"/api/external/v1/editor/images/generations",
|
||||
"spec",
|
||||
expected_reference_source,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn direct_taonier_art_base_is_valid(root: &Path) -> bool {
|
||||
let Some(art_spec) = direct_taonier_art_asset_identity(
|
||||
root,
|
||||
@@ -2361,7 +2337,14 @@ fn direct_taonier_art_base_is_valid(root: &Path) -> bool {
|
||||
) else {
|
||||
return false;
|
||||
};
|
||||
let Some(background) = direct_taonier_background_asset_identity(root, Some(&art_spec)) else {
|
||||
let Some(background) = direct_taonier_art_asset_identity(
|
||||
root,
|
||||
DIRECT_CODEX_BACKGROUND_ASSET_PATH,
|
||||
GameCreationAppAssetKind::Scene,
|
||||
"/api/external/v1/editor/images/generations",
|
||||
"spec",
|
||||
Some(&art_spec),
|
||||
) else {
|
||||
return false;
|
||||
};
|
||||
let _ = (background, art_spec);
|
||||
@@ -3342,7 +3325,15 @@ pub(crate) async fn ensure_direct_taonier_art_package_at(
|
||||
})
|
||||
.flatten();
|
||||
let existing_art_spec_and_background = existing_art_spec.as_ref().is_some_and(|art_spec| {
|
||||
direct_taonier_background_asset_identity(root, Some(art_spec)).is_some()
|
||||
direct_taonier_art_asset_identity(
|
||||
root,
|
||||
DIRECT_CODEX_BACKGROUND_ASSET_PATH,
|
||||
GameCreationAppAssetKind::Scene,
|
||||
"/api/external/v1/editor/images/generations",
|
||||
"spec",
|
||||
Some(art_spec),
|
||||
)
|
||||
.is_some()
|
||||
});
|
||||
let art_spec = match existing_art_spec {
|
||||
Some(identity) => identity,
|
||||
@@ -3418,7 +3409,15 @@ pub(crate) async fn ensure_direct_taonier_art_package_at(
|
||||
}
|
||||
};
|
||||
if mode.regenerates_existing()
|
||||
|| direct_taonier_background_asset_identity(root, Some(&art_spec)).is_none()
|
||||
|| direct_taonier_art_asset_identity(
|
||||
root,
|
||||
DIRECT_CODEX_BACKGROUND_ASSET_PATH,
|
||||
GameCreationAppAssetKind::Scene,
|
||||
"/api/external/v1/editor/images/generations",
|
||||
"spec",
|
||||
Some(&art_spec),
|
||||
)
|
||||
.is_none()
|
||||
{
|
||||
emit_direct_game_creator_progress(root, "art.background", "正在生成 16:9 游戏场景背景图");
|
||||
let outcome = match generate_direct_taonier_art_asset_at(
|
||||
@@ -8295,8 +8294,8 @@ mod tests {
|
||||
vec!["taonier-resource-icon-spec".to_string()],
|
||||
),
|
||||
GameCreationAppAssetKind::Scene => (
|
||||
"/api/external/v1/editor/scenes/generations",
|
||||
"scene",
|
||||
"/api/external/v1/editor/images/generations",
|
||||
"spec",
|
||||
vec!["taonier-resource-icon-spec".to_string()],
|
||||
),
|
||||
_ => (
|
||||
@@ -8680,46 +8679,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_route_background_registration_still_counts_as_valid_base() {
|
||||
let root = tempfile::tempdir().expect("temp dir");
|
||||
init_local_game_project_at(root.path(), "legacy-background-route", "旧路由背景登记")
|
||||
.expect("init project");
|
||||
std::fs::create_dir_all(root.path().join("assets")).expect("assets dir");
|
||||
register_direct_taonier_art_asset_fixture(
|
||||
root.path(),
|
||||
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
|
||||
GameCreationAppAssetKind::IconSpec,
|
||||
);
|
||||
std::fs::write(
|
||||
root.path().join(DIRECT_CODEX_BACKGROUND_ASSET_PATH),
|
||||
tiny_visible_png(),
|
||||
)
|
||||
.expect("background bytes");
|
||||
register_local_asset_at(
|
||||
root.path(),
|
||||
DIRECT_CODEX_BACKGROUND_ASSET_PATH,
|
||||
GameCreationAppAssetKind::Scene,
|
||||
"image/png",
|
||||
"platform-art",
|
||||
GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Canvas,
|
||||
canvas_project_id: Some("taonier-project".to_string()),
|
||||
resource_id: Some("taonier-resource-game-background".to_string()),
|
||||
asset_object_id: Some("taonier-object-game-background".to_string()),
|
||||
task_id: Some("taonier-task-game-background".to_string()),
|
||||
prompt: None,
|
||||
model: Some("gpt-image-2".to_string()),
|
||||
generation_route: Some("/api/external/v1/editor/images/generations".to_string()),
|
||||
generation_kind: Some("spec".to_string()),
|
||||
reference_resource_ids: vec!["taonier-resource-icon-spec".to_string()],
|
||||
},
|
||||
)
|
||||
.expect("register legacy-route background");
|
||||
|
||||
assert!(direct_taonier_art_base_is_valid(root.path()));
|
||||
}
|
||||
|
||||
fn tiny_visible_png() -> Vec<u8> {
|
||||
let mut bytes = Vec::new();
|
||||
let image = image::DynamicImage::ImageRgba8(image::RgbaImage::from_pixel(
|
||||
|
||||
@@ -2835,15 +2835,14 @@ pub(in crate::agent) fn retained_platform_art_generation_runtime_state_matches_d
|
||||
&& art_spec_asset_type == Some("icon-spec")
|
||||
}
|
||||
GameCreationAppAssetKind::Scene => {
|
||||
snapshot.endpoint == "/api/external/v1/editor/scenes/generations"
|
||||
&& snapshot.generation_kind == "scene"
|
||||
snapshot.endpoint == "/api/external/v1/editor/images/generations"
|
||||
&& snapshot.generation_kind == "spec"
|
||||
&& platform_art_runtime_references_match_request_contract(
|
||||
&snapshot.reference_resource_ids,
|
||||
GameCreationAppAssetKind::Scene,
|
||||
)
|
||||
&& json_string_field(&request_body, "sceneContent")
|
||||
.is_some_and(|value| !value.trim().is_empty())
|
||||
&& json_string_field(&request_body, "stylePreset").as_deref() == Some("custom")
|
||||
&& request_asset_kind.as_deref() == Some("game-background")
|
||||
&& art_spec_asset_type == Some("background")
|
||||
}
|
||||
GameCreationAppAssetKind::IconSpritesheet => {
|
||||
snapshot.endpoint == "/api/external/v1/editor/icon-spritesheets/generations"
|
||||
@@ -3249,7 +3248,6 @@ pub(in crate::agent) async fn request_platform_art_asset_with_runtime_options_at
|
||||
GameCreationAppAssetKind::Image => "image",
|
||||
GameCreationAppAssetKind::Character => "character",
|
||||
GameCreationAppAssetKind::PublicationMaterial => "publication-material",
|
||||
GameCreationAppAssetKind::Scene => "scene",
|
||||
_ => "spec",
|
||||
};
|
||||
let is_canonical_art_spritesheet =
|
||||
@@ -3293,25 +3291,6 @@ pub(in crate::agent) async fn request_platform_art_asset_with_runtime_options_at
|
||||
},
|
||||
}),
|
||||
)
|
||||
} else if options.asset_kind == GameCreationAppAssetKind::Scene {
|
||||
(
|
||||
"/api/external/v1/editor/scenes/generations",
|
||||
serde_json::json!({
|
||||
"sceneContent": generation_prompt,
|
||||
"stylePreset": "custom",
|
||||
"customStyle": prompt_text!("media.scene_style"),
|
||||
"aspectRatio": options.aspect_ratio,
|
||||
"imageSize": options.image_size,
|
||||
"assetLabel": options.asset_label,
|
||||
"projectId": canvas_context.project_id,
|
||||
"assetFolderId": canvas_context.asset_folder_id,
|
||||
"referenceImageSrcs": references.ordered.clone(),
|
||||
"canvasCompletion": {
|
||||
"title": options.asset_label,
|
||||
"placeholder": external_canvas_placeholder(&options.aspect_ratio),
|
||||
},
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
"/api/external/v1/editor/images/generations",
|
||||
@@ -13293,13 +13272,14 @@ mod canvas_generation_tests {
|
||||
let background = write_retained_stage(
|
||||
root,
|
||||
"run-background-canonical-and-user-references",
|
||||
"/api/external/v1/editor/scenes/generations",
|
||||
"/api/external/v1/editor/images/generations",
|
||||
serde_json::json!({
|
||||
"sceneContent": "保留账本参考合同",
|
||||
"stylePreset": "custom",
|
||||
"customStyle": "原创横屏 Web 游戏场景背景",
|
||||
"prompt": "保留账本参考合同",
|
||||
"kind": "spec",
|
||||
"assetKind": "game-background",
|
||||
"projectId": "test-canvas-project",
|
||||
"assetFolderId": "test-asset-folder",
|
||||
"generationInputs": { "artSpec": { "assetType": "background" } },
|
||||
"referenceImageSrcs": ["resource-icon-spec", "user-reference-1"],
|
||||
}),
|
||||
);
|
||||
|
||||
-24
@@ -735,30 +735,6 @@ pub(super) fn platform_art_generation_runtime_request_snapshot(
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
(generation_kind, reference_resource_ids)
|
||||
}
|
||||
"/api/external/v1/editor/scenes/generations" => {
|
||||
if json_string_field(&request_body, "sceneContent")
|
||||
.is_none_or(|value| value.trim().is_empty())
|
||||
{
|
||||
return Err("External Editor 场景生成账本请求缺少 sceneContent".to_string());
|
||||
}
|
||||
let reference_resource_ids = request_body
|
||||
.get("referenceImageSrcs")
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.ok_or_else(|| {
|
||||
"External Editor 场景生成账本请求缺少 referenceImageSrcs".to_string()
|
||||
})?
|
||||
.iter()
|
||||
.map(|value| {
|
||||
value
|
||||
.as_str()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(str::to_string)
|
||||
.ok_or_else(|| "External Editor 场景生成账本引用资源 ID 无效".to_string())
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
("scene".to_string(), reference_resource_ids)
|
||||
}
|
||||
"/api/external/v1/editor/icon-spritesheets/generations" => {
|
||||
let reference_resource_id = json_string_field(&request_body, "referenceId")
|
||||
.or_else(|| json_string_field(&request_body, "referenceImageSrc"))
|
||||
|
||||
@@ -555,7 +555,6 @@ fn platform_art_generation_request_snapshot_is_valid(payload: &serde_json::Value
|
||||
endpoint,
|
||||
Some(
|
||||
"/api/external/v1/editor/images/generations"
|
||||
| "/api/external/v1/editor/scenes/generations"
|
||||
| "/api/external/v1/editor/icon-spritesheets/generations"
|
||||
)
|
||||
);
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
#[path = "../build_support/godot_bundle.rs"]
|
||||
mod godot_bundle;
|
||||
|
||||
// 复用随包资源声明的既有单测(校验通过/拒绝用例),生产运行时只经 codex_bundle 使用布局。
|
||||
#[cfg(test)]
|
||||
#[path = "../build_support/package_layout.rs"]
|
||||
mod package_layout;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs;
|
||||
use std::fs::{File, OpenOptions};
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from '../scripts/start-dev-stack.mjs';
|
||||
import {
|
||||
buildTauriArguments,
|
||||
buildTauriDevProcessEnv,
|
||||
runTauriDev as runTauriDevImpl,
|
||||
withDevCargoFeatures,
|
||||
} from '../scripts/start-tauri-dev.mjs';
|
||||
@@ -27,7 +28,13 @@ const resolveTestEndpoint = async () => testEndpoint;
|
||||
const runTauriDev = (
|
||||
argv: string[],
|
||||
options: Parameters<typeof runTauriDevImpl>[1],
|
||||
) => runTauriDevImpl(argv, { prepareFrontend: async () => {}, ...options });
|
||||
) =>
|
||||
runTauriDevImpl(argv, {
|
||||
prepareFrontend: async () => {},
|
||||
// 随包资源准备会读取真实上游包与仓库插件工作区;需要断言的用例自行注入。
|
||||
prepareResources: () => {},
|
||||
...options,
|
||||
});
|
||||
|
||||
async function waitForFile(path: string, timeoutMs = 5000) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
@@ -152,6 +159,10 @@ describe('AI 游戏创作 Tauri dev 生命周期', () => {
|
||||
prepareFrontend: async () => {
|
||||
order.push('frontend-ready');
|
||||
},
|
||||
prepareResources: (features) => {
|
||||
expect(Array.isArray(features)).toBe(true);
|
||||
order.push('resources');
|
||||
},
|
||||
spawnCli: () => {
|
||||
order.push('spawn');
|
||||
return child;
|
||||
@@ -171,6 +182,7 @@ describe('AI 游戏创作 Tauri dev 生命周期', () => {
|
||||
expect(order).toEqual([
|
||||
'preflight',
|
||||
'frontend-ready',
|
||||
'resources',
|
||||
'spawn',
|
||||
'exit',
|
||||
'cleanup',
|
||||
@@ -300,3 +312,54 @@ describe('AI 游戏创作 Tauri dev 生命周期', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('AI 游戏创作 Tauri dev 进程环境', () => {
|
||||
const posixTest = process.platform === 'win32' ? test.skip : test;
|
||||
|
||||
// 本机 `~/.cargo/config.toml` 或仓库级 Cargo 配置里的 sccache wrapper 只有在环境变量
|
||||
// 非空时才会被覆盖;这里必须显式写入要交给 Tauri Cargo 的 wrapper 决策结果。
|
||||
posixTest('本地 dev 不把 sccache 交给 Tauri Cargo', () => {
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
try {
|
||||
const env = buildTauriDevProcessEnv(testEndpoint, {
|
||||
RUSTC_WRAPPER: 'sccache',
|
||||
CARGO_BUILD_RUSTC_WRAPPER: 'sccache',
|
||||
});
|
||||
|
||||
expect(env.RUSTC_WRAPPER).toBe('/usr/bin/env');
|
||||
expect(env.CARGO_BUILD_RUSTC_WRAPPER).toBe('/usr/bin/env');
|
||||
} finally {
|
||||
warn.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
posixTest('未显式配置 wrapper 时清空两个变量', () => {
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
try {
|
||||
const env = buildTauriDevProcessEnv(testEndpoint, {
|
||||
CARGO_TERM_COLOR: 'never',
|
||||
});
|
||||
|
||||
expect(env.RUSTC_WRAPPER).toBe('');
|
||||
expect(env.CARGO_BUILD_RUSTC_WRAPPER).toBe('');
|
||||
expect(env.CARGO_TERM_COLOR).toBe('never');
|
||||
} finally {
|
||||
warn.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
posixTest('保留显式自定义 wrapper 且不改写调用方 env', () => {
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
try {
|
||||
const input = { RUSTC_WRAPPER: '/opt/custom/rustc-wrapper' };
|
||||
const env = buildTauriDevProcessEnv(testEndpoint, input);
|
||||
|
||||
expect(env.RUSTC_WRAPPER).toBe('/opt/custom/rustc-wrapper');
|
||||
expect(env.CARGO_BUILD_RUSTC_WRAPPER).toBe('/opt/custom/rustc-wrapper');
|
||||
expect(env.GENARRATIVE_AGC_VITE_PORT).toBe(String(testEndpoint.port));
|
||||
expect(input).toEqual({ RUSTC_WRAPPER: '/opt/custom/rustc-wrapper' });
|
||||
} finally {
|
||||
warn.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+1
-1
@@ -23,7 +23,6 @@
|
||||
- [外部 OpenAPI 与 API Key 接入方案](./【后端架构】外部OpenAPI与APIKey接入方案-2026-06-19.md)
|
||||
- [外部 MCP 语义工具说明与参数设计](./technical/【技术方案】外部MCP语义工具说明与参数设计-2026-09-23.md):15 个新增语义工具与全部原工具并存,复用现有 External API;包含工具说明、action、参数、幂等和兼容合同。
|
||||
- [External v1 OpenAPI](./openapi/genarrative-external-v1.openapi.json):公开 HTTP 契约唯一机器可读来源。
|
||||
- [External v1 游戏场景生成路由](./technical/【技术方案】ExternalV1游戏场景生成路由-2026-09-24.md):external v1 结构化场景生成专用路由与 AGC 美术包背景阶段迁移合同。
|
||||
|
||||
## AI 游戏创作与 Agent Runtime
|
||||
|
||||
@@ -38,6 +37,7 @@
|
||||
|
||||
- [AI 游戏创作智能体 App 实施计划](./technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md):当前 DirectProject、受控语义工具、UI workflow、资源和运行时合同。
|
||||
- [AGC 后端框架整理与演进路线](./technical/【技术方案】AGC后端框架整理与演进路线-2026-09-18.md):共享 Runtime、本地执行宿主、云端控制面、领域/平台适配器及分阶段收口边界。
|
||||
- [AGC 随包资源 staging 归位](./technical/【技术方案】AGC随包资源staging归位-2026-09-26.md):随包资源改由准备步骤在 `tauri dev|build` 之前一次性生成、`build.rs` 退化为校验者;含缓存与原子性合同、入口接线、验收判据与里程碑拆分。
|
||||
- [AGC 异步操作可恢复闭环](./【技术方案】AGC异步操作可恢复闭环-2026-09-14.md):认证响应体、最近项目检查和首页自动创建的超时、逐项恢复与跨页防重合同。
|
||||
- [AGC 客户端稳定版生命周期大切换](./【技术方案】AGC客户端稳定版生命周期大切换-2026-09-14.md):统一 operation、认证/Runner、项目入口、本地恢复和 dev-stack 身份边界。
|
||||
- [策划会话 Runtime V2 接入与旧链路退役方案](./technical/【技术方案】策划会话RuntimeV2接入与旧链路退役-2026-09-03.md):历史方案,仅用于追溯 V2 的实现与退役过程,不作为当前实现依据。
|
||||
|
||||
@@ -1052,60 +1052,7 @@
|
||||
"$ref": "#/components/responses/UpstreamError"
|
||||
}
|
||||
},
|
||||
"description": "支持普通生图、规范图、角色图、快速编辑参考图、UI 设计图和宣发素材生成。kind 可取 spec、character、quick-edit、ui-design、publication-material。游戏场景不接受本接口的 kind/assetKind = scene,必须使用 /api/external/v1/editor/scenes/generations 提交结构化场景意图。"
|
||||
}
|
||||
},
|
||||
"/api/external/v1/editor/scenes/generations": {
|
||||
"post": {
|
||||
"x-mcp-excluded": true,
|
||||
"tags": ["Editor Images"],
|
||||
"operationId": "generateExternalEditorScene",
|
||||
"summary": "生成编辑器游戏场景(结构化场景意图)",
|
||||
"description": "只接受结构化场景意图:sceneContent + stylePreset(custom 时必须提供 customStyle),完整 Provider Prompt 由服务端组装,不接受调用方提交的完整 prompt。入队后按 kind/assetKind = scene 持久化,产物保存 scene.generate V2 配方;队列、计费、资源入库与画布写回与站内场景路由一致。",
|
||||
"security": [
|
||||
{
|
||||
"ExternalApiKey": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/IdempotencyKey"
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/EditorSceneGenerationRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"202": {
|
||||
"description": "生成任务已持久化入队",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ExternalEditorGenerationSubmissionResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#/components/responses/BadRequest"
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/Unauthorized"
|
||||
},
|
||||
"403": {
|
||||
"$ref": "#/components/responses/Forbidden"
|
||||
},
|
||||
"502": {
|
||||
"$ref": "#/components/responses/UpstreamError"
|
||||
}
|
||||
}
|
||||
"description": "支持普通生图、规范图、角色图、快速编辑参考图、UI 设计图和宣发素材生成。kind 可取 spec、character、quick-edit、ui-design、publication-material。"
|
||||
}
|
||||
},
|
||||
"/api/external/v1/editor/images/edits": {
|
||||
@@ -3018,69 +2965,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"EditorSceneGenerationRequest": {
|
||||
"type": "object",
|
||||
"required": ["sceneContent", "stylePreset"],
|
||||
"properties": {
|
||||
"sceneContent": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "画面内容(结构化场景意图主体)。纯空白在入队前返回 400。"
|
||||
},
|
||||
"stylePreset": {
|
||||
"type": "string",
|
||||
"enum": ["anime", "watercolor", "flat", "stop-motion", "custom"],
|
||||
"description": "视觉风格预设。custom 时必须同时提供非空 customStyle,否则返回 400。"
|
||||
},
|
||||
"customStyle": {
|
||||
"type": ["string", "null"],
|
||||
"description": "自定义画风描述,仅 stylePreset = custom 时使用。"
|
||||
},
|
||||
"model": {
|
||||
"type": ["string", "null"],
|
||||
"description": "图片模型,省略时使用服务端默认场景模型。"
|
||||
},
|
||||
"aspectRatio": {
|
||||
"type": ["string", "null"],
|
||||
"default": "16:9"
|
||||
},
|
||||
"imageSize": {
|
||||
"type": ["string", "null"],
|
||||
"default": "1K"
|
||||
},
|
||||
"referenceImageSrcs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "可选参考图,沿用普通图片生成的参考图口径。"
|
||||
},
|
||||
"projectId": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"generationInputs": {
|
||||
"$ref": "#/components/schemas/JsonValue",
|
||||
"description": "场景配方由服务端重建;仅保留 source 精确等于 ai-game-creator-client 的客户端来源标记,用于选择 AGC 队列结果与幂等命名空间。调用方 fields、action 和引用 provenance 不会覆盖服务端配方。"
|
||||
},
|
||||
"assetFolderId": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"assetLabel": {
|
||||
"type": ["string", "null"],
|
||||
"description": "省略或纯空白时统一使用「游戏场景」。"
|
||||
},
|
||||
"canvasCompletion": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/EditorCanvasGenerationCompletion"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"EditorImageGenerationRequest": {
|
||||
"type": "object",
|
||||
"required": ["prompt"],
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user