Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9107d887ed | |||
| 7605250697 | |||
| 1be6e6756c | |||
| fd904ea144 | |||
| 299877b197 | |||
| b67cb18b45 | |||
| 8ba9835d10 | |||
| 02ff914bd3 | |||
| dd36de3aa6 | |||
| 9d2ad69a75 | |||
| 7df526868c | |||
| 3c1711c7df |
@@ -40,6 +40,8 @@ temp*build*/
|
|||||||
/apps/ai-game-creator-shell/src-tauri/resources/codex/win-x64/codex-path/
|
/apps/ai-game-creator-shell/src-tauri/resources/codex/win-x64/codex-path/
|
||||||
/apps/ai-game-creator-shell/src-tauri/resources/codex/win-x64/codex-resources/
|
/apps/ai-game-creator-shell/src-tauri/resources/codex/win-x64/codex-resources/
|
||||||
/apps/ai-game-creator-shell/src-tauri/resources/codex/win-x64/codex-package.json
|
/apps/ai-game-creator-shell/src-tauri/resources/codex/win-x64/codex-package.json
|
||||||
|
/apps/ai-game-creator-shell/src-tauri/resources/plugins/
|
||||||
|
/plugins/agc-cocos-editor/native/payload/
|
||||||
/apps/ai-game-creator-shell/logs/
|
/apps/ai-game-creator-shell/logs/
|
||||||
/apps/ai-game-creator-shell/.llm-drafts/
|
/apps/ai-game-creator-shell/.llm-drafts/
|
||||||
/apps/ai-game-creator-shell/game-creator.config.local.json
|
/apps/ai-game-creator-shell/game-creator.config.local.json
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# resources/plugins 由 build.rs 从 plugins/ 复制生成,属于构建产物。
|
||||||
|
# 它在 dev 监听范围内,重新生成会让 Tauri dev 误判为源码改动而触发
|
||||||
|
# “构建 -> 监听 -> 再构建”的自触发循环。
|
||||||
|
resources/plugins/
|
||||||
@@ -4,6 +4,11 @@ import fs from 'node:fs';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
import {
|
||||||
|
defaultEditorFeatures,
|
||||||
|
withDefaultCargoFeatures,
|
||||||
|
} from './cargo-features.mjs';
|
||||||
|
|
||||||
const appRoot = fileURLToPath(new URL('..', import.meta.url));
|
const appRoot = fileURLToPath(new URL('..', import.meta.url));
|
||||||
const defaultReleaseTarget = 'x86_64-pc-windows-msvc';
|
const defaultReleaseTarget = 'x86_64-pc-windows-msvc';
|
||||||
const releaseTarget =
|
const releaseTarget =
|
||||||
@@ -159,10 +164,30 @@ export async function prepareReleaseVersion() {
|
|||||||
return nextVersion;
|
return nextVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function runTauriBuild(args = []) {
|
export function buildTauriBuildArguments(
|
||||||
|
args = [],
|
||||||
|
target = releaseTarget,
|
||||||
|
platform = process.platform,
|
||||||
|
) {
|
||||||
const noBundle = args.includes('--no-bundle');
|
const noBundle = args.includes('--no-bundle');
|
||||||
const hasTarget = args.includes('--target');
|
const targetIndex = args.indexOf('--target');
|
||||||
const targetArgs = noBundle || hasTarget ? [] : ['--target', releaseTarget];
|
const explicitTarget =
|
||||||
|
targetIndex >= 0
|
||||||
|
? args[targetIndex + 1]
|
||||||
|
: args
|
||||||
|
.find((value) => value.startsWith('--target='))
|
||||||
|
?.slice('--target='.length);
|
||||||
|
const targetArgs = noBundle || explicitTarget ? [] : ['--target', target];
|
||||||
|
const features = defaultEditorFeatures(
|
||||||
|
explicitTarget || (noBundle ? platform : target),
|
||||||
|
);
|
||||||
|
return [
|
||||||
|
'build',
|
||||||
|
...withDefaultCargoFeatures([...targetArgs, ...args], features),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runTauriBuild(args = []) {
|
||||||
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
||||||
const result = spawnSync(
|
const result = spawnSync(
|
||||||
npmCommand,
|
npmCommand,
|
||||||
@@ -172,9 +197,7 @@ export function runTauriBuild(args = []) {
|
|||||||
'exec',
|
'exec',
|
||||||
'tauri',
|
'tauri',
|
||||||
'--',
|
'--',
|
||||||
'build',
|
...buildTauriBuildArguments(args),
|
||||||
...targetArgs,
|
|
||||||
...args,
|
|
||||||
],
|
],
|
||||||
{ cwd: appRoot, stdio: 'inherit', shell: process.platform === 'win32' },
|
{ cwd: appRoot, stdio: 'inherit', shell: process.platform === 'win32' },
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/** 默认桌面能力;显式 feature 参数优先,不把应用参数当 Cargo 参数。 */
|
||||||
|
export function withDefaultCargoFeatures(argv, features) {
|
||||||
|
const separator = argv.indexOf('--');
|
||||||
|
const cargoArgs = separator < 0 ? argv : argv.slice(0, separator);
|
||||||
|
if (
|
||||||
|
!features.length ||
|
||||||
|
cargoArgs.some(
|
||||||
|
(value) =>
|
||||||
|
value === '--features' ||
|
||||||
|
value === '-f' ||
|
||||||
|
value.startsWith('--features=') ||
|
||||||
|
/^-f.+/u.test(value),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return argv;
|
||||||
|
}
|
||||||
|
return [`--features=${features.join(',')}`, ...argv];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function defaultEditorFeatures(target) {
|
||||||
|
return target === 'win32' || target.includes('windows')
|
||||||
|
? ['cocos-editor-execute']
|
||||||
|
: [];
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { test } from 'node:test';
|
||||||
|
|
||||||
|
import { buildTauriBuildArguments } from './build-release.mjs';
|
||||||
|
import { withDefaultCargoFeatures } from './cargo-features.mjs';
|
||||||
|
|
||||||
|
test('Windows release includes the same editor feature as development', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
buildTauriBuildArguments([], 'x86_64-pc-windows-msvc', 'win32'),
|
||||||
|
[
|
||||||
|
'build',
|
||||||
|
'--features=cocos-editor-execute',
|
||||||
|
'--target',
|
||||||
|
'x86_64-pc-windows-msvc',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
buildTauriBuildArguments(
|
||||||
|
['--no-bundle'],
|
||||||
|
'x86_64-pc-windows-msvc',
|
||||||
|
'linux',
|
||||||
|
),
|
||||||
|
['build', '--no-bundle'],
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
buildTauriBuildArguments(['--target=aarch64-apple-darwin']),
|
||||||
|
['build', '--target=aarch64-apple-darwin'],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
test('explicit Cargo features override defaults in every supported spelling', () => {
|
||||||
|
for (const args of [
|
||||||
|
['--features', 'custom'],
|
||||||
|
['--features=custom'],
|
||||||
|
['-f', 'custom'],
|
||||||
|
['-fcustom'],
|
||||||
|
]) {
|
||||||
|
assert.deepEqual(
|
||||||
|
withDefaultCargoFeatures(args, ['cocos-editor-execute']),
|
||||||
|
args,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert.deepEqual(
|
||||||
|
withDefaultCargoFeatures(
|
||||||
|
['--', '--features=app'],
|
||||||
|
['cocos-editor-execute'],
|
||||||
|
),
|
||||||
|
['--features=cocos-editor-execute', '--', '--features=app'],
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -117,6 +117,16 @@ const allowedUncalledTauriCommands = [
|
|||||||
'stop_local_game_preview_if_matches',
|
'stop_local_game_preview_if_matches',
|
||||||
'start_game_creator_external_mcp',
|
'start_game_creator_external_mcp',
|
||||||
'stop_game_creator_external_mcp',
|
'stop_game_creator_external_mcp',
|
||||||
|
'list_agc_plugins',
|
||||||
|
'list_agc_extensions',
|
||||||
|
'refresh_agc_plugins',
|
||||||
|
'start_agc_plugin',
|
||||||
|
'stop_agc_plugin',
|
||||||
|
'reload_agc_plugin',
|
||||||
|
'call_agc_plugin',
|
||||||
|
'read_agc_plugin_panel',
|
||||||
|
'set_agc_plugin_project_path',
|
||||||
|
'set_agc_plugin_enabled',
|
||||||
];
|
];
|
||||||
const sourceExtensions = new Set([
|
const sourceExtensions = new Set([
|
||||||
'.json',
|
'.json',
|
||||||
@@ -1285,7 +1295,7 @@ if (tauriConfig.identifier !== 'world.genarrative.ai-game-creator') {
|
|||||||
throw new Error('AI game creator shell identifier drifted');
|
throw new Error('AI game creator shell identifier drifted');
|
||||||
}
|
}
|
||||||
|
|
||||||
const expectedBundledCodexResources = {
|
const expectedBundledWindowsResources = {
|
||||||
'resources/codex/win-x64/bin/codex.exe': 'codex/win-x64/bin/codex.exe',
|
'resources/codex/win-x64/bin/codex.exe': 'codex/win-x64/bin/codex.exe',
|
||||||
'resources/codex/win-x64/bin/codex-code-mode-host.exe':
|
'resources/codex/win-x64/bin/codex-code-mode-host.exe':
|
||||||
'codex/win-x64/bin/codex-code-mode-host.exe',
|
'codex/win-x64/bin/codex-code-mode-host.exe',
|
||||||
@@ -1299,6 +1309,7 @@ const expectedBundledCodexResources = {
|
|||||||
'codex/win-x64/codex-package.json',
|
'codex/win-x64/codex-package.json',
|
||||||
'resources/codex/win-x64/NOTICE.md': 'codex/win-x64/NOTICE.md',
|
'resources/codex/win-x64/NOTICE.md': 'codex/win-x64/NOTICE.md',
|
||||||
'resources/codex/win-x64/manifest.json': 'codex/win-x64/manifest.json',
|
'resources/codex/win-x64/manifest.json': 'codex/win-x64/manifest.json',
|
||||||
|
'resources/plugins': 'plugins',
|
||||||
};
|
};
|
||||||
if (tauriConfig.bundle?.resources !== undefined) {
|
if (tauriConfig.bundle?.resources !== undefined) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -1307,8 +1318,8 @@ if (tauriConfig.bundle?.resources !== undefined) {
|
|||||||
}
|
}
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
windowsTauriConfig.bundle?.resources,
|
windowsTauriConfig.bundle?.resources,
|
||||||
expectedBundledCodexResources,
|
expectedBundledWindowsResources,
|
||||||
'AI game creator shell Windows Tauri config must bundle the complete pinned Codex resource set',
|
'AI game creator shell Windows Tauri config must bundle the complete pinned Codex resource set and Cocos bridge payload directory',
|
||||||
);
|
);
|
||||||
if (windowsTauriConfig.bundle?.useLocalToolsDir !== true) {
|
if (windowsTauriConfig.bundle?.useLocalToolsDir !== true) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
@@ -436,6 +436,10 @@ function spawnChild(command, args, options, spawnImpl = spawn) {
|
|||||||
const child = spawnImpl(command, args, {
|
const child = spawnImpl(command, args, {
|
||||||
...options,
|
...options,
|
||||||
shell: useShell,
|
shell: useShell,
|
||||||
|
// npm.cmd and the Windows shell otherwise create a visible console for
|
||||||
|
// every service in the dev stack. Their stdout/stderr is already inherited
|
||||||
|
// by the launcher, so no separate terminal window is useful.
|
||||||
|
windowsHide: process.platform === 'win32' ? true : options.windowsHide,
|
||||||
// POSIX 下让每个长驻服务拥有独立进程组,退出时可以连同 npm、
|
// POSIX 下让每个长驻服务拥有独立进程组,退出时可以连同 npm、
|
||||||
// Node、Cargo 及其子进程一起清理,避免残留订阅任务持续刷日志。
|
// Node、Cargo 及其子进程一起清理,避免残留订阅任务持续刷日志。
|
||||||
detached: isPosix,
|
detached: isPosix,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
import { withDefaultCargoFeatures } from './cargo-features.mjs';
|
||||||
import {
|
import {
|
||||||
readAgcDevEndpoint,
|
readAgcDevEndpoint,
|
||||||
resolveAgcDevEndpoint,
|
resolveAgcDevEndpoint,
|
||||||
@@ -43,6 +44,24 @@ function buildTauriArguments(argv, devUrl = readAgcDevEndpoint().url) {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `agc_cocos_execute` 与 Cocos 编辑器适配器只在 `cocos-editor-execute` feature 下
|
||||||
|
// 注册。开发构建默认在 Windows 打开它,否则 Agent 的工具清单里根本没有该工具,
|
||||||
|
// 只能退化成改写脚本。可用 AGC_DEV_CARGO_FEATURES(逗号分隔)覆盖,传空串即关闭。
|
||||||
|
function readDevCargoFeatures(env = process.env) {
|
||||||
|
const override = env.AGC_DEV_CARGO_FEATURES;
|
||||||
|
if (override !== undefined) {
|
||||||
|
return override
|
||||||
|
.split(',')
|
||||||
|
.map((value) => value.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
return process.platform === 'win32' ? ['cocos-editor-execute'] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function withDevCargoFeatures(argv, features = readDevCargoFeatures()) {
|
||||||
|
return withDefaultCargoFeatures(argv, features);
|
||||||
|
}
|
||||||
|
|
||||||
function spawnTauriCli(argv, { env = process.env } = {}) {
|
function spawnTauriCli(argv, { env = process.env } = {}) {
|
||||||
return spawnChild(process.execPath, [tauriCliPath, ...argv], {
|
return spawnChild(process.execPath, [tauriCliPath, ...argv], {
|
||||||
cwd: appRoot,
|
cwd: appRoot,
|
||||||
@@ -106,7 +125,10 @@ async function runTauriDev(
|
|||||||
shutdownRequested.then(() => false),
|
shutdownRequested.then(() => false),
|
||||||
]);
|
]);
|
||||||
if (!prepared || shutdownSignal) return 1;
|
if (!prepared || shutdownSignal) return 1;
|
||||||
const tauriArguments = buildTauriArguments(argv, endpoint.url);
|
const tauriArguments = buildTauriArguments(
|
||||||
|
withDevCargoFeatures(argv),
|
||||||
|
endpoint.url,
|
||||||
|
);
|
||||||
child = spawnCli(tauriArguments, {
|
child = spawnCli(tauriArguments, {
|
||||||
env: withAgcDevEndpointEnv(endpoint),
|
env: withAgcDevEndpointEnv(endpoint),
|
||||||
});
|
});
|
||||||
@@ -193,6 +215,7 @@ export {
|
|||||||
isDirectModuleExecution,
|
isDirectModuleExecution,
|
||||||
runTauriDev,
|
runTauriDev,
|
||||||
spawnTauriCli,
|
spawnTauriCli,
|
||||||
|
withDevCargoFeatures,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isDirectModuleExecution()) {
|
if (isDirectModuleExecution()) {
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# resources/plugins 由 build.rs 从 plugins/ 复制生成,属于构建产物。
|
||||||
|
# 它在 dev 监听范围内,重新生成会让 Tauri dev 误判为源码改动而触发
|
||||||
|
# “构建 -> 监听 -> 再构建”的自触发循环。
|
||||||
|
resources/plugins/
|
||||||
+25
@@ -733,6 +733,20 @@ dependencies = [
|
|||||||
"error-code",
|
"error-code",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cocos-editor-bridge"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"editor-adapter-api",
|
||||||
|
"reqwest 0.12.28",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"sha2",
|
||||||
|
"tungstenite",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "combine"
|
name = "combine"
|
||||||
version = "4.6.7"
|
version = "4.6.7"
|
||||||
@@ -1205,6 +1219,14 @@ version = "1.0.20"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "editor-adapter-api"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.16.0"
|
version = "1.16.0"
|
||||||
@@ -1709,6 +1731,8 @@ dependencies = [
|
|||||||
"axum",
|
"axum",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"chromiumoxide",
|
"chromiumoxide",
|
||||||
|
"cocos-editor-bridge",
|
||||||
|
"editor-adapter-api",
|
||||||
"futures",
|
"futures",
|
||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
"http",
|
"http",
|
||||||
@@ -4281,6 +4305,7 @@ dependencies = [
|
|||||||
"cookie",
|
"cookie",
|
||||||
"cookie_store",
|
"cookie_store",
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"h2",
|
"h2",
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ publish = false
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
|
cocos-editor = ["cocos-editor-bridge/process-discovery"]
|
||||||
|
cocos-editor-execute = ["cocos-editor", "cocos-editor-bridge/windows-bootstrap"]
|
||||||
|
cocos-editor-injection = ["cocos-editor-execute", "cocos-editor-bridge/windows-injection"]
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
@@ -19,6 +22,8 @@ ts-rs = "12.0.1"
|
|||||||
typed_floats = { version = "1.0.7", features = ["serde"] }
|
typed_floats = { version = "1.0.7", features = ["serde"] }
|
||||||
nalgebra = { version = "0.35.0", features = ["serde-serialize"] }
|
nalgebra = { version = "0.35.0", features = ["serde-serialize"] }
|
||||||
agent-runtime-core = { path = "../../../server-rs/crates/agent-runtime-core" }
|
agent-runtime-core = { path = "../../../server-rs/crates/agent-runtime-core" }
|
||||||
|
cocos-editor-bridge = { path = "../../../plugins/agc-cocos-editor/native/cocos-editor-bridge", default-features = false }
|
||||||
|
editor-adapter-api = { path = "../../../server-rs/crates/editor-adapter-api" }
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
axum = "0.8"
|
axum = "0.8"
|
||||||
chromiumoxide = "0.9.1"
|
chromiumoxide = "0.9.1"
|
||||||
@@ -74,14 +79,6 @@ codegen-units = 256
|
|||||||
lto = "off"
|
lto = "off"
|
||||||
incremental = true
|
incremental = true
|
||||||
|
|
||||||
# Runner 启动阶段会对当前 Debug 可执行文件计算 SHA-256。仅优化密码学依赖,
|
|
||||||
# 保持业务代码的 Debug 编译速度,同时避免整份 Debug 构建因未优化 hash 热点而阻塞启动。
|
|
||||||
[profile.dev.package.sha2]
|
|
||||||
opt-level = 3
|
|
||||||
|
|
||||||
[profile.dev.package.digest]
|
|
||||||
opt-level = 3
|
|
||||||
|
|
||||||
[profile.test]
|
[profile.test]
|
||||||
opt-level = 0
|
opt-level = 0
|
||||||
debug = 1
|
debug = 1
|
||||||
|
|||||||
@@ -182,6 +182,8 @@ fn main() {
|
|||||||
);
|
);
|
||||||
let manifest_path = manifest_dir.join("prompts/runtime/manifest.json");
|
let manifest_path = manifest_dir.join("prompts/runtime/manifest.json");
|
||||||
stage_bundled_codex_cli(&manifest_dir);
|
stage_bundled_codex_cli(&manifest_dir);
|
||||||
|
stage_plugin_workspace(&manifest_dir);
|
||||||
|
stage_cocos_editor_payload(&manifest_dir);
|
||||||
let compiled = runtime_prompt_bundle::compile_manifest(&manifest_path)
|
let compiled = runtime_prompt_bundle::compile_manifest(&manifest_path)
|
||||||
.unwrap_or_else(|error| panic!("Prompt Bundle 编译失败:{error}"));
|
.unwrap_or_else(|error| panic!("Prompt Bundle 编译失败:{error}"));
|
||||||
validate_seed_task_catalog(&compiled);
|
validate_seed_task_catalog(&compiled);
|
||||||
@@ -203,3 +205,146 @@ fn main() {
|
|||||||
}
|
}
|
||||||
tauri_build::build()
|
tauri_build::build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn stage_cocos_editor_payload(manifest_dir: &std::path::Path) {
|
||||||
|
if std::env::var_os("CARGO_FEATURE_COCOS_EDITOR_INJECTION").is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let out_dir = std::path::PathBuf::from(std::env::var_os("OUT_DIR").expect("OUT_DIR"));
|
||||||
|
let profile_dir = out_dir
|
||||||
|
.ancestors()
|
||||||
|
.find(|path| path.file_name().is_some_and(|name| name == "build"))
|
||||||
|
.and_then(|build_dir| build_dir.parent())
|
||||||
|
.expect("AGC Cargo profile directory not found");
|
||||||
|
let candidates = [
|
||||||
|
profile_dir.join("deps/cocos_editor_bridge.dll"),
|
||||||
|
profile_dir.join("cocos_editor_bridge.dll"),
|
||||||
|
];
|
||||||
|
let source = candidates
|
||||||
|
.iter()
|
||||||
|
.find(|path| path.is_file())
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
panic!(
|
||||||
|
"Cocos bridge native payload 未构建:{}",
|
||||||
|
candidates
|
||||||
|
.iter()
|
||||||
|
.map(|p| p.display().to_string())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(";")
|
||||||
|
)
|
||||||
|
});
|
||||||
|
for destination in [
|
||||||
|
// 插件工作区里的 payload 是开发态与打包态的唯一真源。
|
||||||
|
manifest_dir
|
||||||
|
.join("../../../plugins/agc-cocos-editor/native/payload/cocos-editor-bridge.dll"),
|
||||||
|
// 随包资源目录与 tauri.windows.conf.json 的 `resources/plugins` 映射保持一致。
|
||||||
|
manifest_dir
|
||||||
|
.join("resources/plugins/agc-cocos-editor/native/payload/cocos-editor-bridge.dll"),
|
||||||
|
] {
|
||||||
|
std::fs::create_dir_all(destination.parent().expect("payload resource parent"))
|
||||||
|
.expect("创建 Cocos bridge payload 目录失败");
|
||||||
|
std::fs::copy(source, &destination).expect("复制 Cocos bridge native payload 失败");
|
||||||
|
}
|
||||||
|
println!("cargo:rerun-if-changed={}", source.display());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
fn stage_cocos_editor_payload(_manifest_dir: &std::path::Path) {}
|
||||||
|
|
||||||
|
/// 把 `plugins/` 工作区里的插件包随包映射到应用资源目录。
|
||||||
|
///
|
||||||
|
/// 只复制插件运行需要的清单、入口、面板和 native payload,不复制 native 源码、
|
||||||
|
/// Cargo target 目录或 node_modules。
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn stage_plugin_workspace(manifest_dir: &std::path::Path) {
|
||||||
|
let repo_root = manifest_dir
|
||||||
|
.parent()
|
||||||
|
.and_then(|app_root| app_root.parent())
|
||||||
|
.and_then(|apps_dir| apps_dir.parent())
|
||||||
|
.expect("AGC 应用必须位于仓库 apps 目录下")
|
||||||
|
.to_path_buf();
|
||||||
|
let workspace = repo_root.join("plugins");
|
||||||
|
let destination_root = manifest_dir.join("resources/plugins");
|
||||||
|
std::fs::create_dir_all(&destination_root).expect("创建插件资源目录失败");
|
||||||
|
let entries = match std::fs::read_dir(&workspace) {
|
||||||
|
Ok(entries) => entries,
|
||||||
|
Err(_) => return,
|
||||||
|
};
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let plugin_root = entry.path();
|
||||||
|
if !plugin_root.is_dir() || !plugin_root.join("plugin.json").is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let name = entry.file_name();
|
||||||
|
let destination = destination_root.join(&name);
|
||||||
|
copy_plugin_file(
|
||||||
|
&plugin_root.join("plugin.json"),
|
||||||
|
&destination.join("plugin.json"),
|
||||||
|
);
|
||||||
|
for relative in [
|
||||||
|
std::path::PathBuf::from("src"),
|
||||||
|
std::path::PathBuf::from("panels"),
|
||||||
|
std::path::PathBuf::from("native/payload"),
|
||||||
|
] {
|
||||||
|
copy_plugin_tree(&plugin_root.join(&relative), &destination.join(&relative));
|
||||||
|
}
|
||||||
|
println!("cargo:rerun-if-changed={}", plugin_root.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn stage_plugin_file(source: &std::path::Path, destination: &std::path::Path) {
|
||||||
|
let Ok(bytes) = std::fs::read(source) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if std::fs::read(destination).is_ok_and(|existing| existing == bytes) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(parent) = destination.parent() {
|
||||||
|
std::fs::create_dir_all(parent).expect("创建插件资源目录失败");
|
||||||
|
}
|
||||||
|
std::fs::write(destination, bytes).expect("复制插件资源失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn copy_plugin_tree(source: &std::path::Path, destination: &std::path::Path) {
|
||||||
|
let entries = match std::fs::read_dir(source) {
|
||||||
|
Ok(entries) => entries,
|
||||||
|
Err(_) => return,
|
||||||
|
};
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let target = destination.join(entry.file_name());
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_dir() {
|
||||||
|
let name = entry.file_name();
|
||||||
|
let name = name.to_string_lossy();
|
||||||
|
if matches!(name.as_ref(), "target" | "node_modules" | ".git") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
std::fs::create_dir_all(&target).expect("创建插件资源目录失败");
|
||||||
|
copy_plugin_tree(&path, &target);
|
||||||
|
} else {
|
||||||
|
// 测试文件不随包分发。
|
||||||
|
let name = entry.file_name();
|
||||||
|
let name = name.to_string_lossy();
|
||||||
|
if name.contains(".test.") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
stage_plugin_file(&path, &target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn copy_plugin_file(source: &std::path::Path, destination: &std::path::Path) {
|
||||||
|
if !source.is_file() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::fs::create_dir_all(destination.parent().expect("插件资源父目录"))
|
||||||
|
.expect("创建插件资源目录失败");
|
||||||
|
std::fs::copy(source, destination).expect("复制插件资源失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
fn stage_plugin_workspace(_manifest_dir: &std::path::Path) {}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
*.dll
|
||||||
@@ -305,6 +305,35 @@ fn game_creator_codex_app_server_error_detail_indicates_auth_failure(
|
|||||||
|| detail.contains("http 403")
|
|| detail.contains("http 403")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn game_creator_codex_app_server_error_detail(error: &serde_json::Value) -> String {
|
||||||
|
let Some(error) = error.as_object() else {
|
||||||
|
return String::new();
|
||||||
|
};
|
||||||
|
["message", "additionalDetails", "code"]
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|field| error.get(field).and_then(serde_json::Value::as_str))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ")
|
||||||
|
.to_ascii_lowercase()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn game_creator_codex_app_server_error_detail_indicates_stream_requirement(
|
||||||
|
error: &serde_json::Value,
|
||||||
|
) -> bool {
|
||||||
|
let detail = game_creator_codex_app_server_error_detail(error);
|
||||||
|
detail.contains("stream must be set to true")
|
||||||
|
|| detail.contains("stream=true")
|
||||||
|
|| detail.contains("stream is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn game_creator_codex_app_server_error_detail_indicates_timeout(error: &serde_json::Value) -> bool {
|
||||||
|
let detail = game_creator_codex_app_server_error_detail(error);
|
||||||
|
detail.contains("timed out")
|
||||||
|
|| detail.contains("timeout")
|
||||||
|
|| detail.contains("request deadline exceeded")
|
||||||
|
|| detail.contains("deadline exceeded")
|
||||||
|
}
|
||||||
|
|
||||||
fn game_creator_codex_app_server_error_detail_indicates_request_too_large(
|
fn game_creator_codex_app_server_error_detail_indicates_request_too_large(
|
||||||
error: &serde_json::Value,
|
error: &serde_json::Value,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
@@ -362,6 +391,15 @@ fn game_creator_codex_app_server_failed_turn_error(
|
|||||||
if game_creator_codex_app_server_error_detail_indicates_request_too_large(error) {
|
if game_creator_codex_app_server_error_detail_indicates_request_too_large(error) {
|
||||||
return game_creator_codex_app_server_error_kind("request-too-large");
|
return game_creator_codex_app_server_error_kind("request-too-large");
|
||||||
}
|
}
|
||||||
|
if game_creator_codex_app_server_error_detail_indicates_stream_requirement(error) {
|
||||||
|
return game_creator_codex_app_server_error_kind("stream-required");
|
||||||
|
}
|
||||||
|
if game_creator_codex_app_server_error_detail_indicates_timeout(error) {
|
||||||
|
return platform_llm::LlmError::Connectivity {
|
||||||
|
attempts: 1,
|
||||||
|
message: "Codex app-server 上游请求超时".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
if game_creator_codex_app_server_error_detail_indicates_auth_failure(error) {
|
if game_creator_codex_app_server_error_detail_indicates_auth_failure(error) {
|
||||||
return game_creator_codex_app_server_error_kind("unauthorized");
|
return game_creator_codex_app_server_error_kind("unauthorized");
|
||||||
}
|
}
|
||||||
@@ -1070,6 +1108,7 @@ fn game_creator_codex_app_server_pool_key(
|
|||||||
"skillPackIdentity": skill_pack_identity,
|
"skillPackIdentity": skill_pack_identity,
|
||||||
"clientSkillIdentity": client_skill_identity,
|
"clientSkillIdentity": client_skill_identity,
|
||||||
"clientMcpIdentity": client_mcp_identity,
|
"clientMcpIdentity": client_mcp_identity,
|
||||||
|
"builtinPluginTools": crate::builtin_plugins::available_agent_tools(),
|
||||||
"controlledWebSearch": llm.web_search_enabled,
|
"controlledWebSearch": llm.web_search_enabled,
|
||||||
"directToolBridgeProtocol": if workspace_mode == CodexAppServerWorkspaceMode::DirectProject { DIRECT_TOOL_BRIDGE_PROTOCOL } else { "disabled" },
|
"directToolBridgeProtocol": if workspace_mode == CodexAppServerWorkspaceMode::DirectProject { DIRECT_TOOL_BRIDGE_PROTOCOL } else { "disabled" },
|
||||||
"providerProxyProtocol": if workspace_mode == CodexAppServerWorkspaceMode::DirectProject { CODEX_PROVIDER_PROXY_PROTOCOL } else { "disabled" },
|
"providerProxyProtocol": if workspace_mode == CodexAppServerWorkspaceMode::DirectProject { CODEX_PROVIDER_PROXY_PROTOCOL } else { "disabled" },
|
||||||
@@ -2463,10 +2502,12 @@ impl CodexAppServerConnection {
|
|||||||
// Codex's default provider in Debug builds.
|
// Codex's default provider in Debug builds.
|
||||||
self.inner._provider_proxy.is_some() || !llm.api_key.trim().is_empty(),
|
self.inner._provider_proxy.is_some() || !llm.api_key.trim().is_empty(),
|
||||||
);
|
);
|
||||||
let result = self
|
let result = match self.request("thread/start", params).await {
|
||||||
.request("thread/start", params)
|
Ok(result) => result,
|
||||||
.await
|
Err(error) => {
|
||||||
.map_err(platform_llm::LlmError::Transport)?;
|
return Err(platform_llm::LlmError::Transport(error));
|
||||||
|
}
|
||||||
|
};
|
||||||
let thread_id = result
|
let thread_id = result
|
||||||
.pointer("/thread/id")
|
.pointer("/thread/id")
|
||||||
.and_then(serde_json::Value::as_str)
|
.and_then(serde_json::Value::as_str)
|
||||||
@@ -4809,6 +4850,37 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn codex_app_server_failed_turn_maps_stream_and_timeout_details() {
|
||||||
|
let stream_error = game_creator_codex_app_server_failed_turn_error(&serde_json::json!({
|
||||||
|
"status": "failed",
|
||||||
|
"error": {
|
||||||
|
"message": "Stream must be set to true",
|
||||||
|
"codexErrorInfo": "other"
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
assert_eq!(
|
||||||
|
stream_error,
|
||||||
|
platform_llm::LlmError::InvalidRequest(
|
||||||
|
"codex-app-server-error:stream-required".to_string()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
let timeout_error = game_creator_codex_app_server_failed_turn_error(&serde_json::json!({
|
||||||
|
"status": "failed",
|
||||||
|
"error": {
|
||||||
|
"message": "provider request timed out",
|
||||||
|
"codexErrorInfo": "other"
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
assert_eq!(
|
||||||
|
timeout_error,
|
||||||
|
platform_llm::LlmError::Connectivity {
|
||||||
|
attempts: 1,
|
||||||
|
message: "Codex app-server 上游请求超时".to_string()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn codex_app_server_failed_turn_maps_insufficient_mud_points_to_stable_upstream_error() {
|
fn codex_app_server_failed_turn_maps_insufficient_mud_points_to_stable_upstream_error() {
|
||||||
for detail in [
|
for detail in [
|
||||||
@@ -5050,6 +5122,42 @@ while IFS= read -r line; do :; done
|
|||||||
assert_ne!(disabled, enabled);
|
assert_ne!(disabled, enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "cocos-editor-execute")]
|
||||||
|
#[test]
|
||||||
|
fn codex_app_server_pool_key_tracks_builtin_plugin_switch() {
|
||||||
|
let _guard = crate::builtin_plugins::test_lock();
|
||||||
|
let config = tempfile::tempdir().unwrap();
|
||||||
|
crate::builtin_plugins::initialize(config.path()).unwrap();
|
||||||
|
let key = || {
|
||||||
|
game_creator_codex_app_server_pool_key(
|
||||||
|
&test_llm(),
|
||||||
|
"codex-cli 0.147.0",
|
||||||
|
&test_snapshot(),
|
||||||
|
"credential",
|
||||||
|
CodexAppServerWorkspaceMode::DirectProject,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
crate::builtin_plugins::set_enabled(
|
||||||
|
crate::builtin_plugins::AGC_COCOS_EDITOR_PLUGIN_ID,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let disabled = key();
|
||||||
|
crate::builtin_plugins::set_enabled(
|
||||||
|
crate::builtin_plugins::AGC_COCOS_EDITOR_PLUGIN_ID,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let enabled = key();
|
||||||
|
assert_ne!(disabled, enabled);
|
||||||
|
crate::builtin_plugins::set_enabled(
|
||||||
|
crate::builtin_plugins::AGC_COCOS_EDITOR_PLUGIN_ID,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(disabled, key());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn direct_file_change_approval_is_limited_to_workspace() {
|
fn direct_file_change_approval_is_limited_to_workspace() {
|
||||||
let temp = tempfile::tempdir().expect("temp dir");
|
let temp = tempfile::tempdir().expect("temp dir");
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use super::runtime_actions::acquire_game_creator_agent_runtime_project_write_lock_with_wait;
|
||||||
use crate::config::prepare_game_creator_private_path_for_read;
|
use crate::config::prepare_game_creator_private_path_for_read;
|
||||||
use crate::project::{
|
use crate::project::{
|
||||||
append_jsonl_line_unlocked, enforce_project_permission_policy, project_append_lock_for,
|
append_jsonl_line_unlocked, enforce_project_permission_policy, project_append_lock_for,
|
||||||
@@ -204,7 +205,15 @@ fn append_direct_project_history_item_at_with_user_policy(
|
|||||||
if !allow_user_item && is_direct_project_codex_user_item(item) {
|
if !allow_user_item && is_direct_project_codex_user_item(item) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let _project_lock = crate::project::acquire_project_write_lock(root, "conversation.write")?;
|
// DirectProject 历史与 `agc_write_file` 共用项目写锁。文件写入在锁内要跑 Windows
|
||||||
|
// 私有路径准备与原子替换,现场实测一次 2.6KB 写入占锁 5.5 秒;零等待取锁会让
|
||||||
|
// 流式历史落盘在写文件期间直接失败,并把整轮判成“项目正在被其他写操作占用”
|
||||||
|
// (持锁方 commandId=direct-codex.file.write、ownerIsSelf=true)。这里与其它写入口
|
||||||
|
// 保持同一档有界等待;主路径已在阻塞线程池中执行。
|
||||||
|
let _project_lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
|
||||||
|
root,
|
||||||
|
"conversation.write",
|
||||||
|
)?;
|
||||||
let path = history_path(root);
|
let path = history_path(root);
|
||||||
let history_exists =
|
let history_exists =
|
||||||
prepare_game_creator_private_path_for_read(&path, false, "DirectProject 历史")?;
|
prepare_game_creator_private_path_for_read(&path, false, "DirectProject 历史")?;
|
||||||
@@ -429,6 +438,50 @@ mod tests {
|
|||||||
assert_eq!(items, vec![item]);
|
assert_eq!(items, vec![item]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn append_waits_for_a_same_process_project_writer() {
|
||||||
|
// 用 canonical 临时根:Windows 上 `%TEMP%` 的 8.3 短路径会让私有路径所有者
|
||||||
|
// 校验把测试目录判成“不属于当前用户”。
|
||||||
|
let temp_root = std::env::temp_dir()
|
||||||
|
.canonicalize()
|
||||||
|
.unwrap_or_else(|_| std::env::temp_dir());
|
||||||
|
let root = temp_root.join(format!(
|
||||||
|
"genarrative-agc-history-wait-{}-{}",
|
||||||
|
std::process::id(),
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.map(|value| value.as_millis())
|
||||||
|
.unwrap_or_default()
|
||||||
|
));
|
||||||
|
std::fs::create_dir_all(&root).expect("create project root");
|
||||||
|
crate::init_local_game_project_at(&root, "history-wait", "历史等待").expect("init project");
|
||||||
|
// 模拟 `agc_write_file`:它持锁期间历史落盘必须排队等待,而不是零等待失败后
|
||||||
|
// 把整轮判成“项目正在被其他写操作占用”。
|
||||||
|
let holder = crate::project::acquire_project_write_lock(&root, "direct-codex.file.write")
|
||||||
|
.expect("hold project write lock");
|
||||||
|
let worker_root = root.clone();
|
||||||
|
let worker = std::thread::spawn(move || {
|
||||||
|
append_direct_project_history_item_at(
|
||||||
|
&worker_root,
|
||||||
|
&serde_json::json!({
|
||||||
|
"type": "message",
|
||||||
|
"role": "assistant",
|
||||||
|
"id": "waits-for-writer",
|
||||||
|
"content": [{"type": "output_text", "text": "排队等待"}]
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(300));
|
||||||
|
drop(holder);
|
||||||
|
worker
|
||||||
|
.join()
|
||||||
|
.expect("append worker")
|
||||||
|
.expect("history append must wait for the writer");
|
||||||
|
let items = read_direct_project_history_items_at(&root).expect("read history");
|
||||||
|
assert_eq!(items.len(), 1);
|
||||||
|
let _ = std::fs::remove_dir_all(&root);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn codex_user_echo_is_filtered_but_agc_user_message_is_persisted() {
|
fn codex_user_echo_is_filtered_but_agc_user_message_is_persisted() {
|
||||||
let root = tempfile::tempdir().expect("temp project");
|
let root = tempfile::tempdir().expect("temp project");
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -54,6 +54,8 @@ struct DirectToolBridgeState {
|
|||||||
regeneration_gate: tokio::sync::Mutex<()>,
|
regeneration_gate: tokio::sync::Mutex<()>,
|
||||||
resource_generation_gate: tokio::sync::Mutex<()>,
|
resource_generation_gate: tokio::sync::Mutex<()>,
|
||||||
image_generation_gate: tokio::sync::Mutex<()>,
|
image_generation_gate: tokio::sync::Mutex<()>,
|
||||||
|
#[cfg(all(windows, feature = "cocos-editor-execute"))]
|
||||||
|
cocos_execute_uncertain: tokio::sync::Mutex<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@@ -687,6 +689,8 @@ fn direct_tool_bridge_state_with_search(
|
|||||||
regeneration_gate: tokio::sync::Mutex::new(()),
|
regeneration_gate: tokio::sync::Mutex::new(()),
|
||||||
resource_generation_gate: tokio::sync::Mutex::new(()),
|
resource_generation_gate: tokio::sync::Mutex::new(()),
|
||||||
image_generation_gate: tokio::sync::Mutex::new(()),
|
image_generation_gate: tokio::sync::Mutex::new(()),
|
||||||
|
#[cfg(all(windows, feature = "cocos-editor-execute"))]
|
||||||
|
cocos_execute_uncertain: tokio::sync::Mutex::new(false),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1477,12 +1481,28 @@ fn bridge_write_file(root: &Path, arguments: &Value) -> Value {
|
|||||||
// 而 `file.write / file.patch / file.delete` 等写入口用的是约 10 秒有界等待。
|
// 而 `file.write / file.patch / file.delete` 等写入口用的是约 10 秒有界等待。
|
||||||
// 这是用户直接触发、失败即整轮无法落盘的项目写入通道,必须和其它写入口同语义:
|
// 这是用户直接触发、失败即整轮无法落盘的项目写入通道,必须和其它写入口同语义:
|
||||||
// 短暂重叠排队等成功,只有预算耗尽才报出带持锁方身份的错误。
|
// 短暂重叠排队等成功,只有预算耗尽才报出带持锁方身份的错误。
|
||||||
|
let acquire_started = std::time::Instant::now();
|
||||||
let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
|
let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
|
||||||
root,
|
root,
|
||||||
"direct-codex.file.write",
|
"direct-codex.file.write",
|
||||||
)?;
|
)?;
|
||||||
|
let lock_wait_ms = acquire_started.elapsed().as_millis();
|
||||||
|
let write_started = std::time::Instant::now();
|
||||||
let written = write_local_project_file_at(root, &path, content)?;
|
let written = write_local_project_file_at(root, &path, content)?;
|
||||||
|
let write_ms = write_started.elapsed().as_millis();
|
||||||
|
let revision_started = std::time::Instant::now();
|
||||||
let revision = advance_agent_runtime_project_revision_locked(root)?;
|
let revision = advance_agent_runtime_project_revision_locked(root)?;
|
||||||
|
// 现场一次 2.6KB 写入实测 5.5 秒。只在明显偏慢时记账,正常写入不刷日志。
|
||||||
|
if lock_wait_ms + write_ms > 200 {
|
||||||
|
app_log!(
|
||||||
|
"direct.file.write.timing path={} bytes={} lockWaitMs={} writeMs={} revisionMs={}",
|
||||||
|
written.path,
|
||||||
|
content.len(),
|
||||||
|
lock_wait_ms,
|
||||||
|
write_ms,
|
||||||
|
revision_started.elapsed().as_millis()
|
||||||
|
);
|
||||||
|
}
|
||||||
Ok::<_, String>(json!({
|
Ok::<_, String>(json!({
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"path": written.path,
|
"path": written.path,
|
||||||
@@ -2323,11 +2343,122 @@ async fn bridge_web_search_at(root: &Path, arguments: &Value, search_url: &str)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(all(windows, feature = "cocos-editor-execute"))]
|
||||||
|
async fn bridge_cocos_execute(state: &DirectToolBridgeState, arguments: &Value) -> Value {
|
||||||
|
if !crate::builtin_plugins::cocos_editor_agent_tool_available() {
|
||||||
|
return bridge_tool_result(
|
||||||
|
"Cocos Creator 插件已禁用,agc_cocos_execute 不可用".to_string(),
|
||||||
|
Vec::new(),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let prepared = (|| {
|
||||||
|
bridge_reject_unknown_fields(arguments, &["code"])?;
|
||||||
|
enforce_project_permission_policy(&state.root, "cocos.editor.execute")?;
|
||||||
|
let code = arguments
|
||||||
|
.get("code")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.ok_or_else(|| "code 必须是 JavaScript 函数体".to_string())?;
|
||||||
|
cocos_editor_bridge::validate_execute_code(code).map_err(|error| error.to_string())?;
|
||||||
|
Ok::<_, String>(code.to_string())
|
||||||
|
})();
|
||||||
|
let code = match prepared {
|
||||||
|
Ok(code) => code,
|
||||||
|
Err(error) => {
|
||||||
|
return bridge_tool_result(
|
||||||
|
redact_agent_runtime_error(&state.root, &error, 480),
|
||||||
|
Vec::new(),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut uncertain = state.cocos_execute_uncertain.lock().await;
|
||||||
|
if *uncertain {
|
||||||
|
return bridge_tool_result(
|
||||||
|
json!({
|
||||||
|
"status": "needs-reconciliation", "retryAllowed": false,
|
||||||
|
"message": "先前 Cocos execute 结果待核对,当前 bridge 不再发送执行命令"
|
||||||
|
})
|
||||||
|
.to_string(),
|
||||||
|
Vec::new(),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let root = state.root.clone();
|
||||||
|
let result = tokio::task::spawn_blocking(move || {
|
||||||
|
// Cocos execute talks to the already-open Creator process through its
|
||||||
|
// validated Inspector/pipe bridge. It does not mutate AGC's project
|
||||||
|
// files or manifest, so it must not wait on `.agent/project.lock`.
|
||||||
|
// File-writing tools keep their own project lock separately.
|
||||||
|
if !crate::builtin_plugins::cocos_editor_agent_tool_available() {
|
||||||
|
return Err(cocos_editor_bridge::BridgeError::InvalidInput(
|
||||||
|
"Cocos Creator 插件已禁用".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
cocos_editor_bridge::execute_cocos_editor_code_for_project(
|
||||||
|
root.to_string_lossy().as_ref(),
|
||||||
|
&code,
|
||||||
|
cocos_editor_bridge::DEFAULT_COMMAND_TIMEOUT_MS,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
match result {
|
||||||
|
Ok(Ok(response)) => {
|
||||||
|
let is_error = !response.ok;
|
||||||
|
let text = json!({
|
||||||
|
"status": if response.ok { "completed" } else { "failed" },
|
||||||
|
"requestId": response.request_id,
|
||||||
|
"result": response.result,
|
||||||
|
"error": response.error,
|
||||||
|
})
|
||||||
|
.to_string();
|
||||||
|
bridge_tool_result(
|
||||||
|
redact_agent_runtime_project_paths(&state.root, &text, 32_000),
|
||||||
|
Vec::new(),
|
||||||
|
is_error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
failed => {
|
||||||
|
let (is_uncertain, error) = match failed {
|
||||||
|
Ok(Err(error)) => (
|
||||||
|
matches!(
|
||||||
|
&error,
|
||||||
|
cocos_editor_bridge::BridgeError::ExecutionUncertain(_)
|
||||||
|
),
|
||||||
|
error.to_string(),
|
||||||
|
),
|
||||||
|
Err(_) => (
|
||||||
|
true,
|
||||||
|
"Cocos execute worker 退出,执行结果需要核对".to_string(),
|
||||||
|
),
|
||||||
|
Ok(Ok(_)) => unreachable!(),
|
||||||
|
};
|
||||||
|
*uncertain = is_uncertain;
|
||||||
|
bridge_tool_result(
|
||||||
|
json!({
|
||||||
|
"status": if is_uncertain { "needs-reconciliation" } else { "failed" },
|
||||||
|
"retryAllowed": !is_uncertain,
|
||||||
|
"message": redact_agent_runtime_error(&state.root, &error, 480),
|
||||||
|
})
|
||||||
|
.to_string(),
|
||||||
|
Vec::new(),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_direct_tool_bridge(
|
async fn handle_direct_tool_bridge(
|
||||||
State(state): State<Arc<DirectToolBridgeState>>,
|
State(state): State<Arc<DirectToolBridgeState>>,
|
||||||
Json(request): Json<DirectToolBridgeRequest>,
|
Json(request): Json<DirectToolBridgeRequest>,
|
||||||
) -> Json<Value> {
|
) -> Json<Value> {
|
||||||
let result = match request.tool.as_str() {
|
let result = match request.tool.as_str() {
|
||||||
|
// 隔离 MCP 只取工具名,不接触真实 AppData 或读取权限。
|
||||||
|
"builtin.plugins.tools" => bridge_tool_result(
|
||||||
|
json!({"tools": crate::builtin_plugins::available_agent_tools()}).to_string(),
|
||||||
|
Vec::new(),
|
||||||
|
false,
|
||||||
|
),
|
||||||
"taonier_prepare_game_art" => bridge_prepare_game_art(&state, &request.arguments).await,
|
"taonier_prepare_game_art" => bridge_prepare_game_art(&state, &request.arguments).await,
|
||||||
"agc_generate_image" => bridge_generate_image(&state, &request.arguments).await,
|
"agc_generate_image" => bridge_generate_image(&state, &request.arguments).await,
|
||||||
"agc_edit_image" => bridge_edit_image(&state, &request.arguments).await,
|
"agc_edit_image" => bridge_edit_image(&state, &request.arguments).await,
|
||||||
@@ -2335,6 +2466,8 @@ async fn handle_direct_tool_bridge(
|
|||||||
bridge_list_registered_assets(&state.root, &request.arguments)
|
bridge_list_registered_assets(&state.root, &request.arguments)
|
||||||
}
|
}
|
||||||
"agc_list_project_files" => bridge_list_project_files(&state.root, &request.arguments),
|
"agc_list_project_files" => bridge_list_project_files(&state.root, &request.arguments),
|
||||||
|
#[cfg(all(windows, feature = "cocos-editor-execute"))]
|
||||||
|
"agc_cocos_execute" => bridge_cocos_execute(&state, &request.arguments).await,
|
||||||
"agc_write_file" => {
|
"agc_write_file" => {
|
||||||
bridge_write_file_in_blocking_pool(state.root.clone(), request.arguments).await
|
bridge_write_file_in_blocking_pool(state.root.clone(), request.arguments).await
|
||||||
}
|
}
|
||||||
@@ -2380,14 +2513,11 @@ pub(crate) async fn start_direct_tool_bridge(
|
|||||||
.route(&route, post(handle_direct_tool_bridge))
|
.route(&route, post(handle_direct_tool_bridge))
|
||||||
.layer(DefaultBodyLimit::max(DIRECT_TOOL_BRIDGE_MAX_REQUEST_BYTES))
|
.layer(DefaultBodyLimit::max(DIRECT_TOOL_BRIDGE_MAX_REQUEST_BYTES))
|
||||||
.with_state(Arc::clone(&state));
|
.with_state(Arc::clone(&state));
|
||||||
|
let url = format!("http://127.0.0.1:{}{route}", address.port());
|
||||||
let task = tokio::spawn(async move {
|
let task = tokio::spawn(async move {
|
||||||
let _ = axum::serve(listener, app).await;
|
let _ = axum::serve(listener, app).await;
|
||||||
});
|
});
|
||||||
Ok(DirectToolBridge {
|
Ok(DirectToolBridge { url, state, task })
|
||||||
url: format!("http://127.0.0.1:{}{route}", address.port()),
|
|
||||||
state,
|
|
||||||
task,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -74,11 +74,36 @@ pub(crate) fn run_direct_tools_mcp_if_requested(args: &[String]) -> Option<i32>
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn direct_tools_mcp_specs() -> Value {
|
async fn direct_tools_mcp_specs() -> Value {
|
||||||
direct_tools_mcp_specs_for(controlled_web_search_enabled())
|
let mut cocos_editor_available = false;
|
||||||
|
if cfg!(all(windows, feature = "cocos-editor-execute")) {
|
||||||
|
// 每次 tools/list 询问绑定的宿主;失败时不广告可选插件工具。
|
||||||
|
if let Ok(result) = tokio::time::timeout(
|
||||||
|
std::time::Duration::from_secs(5),
|
||||||
|
call_client_tool_bridge("builtin.plugins.tools", &json!({})),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
if result["isError"] == false {
|
||||||
|
let availability = result
|
||||||
|
.pointer("/content/0/text")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.and_then(|text| serde_json::from_str::<Value>(text).ok());
|
||||||
|
cocos_editor_available = availability
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|v| v["tools"].as_array())
|
||||||
|
.is_some_and(|tools| {
|
||||||
|
tools
|
||||||
|
.iter()
|
||||||
|
.any(|tool| tool == crate::builtin_plugins::AGC_COCOS_EDITOR_TOOL_NAME)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
direct_tools_mcp_specs_for(controlled_web_search_enabled(), cocos_editor_available)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn direct_tools_mcp_specs_for(controlled_web_search: bool) -> Value {
|
fn direct_tools_mcp_specs_for(controlled_web_search: bool, _cocos_editor_available: bool) -> Value {
|
||||||
let tools = vec![
|
let tools = vec![
|
||||||
json!({
|
json!({
|
||||||
"name": "client.session.info",
|
"name": "client.session.info",
|
||||||
@@ -431,6 +456,19 @@ fn direct_tools_mcp_specs_for(controlled_web_search: bool) -> Value {
|
|||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
let mut tools = tools;
|
let mut tools = tools;
|
||||||
|
#[cfg(all(windows, feature = "cocos-editor-execute"))]
|
||||||
|
if _cocos_editor_available {
|
||||||
|
tools.push(json!({
|
||||||
|
"name": "agc_cocos_execute",
|
||||||
|
"description": "在当前项目已连接的 Cocos Creator 主进程执行 JavaScript 函数体,支持 await 和 return。宿主绑定项目和目标进程,只提交 code。结果待核对或超时后禁止自动重发;使用 Editor.Message 调用 Creator API。",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": { "code": { "type": "string", "minLength": 1, "maxLength": cocos_editor_bridge::MAX_EXECUTE_CODE_BYTES } },
|
||||||
|
"required": ["code"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
if controlled_web_search {
|
if controlled_web_search {
|
||||||
tools.push(json!({
|
tools.push(json!({
|
||||||
"name": "agc_web_search",
|
"name": "agc_web_search",
|
||||||
@@ -503,6 +541,21 @@ fn validate_write_file_arguments(arguments: &Value) -> Result<(), String> {
|
|||||||
normalize_relative_path(&path).map(|_| ())
|
normalize_relative_path(&path).map(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(all(windows, feature = "cocos-editor-execute"))]
|
||||||
|
async fn call_agc_cocos_execute(arguments: &Value) -> Value {
|
||||||
|
let validated = validate_tool_object_fields(arguments, &["code"]).and_then(|()| {
|
||||||
|
let code = arguments
|
||||||
|
.get("code")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.ok_or_else(|| "code 必须是 JavaScript 函数体".to_string())?;
|
||||||
|
cocos_editor_bridge::validate_execute_code(code).map_err(|error| error.to_string())
|
||||||
|
});
|
||||||
|
if let Err(error) = validated {
|
||||||
|
return mcp_tool_result(error, Vec::new(), true);
|
||||||
|
}
|
||||||
|
call_client_tool_bridge("agc_cocos_execute", arguments).await
|
||||||
|
}
|
||||||
|
|
||||||
fn mcp_success(id: Value, result: Value) -> Value {
|
fn mcp_success(id: Value, result: Value) -> Value {
|
||||||
json!({ "jsonrpc": "2.0", "id": id, "result": result })
|
json!({ "jsonrpc": "2.0", "id": id, "result": result })
|
||||||
}
|
}
|
||||||
@@ -1335,7 +1388,12 @@ fn external_mcp_record_response(root: &Path, arguments: &Value) -> Value {
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let _project_lock = match acquire_project_write_lock(root, "conversation.write") {
|
// 与 DirectProject 历史落盘同一档有界等待:`agc_write_file` 持锁期间可能持续数秒,
|
||||||
|
// 零等待取锁会让 Codex 返回记录直接丢失。调用方已把本函数放进阻塞线程池。
|
||||||
|
let _project_lock = match acquire_game_creator_agent_runtime_project_write_lock_with_wait(
|
||||||
|
root,
|
||||||
|
"conversation.write",
|
||||||
|
) {
|
||||||
Ok(lock) => lock,
|
Ok(lock) => lock,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
return mcp_tool_result(format!("项目对话锁不可用:{error}"), Vec::new(), true)
|
return mcp_tool_result(format!("项目对话锁不可用:{error}"), Vec::new(), true)
|
||||||
@@ -1556,7 +1614,7 @@ async fn handle_direct_tools_mcp_request(root: &Path, request: Value) -> Option<
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"tools/list" => Some(mcp_success(id, direct_tools_mcp_specs())),
|
"tools/list" => Some(mcp_success(id, direct_tools_mcp_specs().await)),
|
||||||
"tools/call" => {
|
"tools/call" => {
|
||||||
let tool = request
|
let tool = request
|
||||||
.pointer("/params/name")
|
.pointer("/params/name")
|
||||||
@@ -1569,12 +1627,29 @@ async fn handle_direct_tools_mcp_request(root: &Path, request: Value) -> Option<
|
|||||||
let result = match tool {
|
let result = match tool {
|
||||||
"client.session.info" => external_mcp_session_info(root),
|
"client.session.info" => external_mcp_session_info(root),
|
||||||
"conversation.record_codex_response" => {
|
"conversation.record_codex_response" => {
|
||||||
external_mcp_record_response(root, &arguments)
|
// 取锁等待是同步轮询(最多约 10 秒),必须放到阻塞线程池,
|
||||||
|
// 否则会占住 runtime worker。
|
||||||
|
let journal_root = root.to_path_buf();
|
||||||
|
let journal_arguments = arguments.clone();
|
||||||
|
match tokio::task::spawn_blocking(move || {
|
||||||
|
external_mcp_record_response(&journal_root, &journal_arguments)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(result) => result,
|
||||||
|
Err(error) => mcp_tool_result(
|
||||||
|
format!("Codex 返回记录任务未返回:{error}"),
|
||||||
|
Vec::new(),
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"conversation.list" => external_mcp_conversation_list(root, &arguments),
|
"conversation.list" => external_mcp_conversation_list(root, &arguments),
|
||||||
"conversation.read" => external_mcp_conversation_read(root, &arguments),
|
"conversation.read" => external_mcp_conversation_read(root, &arguments),
|
||||||
"agc_read_skill_resource" => call_agc_read_skill_resource(&arguments),
|
"agc_read_skill_resource" => call_agc_read_skill_resource(&arguments),
|
||||||
"agc_write_file" => call_agc_write_file(&arguments).await,
|
"agc_write_file" => call_agc_write_file(&arguments).await,
|
||||||
|
#[cfg(all(windows, feature = "cocos-editor-execute"))]
|
||||||
|
"agc_cocos_execute" => call_agc_cocos_execute(&arguments).await,
|
||||||
"taonier_prepare_game_art" => call_taonier_prepare_game_art(&arguments).await,
|
"taonier_prepare_game_art" => call_taonier_prepare_game_art(&arguments).await,
|
||||||
"agc_generate_image" => call_agc_generate_image(&arguments).await,
|
"agc_generate_image" => call_agc_generate_image(&arguments).await,
|
||||||
"agc_edit_image" => call_agc_edit_image(&arguments).await,
|
"agc_edit_image" => call_agc_edit_image(&arguments).await,
|
||||||
@@ -1767,6 +1842,119 @@ pub(crate) fn stop_game_creator_external_mcp() -> Result<(), String> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[cfg(all(windows, feature = "cocos-editor-execute"))]
|
||||||
|
#[test]
|
||||||
|
fn builtin_mcp_process_probe() {
|
||||||
|
let Ok(expected) = std::env::var("AGC_MCP_TEST_COCOS_EXPECTED") else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
assert!(crate::game_creator_runtime_config_dir().is_none());
|
||||||
|
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
let specs = runtime.block_on(direct_tools_mcp_specs());
|
||||||
|
assert_eq!(
|
||||||
|
specs["tools"]
|
||||||
|
.as_array()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.any(|tool| tool["name"] == "agc_cocos_execute"),
|
||||||
|
expected == "true"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(windows, feature = "cocos-editor-execute"))]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn builtin_tools_follow_host_switch_in_isolated_mcp_processes() {
|
||||||
|
let _guard = crate::builtin_plugins::test_lock();
|
||||||
|
let config = tempfile::tempdir().unwrap();
|
||||||
|
crate::builtin_plugins::initialize(config.path()).unwrap();
|
||||||
|
let project = crate::tests::canonical_test_tempdir("builtin-mcp-project-");
|
||||||
|
// 本用例只访问可用工具摘要和禁用入口,无需初始化完整游戏项目。
|
||||||
|
std::fs::create_dir_all(project.path().join(".agent")).unwrap();
|
||||||
|
std::fs::write(project.path().join(".agent/manifest.json"), "{}").unwrap();
|
||||||
|
let bridge =
|
||||||
|
super::super::direct_tool_bridge::start_direct_tool_bridge(project.path(), false)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
for enabled in [false, true, false, true] {
|
||||||
|
crate::builtin_plugins::set_enabled(
|
||||||
|
crate::builtin_plugins::AGC_COCOS_EDITOR_PLUGIN_ID,
|
||||||
|
enabled,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
// 同一个 MCP 服务重复 tools/list,同时覆盖原生函数永久缓存的切换。
|
||||||
|
let specs = EXTERNAL_MCP_BRIDGE_URL
|
||||||
|
.scope(bridge.url().to_string(), direct_tools_mcp_specs())
|
||||||
|
.await;
|
||||||
|
assert_eq!(
|
||||||
|
specs["tools"]
|
||||||
|
.as_array()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.any(|tool| tool["name"] == "agc_cocos_execute"),
|
||||||
|
enabled
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
crate::agent_native_tools::native_runtime_function_name(
|
||||||
|
crate::builtin_plugins::AGC_COCOS_EDITOR_TOOL_NAME
|
||||||
|
)
|
||||||
|
.is_some(),
|
||||||
|
enabled
|
||||||
|
);
|
||||||
|
if !enabled {
|
||||||
|
let response = EXTERNAL_MCP_BRIDGE_URL
|
||||||
|
.scope(
|
||||||
|
bridge.url().to_string(),
|
||||||
|
call_agc_cocos_execute(&json!({"code":"return 1;"})),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(response["isError"], true);
|
||||||
|
assert!(response.to_string().contains("插件已禁用"));
|
||||||
|
}
|
||||||
|
// 子进程没有真实 AppData,必须只从绑定宿主获取可用性。
|
||||||
|
let url = bridge.url().to_string();
|
||||||
|
let result = tokio::task::spawn_blocking(move || {
|
||||||
|
std::process::Command::new(std::env::current_exe().unwrap())
|
||||||
|
.args([
|
||||||
|
"--exact",
|
||||||
|
"agent::direct_tools_mcp::tests::builtin_mcp_process_probe",
|
||||||
|
"--nocapture",
|
||||||
|
])
|
||||||
|
.env(DIRECT_TOOL_BRIDGE_URL_ENV, url)
|
||||||
|
.env("AGC_MCP_TEST_COCOS_EXPECTED", enabled.to_string())
|
||||||
|
.output()
|
||||||
|
.unwrap()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
result.status.success(),
|
||||||
|
"{} {}",
|
||||||
|
String::from_utf8_lossy(&result.stdout),
|
||||||
|
String::from_utf8_lossy(&result.stderr)
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
String::from_utf8_lossy(&result.stdout).contains("1 passed"),
|
||||||
|
"child probe must run"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
std::fs::write(config.path().join("extensions/builtin-plugins.json"), "{").unwrap();
|
||||||
|
let specs = EXTERNAL_MCP_BRIDGE_URL
|
||||||
|
.scope(bridge.url().to_string(), direct_tools_mcp_specs())
|
||||||
|
.await;
|
||||||
|
assert!(!specs.to_string().contains("agc_cocos_execute"));
|
||||||
|
drop(bridge);
|
||||||
|
let specs = EXTERNAL_MCP_BRIDGE_URL
|
||||||
|
.scope(
|
||||||
|
"http://127.0.0.1:1/tool-unavailable".to_string(),
|
||||||
|
direct_tools_mcp_specs(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(!specs.to_string().contains("agc_cocos_execute"));
|
||||||
|
}
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -1842,7 +2030,7 @@ mod tests {
|
|||||||
DIRECT_TOOLS_MCP_MAX_REQUEST_BYTES > DIRECT_TOOLS_MCP_MAX_WRITE_CONTENT_BYTES + 1024,
|
DIRECT_TOOLS_MCP_MAX_REQUEST_BYTES > DIRECT_TOOLS_MCP_MAX_WRITE_CONTENT_BYTES + 1024,
|
||||||
"MCP request envelope must fit the advertised file-write payload"
|
"MCP request envelope must fit the advertised file-write payload"
|
||||||
);
|
);
|
||||||
let specs = direct_tools_mcp_specs_for(false);
|
let specs = direct_tools_mcp_specs_for(false, true);
|
||||||
let names = specs["tools"]
|
let names = specs["tools"]
|
||||||
.as_array()
|
.as_array()
|
||||||
.expect("tool array")
|
.expect("tool array")
|
||||||
@@ -1869,6 +2057,11 @@ mod tests {
|
|||||||
"agc_remove_background",
|
"agc_remove_background",
|
||||||
"agc_browser_playtest",
|
"agc_browser_playtest",
|
||||||
]
|
]
|
||||||
|
.into_iter()
|
||||||
|
.chain(
|
||||||
|
cfg!(all(windows, feature = "cocos-editor-execute")).then_some("agc_cocos_execute")
|
||||||
|
)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
);
|
);
|
||||||
let serialized = specs.to_string();
|
let serialized = specs.to_string();
|
||||||
assert!(!serialized.contains("agc_web_search"));
|
assert!(!serialized.contains("agc_web_search"));
|
||||||
@@ -1975,7 +2168,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tool_catalog_adds_controlled_web_search_only_when_enabled() {
|
fn tool_catalog_adds_controlled_web_search_only_when_enabled() {
|
||||||
let specs = direct_tools_mcp_specs_for(true);
|
let specs = direct_tools_mcp_specs_for(true, true);
|
||||||
let search = specs["tools"]
|
let search = specs["tools"]
|
||||||
.as_array()
|
.as_array()
|
||||||
.expect("tool array")
|
.expect("tool array")
|
||||||
|
|||||||
@@ -1606,6 +1606,17 @@ pub(crate) fn agent_runtime_tool_action_input_summary(
|
|||||||
.unwrap_or(160)
|
.unwrap_or(160)
|
||||||
),
|
),
|
||||||
"command.run_limited" => format!("commandId={}", text(&["commandId", "command_id", "id"])),
|
"command.run_limited" => format!("commandId={}", text(&["commandId", "command_id", "id"])),
|
||||||
|
"cocos.editor.execute" => format!(
|
||||||
|
"codeChars={} · codeSha256={:x}",
|
||||||
|
chars(&["code"]),
|
||||||
|
Sha256::digest(
|
||||||
|
input
|
||||||
|
.get("code")
|
||||||
|
.and_then(serde_json::Value::as_str)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_bytes()
|
||||||
|
)
|
||||||
|
),
|
||||||
"preview.validate" => {
|
"preview.validate" => {
|
||||||
let viewports = input
|
let viewports = input
|
||||||
.get("viewports")
|
.get("viewports")
|
||||||
|
|||||||
@@ -362,6 +362,16 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_
|
|||||||
observe_agent_runtime_limited_command(root, agent_id, run_id, &action.input)
|
observe_agent_runtime_limited_command(root, agent_id, run_id, &action.input)
|
||||||
}
|
}
|
||||||
"preview.start" => observe_agent_runtime_preview_start(root, agent_id, run_id),
|
"preview.start" => observe_agent_runtime_preview_start(root, agent_id, run_id),
|
||||||
|
"cocos.editor.execute" => observe_agent_runtime_project_snapshot_with_lock(
|
||||||
|
root,
|
||||||
|
agent_id,
|
||||||
|
run_id,
|
||||||
|
action,
|
||||||
|
&action_fingerprint,
|
||||||
|
pending_action,
|
||||||
|
true,
|
||||||
|
|| observe_agent_runtime_cocos_editor_execute(root, action, pending_action),
|
||||||
|
),
|
||||||
"preview.validate" => {
|
"preview.validate" => {
|
||||||
observe_agent_runtime_preview_validate(
|
observe_agent_runtime_preview_validate(
|
||||||
root,
|
root,
|
||||||
|
|||||||
@@ -95,6 +95,8 @@ pub(in crate::agent) fn game_creator_agent_runtime_tool_command_id(
|
|||||||
"command.stdin" => Some("command.stdin"),
|
"command.stdin" => Some("command.stdin"),
|
||||||
"command.terminate" => Some("command.terminate"),
|
"command.terminate" => Some("command.terminate"),
|
||||||
"command.run_limited" => Some("command.run_limited"),
|
"command.run_limited" => Some("command.run_limited"),
|
||||||
|
#[cfg(feature = "cocos-editor-execute")]
|
||||||
|
"cocos.editor.execute" => Some("cocos.editor.execute"),
|
||||||
"preview.start" => Some("preview.start"),
|
"preview.start" => Some("preview.start"),
|
||||||
"preview.validate" => Some("preview.validate"),
|
"preview.validate" => Some("preview.validate"),
|
||||||
"image.inspect" => Some("image.inspect"),
|
"image.inspect" => Some("image.inspect"),
|
||||||
|
|||||||
+8
-2
@@ -17,7 +17,7 @@ mod canvas_asset_kind_contract_tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn agent_runtime_executable_tools() -> Vec<&'static str> {
|
pub(crate) fn agent_runtime_executable_tools() -> Vec<&'static str> {
|
||||||
vec![
|
let mut tools = vec![
|
||||||
GAME_CREATOR_USER_INPUT_REQUEST_TOOL,
|
GAME_CREATOR_USER_INPUT_REQUEST_TOOL,
|
||||||
"memory.read",
|
"memory.read",
|
||||||
"memory.write",
|
"memory.write",
|
||||||
@@ -63,7 +63,13 @@ pub(crate) fn agent_runtime_executable_tools() -> Vec<&'static str> {
|
|||||||
"agent.schedule_ready",
|
"agent.schedule_ready",
|
||||||
"agent.action_history",
|
"agent.action_history",
|
||||||
"agent.run_status",
|
"agent.run_status",
|
||||||
]
|
];
|
||||||
|
// 内置插件被用户禁用后,对应 Runtime 工具不再进入工具目录、Agent 上下文
|
||||||
|
// 和工具策略快照。
|
||||||
|
if crate::builtin_plugins::cocos_editor_agent_tool_available() {
|
||||||
|
tools.push(crate::builtin_plugins::AGC_COCOS_EDITOR_TOOL_NAME);
|
||||||
|
}
|
||||||
|
tools
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn agent_runtime_native_executable_tools() -> Vec<&'static str> {
|
pub(crate) fn agent_runtime_native_executable_tools() -> Vec<&'static str> {
|
||||||
|
|||||||
+1
-1
@@ -1908,7 +1908,7 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.expect("first persist");
|
.expect("first persist");
|
||||||
rewind_session_keep_gdd_file(&root, &session);
|
rewind_session_keep_gdd_file(&root, &session);
|
||||||
let hydrated = hydrate_planning_session_v2(
|
let hydrated = hydrate_planning_session_v2_sync(
|
||||||
root.to_string_lossy().to_string(),
|
root.to_string_lossy().to_string(),
|
||||||
Some(session.session_id.clone()),
|
Some(session.session_id.clone()),
|
||||||
)
|
)
|
||||||
|
|||||||
+12
-1
@@ -1465,7 +1465,18 @@ async fn run_planning_session_v2_command(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub(crate) fn hydrate_planning_session_v2(
|
pub(crate) async fn hydrate_planning_session_v2(
|
||||||
|
project_path: String,
|
||||||
|
session_id: Option<String>,
|
||||||
|
) -> Result<Option<PlanningSessionCommandResultV2>, String> {
|
||||||
|
tauri::async_runtime::spawn_blocking(move || {
|
||||||
|
hydrate_planning_session_v2_sync(project_path, session_id)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|error| format!("恢复 Planning V2 后台任务失败:{error}"))?
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn hydrate_planning_session_v2_sync(
|
||||||
project_path: String,
|
project_path: String,
|
||||||
session_id: Option<String>,
|
session_id: Option<String>,
|
||||||
) -> Result<Option<PlanningSessionCommandResultV2>, String> {
|
) -> Result<Option<PlanningSessionCommandResultV2>, String> {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user