Compare commits

..

5 Commits

Author SHA1 Message Date
k88936 bd94db73c9 修复资源卡片 Lucide 图标描边宽度
Project CI / Repository checks (pull_request) Failing after 13s
Project CI / Backend tests (pull_request) Failing after 9s
Project CI / Frontend tests (pull_request) Successful in 3m14s
Project CI / Native shell tests (pull_request) Successful in 18m2s
恢复 CanvasWorld 超采样下资源卡片 SVG 图标的 inverse-scale 描边补偿

新增资源工作台 CSS 回归断言,防止局部图标规则被误删

同步 CanvasWorld 超采样常量与图标描边约定文档
2026-09-12 15:04:08 +08:00
k88936 e50d678411 Merge pull request '修复[dev]下agc启动时长时间白屏' (#333) from boot-time-diagnosis into master
Project CI / Repository checks (push) Successful in 2m47s
Project CI / Frontend tests (push) Successful in 3m19s
Project CI / Backend tests (push) Successful in 7m46s
Project CI / Native shell tests (push) Successful in 18m17s
Project CI / Repository checks (pull_request) Failing after 10s
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Reviewed-on: #333
2026-09-12 11:19:25 +08:00
k88936 727794bc95 Merge branch 'master' into boot-time-diagnosis
Project CI / Repository checks (pull_request) Successful in 2m43s
Project CI / Frontend tests (pull_request) Successful in 3m12s
Project CI / Backend tests (pull_request) Successful in 7m13s
Project CI / Native shell tests (pull_request) Successful in 20m20s
2026-09-12 10:58:33 +08:00
k88936 cc9b95ddef 优化调试配置:提升SHA-256计算性能,避免调试构建阻塞
Project CI / Repository checks (pull_request) Failing after 14s
Project CI / Backend tests (pull_request) Failing after 14s
Project CI / Frontend tests (pull_request) Successful in 3m5s
Project CI / Native shell tests (pull_request) Successful in 20m20s
2026-09-12 10:54:51 +08:00
kdletters fafe6b63cd 修复AGC模型请求与对话登录态自动续期 (#329)
Project CI / Repository checks (push) Successful in 2m40s
Project CI / Frontend tests (push) Successful in 3m23s
Project CI / Backend tests (push) Successful in 5m46s
Project CI / Native shell tests (push) Successful in 18m30s
## 变更说明

- AGC 客户端配套后端 API 在 401 时自动刷新登录态并重试一次,覆盖模型目录请求。
- DirectProject 对话鉴权失效时刷新平台会话、同步 Rust/Runner 凭据并重试同一 clientTurnId。
- 403 权限拒绝不触发续期;账号切换后不重发旧请求。
- 补充并发续期、失败保留原错误、权限边界和 Direct 对话重试测试。

## 验证

- npm run test -- apps/ai-game-creator-shell/tests/appSurface.test.ts apps/ai-game-creator-shell/tests/clientApi.test.ts apps/ai-game-creator-shell/tests/clientHttp.test.ts apps/ai-game-creator-shell/tests/conversationModelSelect.test.tsx
- npm run agc:typecheck
- npm run check:encoding
- git diff --check

Reviewed-on: #329
Co-authored-by: kdletters <kdletters@qq.com>
Co-committed-by: kdletters <kdletters@qq.com>
2026-09-11 18:09:48 +08:00
106 changed files with 566 additions and 10356 deletions
-2
View File
@@ -40,8 +40,6 @@ 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-resources/
/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/.llm-drafts/
/apps/ai-game-creator-shell/game-creator.config.local.json
-4
View File
@@ -1,4 +0,0 @@
# resources/plugins 由 build.rs 从 plugins/ 复制生成,属于构建产物。
# 它在 dev 监听范围内,重新生成会让 Tauri dev 误判为源码改动而触发
# “构建 -> 监听 -> 再构建”的自触发循环。
resources/plugins/
@@ -4,11 +4,6 @@ import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
defaultEditorFeatures,
withDefaultCargoFeatures,
} from './cargo-features.mjs';
const appRoot = fileURLToPath(new URL('..', import.meta.url));
const defaultReleaseTarget = 'x86_64-pc-windows-msvc';
const releaseTarget =
@@ -164,30 +159,10 @@ export async function prepareReleaseVersion() {
return nextVersion;
}
export function buildTauriBuildArguments(
args = [],
target = releaseTarget,
platform = process.platform,
) {
const noBundle = args.includes('--no-bundle');
const targetIndex = args.indexOf('--target');
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 noBundle = args.includes('--no-bundle');
const hasTarget = args.includes('--target');
const targetArgs = noBundle || hasTarget ? [] : ['--target', releaseTarget];
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
const result = spawnSync(
npmCommand,
@@ -197,7 +172,9 @@ export function runTauriBuild(args = []) {
'exec',
'tauri',
'--',
...buildTauriBuildArguments(args),
'build',
...targetArgs,
...args,
],
{ cwd: appRoot, stdio: 'inherit', shell: process.platform === 'win32' },
);
@@ -1,24 +0,0 @@
/** 默认桌面能力;显式 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']
: [];
}
@@ -1,49 +0,0 @@
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,16 +117,6 @@ const allowedUncalledTauriCommands = [
'stop_local_game_preview_if_matches',
'start_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([
'.json',
@@ -1295,7 +1285,7 @@ if (tauriConfig.identifier !== 'world.genarrative.ai-game-creator') {
throw new Error('AI game creator shell identifier drifted');
}
const expectedBundledWindowsResources = {
const expectedBundledCodexResources = {
'resources/codex/win-x64/bin/codex.exe': 'codex/win-x64/bin/codex.exe',
'resources/codex/win-x64/bin/codex-code-mode-host.exe':
'codex/win-x64/bin/codex-code-mode-host.exe',
@@ -1309,7 +1299,6 @@ const expectedBundledWindowsResources = {
'codex/win-x64/codex-package.json',
'resources/codex/win-x64/NOTICE.md': 'codex/win-x64/NOTICE.md',
'resources/codex/win-x64/manifest.json': 'codex/win-x64/manifest.json',
'resources/plugins': 'plugins',
};
if (tauriConfig.bundle?.resources !== undefined) {
throw new Error(
@@ -1318,8 +1307,8 @@ if (tauriConfig.bundle?.resources !== undefined) {
}
assert.deepEqual(
windowsTauriConfig.bundle?.resources,
expectedBundledWindowsResources,
'AI game creator shell Windows Tauri config must bundle the complete pinned Codex resource set and Cocos bridge payload directory',
expectedBundledCodexResources,
'AI game creator shell Windows Tauri config must bundle the complete pinned Codex resource set',
);
if (windowsTauriConfig.bundle?.useLocalToolsDir !== true) {
throw new Error(
@@ -436,10 +436,6 @@ function spawnChild(command, args, options, spawnImpl = spawn) {
const child = spawnImpl(command, args, {
...options,
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、
// Node、Cargo 及其子进程一起清理,避免残留订阅任务持续刷日志。
detached: isPosix,
@@ -1,7 +1,6 @@
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { withDefaultCargoFeatures } from './cargo-features.mjs';
import {
readAgcDevEndpoint,
resolveAgcDevEndpoint,
@@ -44,24 +43,6 @@ 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 } = {}) {
return spawnChild(process.execPath, [tauriCliPath, ...argv], {
cwd: appRoot,
@@ -125,10 +106,7 @@ async function runTauriDev(
shutdownRequested.then(() => false),
]);
if (!prepared || shutdownSignal) return 1;
const tauriArguments = buildTauriArguments(
withDevCargoFeatures(argv),
endpoint.url,
);
const tauriArguments = buildTauriArguments(argv, endpoint.url);
child = spawnCli(tauriArguments, {
env: withAgcDevEndpointEnv(endpoint),
});
@@ -215,7 +193,6 @@ export {
isDirectModuleExecution,
runTauriDev,
spawnTauriCli,
withDevCargoFeatures,
};
if (isDirectModuleExecution()) {
@@ -1,4 +0,0 @@
# resources/plugins 由 build.rs 从 plugins/ 复制生成,属于构建产物。
# 它在 dev 监听范围内,重新生成会让 Tauri dev 误判为源码改动而触发
# “构建 -> 监听 -> 再构建”的自触发循环。
resources/plugins/
-25
View File
@@ -733,20 +733,6 @@ dependencies = [
"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]]
name = "combine"
version = "4.6.7"
@@ -1219,14 +1205,6 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "editor-adapter-api"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "either"
version = "1.16.0"
@@ -1731,8 +1709,6 @@ dependencies = [
"axum",
"base64 0.22.1",
"chromiumoxide",
"cocos-editor-bridge",
"editor-adapter-api",
"futures",
"getrandom 0.3.4",
"http",
@@ -4305,7 +4281,6 @@ dependencies = [
"cookie",
"cookie_store",
"encoding_rs",
"futures-channel",
"futures-core",
"futures-util",
"h2",
@@ -6,9 +6,6 @@ publish = false
[features]
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]
serde = { version = "1", features = ["derive"] }
@@ -22,8 +19,6 @@ ts-rs = "12.0.1"
typed_floats = { version = "1.0.7", features = ["serde"] }
nalgebra = { version = "0.35.0", features = ["serde-serialize"] }
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"
axum = "0.8"
chromiumoxide = "0.9.1"
@@ -79,6 +74,14 @@ codegen-units = 256
lto = "off"
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]
opt-level = 0
debug = 1
@@ -182,8 +182,6 @@ fn main() {
);
let manifest_path = manifest_dir.join("prompts/runtime/manifest.json");
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)
.unwrap_or_else(|error| panic!("Prompt Bundle 编译失败:{error}"));
validate_seed_task_catalog(&compiled);
@@ -205,146 +203,3 @@ fn main() {
}
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) {}
@@ -305,35 +305,6 @@ fn game_creator_codex_app_server_error_detail_indicates_auth_failure(
|| 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(
error: &serde_json::Value,
) -> bool {
@@ -391,15 +362,6 @@ fn game_creator_codex_app_server_failed_turn_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");
}
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) {
return game_creator_codex_app_server_error_kind("unauthorized");
}
@@ -1108,7 +1070,6 @@ fn game_creator_codex_app_server_pool_key(
"skillPackIdentity": skill_pack_identity,
"clientSkillIdentity": client_skill_identity,
"clientMcpIdentity": client_mcp_identity,
"builtinPluginTools": crate::builtin_plugins::available_agent_tools(),
"controlledWebSearch": llm.web_search_enabled,
"directToolBridgeProtocol": if workspace_mode == CodexAppServerWorkspaceMode::DirectProject { DIRECT_TOOL_BRIDGE_PROTOCOL } else { "disabled" },
"providerProxyProtocol": if workspace_mode == CodexAppServerWorkspaceMode::DirectProject { CODEX_PROVIDER_PROXY_PROTOCOL } else { "disabled" },
@@ -2502,12 +2463,10 @@ impl CodexAppServerConnection {
// Codex's default provider in Debug builds.
self.inner._provider_proxy.is_some() || !llm.api_key.trim().is_empty(),
);
let result = match self.request("thread/start", params).await {
Ok(result) => result,
Err(error) => {
return Err(platform_llm::LlmError::Transport(error));
}
};
let result = self
.request("thread/start", params)
.await
.map_err(platform_llm::LlmError::Transport)?;
let thread_id = result
.pointer("/thread/id")
.and_then(serde_json::Value::as_str)
@@ -4850,37 +4809,6 @@ 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]
fn codex_app_server_failed_turn_maps_insufficient_mud_points_to_stable_upstream_error() {
for detail in [
@@ -5122,42 +5050,6 @@ while IFS= read -r line; do :; done
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]
fn direct_file_change_approval_is_limited_to_workspace() {
let temp = tempfile::tempdir().expect("temp dir");
@@ -1,4 +1,3 @@
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::project::{
append_jsonl_line_unlocked, enforce_project_permission_policy, project_append_lock_for,
@@ -205,15 +204,7 @@ fn append_direct_project_history_item_at_with_user_policy(
if !allow_user_item && is_direct_project_codex_user_item(item) {
return Ok(());
}
// 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 _project_lock = crate::project::acquire_project_write_lock(root, "conversation.write")?;
let path = history_path(root);
let history_exists =
prepare_game_creator_private_path_for_read(&path, false, "DirectProject 历史")?;
@@ -438,50 +429,6 @@ mod tests {
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]
fn codex_user_echo_is_filtered_but_agc_user_message_is_persisted() {
let root = tempfile::tempdir().expect("temp project");
File diff suppressed because one or more lines are too long
@@ -54,8 +54,6 @@ struct DirectToolBridgeState {
regeneration_gate: tokio::sync::Mutex<()>,
resource_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)]
@@ -689,8 +687,6 @@ fn direct_tool_bridge_state_with_search(
regeneration_gate: tokio::sync::Mutex::new(()),
resource_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),
})
}
@@ -1481,28 +1477,12 @@ fn bridge_write_file(root: &Path, arguments: &Value) -> Value {
// 而 `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(
root,
"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 write_ms = write_started.elapsed().as_millis();
let revision_started = std::time::Instant::now();
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!({
"status": "completed",
"path": written.path,
@@ -2343,122 +2323,11 @@ 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(
State(state): State<Arc<DirectToolBridgeState>>,
Json(request): Json<DirectToolBridgeRequest>,
) -> Json<Value> {
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,
"agc_generate_image" => bridge_generate_image(&state, &request.arguments).await,
"agc_edit_image" => bridge_edit_image(&state, &request.arguments).await,
@@ -2466,8 +2335,6 @@ async fn handle_direct_tool_bridge(
bridge_list_registered_assets(&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" => {
bridge_write_file_in_blocking_pool(state.root.clone(), request.arguments).await
}
@@ -2513,11 +2380,14 @@ pub(crate) async fn start_direct_tool_bridge(
.route(&route, post(handle_direct_tool_bridge))
.layer(DefaultBodyLimit::max(DIRECT_TOOL_BRIDGE_MAX_REQUEST_BYTES))
.with_state(Arc::clone(&state));
let url = format!("http://127.0.0.1:{}{route}", address.port());
let task = tokio::spawn(async move {
let _ = axum::serve(listener, app).await;
});
Ok(DirectToolBridge { url, state, task })
Ok(DirectToolBridge {
url: format!("http://127.0.0.1:{}{route}", address.port()),
state,
task,
})
}
#[cfg(test)]
@@ -74,36 +74,11 @@ pub(crate) fn run_direct_tools_mcp_if_requested(args: &[String]) -> Option<i32>
})
}
async fn direct_tools_mcp_specs() -> Value {
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() -> Value {
direct_tools_mcp_specs_for(controlled_web_search_enabled())
}
fn direct_tools_mcp_specs_for(controlled_web_search: bool, _cocos_editor_available: bool) -> Value {
fn direct_tools_mcp_specs_for(controlled_web_search: bool) -> Value {
let tools = vec![
json!({
"name": "client.session.info",
@@ -456,19 +431,6 @@ fn direct_tools_mcp_specs_for(controlled_web_search: bool, _cocos_editor_availab
}),
];
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 {
tools.push(json!({
"name": "agc_web_search",
@@ -541,21 +503,6 @@ fn validate_write_file_arguments(arguments: &Value) -> Result<(), String> {
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 {
json!({ "jsonrpc": "2.0", "id": id, "result": result })
}
@@ -1388,12 +1335,7 @@ fn external_mcp_record_response(root: &Path, arguments: &Value) -> Value {
true,
);
}
// 与 DirectProject 历史落盘同一档有界等待:`agc_write_file` 持锁期间可能持续数秒,
// 零等待取锁会让 Codex 返回记录直接丢失。调用方已把本函数放进阻塞线程池。
let _project_lock = match acquire_game_creator_agent_runtime_project_write_lock_with_wait(
root,
"conversation.write",
) {
let _project_lock = match acquire_project_write_lock(root, "conversation.write") {
Ok(lock) => lock,
Err(error) => {
return mcp_tool_result(format!("项目对话锁不可用:{error}"), Vec::new(), true)
@@ -1614,7 +1556,7 @@ async fn handle_direct_tools_mcp_request(root: &Path, request: Value) -> Option<
))
}
}
"tools/list" => Some(mcp_success(id, direct_tools_mcp_specs().await)),
"tools/list" => Some(mcp_success(id, direct_tools_mcp_specs())),
"tools/call" => {
let tool = request
.pointer("/params/name")
@@ -1627,29 +1569,12 @@ async fn handle_direct_tools_mcp_request(root: &Path, request: Value) -> Option<
let result = match tool {
"client.session.info" => external_mcp_session_info(root),
"conversation.record_codex_response" => {
// 取锁等待是同步轮询(最多约 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,
),
}
external_mcp_record_response(root, &arguments)
}
"conversation.list" => external_mcp_conversation_list(root, &arguments),
"conversation.read" => external_mcp_conversation_read(root, &arguments),
"agc_read_skill_resource" => call_agc_read_skill_resource(&arguments),
"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,
"agc_generate_image" => call_agc_generate_image(&arguments).await,
"agc_edit_image" => call_agc_edit_image(&arguments).await,
@@ -1842,119 +1767,6 @@ pub(crate) fn stop_game_creator_external_mcp() -> Result<(), String> {
#[cfg(test)]
mod tests {
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};
#[tokio::test]
@@ -2030,7 +1842,7 @@ mod tests {
DIRECT_TOOLS_MCP_MAX_REQUEST_BYTES > DIRECT_TOOLS_MCP_MAX_WRITE_CONTENT_BYTES + 1024,
"MCP request envelope must fit the advertised file-write payload"
);
let specs = direct_tools_mcp_specs_for(false, true);
let specs = direct_tools_mcp_specs_for(false);
let names = specs["tools"]
.as_array()
.expect("tool array")
@@ -2057,11 +1869,6 @@ mod tests {
"agc_remove_background",
"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();
assert!(!serialized.contains("agc_web_search"));
@@ -2168,7 +1975,7 @@ mod tests {
#[test]
fn tool_catalog_adds_controlled_web_search_only_when_enabled() {
let specs = direct_tools_mcp_specs_for(true, true);
let specs = direct_tools_mcp_specs_for(true);
let search = specs["tools"]
.as_array()
.expect("tool array")
@@ -1606,17 +1606,6 @@ pub(crate) fn agent_runtime_tool_action_input_summary(
.unwrap_or(160)
),
"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" => {
let viewports = input
.get("viewports")
@@ -362,16 +362,6 @@ 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)
}
"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" => {
observe_agent_runtime_preview_validate(
root,
@@ -95,8 +95,6 @@ pub(in crate::agent) fn game_creator_agent_runtime_tool_command_id(
"command.stdin" => Some("command.stdin"),
"command.terminate" => Some("command.terminate"),
"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.validate" => Some("preview.validate"),
"image.inspect" => Some("image.inspect"),
@@ -17,7 +17,7 @@ mod canvas_asset_kind_contract_tests {
}
pub(crate) fn agent_runtime_executable_tools() -> Vec<&'static str> {
let mut tools = vec![
vec![
GAME_CREATOR_USER_INPUT_REQUEST_TOOL,
"memory.read",
"memory.write",
@@ -63,13 +63,7 @@ pub(crate) fn agent_runtime_executable_tools() -> Vec<&'static str> {
"agent.schedule_ready",
"agent.action_history",
"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> {
@@ -1908,7 +1908,7 @@ mod tests {
)
.expect("first persist");
rewind_session_keep_gdd_file(&root, &session);
let hydrated = hydrate_planning_session_v2_sync(
let hydrated = hydrate_planning_session_v2(
root.to_string_lossy().to_string(),
Some(session.session_id.clone()),
)
@@ -1465,18 +1465,7 @@ async fn run_planning_session_v2_command(
}
#[tauri::command]
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(
pub(crate) fn hydrate_planning_session_v2(
project_path: String,
session_id: Option<String>,
) -> Result<Option<PlanningSessionCommandResultV2>, String> {

Some files were not shown because too many files have changed in this diff Show More