合并master并保留Spine序列帧修复
合并master当前工程、后端、前端和文档更新 按已确认方案解决Spine序列帧多模态、帧数和快速编辑冲突 保留当前分支底部工具栏宽度与隐藏滚动条样式
This commit is contained in:
@@ -29,7 +29,7 @@ Prefer `scripts/genarrative_external_api.py` for runnable REST calls. It uses on
|
||||
- Authenticate MCP and business API calls with `Authorization: Bearer <tnr_sk_...>`. Never ask the user to paste a key into chat or place one in repository files.
|
||||
- All eight generation POST routes require `Idempotency-Key` and return HTTP `202`; `202` is durable acceptance, not a media result.
|
||||
- Retry an uncertain submission only with the exact same body and the same idempotency key. A polling timeout is not permission to generate again.
|
||||
- Use stable references such as `objectKey`, project resource ID, or asset ID in generation requests. Use `/assets/read-url` only for temporary preview/download access.
|
||||
- Use stable references such as `objectKey`, project resource ID, or asset ID where each operation permits them. Image edit/redraw is stricter: `sourceReferenceId` accepts only a registered project resource ID or asset ID; upload confirmation alone is not enough. Use `/assets/read-url` only for temporary preview/download access.
|
||||
- Preserve both warning channels after completion. A general `warning` can coexist with `sliceWarning`; do not discard either.
|
||||
- Do not invent missing derivatives. A source-preserved warning means the main source remains usable but requested post-processing failed. A slice warning means the complete transparent sheet is usable but individual slices are absent.
|
||||
- For successful `style="pixelArt"`, treat completed-result and nested resource/asset dimensions as the final logical-grid PNG dimensions. They may differ from `size`, `imageSize`, the provider image, and `canvasCompletion.placeholder`; do not rescale or reject the artifact to match those inputs.
|
||||
|
||||
@@ -50,7 +50,7 @@ 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` |
|
||||
| Image edit/redraw | `/api/external/v1/editor/images/edits` | `prompt`, `sourceImageSrc` | `referenceImageSrcs`, `model`, `size`, `projectId`, `assetFolderId`, `assetLabel`, `sourceResourceId`, `targetLayerId`, `canvasCompletion` |
|
||||
| Image edit/redraw | `/api/external/v1/editor/images/edits` | `prompt`, `sourceReferenceId` | `referenceImageSrcs`, `model`, `size`, `projectId`, `assetFolderId`, `assetLabel`, `targetLayerId`, `canvasCompletion` |
|
||||
| Icon spritesheet | `/api/external/v1/editor/icon-spritesheets/generations` | `referenceId`, `iconDescriptions` | `style`, `referenceImageSrcs`, `screenColor`, `model`, `aspectRatio`, `imageSize`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
|
||||
| UI asset extraction | `/api/external/v1/editor/ui-designs/assets/extractions` | `sourceImageSrc`, `aspectRatio`, `imageSize` | `screenColor`, `model`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `spritesheetLabel`, `canvasCompletion` |
|
||||
| Character animation | `/api/external/v1/editor/character-animations/generations` | `sourceLayerId`, `sourceImageSrc`, `sourceWidth`, `sourceHeight`, `promptText`, `resolution`, `ratio`, `frameCount`, `durationSeconds`, `model` | `projectId`, `sourceResourceId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
|
||||
@@ -82,13 +82,13 @@ After confirming a local upload, pass its stable `objectKey` into operations tha
|
||||
| Target capability | Field |
|
||||
| --- | --- |
|
||||
| Image generation | `referenceImageSrcs` |
|
||||
| Image edit/redraw | `sourceImageSrc`; additional references in `referenceImageSrcs` |
|
||||
| Image edit/redraw | `sourceReferenceId` must be a registered project resource ID or asset ID; additional references remain in `referenceImageSrcs` |
|
||||
| Icon spritesheet | Register the primary spec as an `assetKind="icon-spec"` project resource or asset, then pass its returned ID as `referenceId`; additional style references remain in `referenceImageSrcs` |
|
||||
| UI design extraction | `sourceImageSrc`; additional references in `referenceImageSrcs` |
|
||||
| Character animation | `sourceImageSrc` |
|
||||
| Video with image references | `referenceImageSrcs` |
|
||||
|
||||
Use video/audio reference arrays only with models that support them. Do not pass an expiring signed read URL as a generation reference.
|
||||
For image edit/redraw, confirming an upload is not sufficient: create a project resource or asset-library record first, then pass that record's ID as `sourceReferenceId`. The main source never accepts objectKey, URL, Data URL, or Blob URL. Use video/audio reference arrays only with models that support them. Do not pass an expiring signed read URL as a generation reference.
|
||||
|
||||
The icon-spritesheet primary `referenceId` is intentionally stricter than ordinary image references: it accepts only a current-owner project resource ID or asset ID whose authoritative `assetKind` is `icon-spec`. It does not accept an `objectKey`, URL, Data URL, or Blob URL.
|
||||
|
||||
|
||||
@@ -169,6 +169,8 @@ client.generate_image(
|
||||
)
|
||||
```
|
||||
|
||||
Image edit/redraw has a stricter main-source identity rule. After upload confirmation, create either a project resource or an asset-library record and pass its `resourceId` or `assetId` as `sourceReferenceId`. Do not pass the uploaded objectKey as the main source; objectKey remains valid only for auxiliary `referenceImageSrcs` where the OpenAPI permits it.
|
||||
|
||||
Icon spritesheet generation has a stricter primary-spec contract. After upload confirmation, create a project resource or asset record with `assetKind: "icon-spec"`, retain its returned `resourceId` or `assetId`, and pass that ID as `referenceId`. The primary spec does not accept the uploaded `objectKey` directly; only additional style references may continue to use stable object keys in `referenceImageSrcs`.
|
||||
|
||||
For character animation from a local-only source, use actual dimensions and a stable synthetic layer ID:
|
||||
|
||||
@@ -548,13 +548,16 @@ class GenarrativeExternalClient:
|
||||
idempotency_key=idempotency_key,
|
||||
)
|
||||
|
||||
def edit_image(self, prompt: str, source_image_src: str, **fields: Any) -> Any:
|
||||
def edit_image(self, prompt: str, source_reference_id: str, **fields: Any) -> Any:
|
||||
source_reference_id = source_reference_id.strip()
|
||||
if not source_reference_id:
|
||||
raise GenarrativeApiError("source_reference_id must be a registered resource or asset ID")
|
||||
self._apply_canvas_session_fields(fields, prompt, 1024, 1024)
|
||||
prompt = self._apply_art_spec(fields, prompt)
|
||||
idempotency_key = fields.pop("idempotencyKey", None)
|
||||
return self.submit_and_wait_generation(
|
||||
"/api/external/v1/editor/images/edits",
|
||||
{"prompt": prompt, "sourceImageSrc": source_image_src, **fields},
|
||||
{"prompt": prompt, "sourceReferenceId": source_reference_id, **fields},
|
||||
idempotency_key=idempotency_key,
|
||||
)
|
||||
|
||||
|
||||
@@ -69,23 +69,8 @@ jobs:
|
||||
- name: Install npm dependencies
|
||||
run: bash scripts/ci-npm-ci-with-retry.sh
|
||||
|
||||
- name: Run repository lint gates
|
||||
run: npm run lint
|
||||
|
||||
- name: Build web applications
|
||||
run: npm run build
|
||||
|
||||
- name: Validate content data
|
||||
run: npm run check:content
|
||||
|
||||
- name: Check committed whitespace
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
base_ref="${SPACETIME_SCHEMA_BASE_REF:-}"
|
||||
test -n "${base_ref}"
|
||||
git cat-file -e "${base_ref}^{commit}"
|
||||
git diff --check "${base_ref}"...HEAD
|
||||
- name: Run repository checks
|
||||
run: npm run check:repository-ci
|
||||
|
||||
frontend-tests:
|
||||
name: Frontend tests
|
||||
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
npm run format:staged
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
npm run check:pre-push-master -- "$@"
|
||||
+1
@@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"@lexical/react": "^0.47.0",
|
||||
"@lexical/utils": "^0.47.0",
|
||||
"@tauri-apps/api": "^2.11.1",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.3.2",
|
||||
"@tauri-apps/plugin-http": "^2.5.9",
|
||||
"@tauri-apps/plugin-opener": "~2",
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"dependencies": {
|
||||
"@lexical/react": "^0.47.0",
|
||||
"@lexical/utils": "^0.47.0",
|
||||
"@tauri-apps/api": "^2.11.1",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.3.2",
|
||||
"@tauri-apps/plugin-http": "^2.5.9",
|
||||
"@tauri-apps/plugin-opener": "~2",
|
||||
|
||||
@@ -253,12 +253,24 @@ export function defaultRuntimeConfigDirCandidates({
|
||||
),
|
||||
);
|
||||
} else {
|
||||
const configuredRoot = environment.XDG_CONFIG_HOME;
|
||||
const posixAbsoluteConfiguredRoot =
|
||||
configuredRoot && path.posix.isAbsolute(configuredRoot);
|
||||
const hostAbsoluteConfiguredRoot =
|
||||
configuredRoot &&
|
||||
!posixAbsoluteConfiguredRoot &&
|
||||
path.isAbsolute(configuredRoot);
|
||||
const configRoot =
|
||||
environment.XDG_CONFIG_HOME &&
|
||||
path.posix.isAbsolute(environment.XDG_CONFIG_HOME)
|
||||
? environment.XDG_CONFIG_HOME
|
||||
configuredRoot &&
|
||||
(posixAbsoluteConfiguredRoot || hostAbsoluteConfiguredRoot)
|
||||
? configuredRoot
|
||||
: path.posix.join(homeDirectory, '.config');
|
||||
pushUnique(candidates, path.posix.join(configRoot, appIdentifier));
|
||||
pushUnique(
|
||||
candidates,
|
||||
hostAbsoluteConfiguredRoot
|
||||
? path.join(configRoot, appIdentifier)
|
||||
: path.posix.join(configRoot, appIdentifier),
|
||||
);
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
@@ -114,9 +114,22 @@ const rustSharedContractSource = fs.readFileSync(
|
||||
'utf8',
|
||||
);
|
||||
const allowedUncalledTauriCommands = [
|
||||
'archive_failed_local_project_resource_edit',
|
||||
'chat_with_game_creator_agent',
|
||||
'commit_local_project_asset',
|
||||
'confirm_local_project_asset_canvas_generation_service_identity',
|
||||
'create_local_project_asset_canvas_draft',
|
||||
'discard_local_project_asset_canvas_draft',
|
||||
'generate_local_project_asset_canvas_image',
|
||||
'open_game_creator_launcher_window',
|
||||
'open_game_creator_workspace_window',
|
||||
'read_local_project_asset_canvas_draft',
|
||||
'read_local_project_asset_canvas_media',
|
||||
'recover_local_project_asset_canvas_transactions',
|
||||
'recover_local_project_asset_canvas_generations',
|
||||
'stage_local_project_asset_canvas_image',
|
||||
'store_local_project_asset_canvas_media',
|
||||
'update_local_project_asset_canvas_draft',
|
||||
];
|
||||
const sourceExtensions = new Set([
|
||||
'.json',
|
||||
@@ -1581,7 +1594,8 @@ for (const snippet of [
|
||||
'const GAME_CREATOR_LOCAL_CONFIG_FILE_NAME: &str = "game-creator.config.local.json"',
|
||||
'const DEFAULT_GAME_CREATOR_APP_CONFIG_JSON: &str = include_str!("../../game-creator.config.json")',
|
||||
'fn configure_game_creator_runtime_config_dir(',
|
||||
'app.path().app_config_dir()?',
|
||||
'game_creator_runtime_config_dir()',
|
||||
'.unwrap_or_else(|| app.path().app_config_dir())?',
|
||||
'fn load_game_creator_app_config()',
|
||||
'fn read_game_creator_app_config()',
|
||||
'fn write_game_creator_app_config(',
|
||||
@@ -1675,7 +1689,8 @@ for (const snippet of [
|
||||
"'write_game_creator_app_config'",
|
||||
'aria-label="运行时配置"',
|
||||
'LLM API Key',
|
||||
'画板 API Key',
|
||||
'External Editor Base URL',
|
||||
'External Editor API Key',
|
||||
'runtime_config.save',
|
||||
"'/run:运行自检,启动本地 HTTP 预览并载入客户端运行视图'",
|
||||
"'activate_local_game_preview'",
|
||||
|
||||
@@ -46,6 +46,8 @@ export function buildProcessSessionFixtureSource({
|
||||
' if (!echoed && line === challenge) {',
|
||||
' echoed = true;',
|
||||
" console.log(echoPrefix + ' ' + challenge);",
|
||||
" } else if (line === challenge + ':stop') {",
|
||||
' stop();',
|
||||
' }',
|
||||
' }',
|
||||
'});',
|
||||
|
||||
@@ -4,8 +4,9 @@ import fs from 'node:fs/promises';
|
||||
import http from 'node:http';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const appRoot = path.resolve(new URL('..', import.meta.url).pathname);
|
||||
const appRoot = fileURLToPath(new URL('..', import.meta.url));
|
||||
const localConfigPath = path.join(appRoot, 'game-creator.config.local.json');
|
||||
const projectRoot = path.join(
|
||||
os.tmpdir(),
|
||||
@@ -884,7 +885,28 @@ function readBrowserDom(url) {
|
||||
}
|
||||
|
||||
function resolveChromeBin() {
|
||||
const windowsRoot = path.parse(os.homedir()).root;
|
||||
for (const candidate of [
|
||||
path.join(
|
||||
windowsRoot,
|
||||
'Program Files/Google/Chrome/Application/chrome.exe',
|
||||
),
|
||||
path.join(
|
||||
windowsRoot,
|
||||
'Program Files (x86)/Google/Chrome/Application/chrome.exe',
|
||||
),
|
||||
path.join(
|
||||
os.homedir(),
|
||||
'AppData/Local/Google/Chrome/Application/chrome.exe',
|
||||
),
|
||||
path.join(
|
||||
windowsRoot,
|
||||
'Program Files/Microsoft/Edge/Application/msedge.exe',
|
||||
),
|
||||
path.join(
|
||||
windowsRoot,
|
||||
'Program Files (x86)/Microsoft/Edge/Application/msedge.exe',
|
||||
),
|
||||
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
||||
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
||||
|
||||
@@ -45,12 +45,16 @@ function buildTauriArguments(argv, devUrl = readAgcDevEndpoint().url) {
|
||||
if (separatorIndex < 0) {
|
||||
return ['dev', ...args, '--config', configOverride];
|
||||
}
|
||||
const separatedArguments = args.slice(separatorIndex);
|
||||
if (separatedArguments[1] !== '--') {
|
||||
separatedArguments.unshift('--');
|
||||
}
|
||||
return [
|
||||
'dev',
|
||||
...args.slice(0, separatorIndex),
|
||||
'--config',
|
||||
configOverride,
|
||||
...args.slice(separatorIndex),
|
||||
...separatedArguments,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
+35
-37
@@ -725,16 +725,6 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.10.1"
|
||||
@@ -758,7 +748,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation",
|
||||
"core-graphics-types",
|
||||
"foreign-types 0.5.0",
|
||||
"libc",
|
||||
@@ -771,7 +761,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -1644,6 +1634,7 @@ dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chromiumoxide",
|
||||
"futures",
|
||||
"getrandom 0.3.4",
|
||||
"http",
|
||||
"image",
|
||||
"jsonschema",
|
||||
@@ -2090,11 +2081,9 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2248,10 +2237,23 @@ checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder-lite",
|
||||
"image-webp",
|
||||
"moxcms",
|
||||
"num-traits",
|
||||
"png 0.18.1",
|
||||
"tiff",
|
||||
"zune-core",
|
||||
"zune-jpeg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image-webp"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"
|
||||
dependencies = [
|
||||
"byteorder-lite",
|
||||
"quick-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2630,6 +2632,16 @@ version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mime_guess"
|
||||
version = "2.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
||||
dependencies = [
|
||||
"mime",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
@@ -4102,6 +4114,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"native-tls",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
@@ -4396,7 +4409,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
@@ -4952,27 +4965,6 @@ dependencies = [
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"core-foundation 0.9.4",
|
||||
"system-configuration-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration-sys"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-deps"
|
||||
version = "6.2.2"
|
||||
@@ -4994,7 +4986,7 @@ checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"block2",
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation",
|
||||
"core-graphics",
|
||||
"crossbeam-channel",
|
||||
"dbus",
|
||||
@@ -5905,6 +5897,12 @@ dependencies = [
|
||||
"unic-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-general-category"
|
||||
version = "1.1.0"
|
||||
|
||||
@@ -19,8 +19,9 @@ agent-runtime-core = { path = "../../../server-rs/crates/agent-runtime-core" }
|
||||
base64 = "0.22"
|
||||
chromiumoxide = "0.9.1"
|
||||
futures = "0.3"
|
||||
getrandom = "0.3"
|
||||
http = "1"
|
||||
image = { version = "0.25", default-features = false, features = ["png"] }
|
||||
image = { version = "0.25", default-features = false, features = ["jpeg", "png", "webp"] }
|
||||
jsonschema = { version = "0.49.3", default-features = false }
|
||||
oxc_allocator = "0.143.0"
|
||||
oxc_ast = "0.143.0"
|
||||
@@ -37,11 +38,11 @@ similar = "2.7"
|
||||
platform-llm = { path = "../../../server-rs/crates/platform-llm" }
|
||||
platform-agent = { path = "../../../server-rs/crates/platform-agent" }
|
||||
portable-pty = "0.9"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "native-tls"] }
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "multipart", "native-tls"] }
|
||||
shared-contracts = { path = "../../../server-rs/crates/shared-contracts", default-features = false }
|
||||
tauri = { version = "2.11.2", features = [] }
|
||||
tauri-plugin-dialog = "2.7.1"
|
||||
tauri-plugin-http = "2.5.9"
|
||||
tauri-plugin-http = { version = "2.5.9", default-features = false, features = ["charset", "cookies", "http2", "rustls-tls"] }
|
||||
tauri-plugin-opener = "2"
|
||||
tempfile = "3"
|
||||
tokio = { version = "1", features = ["io-util", "macros", "process", "rt-multi-thread", "signal", "sync", "time"] }
|
||||
|
||||
@@ -16,4 +16,4 @@ git.inspect 会返回 commitSnapshotFingerprint;只有当前非零 revision
|
||||
|
||||
用户输入请求协议:user.input_request 使用 {"questions":[{"id":"唯一 snake_case","header":"最多 12 字符","question":"单句问题","options":[{"label":"短选项","description":"一条影响说明"},{"label":"另一选项","description":"一条影响说明"}]}]},一次 1-3 题、每题 2-3 个选项且始终允许自由输入。它必须是本轮唯一函数调用,不得同批调用 update_agent_plan、其他动作函数或 respond_to_user。只有 Project Supervisor 或没有父委派身份的静态 Agent 开发试聊可直接调用;委派专业 Agent 和动态隔离 child 必须把澄清需要回传父 Agent。
|
||||
|
||||
静态委派协议:新 agent.delegate 必须提交 1-8 条 acceptanceCriteria、0-16 个精确项目内非私有 expectedArtifacts,以及 nullable repairOfDelegationId/runId。专业 Agent 收到的 task 会携带完整合同。Supervisor 认领回执后必须区分 evidence-ready 与 needs-repair;前者仍需语义验收,后者不能作为成功。
|
||||
静态委派协议:新 agent.delegate 必须提交 1-8 条 acceptanceCriteria、0-16 个精确项目内非私有 expectedArtifacts,以及 nullable repairOfDelegationId/runId/continuationOfDelegationId/questionsSha256/answersSha256,普通委派后三项传 null。专业 Agent 收到的 task 会携带完整合同。Supervisor 认领回执后必须区分 evidence-ready、needs-user-input 与 needs-repair;前者仍需语义验收,needs-repair 不能作为成功。专业 Agent 若缺少会实质改变结果的用户事实,不能调用 user.input_request,必须以最终回复首行 `AGC_NEEDS_USER_INPUT_V1`,下一行短 JSON `{"questions":[...]}` 返回 1-3 个结构化问题;Runtime 会把它作为内部回执交给 Supervisor。Supervisor 对每个原 delivery 逐一用现有 user.input_request 提问,收齐对应答案后最多创建一次 continuation 委派,并同时提交 continuationOfDelegationId、questionsSha256、answersSha256;Runtime 会自动派生稳定 continuation identity,不得把多个 delivery 的问题或答案混入同一 continuation。
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "genarrative.agent-runtime",
|
||||
"version": "2026-08-07.2",
|
||||
"version": "2026-08-11.1",
|
||||
"sections": {
|
||||
"common": "common.md",
|
||||
"isolatedTemplateCatalogIntro": "isolated-template-catalog-intro.md",
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
本片段只适用于普通 GUI / CLI 的完整 `autonomous-game-build` manifest DAG,不适用于持久 source 为 `project-supervisor-game-chat` 的单主 route。普通 DAG 中,正式 manifest 任务图是唯一首轮专业执行链:不得在 manifest 之前另行创建 code-prototype、quality-review、art-director、design-foundation 或 art-asset-plan 的首批 agent.delegate;这些角色会由 Runtime 按 manifest 依赖顺序调度。没有待认领的显式返工合同时也不得额外委派。game-chat 则由其专用路由提示决定:Supervisor 持久化意图后只启动 code-prototype,只有该主 Agent 的 asset.list 审计证实真实缺口时才可委派受限美术 child。请直接推进/观察适用于当前 root source 的任务路径;Runtime 会在你尝试收束时调度 ready task,并在任务图完成前阻止最终交付。
|
||||
本片段只适用于普通 GUI / CLI 的完整 `autonomous-game-build` manifest DAG,不适用于持久 source 为 `project-supervisor-game-chat` 的单主 route。任何根路径都必须先由 Supervisor 用 `agent.goal_contract` 冻结其对当前用户最终意图、约束、开放问题和动态验收图的理解;每个 required 节点的 requiredEvidence 必须逐项写成 `tool:<Runtime 工具名>`,由对应工具在当前 revision 的真实成功回执证明,不能写自然语言证据描述或拿无关成功动作替代。固定 manifest 和 game-chat route 只提供执行上下文,不能替代这项语义决定。普通 DAG 中,正式 manifest 任务图是 Goal Contract 之后的唯一首轮专业执行链:不得在 manifest 之前另行创建 code-prototype、quality-review、art-director、design-foundation 或 art-asset-plan 的首批 agent.delegate;这些角色会由 Runtime 按 manifest 依赖顺序调度。没有待认领的显式返工合同时也不得额外委派。game-chat 则由其专用路由提示决定:Supervisor 持久化意图后只启动 code-prototype,只有该主 Agent 的 asset.list 审计证实真实缺口时才可委派受限美术 child。请直接推进/观察适用于当前 root source 的任务路径;Runtime 会在你尝试收束时调度 ready task,并在任务图或动态验收图完成前阻止最终交付。
|
||||
|
||||
+3
-1
@@ -1,3 +1,5 @@
|
||||
game-chat 的固定关键词和资产探测只作为 `advisoryOnly=true` 的补充上下文,不能直接选择、重置或跳过 manifest 节点。当前根 Run 尚无工作流决策时,你必须先自行理解用户真正要做的事,并把本轮唯一动作设为 `agent.route_manifest`:`strategy=audit-existing-first, missingAssetSlots=[]` 是固定执行安全策略;`intentSummary` 必须由你概括用户意图,例如“把已有美术资源接入当前游戏”或“按用户要求刷新整体视觉方向”,不得照抄固定信号代替理解。这个动作只记录意图,不代表生成许可,也不能把整体重做解释成整套美术的强制重生成。不得根据关键词自行声称已有资产完整,也不得在该结构化决策前委派或调度美术 Agent。
|
||||
game-chat 的固定关键词和资产探测只作为 `advisoryOnly=true` 的补充上下文,不能直接选择、重置或跳过任务节点。当前根 Run 尚无 Goal Contract 时,你必须先自行理解用户真正要做的事,并把本轮唯一动作设为 `agent.goal_contract`:完整区分最终 outcome、不可协商约束、偏好、禁止假设、开放问题和本次任务动态生成的 acceptance nodes;不得照抄固定信号、玩法模板或素材类型代替理解。每个 required 节点使用稳定 criterionId;requiredEvidence 必须逐项写成 `tool:<Runtime 工具名>`,声明真正能够证明该标准的工具回执,不能写自然语言证据描述,也不能用无关成功动作自证;只有用户目标确实不要求的标准才可标为 optional。
|
||||
|
||||
Goal Contract 冻结后,当前根 Run 尚无工作流决策时,才把本轮唯一动作设为 `agent.route_manifest`。`strategy=audit-existing-first, missingAssetSlots=[]` 是防止未经审计生成或重做素材的执行安全上下文;`intentSummary` 必须忠实概括已冻结 Goal Contract,不得照抄固定信号代替理解。这个动作只记录执行路由,不代表生成许可,也不能把整体重做解释成整套美术的强制重生成。不得根据关键词自行声称已有资产完整,也不得在 Goal Contract 和结构化路由前委派或调度美术 Agent。
|
||||
|
||||
这一步只持久化用户意图和单主路径,不审计资产、不代替 `code-prototype` 判断缺口,也不得创建 `design-director`、`code-director`、`art-director`、`art-asset-plan`、试玩或其它固定首波节点。持久路由后 Runtime 只启动同一根 Run 的 `code-prototype`。它先以 `asset.list` 取得权威资产、Canvas 登记和可复用状态;只有该审计证明 `art-spec` 或核心 spritesheet 确实缺失,才可由该主 Agent 向对应美术角色发起一次受限的 durable 委派。美术 child 仅可写 `assets/**`,回执由同一 `code-prototype` 认领后恢复其原 Run 接入、静态检查和桌面/移动试玩。完整覆盖时不得生成、委派或扣费;整体重做意图同样必须经过这次审计,不能绕过资产复用或授权整套美术重生成。Runtime 负责校验根/父子身份、路径、Canvas 登记、合同指纹、缺口一致性和写入范围,但不替你解释用户意图或生成美术。
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
互不重叠的临时并行检查通过 agent.spawn_isolated 分派;当同一目标同时需要边界清晰的专业委派和互不重叠的临时检查时,必须把两类协作放进同一个 native planning 批次一次性提交,不能拆成先后轮次。提交首个协作批次前,先分别完整枚举当前目标中已经生效的长期专业交付和临时隔离检查;两类都非空时,遗漏任一类的批次都不得提交。仓库合同明确把临时检查分为先行和后续独立阶段时,首批只提交当前已经生效的检查;先行组 ready 后优先创建刚生效的后续组,所有必要组创建前不得调用 agent.run_status 认领先行组,全部 ready 后用一次 agent.run_status 收齐。已有委派未收束时不要重复委派。
|
||||
|
||||
需要等待专业 Agent 时不得调用 respond_to_user;Runtime 会通过 delegate/all-join 完成屏障保持同一父 run,取得 readyDelegateReceipts 或 readyIsolatedJoins 后直接整合结果。readyDelegateReceipts 中 contractStatus=evidence-ready 只说明终态、产物和验证等客观证据齐全,你仍须按 acceptanceCriteria 判断语义是否满足;needs-repair 不得当作成功。客观或语义不满足时可以发起一次新 agent.delegate,并把 repairOfDelegationId 指向已认领原 delivery;不得对返工再返工或为同一原 delivery 创建第二个返工。专业结果冲突且无法依据用户目标裁决时,合并问题后用一次 user.input_request 询问用户。只有实现路径、产品取舍或缺失事实会实质改变结果时才调用 user.input_request;项目内可读取事实、权限确认和工具失败不得伪装成用户问题。
|
||||
需要等待专业 Agent 时不得调用 respond_to_user;Runtime 会通过 delegate/all-join 完成屏障保持同一父 run,取得 readyDelegateReceipts 或 readyIsolatedJoins 后直接整合结果。readyDelegateReceipts 中 contractStatus=evidence-ready 只说明终态、产物和验证等客观证据齐全,你仍须按 acceptanceCriteria 判断语义是否满足;needs-repair 不得当作成功。contractStatus=needs-user-input 时,Runtime 会按原 delivery 逐一发起 user.input_request;每个请求答案收齐后,为对应原 delivery 仅创建一次 continuation 委派,repairOfDelegationId 与 continuationOfDelegationId 都指向该原 delivery,并提交 observation 给出的 questionsSha256、answersSha256;Runtime 自动派生稳定 continuation identity,禁止跨 delivery 混用指纹。客观或语义不满足时可以发起一次新 agent.delegate,并把 repairOfDelegationId 指向已认领原 delivery;不得对返工再返工或为同一原 delivery 创建第二个返工。专业结果冲突且无法依据用户目标裁决时,合并问题后用一次 user.input_request 询问用户。只有实现路径、产品取舍或缺失事实会实质改变结果时才调用 user.input_request;项目内可读取事实、权限确认和工具失败不得伪装成用户问题。
|
||||
|
||||
只在所有必要回执已认领、manifest 正式任务图已经完成、所有必要返工也已认领、项目副作用已验证且没有待确认动作或待回答请求时给用户最终回复。不要向用户暴露内部 task/event、工具计划、动态 child ID 或调试状态。
|
||||
|
||||
@@ -22,6 +22,9 @@ mod runtime_state;
|
||||
mod runtime_tools;
|
||||
use codex_app_server::*;
|
||||
use codex_cli::*;
|
||||
pub(crate) use codex_cli::{
|
||||
game_creator_codex_cli_executable_path, game_creator_codex_cli_version_identity,
|
||||
};
|
||||
pub(crate) use generation::*;
|
||||
pub(crate) use interaction::*;
|
||||
pub(crate) use prompt::*;
|
||||
|
||||
@@ -7,7 +7,6 @@ use std::sync::{Arc, OnceLock, Weak};
|
||||
use tokio::io::{AsyncBufRead, AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
|
||||
use tokio::sync::{mpsc, oneshot, Mutex};
|
||||
|
||||
const GAME_CREATOR_CODEX_APP_SERVER_EXECUTABLE: &str = "codex";
|
||||
const GAME_CREATOR_CODEX_APP_SERVER_PROVIDER_ID: &str = "genarrative_agc";
|
||||
const GAME_CREATOR_CODEX_APP_SERVER_API_KEY_ENV: &str = "GENARRATIVE_AGC_CODEX_API_KEY";
|
||||
const GAME_CREATOR_CODEX_APP_SERVER_PROTOCOL: &str = "genarrative-codex-app-server.v2";
|
||||
@@ -20,6 +19,8 @@ const GAME_CREATOR_CODEX_APP_SERVER_THREAD_MAX: usize = 128;
|
||||
const GAME_CREATOR_CODEX_APP_SERVER_RPC_TIMEOUT_MS: u64 = 30_000;
|
||||
pub(in crate::agent) const GAME_CREATOR_CODEX_APP_SERVER_TERMINAL_UNKNOWN_PREFIX: &str =
|
||||
"codex-app-server-terminal-unknown:";
|
||||
pub(in crate::agent) const GAME_CREATOR_CODEX_APP_SERVER_ERROR_KIND_PREFIX: &str =
|
||||
"codex-app-server-error:";
|
||||
|
||||
type RpcResult = Result<serde_json::Value, String>;
|
||||
|
||||
@@ -177,6 +178,92 @@ fn game_creator_codex_app_server_terminal_unknown(
|
||||
))
|
||||
}
|
||||
|
||||
fn game_creator_codex_app_server_error_kind(kind: &str) -> platform_llm::LlmError {
|
||||
platform_llm::LlmError::InvalidRequest(format!(
|
||||
"{GAME_CREATOR_CODEX_APP_SERVER_ERROR_KIND_PREFIX}{kind}"
|
||||
))
|
||||
}
|
||||
|
||||
fn game_creator_codex_app_server_error_http_status(
|
||||
info: &serde_json::Value,
|
||||
field: &str,
|
||||
) -> Option<u16> {
|
||||
info.get(field)?
|
||||
.get("httpStatusCode")?
|
||||
.as_u64()
|
||||
.and_then(|status| u16::try_from(status).ok())
|
||||
.filter(|status| (100..=599).contains(status))
|
||||
}
|
||||
|
||||
fn game_creator_codex_app_server_connection_error(
|
||||
info: &serde_json::Value,
|
||||
field: &str,
|
||||
) -> platform_llm::LlmError {
|
||||
match game_creator_codex_app_server_error_http_status(info, field) {
|
||||
Some(401 | 403) => game_creator_codex_app_server_error_kind("unauthorized"),
|
||||
Some(status_code) => platform_llm::LlmError::Upstream {
|
||||
status_code,
|
||||
message: "Codex app-server 连接上游失败".to_string(),
|
||||
},
|
||||
None => platform_llm::LlmError::Connectivity {
|
||||
attempts: 1,
|
||||
message: "Codex app-server 连接失败".to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn game_creator_codex_app_server_failed_turn_error(
|
||||
turn: &serde_json::Value,
|
||||
) -> platform_llm::LlmError {
|
||||
let Some(info) = turn
|
||||
.get("error")
|
||||
.and_then(|error| error.get("codexErrorInfo"))
|
||||
.filter(|info| !info.is_null())
|
||||
else {
|
||||
return game_creator_codex_app_server_error_kind("other");
|
||||
};
|
||||
if let Some(kind) = info.as_str() {
|
||||
return match kind {
|
||||
"contextWindowExceeded" => {
|
||||
game_creator_codex_app_server_error_kind("context-window-exceeded")
|
||||
}
|
||||
"sessionBudgetExceeded" => {
|
||||
game_creator_codex_app_server_error_kind("session-budget-exceeded")
|
||||
}
|
||||
"usageLimitExceeded" => {
|
||||
game_creator_codex_app_server_error_kind("usage-limit-exceeded")
|
||||
}
|
||||
"serverOverloaded" | "internalServerError" => platform_llm::LlmError::Upstream {
|
||||
status_code: 503,
|
||||
message: "Codex app-server 上游服务暂时不可用".to_string(),
|
||||
},
|
||||
"cyberPolicy" => game_creator_codex_app_server_error_kind("cyber-policy"),
|
||||
"unauthorized" => game_creator_codex_app_server_error_kind("unauthorized"),
|
||||
"badRequest" => game_creator_codex_app_server_error_kind("bad-request"),
|
||||
"threadRollbackFailed" => {
|
||||
game_creator_codex_app_server_error_kind("thread-rollback-failed")
|
||||
}
|
||||
"sandboxError" => game_creator_codex_app_server_error_kind("sandbox-error"),
|
||||
"other" => game_creator_codex_app_server_error_kind("other"),
|
||||
_ => game_creator_codex_app_server_error_kind("other"),
|
||||
};
|
||||
}
|
||||
for field in [
|
||||
"httpConnectionFailed",
|
||||
"responseStreamConnectionFailed",
|
||||
"responseStreamDisconnected",
|
||||
"responseTooManyFailedAttempts",
|
||||
] {
|
||||
if info.get(field).is_some() {
|
||||
return game_creator_codex_app_server_connection_error(info, field);
|
||||
}
|
||||
}
|
||||
if info.get("activeTurnNotSteerable").is_some() {
|
||||
return game_creator_codex_app_server_error_kind("active-turn-not-steerable");
|
||||
}
|
||||
game_creator_codex_app_server_error_kind("other")
|
||||
}
|
||||
|
||||
async fn isolate_game_creator_codex_app_server_terminal_unknown(
|
||||
inner: &Arc<CodexAppServerInner>,
|
||||
detail: impl Into<String>,
|
||||
@@ -394,10 +481,8 @@ fn configure_game_creator_codex_app_server_command(
|
||||
"plugins",
|
||||
"remote_plugin",
|
||||
"shell_tool",
|
||||
"skill_search",
|
||||
"tool_suggest",
|
||||
"unified_exec",
|
||||
"view_image",
|
||||
"workspace_dependencies",
|
||||
] {
|
||||
command.arg("--disable").arg(feature);
|
||||
@@ -520,12 +605,9 @@ impl CodexAppServerConnection {
|
||||
llm: &GameCreatorLlmConfig,
|
||||
credential: &CodexAppServerCredential,
|
||||
) -> Result<Self, platform_llm::LlmError> {
|
||||
Self::spawn_with_executable_and_credential(
|
||||
llm,
|
||||
credential,
|
||||
std::ffi::OsStr::new(GAME_CREATOR_CODEX_APP_SERVER_EXECUTABLE),
|
||||
)
|
||||
.await
|
||||
let executable = game_creator_codex_cli_executable_path()
|
||||
.map_err(platform_llm::LlmError::InvalidConfig)?;
|
||||
Self::spawn_with_executable_and_credential(llm, credential, executable.as_os_str()).await
|
||||
}
|
||||
|
||||
async fn spawn_with_executable(
|
||||
@@ -1012,9 +1094,7 @@ impl CodexAppServerConnection {
|
||||
))
|
||||
}
|
||||
"failed" => {
|
||||
return Err(platform_llm::LlmError::InvalidRequest(
|
||||
"Codex app-server turn 执行失败".to_string(),
|
||||
))
|
||||
return Err(game_creator_codex_app_server_failed_turn_error(turn))
|
||||
}
|
||||
status => {
|
||||
return Err(platform_llm::LlmError::Deserialize(format!(
|
||||
@@ -1577,6 +1657,81 @@ mod tests {
|
||||
assert!(response.text.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_app_server_failed_turn_uses_structured_error_info_without_raw_details() {
|
||||
let secret = ["sk", "turn-secret"].join("-");
|
||||
let failed_turn = serde_json::json!({
|
||||
"status": "failed",
|
||||
"error": {
|
||||
"message": format!("private message {secret}"),
|
||||
"additionalDetails": "https://provider.example/private C:\\Users\\victim\\project",
|
||||
"codexErrorInfo": "contextWindowExceeded"
|
||||
}
|
||||
});
|
||||
let error = game_creator_codex_app_server_failed_turn_error(&failed_turn);
|
||||
assert_eq!(
|
||||
error,
|
||||
platform_llm::LlmError::InvalidRequest(
|
||||
"codex-app-server-error:context-window-exceeded".to_string()
|
||||
)
|
||||
);
|
||||
let visible = error.to_string();
|
||||
assert!(!visible.contains(&secret));
|
||||
assert!(!visible.contains("provider.example"));
|
||||
assert!(!visible.contains("victim"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_app_server_failed_turn_maps_stable_categories_and_http_status() {
|
||||
for (info, expected) in [
|
||||
(
|
||||
serde_json::json!("usageLimitExceeded"),
|
||||
platform_llm::LlmError::InvalidRequest(
|
||||
"codex-app-server-error:usage-limit-exceeded".to_string(),
|
||||
),
|
||||
),
|
||||
(
|
||||
serde_json::json!("unauthorized"),
|
||||
platform_llm::LlmError::InvalidRequest(
|
||||
"codex-app-server-error:unauthorized".to_string(),
|
||||
),
|
||||
),
|
||||
(
|
||||
serde_json::json!({"httpConnectionFailed":{"httpStatusCode":429}}),
|
||||
platform_llm::LlmError::Upstream {
|
||||
status_code: 429,
|
||||
message: "Codex app-server 连接上游失败".to_string(),
|
||||
},
|
||||
),
|
||||
(
|
||||
serde_json::json!({"responseStreamDisconnected":{"httpStatusCode":null}}),
|
||||
platform_llm::LlmError::Connectivity {
|
||||
attempts: 1,
|
||||
message: "Codex app-server 连接失败".to_string(),
|
||||
},
|
||||
),
|
||||
] {
|
||||
let turn = serde_json::json!({
|
||||
"status": "failed",
|
||||
"error": {
|
||||
"message": "private upstream body",
|
||||
"additionalDetails": "private diagnostics",
|
||||
"codexErrorInfo": info
|
||||
}
|
||||
});
|
||||
assert_eq!(
|
||||
game_creator_codex_app_server_failed_turn_error(&turn),
|
||||
expected
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
game_creator_codex_app_server_failed_turn_error(
|
||||
&serde_json::json!({"status":"failed","error":null})
|
||||
),
|
||||
platform_llm::LlmError::InvalidRequest("codex-app-server-error:other".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_app_server_rejects_non_responses_key_mapping() {
|
||||
let mut llm = test_llm();
|
||||
@@ -1587,6 +1742,41 @@ mod tests {
|
||||
assert!(game_creator_codex_app_server_validate_llm_config(&llm).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_app_server_command_uses_only_current_cli_feature_flags() {
|
||||
let mut command = tokio::process::Command::new("codex");
|
||||
configure_game_creator_codex_app_server_command(&mut command, &test_llm())
|
||||
.expect("configure app-server command");
|
||||
let arguments = command
|
||||
.as_std()
|
||||
.get_args()
|
||||
.map(|argument| argument.to_string_lossy().into_owned())
|
||||
.collect::<Vec<_>>();
|
||||
assert!(arguments
|
||||
.windows(2)
|
||||
.any(|pair| pair == ["--disable", "shell_tool"]));
|
||||
assert!(!arguments.iter().any(|argument| argument == "skill_search"));
|
||||
assert!(!arguments.iter().any(|argument| argument == "view_image"));
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn codex_app_server_current_cli_accepts_configured_arguments() {
|
||||
let executable = game_creator_codex_cli_executable_path().expect("Codex CLI executable");
|
||||
let mut command = std::process::Command::new(executable);
|
||||
let mut configured = tokio::process::Command::new("codex");
|
||||
configure_game_creator_codex_app_server_command(&mut configured, &test_llm())
|
||||
.expect("configure app-server command");
|
||||
command.args(configured.as_std().get_args());
|
||||
command.arg("--help").stdin(Stdio::null());
|
||||
let output = command.output().expect("run Codex app-server help");
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"configured app-server arguments must be accepted: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_app_server_pool_key_isolated_by_credentials_and_route() {
|
||||
let mut base = test_llm();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Stdio;
|
||||
|
||||
use sha2::{Digest, Sha256};
|
||||
@@ -9,29 +10,128 @@ const GAME_CREATOR_CODEX_CLI_PROMPT_MAX_BYTES: usize = 4 * 1024 * 1024;
|
||||
const GAME_CREATOR_CODEX_CLI_STDOUT_MAX_BYTES: usize = 4 * 1024 * 1024;
|
||||
const GAME_CREATOR_CODEX_CLI_STDERR_MAX_BYTES: usize = 256 * 1024;
|
||||
|
||||
fn game_creator_codex_cli_executable_candidates_for(
|
||||
app_data: Option<&Path>,
|
||||
local_app_data: Option<&Path>,
|
||||
runtime_config_dir: Option<&Path>,
|
||||
path: Option<&std::ffi::OsStr>,
|
||||
) -> Vec<PathBuf> {
|
||||
let mut candidates = Vec::new();
|
||||
#[cfg(windows)]
|
||||
{
|
||||
fn append_native_npm_candidates(candidates: &mut Vec<PathBuf>, npm_root: &Path) {
|
||||
let vendor_root = npm_root
|
||||
.join("node_modules")
|
||||
.join("@openai")
|
||||
.join("codex")
|
||||
.join("node_modules")
|
||||
.join("@openai")
|
||||
.join("codex-win32-x64")
|
||||
.join("vendor");
|
||||
if let Ok(entries) = std::fs::read_dir(vendor_root) {
|
||||
let mut targets = entries
|
||||
.filter_map(Result::ok)
|
||||
.map(|entry| entry.path().join("bin").join("codex.exe"))
|
||||
.collect::<Vec<_>>();
|
||||
targets.sort();
|
||||
candidates.extend(targets);
|
||||
}
|
||||
}
|
||||
|
||||
fn append_desktop_codex_candidates(candidates: &mut Vec<PathBuf>, local_app_data: &Path) {
|
||||
let bin_root = local_app_data.join("OpenAI").join("Codex").join("bin");
|
||||
if let Ok(entries) = std::fs::read_dir(bin_root) {
|
||||
let mut targets = entries
|
||||
.filter_map(Result::ok)
|
||||
.map(|entry| entry.path().join("codex.exe"))
|
||||
.collect::<Vec<_>>();
|
||||
targets.sort();
|
||||
targets.reverse();
|
||||
candidates.extend(targets);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(app_data) = app_data {
|
||||
append_native_npm_candidates(&mut candidates, &app_data.join("npm"));
|
||||
}
|
||||
if let Some(local_app_data) = local_app_data {
|
||||
append_desktop_codex_candidates(&mut candidates, local_app_data);
|
||||
}
|
||||
if let Some(app_data) = runtime_config_dir.and_then(Path::parent) {
|
||||
append_native_npm_candidates(&mut candidates, &app_data.join("npm"));
|
||||
if let Some(user_profile) = app_data.parent() {
|
||||
append_desktop_codex_candidates(&mut candidates, &user_profile.join("Local"));
|
||||
}
|
||||
}
|
||||
if let Some(path) = path {
|
||||
for entry in std::env::split_paths(&path) {
|
||||
append_native_npm_candidates(&mut candidates, &entry);
|
||||
candidates.push(entry.join("codex.exe"));
|
||||
}
|
||||
}
|
||||
}
|
||||
candidates.push(PathBuf::from(GAME_CREATOR_CODEX_CLI_EXECUTABLE));
|
||||
candidates
|
||||
}
|
||||
|
||||
fn game_creator_codex_cli_executable_candidates() -> Vec<PathBuf> {
|
||||
game_creator_codex_cli_executable_candidates_for(
|
||||
std::env::var_os("APPDATA").as_deref().map(Path::new),
|
||||
std::env::var_os("LOCALAPPDATA").as_deref().map(Path::new),
|
||||
game_creator_runtime_config_dir().as_deref(),
|
||||
std::env::var_os("PATH").as_deref(),
|
||||
)
|
||||
}
|
||||
|
||||
fn game_creator_codex_cli_version_at(executable: &Path) -> Result<String, String> {
|
||||
let output = std::process::Command::new(executable)
|
||||
.arg("--version")
|
||||
.stdin(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.output()
|
||||
.map_err(|error| error.to_string())?;
|
||||
if !output.status.success() {
|
||||
return Err(format!("版本检查退出状态为 {}", output.status));
|
||||
}
|
||||
let version = std::str::from_utf8(&output.stdout)
|
||||
.map_err(|_| "版本信息不是 UTF-8".to_string())?
|
||||
.trim();
|
||||
if !version.starts_with("codex-cli ") || version.len() > 120 {
|
||||
return Err("返回了无法识别的版本信息".to_string());
|
||||
}
|
||||
Ok(version.to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn game_creator_codex_cli_executable_path() -> Result<PathBuf, String> {
|
||||
let mut last_error = None;
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for candidate in game_creator_codex_cli_executable_candidates() {
|
||||
let identity = candidate.to_string_lossy().to_ascii_lowercase();
|
||||
if !seen.insert(identity) {
|
||||
continue;
|
||||
}
|
||||
match game_creator_codex_cli_version_at(&candidate) {
|
||||
Ok(_) => return Ok(candidate),
|
||||
Err(error) => last_error = Some(error),
|
||||
}
|
||||
}
|
||||
Err(format!(
|
||||
"Codex CLI 未安装或当前 Agent Runner 无法启动;已检查 PATH 和 npm 全局安装目录{}",
|
||||
last_error
|
||||
.map(|error| format!("(最后错误:{error})"))
|
||||
.unwrap_or_default()
|
||||
))
|
||||
}
|
||||
|
||||
struct CodexCliStderrSummary {
|
||||
byte_len: usize,
|
||||
sha256: String,
|
||||
classification: &'static str,
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn game_creator_codex_cli_version_identity() -> Result<String, String> {
|
||||
let output = std::process::Command::new(GAME_CREATOR_CODEX_CLI_EXECUTABLE)
|
||||
.arg("--version")
|
||||
.stdin(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.output()
|
||||
.map_err(|_| "Codex CLI 未安装或不在当前 Agent Runner PATH 中".to_string())?;
|
||||
if !output.status.success() {
|
||||
return Err("Codex CLI 版本检查失败".to_string());
|
||||
}
|
||||
let version = std::str::from_utf8(&output.stdout)
|
||||
.map_err(|_| "Codex CLI 版本信息不是 UTF-8".to_string())?
|
||||
.trim();
|
||||
if !version.starts_with("codex-cli ") || version.len() > 120 {
|
||||
return Err("Codex CLI 返回了无法识别的版本信息".to_string());
|
||||
}
|
||||
Ok(version.to_string())
|
||||
pub(crate) fn game_creator_codex_cli_version_identity() -> Result<String, String> {
|
||||
let executable = game_creator_codex_cli_executable_path()?;
|
||||
game_creator_codex_cli_version_at(&executable)
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn game_creator_codex_cli_reasoning_effort(
|
||||
@@ -546,11 +646,9 @@ async fn request_game_creator_agent_codex_cli_with_executable(
|
||||
pub(in crate::agent) async fn request_game_creator_agent_codex_cli(
|
||||
request: LlmRunRequest,
|
||||
) -> Result<platform_llm::LlmRunResponse, platform_llm::LlmError> {
|
||||
request_game_creator_agent_codex_cli_with_executable(
|
||||
std::ffi::OsStr::new(GAME_CREATOR_CODEX_CLI_EXECUTABLE),
|
||||
request,
|
||||
)
|
||||
.await
|
||||
let executable =
|
||||
game_creator_codex_cli_executable_path().map_err(platform_llm::LlmError::InvalidConfig)?;
|
||||
request_game_creator_agent_codex_cli_with_executable(executable.as_os_str(), request).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -567,6 +665,103 @@ mod tests {
|
||||
])
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn codex_cli_candidates_prefer_sorted_native_npm_targets_before_path() {
|
||||
let temp = tempfile::tempdir().expect("temp dir");
|
||||
let app_data = temp.path().join("app-data");
|
||||
let vendor = app_data
|
||||
.join("npm/node_modules/@openai/codex/node_modules/@openai/codex-win32-x64/vendor");
|
||||
std::fs::create_dir_all(vendor.join("z-target/bin")).expect("z target");
|
||||
std::fs::create_dir_all(vendor.join("a-target/bin")).expect("a target");
|
||||
let path_dir = temp.path().join("path");
|
||||
std::fs::create_dir_all(&path_dir).expect("path dir");
|
||||
|
||||
let candidates = game_creator_codex_cli_executable_candidates_for(
|
||||
Some(&app_data),
|
||||
None,
|
||||
None,
|
||||
Some(path_dir.as_os_str()),
|
||||
);
|
||||
assert_eq!(
|
||||
candidates[0],
|
||||
vendor.join("a-target/bin/codex.exe"),
|
||||
"native npm targets must be deterministic and precede PATH"
|
||||
);
|
||||
assert_eq!(candidates[1], vendor.join("z-target/bin/codex.exe"));
|
||||
assert_eq!(candidates[2], path_dir.join("codex.exe"));
|
||||
assert_eq!(candidates.last(), Some(&PathBuf::from("codex")));
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn codex_cli_candidates_discover_native_npm_target_from_path_without_appdata() {
|
||||
let temp = tempfile::tempdir().expect("temp dir");
|
||||
let npm_root = temp.path().join("npm");
|
||||
let native = npm_root
|
||||
.join("node_modules/@openai/codex/node_modules/@openai/codex-win32-x64/vendor")
|
||||
.join("x86_64-pc-windows-msvc/bin/codex.exe");
|
||||
std::fs::create_dir_all(native.parent().expect("native parent"))
|
||||
.expect("native target directory");
|
||||
|
||||
let candidates = game_creator_codex_cli_executable_candidates_for(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(npm_root.as_os_str()),
|
||||
);
|
||||
assert_eq!(candidates[0], native);
|
||||
assert_eq!(candidates[1], npm_root.join("codex.exe"));
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn codex_cli_candidates_discover_native_npm_target_from_runtime_config_dir() {
|
||||
let temp = tempfile::tempdir().expect("temp dir");
|
||||
let app_data = temp.path().join("roaming");
|
||||
let config_dir = app_data.join("world.genarrative.ai-game-creator");
|
||||
let native = app_data
|
||||
.join("npm/node_modules/@openai/codex/node_modules/@openai/codex-win32-x64/vendor")
|
||||
.join("x86_64-pc-windows-msvc/bin/codex.exe");
|
||||
std::fs::create_dir_all(native.parent().expect("native parent"))
|
||||
.expect("native target directory");
|
||||
|
||||
let candidates =
|
||||
game_creator_codex_cli_executable_candidates_for(None, None, Some(&config_dir), None);
|
||||
assert_eq!(candidates[0], native);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn codex_cli_candidates_discover_desktop_native_target() {
|
||||
let temp = tempfile::tempdir().expect("temp dir");
|
||||
let local_app_data = temp.path().join("local");
|
||||
let older = local_app_data.join("OpenAI/Codex/bin/111/codex.exe");
|
||||
let newer = local_app_data.join("OpenAI/Codex/bin/222/codex.exe");
|
||||
std::fs::create_dir_all(older.parent().expect("older parent")).expect("older dir");
|
||||
std::fs::create_dir_all(newer.parent().expect("newer parent")).expect("newer dir");
|
||||
|
||||
let candidates = game_creator_codex_cli_executable_candidates_for(
|
||||
None,
|
||||
Some(&local_app_data),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert_eq!(candidates[0], newer);
|
||||
assert_eq!(candidates[1], older);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn codex_cli_resolver_finds_current_native_install() {
|
||||
let executable = game_creator_codex_cli_executable_path().expect("Codex CLI executable");
|
||||
assert!(executable.is_absolute());
|
||||
assert_eq!(
|
||||
game_creator_codex_cli_version_identity().expect("Codex CLI version"),
|
||||
game_creator_codex_cli_version_at(&executable).expect("same executable version")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_cli_mode_renders_runtime_messages_and_structured_tool_contract() {
|
||||
let prompt = render_game_creator_codex_cli_prompt(&tool_request()).expect("render prompt");
|
||||
|
||||
@@ -13,14 +13,28 @@ mod run_lifecycle;
|
||||
mod tests;
|
||||
mod trace;
|
||||
|
||||
pub(crate) use canvas_generation::{
|
||||
classify_external_generation_initial_response, external_canvas_placeholder,
|
||||
external_editor_json_request, external_editor_response_data, external_generation_poll_after_ms,
|
||||
external_generation_result_has_download_reference,
|
||||
external_generation_submit_rejection_is_definitive,
|
||||
platform_art_generation_error_needs_reconciliation, prepare_external_canvas_generation_context,
|
||||
submit_external_generation_request, wait_for_external_generation_result,
|
||||
ExternalCanvasGenerationContext, ExternalGenerationInitialResponse,
|
||||
};
|
||||
pub(in crate::agent) use canvas_generation::{
|
||||
commit_prepared_platform_art_asset_at, commit_prepared_platform_art_asset_strict_slices_at,
|
||||
platform_art_generation_error_needs_reconciliation,
|
||||
platform_art_generation_error_result_unknown,
|
||||
request_platform_art_asset_with_runtime_options_at,
|
||||
validate_platform_art_png_bytes_with_limits,
|
||||
};
|
||||
pub(in crate::agent) use draft_validation::validate_closed_game_script_blocks;
|
||||
pub(crate) use external_generation_state::{
|
||||
classify_platform_art_generation_service_identity,
|
||||
platform_art_generation_external_service_fingerprint,
|
||||
platform_art_generation_external_service_origin, PlatformArtGenerationServiceIdentityMatch,
|
||||
PLATFORM_ART_GENERATION_SERVICE_IDENTITY_SCHEME,
|
||||
};
|
||||
pub(in crate::agent) use external_generation_state::{
|
||||
game_creator_agent_runtime_external_generation_exists,
|
||||
platform_art_generation_runtime_context_from_pending,
|
||||
@@ -30,6 +44,8 @@ pub(in crate::agent) use external_generation_state::{
|
||||
};
|
||||
#[cfg(test)]
|
||||
pub(crate) use external_generation_state::{
|
||||
platform_art_generation_external_configuration_fingerprint,
|
||||
platform_art_generation_legacy_external_configuration_fingerprint,
|
||||
setup_platform_art_generation_runtime_accepted_for_recovery_test,
|
||||
write_platform_art_generation_runtime_accepted_for_test,
|
||||
write_platform_art_generation_runtime_prepared_for_test,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user