Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3fc3399032 |
@@ -221,8 +221,7 @@ jobs:
|
|||||||
- name: Run AI game creator shell agent-run smoke
|
- name: Run AI game creator shell agent-run smoke
|
||||||
run: npm run check:native-shells:agc-rust-smoke
|
run: npm run check:native-shells:agc-rust-smoke
|
||||||
|
|
||||||
# AGC 壳依赖的共享 / 平台 crate 测试用的是 server-rs workspace 与两个无锁独立 crate
|
# AGC 壳依赖的共享 / 平台和编辑器插件 crate 各自预热独立 manifest,再运行对应测试。
|
||||||
# 的 manifest,属另一套依赖图,因此单独一个 job 预热、单独跑。
|
|
||||||
ai-game-creator-shell-rust-crates:
|
ai-game-creator-shell-rust-crates:
|
||||||
name: AI game creator shell Rust crates
|
name: AI game creator shell Rust crates
|
||||||
runs-on: genarrative-ci
|
runs-on: genarrative-ci
|
||||||
@@ -263,13 +262,15 @@ jobs:
|
|||||||
# `cargo test --manifest-path` 单独跑这两个 crate。不在这里预热的话,这两条测试
|
# `cargo test --manifest-path` 单独跑这两个 crate。不在这里预热的话,这两条测试
|
||||||
# 会在测试阶段自己 `Updating crates.io index`,crates.io 一抖动整条 job 就红
|
# 会在测试阶段自己 `Updating crates.io index`,crates.io 一抖动整条 job 就红
|
||||||
# (见 #327 / PR #316 run 1950)。
|
# (见 #327 / PR #316 run 1950)。
|
||||||
# 两个 crate 都没有提交 Cargo.lock,所以这里只能做不带锁标志的 fetch:
|
# Cocos 插件也使用独立且未提交的锁文件,一并预热。
|
||||||
|
# 这些 crate 都没有提交 Cargo.lock,所以这里只能做不带锁标志的 fetch:
|
||||||
# 加锁标志会因为缺少锁文件直接失败。生成的 Cargo.lock 落在两个 crate 目录内,
|
# 加锁标志会因为缺少锁文件直接失败。生成的 Cargo.lock 落在两个 crate 目录内,
|
||||||
# 已被各自的 .gitignore 忽略,只留在容器里;随后的测试阶段因此能用锁定版本
|
# 已被各自的 .gitignore 忽略,只留在容器里;随后的测试阶段因此能用锁定版本
|
||||||
# 解析,不再触碰 registry index。
|
# 解析,不再触碰 registry index。
|
||||||
for manifest_path in \
|
for manifest_path in \
|
||||||
server-rs/crates/agent-runtime-core/Cargo.toml \
|
server-rs/crates/agent-runtime-core/Cargo.toml \
|
||||||
server-rs/crates/agent-runtime-orchestration/Cargo.toml; do
|
server-rs/crates/agent-runtime-orchestration/Cargo.toml \
|
||||||
|
plugins/agc-cocos-editor/native/cocos-editor-bridge/Cargo.toml; do
|
||||||
for attempt in $(seq 1 5); do
|
for attempt in $(seq 1 5); do
|
||||||
if cargo fetch \
|
if cargo fetch \
|
||||||
--target x86_64-unknown-linux-gnu \
|
--target x86_64-unknown-linux-gnu \
|
||||||
@@ -284,6 +285,23 @@ jobs:
|
|||||||
done
|
done
|
||||||
done
|
done
|
||||||
|
|
||||||
|
- name: Prepare Unity plugin Rust dependencies
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
for attempt in $(seq 1 5); do
|
||||||
|
if cargo fetch --locked \
|
||||||
|
--target x86_64-unknown-linux-gnu \
|
||||||
|
--manifest-path plugins/agc-unity-editor/native/unity-editor-bridge/Cargo.toml; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if [[ "${attempt}" -eq 5 ]]; then
|
||||||
|
echo 'Unity plugin Cargo dependency fetch failed after 5 attempts.' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep $((attempt * 2))
|
||||||
|
done
|
||||||
|
|
||||||
- name: Run AI game creator shell shared crate gates
|
- name: Run AI game creator shell shared crate gates
|
||||||
run: npm run check:native-shells:agc-rust-crates
|
run: npm run check:native-shells:agc-rust-crates
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ temp*build*/
|
|||||||
/apps/ai-game-creator-shell/src-tauri/resources/codex/mac-native/manifest.json
|
/apps/ai-game-creator-shell/src-tauri/resources/codex/mac-native/manifest.json
|
||||||
/apps/ai-game-creator-shell/src-tauri/resources/codex/mac-native/NOTICE.md
|
/apps/ai-game-creator-shell/src-tauri/resources/codex/mac-native/NOTICE.md
|
||||||
/plugins/agc-cocos-editor/native/payload/
|
/plugins/agc-cocos-editor/native/payload/
|
||||||
|
/plugins/agc-unity-editor/dotnet/**/bin/
|
||||||
|
/plugins/agc-unity-editor/dotnet/**/obj/
|
||||||
|
/plugins/agc-unity-editor/dotnet/publish/
|
||||||
|
/plugins/agc-unity-editor/dotnet/native-build/
|
||||||
/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
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ export const agcReleasePathPatterns = [
|
|||||||
'packages/',
|
'packages/',
|
||||||
'server-rs/crates/',
|
'server-rs/crates/',
|
||||||
'plugins/agc-cocos-editor/',
|
'plugins/agc-cocos-editor/',
|
||||||
|
'plugins/agc-unity-editor/',
|
||||||
'apps/desktop-shell/src-tauri/icons/',
|
'apps/desktop-shell/src-tauri/icons/',
|
||||||
'package.json',
|
'package.json',
|
||||||
'package-lock.json',
|
'package-lock.json',
|
||||||
|
|||||||
@@ -344,7 +344,11 @@ test('Windows remains the default and explicit Windows overrides macOS environme
|
|||||||
context,
|
context,
|
||||||
{
|
{
|
||||||
spawn: (_binary, command) => {
|
spawn: (_binary, command) => {
|
||||||
assert.ok(command.includes('--features=cocos-editor-execute'));
|
assert.ok(
|
||||||
|
command.includes(
|
||||||
|
'--features=cocos-editor-execute,unity-editor-execute',
|
||||||
|
),
|
||||||
|
);
|
||||||
assert.ok(command.includes('user-config.json'));
|
assert.ok(command.includes('user-config.json'));
|
||||||
const configIndex = command.lastIndexOf('--config');
|
const configIndex = command.lastIndexOf('--config');
|
||||||
const config = JSON.parse(
|
const config = JSON.parse(
|
||||||
|
|||||||
@@ -19,6 +19,6 @@ export function withDefaultCargoFeatures(argv, features) {
|
|||||||
|
|
||||||
export function defaultEditorFeatures(target) {
|
export function defaultEditorFeatures(target) {
|
||||||
return target === 'win32' || target.includes('windows')
|
return target === 'win32' || target.includes('windows')
|
||||||
? ['cocos-editor-execute']
|
? ['cocos-editor-execute', 'unity-editor-execute']
|
||||||
: [];
|
: [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ test('Windows release includes the same editor feature as development', () => {
|
|||||||
buildTauriBuildArguments([], 'x86_64-pc-windows-msvc', 'win32'),
|
buildTauriBuildArguments([], 'x86_64-pc-windows-msvc', 'win32'),
|
||||||
[
|
[
|
||||||
'build',
|
'build',
|
||||||
'--features=cocos-editor-execute',
|
'--features=cocos-editor-execute,unity-editor-execute',
|
||||||
'--target',
|
'--target',
|
||||||
'x86_64-pc-windows-msvc',
|
'x86_64-pc-windows-msvc',
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -143,7 +143,6 @@ const allowedUncalledTauriCommands = [
|
|||||||
'reload_agc_plugin',
|
'reload_agc_plugin',
|
||||||
'call_agc_plugin',
|
'call_agc_plugin',
|
||||||
'read_agc_plugin_panel',
|
'read_agc_plugin_panel',
|
||||||
'set_agc_plugin_project_path',
|
|
||||||
'set_agc_plugin_enabled',
|
'set_agc_plugin_enabled',
|
||||||
];
|
];
|
||||||
const sourceExtensions = new Set([
|
const sourceExtensions = new Set([
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
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 {
|
||||||
|
defaultEditorFeatures,
|
||||||
|
withDefaultCargoFeatures,
|
||||||
|
} from './cargo-features.mjs';
|
||||||
import {
|
import {
|
||||||
readAgcDevEndpoint,
|
readAgcDevEndpoint,
|
||||||
resolveAgcDevEndpoint,
|
resolveAgcDevEndpoint,
|
||||||
@@ -47,9 +50,8 @@ function buildTauriArguments(argv, devUrl = readAgcDevEndpoint().url) {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// `agc_cocos_execute` 与 Cocos 编辑器适配器只在 `cocos-editor-execute` feature 下
|
// 开发和发行构建使用同一平台编辑器 feature 集合。
|
||||||
// 注册。开发构建默认在 Windows 打开它,否则 Agent 的工具清单里根本没有该工具,
|
// 可用 AGC_DEV_CARGO_FEATURES(逗号分隔)覆盖,传空串即关闭。
|
||||||
// 只能退化成改写脚本。可用 AGC_DEV_CARGO_FEATURES(逗号分隔)覆盖,传空串即关闭。
|
|
||||||
function readDevCargoFeatures(env = process.env) {
|
function readDevCargoFeatures(env = process.env) {
|
||||||
const override = env.AGC_DEV_CARGO_FEATURES;
|
const override = env.AGC_DEV_CARGO_FEATURES;
|
||||||
if (override !== undefined) {
|
if (override !== undefined) {
|
||||||
@@ -58,7 +60,7 @@ function readDevCargoFeatures(env = process.env) {
|
|||||||
.map((value) => value.trim())
|
.map((value) => value.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
return process.platform === 'win32' ? ['cocos-editor-execute'] : [];
|
return defaultEditorFeatures(process.platform);
|
||||||
}
|
}
|
||||||
|
|
||||||
function withDevCargoFeatures(argv, features = readDevCargoFeatures()) {
|
function withDevCargoFeatures(argv, features = readDevCargoFeatures()) {
|
||||||
|
|||||||
+11
@@ -1793,6 +1793,7 @@ dependencies = [
|
|||||||
"ttf-parser",
|
"ttf-parser",
|
||||||
"typed_floats",
|
"typed_floats",
|
||||||
"unicode-normalization",
|
"unicode-normalization",
|
||||||
|
"unity-editor-bridge",
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
@@ -6407,6 +6408,16 @@ version = "0.2.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unity-editor-bridge"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"editor-adapter-api",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unsafe-libyaml"
|
name = "unsafe-libyaml"
|
||||||
version = "0.2.11"
|
version = "0.2.11"
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ template-library-fixtures = []
|
|||||||
cocos-editor = ["cocos-editor-bridge/process-discovery"]
|
cocos-editor = ["cocos-editor-bridge/process-discovery"]
|
||||||
cocos-editor-execute = ["cocos-editor", "cocos-editor-bridge/windows-bootstrap"]
|
cocos-editor-execute = ["cocos-editor", "cocos-editor-bridge/windows-bootstrap"]
|
||||||
cocos-editor-injection = ["cocos-editor-execute", "cocos-editor-bridge/windows-injection"]
|
cocos-editor-injection = ["cocos-editor-execute", "cocos-editor-bridge/windows-injection"]
|
||||||
|
unity-editor-execute = []
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
@@ -27,6 +28,7 @@ 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 }
|
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" }
|
editor-adapter-api = { path = "../../../server-rs/crates/editor-adapter-api" }
|
||||||
|
unity-editor-bridge = { path = "../../../plugins/agc-unity-editor/native/unity-editor-bridge" }
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
axum = "0.8"
|
axum = "0.8"
|
||||||
chromiumoxide = "0.9.1"
|
chromiumoxide = "0.9.1"
|
||||||
|
|||||||
@@ -196,6 +196,7 @@ 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);
|
||||||
|
prepare_unity_editor_helper(&manifest_dir);
|
||||||
stage_plugin_workspace(&manifest_dir);
|
stage_plugin_workspace(&manifest_dir);
|
||||||
stage_cocos_editor_payload(&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)
|
||||||
@@ -266,6 +267,107 @@ fn stage_cocos_editor_payload(manifest_dir: &std::path::Path) {
|
|||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
fn stage_cocos_editor_payload(_manifest_dir: &std::path::Path) {}
|
fn stage_cocos_editor_payload(_manifest_dir: &std::path::Path) {}
|
||||||
|
|
||||||
|
/// Unity helper 是插件的随包运行文件。内容指纹避免每次 Cargo 检查都重新发布 .NET。
|
||||||
|
fn prepare_unity_editor_helper(manifest_dir: &std::path::Path) {
|
||||||
|
println!("cargo:rerun-if-env-changed=CARGO_FEATURE_UNITY_EDITOR_EXECUTE");
|
||||||
|
let target = env::var("TARGET").expect("Cargo TARGET");
|
||||||
|
if env::var_os("CARGO_FEATURE_UNITY_EDITOR_EXECUTE").is_none()
|
||||||
|
|| target != "x86_64-pc-windows-msvc"
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let root = manifest_dir.join("../../../plugins/agc-unity-editor/dotnet");
|
||||||
|
let mut sources = Vec::new();
|
||||||
|
collect_unity_helper_sources(&root, &mut sources);
|
||||||
|
sources.sort();
|
||||||
|
let mut fingerprint = Sha256::new();
|
||||||
|
for source in &sources {
|
||||||
|
println!("cargo:rerun-if-changed={}", source.display());
|
||||||
|
fingerprint.update(
|
||||||
|
source
|
||||||
|
.strip_prefix(&root)
|
||||||
|
.expect("helper source")
|
||||||
|
.to_string_lossy()
|
||||||
|
.as_bytes(),
|
||||||
|
);
|
||||||
|
fingerprint.update([0]);
|
||||||
|
fingerprint.update(fs::read(source).expect("读取 Unity helper 源文件失败"));
|
||||||
|
}
|
||||||
|
let fingerprint = format!("{:x}", fingerprint.finalize());
|
||||||
|
let publish = root.join("publish/win-x64");
|
||||||
|
let executable = publish.join("Agc.Unity.Attach.exe");
|
||||||
|
let stamp = publish.join(".agc-source.sha256");
|
||||||
|
println!("cargo:rerun-if-changed={}", executable.display());
|
||||||
|
if unity_helper_publish_complete(&publish)
|
||||||
|
&& fs::read_to_string(&stamp).ok().as_deref() == Some(&fingerprint)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
cfg!(windows),
|
||||||
|
"构建 Unity 插件 helper 需要 Windows .NET 10 与 x64 C++ 工具链"
|
||||||
|
);
|
||||||
|
let status = std::process::Command::new("powershell.exe")
|
||||||
|
.args([
|
||||||
|
"-NoProfile",
|
||||||
|
"-NonInteractive",
|
||||||
|
"-ExecutionPolicy",
|
||||||
|
"Bypass",
|
||||||
|
"-File",
|
||||||
|
])
|
||||||
|
.arg(root.join("build.ps1"))
|
||||||
|
.current_dir(&root)
|
||||||
|
.status()
|
||||||
|
.expect("无法启动 Unity helper 构建脚本");
|
||||||
|
assert!(
|
||||||
|
status.success() && unity_helper_publish_complete(&publish),
|
||||||
|
"Unity helper 构建失败或缺少运行文件/许可"
|
||||||
|
);
|
||||||
|
fs::write(stamp, fingerprint).expect("写入 Unity helper 构建指纹失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unity_helper_publish_complete(publish: &std::path::Path) -> bool {
|
||||||
|
[
|
||||||
|
"Agc.Unity.Attach.exe",
|
||||||
|
"NOTICE",
|
||||||
|
"THIRD-PARTY-NOTICES.txt",
|
||||||
|
"licenses/DotCraft-Apache-2.0.txt",
|
||||||
|
"licenses/Roslyn-MIT.txt",
|
||||||
|
"licenses/upstream.json",
|
||||||
|
"licenses/dotnet-LICENSE.TXT",
|
||||||
|
"licenses/dotnet-THIRD-PARTY-NOTICES.TXT",
|
||||||
|
"licenses/microsoft.codeanalysis.common-ThirdPartyNotices.rtf",
|
||||||
|
"licenses/microsoft.codeanalysis.csharp-ThirdPartyNotices.rtf",
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.all(|name| {
|
||||||
|
fs::symlink_metadata(publish.join(name)).is_ok_and(|metadata| {
|
||||||
|
metadata.is_file() && !metadata.file_type().is_symlink() && metadata.len() > 0
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_unity_helper_sources(root: &std::path::Path, sources: &mut Vec<PathBuf>) {
|
||||||
|
for entry in fs::read_dir(root)
|
||||||
|
.expect("Unity helper 源码目录缺失")
|
||||||
|
.flatten()
|
||||||
|
{
|
||||||
|
let kind = entry.file_type().expect("读取 Unity helper 源文件类型失败");
|
||||||
|
assert!(!kind.is_symlink(), "Unity helper 源码不允许符号链接");
|
||||||
|
let name = entry.file_name();
|
||||||
|
if kind.is_dir() {
|
||||||
|
if !matches!(
|
||||||
|
name.to_str(),
|
||||||
|
Some("bin" | "obj" | "publish" | "native-build")
|
||||||
|
) {
|
||||||
|
collect_unity_helper_sources(&entry.path(), sources);
|
||||||
|
}
|
||||||
|
} else if kind.is_file() {
|
||||||
|
sources.push(entry.path());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 把 `plugins/` 工作区里的插件包随包映射到应用资源目录。
|
/// 把 `plugins/` 工作区里的插件包随包映射到应用资源目录。
|
||||||
///
|
///
|
||||||
/// 只复制插件运行需要的清单、入口、面板和 native payload,不复制 native 源码、
|
/// 只复制插件运行需要的清单、入口、面板和 native payload,不复制 native 源码、
|
||||||
@@ -313,9 +415,15 @@ fn stage_plugin_workspace(manifest_dir: &std::path::Path) {
|
|||||||
for relative in [
|
for relative in [
|
||||||
std::path::PathBuf::from("src"),
|
std::path::PathBuf::from("src"),
|
||||||
std::path::PathBuf::from("panels"),
|
std::path::PathBuf::from("panels"),
|
||||||
|
std::path::PathBuf::from("skills"),
|
||||||
std::path::PathBuf::from("native/payload"),
|
std::path::PathBuf::from("native/payload"),
|
||||||
|
std::path::PathBuf::from("dotnet/publish/win-x64"),
|
||||||
] {
|
] {
|
||||||
if relative == std::path::Path::new("native/payload") && !target.contains("windows") {
|
if (relative == std::path::Path::new("native/payload") && !target.contains("windows"))
|
||||||
|
|| (relative == std::path::Path::new("dotnet/publish/win-x64")
|
||||||
|
&& (target != "x86_64-pc-windows-msvc"
|
||||||
|
|| env::var_os("CARGO_FEATURE_UNITY_EDITOR_EXECUTE").is_none()))
|
||||||
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
copy_plugin_tree(&plugin_root.join(&relative), &destination.join(&relative));
|
copy_plugin_tree(&plugin_root.join(&relative), &destination.join(&relative));
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -2592,6 +2592,51 @@ async fn bridge_web_search_at(root: &Path, arguments: &Value, search_url: &str)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(all(windows, target_arch = "x86_64", feature = "unity-editor-execute"))]
|
||||||
|
async fn bridge_unity_execute(state: &DirectToolBridgeState, arguments: &Value) -> Value {
|
||||||
|
let prepared = (|| {
|
||||||
|
if !crate::builtin_plugins::unity_editor_agent_tool_available_for_project(&state.root) {
|
||||||
|
return Err("当前项目不是 Unity 项目或 Unity 插件不可用".to_string());
|
||||||
|
}
|
||||||
|
enforce_project_permission_policy(&state.root, "unity.editor.execute")?;
|
||||||
|
bridge_reject_unknown_fields(arguments, &["code"])?;
|
||||||
|
let code = arguments
|
||||||
|
.get("code")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.ok_or_else(|| "code 必须是 C# 代码".to_string())?;
|
||||||
|
if code.trim().is_empty() || code.len() > 131072 || code.contains('\0') {
|
||||||
|
return Err("code 不能为空、包含 NUL 或超过 128 KiB".to_string());
|
||||||
|
}
|
||||||
|
Ok(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 root = state.root.clone();
|
||||||
|
let result = tokio::task::spawn_blocking(move || {
|
||||||
|
if !crate::builtin_plugins::unity_editor_agent_tool_available_for_project(&root) {
|
||||||
|
return Err("当前 Unity 插件不可用".to_string());
|
||||||
|
}
|
||||||
|
crate::editor_adapters::execute_unity_editor_code(&root, &code)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
match result {
|
||||||
|
Ok(Ok(response)) => {
|
||||||
|
let failed = response["ok"] != true || response["status"] != "completed";
|
||||||
|
bridge_tool_result(redact_agent_runtime_error(&state.root, &response.to_string(), 32_000), Vec::new(), failed)
|
||||||
|
}
|
||||||
|
Ok(Err(error)) => bridge_tool_result(redact_agent_runtime_error(&state.root, &error, 480), Vec::new(), true),
|
||||||
|
Err(_) => bridge_tool_result(json!({"ok":false,"status":"needs-reconciliation","retryAllowed":false,"error":"Unity 执行任务异常,请人工核对结果"}).to_string(), Vec::new(), true),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(all(windows, feature = "cocos-editor-execute"))]
|
#[cfg(all(windows, feature = "cocos-editor-execute"))]
|
||||||
async fn bridge_cocos_execute(state: &DirectToolBridgeState, arguments: &Value) -> Value {
|
async fn bridge_cocos_execute(state: &DirectToolBridgeState, arguments: &Value) -> Value {
|
||||||
bridge_cocos_call(state, arguments, None).await
|
bridge_cocos_call(state, arguments, None).await
|
||||||
@@ -2603,6 +2648,9 @@ async fn bridge_cocos_call(
|
|||||||
arguments: &Value,
|
arguments: &Value,
|
||||||
operation: Option<&str>,
|
operation: Option<&str>,
|
||||||
) -> Value {
|
) -> Value {
|
||||||
|
if !crate::builtin_plugins::is_enabled(crate::builtin_plugins::AGC_COCOS_EDITOR_PLUGIN_ID) {
|
||||||
|
return bridge_tool_result("Cocos 编辑器插件已禁用".to_string(), Vec::new(), true);
|
||||||
|
}
|
||||||
if !crate::builtin_plugins::cocos_editor_agent_tool_available_for_project(&state.root) {
|
if !crate::builtin_plugins::cocos_editor_agent_tool_available_for_project(&state.root) {
|
||||||
return bridge_tool_result(
|
return bridge_tool_result(
|
||||||
"当前项目不是 Cocos Creator 项目或 Cocos 插件不可用,agc_cocos_execute 不可用"
|
"当前项目不是 Cocos Creator 项目或 Cocos 插件不可用,agc_cocos_execute 不可用"
|
||||||
@@ -2789,6 +2837,8 @@ async fn handle_direct_tool_bridge(
|
|||||||
"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"))]
|
#[cfg(all(windows, feature = "cocos-editor-execute"))]
|
||||||
"agc_cocos_execute" => bridge_cocos_execute(&state, &request.arguments).await,
|
"agc_cocos_execute" => bridge_cocos_execute(&state, &request.arguments).await,
|
||||||
|
#[cfg(all(windows, target_arch = "x86_64", feature = "unity-editor-execute"))]
|
||||||
|
"agc_unity_execute" => bridge_unity_execute(&state, &request.arguments).await,
|
||||||
#[cfg(all(windows, feature = "cocos-editor-execute"))]
|
#[cfg(all(windows, feature = "cocos-editor-execute"))]
|
||||||
operation if cocos_editor_bridge::is_cocos_operation(operation) => {
|
operation if cocos_editor_bridge::is_cocos_operation(operation) => {
|
||||||
bridge_cocos_call(&state, &request.arguments, Some(operation)).await
|
bridge_cocos_call(&state, &request.arguments, Some(operation)).await
|
||||||
|
|||||||
@@ -75,7 +75,14 @@ pub(crate) fn run_direct_tools_mcp_if_requested(args: &[String]) -> Option<i32>
|
|||||||
|
|
||||||
async fn direct_tools_mcp_specs() -> Value {
|
async fn direct_tools_mcp_specs() -> Value {
|
||||||
let mut cocos_editor_available = false;
|
let mut cocos_editor_available = false;
|
||||||
if cfg!(all(windows, feature = "cocos-editor-execute")) {
|
let mut unity_editor_available = false;
|
||||||
|
if cfg!(all(windows, feature = "cocos-editor-execute"))
|
||||||
|
|| cfg!(all(
|
||||||
|
windows,
|
||||||
|
target_arch = "x86_64",
|
||||||
|
feature = "unity-editor-execute"
|
||||||
|
))
|
||||||
|
{
|
||||||
// 每次 tools/list 询问绑定的宿主;失败时不广告可选插件工具。
|
// 每次 tools/list 询问绑定的宿主;失败时不广告可选插件工具。
|
||||||
if let Ok(result) = tokio::time::timeout(
|
if let Ok(result) = tokio::time::timeout(
|
||||||
std::time::Duration::from_secs(5),
|
std::time::Duration::from_secs(5),
|
||||||
@@ -96,10 +103,22 @@ async fn direct_tools_mcp_specs() -> Value {
|
|||||||
.iter()
|
.iter()
|
||||||
.any(|tool| tool == crate::builtin_plugins::AGC_COCOS_EDITOR_TOOL_NAME)
|
.any(|tool| tool == crate::builtin_plugins::AGC_COCOS_EDITOR_TOOL_NAME)
|
||||||
});
|
});
|
||||||
|
unity_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_UNITY_EDITOR_TOOL_NAME)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
direct_tools_mcp_specs_for(controlled_web_search_enabled(), cocos_editor_available)
|
direct_tools_mcp_specs_for_plugins(
|
||||||
|
controlled_web_search_enabled(),
|
||||||
|
cocos_editor_available,
|
||||||
|
unity_editor_available,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 工具 kind(wire 值)对应的提示词上限。
|
/// 工具 kind(wire 值)对应的提示词上限。
|
||||||
@@ -128,7 +147,16 @@ fn resource_tool_prompt_schema_max_chars() -> usize {
|
|||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn direct_tools_mcp_specs_for(controlled_web_search: bool, _cocos_editor_available: bool) -> Value {
|
#[cfg(test)]
|
||||||
|
fn direct_tools_mcp_specs_for(controlled_web_search: bool, cocos_editor_available: bool) -> Value {
|
||||||
|
direct_tools_mcp_specs_for_plugins(controlled_web_search, cocos_editor_available, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn direct_tools_mcp_specs_for_plugins(
|
||||||
|
controlled_web_search: bool,
|
||||||
|
_cocos_editor_available: bool,
|
||||||
|
_unity_editor_available: bool,
|
||||||
|
) -> Value {
|
||||||
let tools = vec![
|
let tools = vec![
|
||||||
json!({
|
json!({
|
||||||
"name": "client.session.info",
|
"name": "client.session.info",
|
||||||
@@ -568,6 +596,14 @@ fn direct_tools_mcp_specs_for(controlled_web_search: bool, _cocos_editor_availab
|
|||||||
.cloned(),
|
.cloned(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
#[cfg(all(windows, target_arch = "x86_64", feature = "unity-editor-execute"))]
|
||||||
|
if _unity_editor_available {
|
||||||
|
tools.push(json!({
|
||||||
|
"name": "agc_unity_execute",
|
||||||
|
"description": "在当前项目已打开的 Windows x64 Unity Mono Editor 执行 C#,可使用 return 返回值。仅提交 code;宿主绑定项目及进程。needs-reconciliation 或超时后禁止自动重发。",
|
||||||
|
"inputSchema": {"type":"object", "properties":{"code":{"type":"string", "minLength":1, "maxLength":131072}}, "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",
|
||||||
@@ -655,6 +691,24 @@ async fn call_agc_cocos_execute(arguments: &Value) -> Value {
|
|||||||
call_client_tool_bridge("agc_cocos_execute", arguments).await
|
call_client_tool_bridge("agc_cocos_execute", arguments).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(all(windows, target_arch = "x86_64", feature = "unity-editor-execute"))]
|
||||||
|
async fn call_agc_unity_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 必须是 C# 代码".to_string())?;
|
||||||
|
if code.trim().is_empty() || code.len() > 131072 || code.contains('\0') {
|
||||||
|
return Err("code 不能为空、包含 NUL 或超过 128 KiB".to_string());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
if let Err(error) = validated {
|
||||||
|
return mcp_tool_result(error, Vec::new(), true);
|
||||||
|
}
|
||||||
|
call_client_tool_bridge("agc_unity_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 })
|
||||||
}
|
}
|
||||||
@@ -1757,6 +1811,8 @@ async fn handle_direct_tools_mcp_request(root: &Path, request: Value) -> Option<
|
|||||||
"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"))]
|
#[cfg(all(windows, feature = "cocos-editor-execute"))]
|
||||||
"agc_cocos_execute" => call_agc_cocos_execute(&arguments).await,
|
"agc_cocos_execute" => call_agc_cocos_execute(&arguments).await,
|
||||||
|
#[cfg(all(windows, target_arch = "x86_64", feature = "unity-editor-execute"))]
|
||||||
|
"agc_unity_execute" => call_agc_unity_execute(&arguments).await,
|
||||||
#[cfg(all(windows, feature = "cocos-editor-execute"))]
|
#[cfg(all(windows, feature = "cocos-editor-execute"))]
|
||||||
operation if cocos_editor_bridge::is_cocos_operation(operation) => {
|
operation if cocos_editor_bridge::is_cocos_operation(operation) => {
|
||||||
call_client_tool_bridge(operation, &arguments).await
|
call_client_tool_bridge(operation, &arguments).await
|
||||||
|
|||||||
@@ -1620,7 +1620,7 @@ 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!(
|
"cocos.editor.execute" | "unity.editor.execute" => format!(
|
||||||
"codeChars={} · codeSha256={:x}",
|
"codeChars={} · codeSha256={:x}",
|
||||||
chars(&["code"]),
|
chars(&["code"]),
|
||||||
Sha256::digest(
|
Sha256::digest(
|
||||||
|
|||||||
@@ -383,6 +383,16 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_
|
|||||||
true,
|
true,
|
||||||
|| observe_agent_runtime_cocos_editor_execute(root, action, pending_action),
|
|| observe_agent_runtime_cocos_editor_execute(root, action, pending_action),
|
||||||
),
|
),
|
||||||
|
"unity.editor.execute" => observe_agent_runtime_project_snapshot_with_lock(
|
||||||
|
root,
|
||||||
|
agent_id,
|
||||||
|
run_id,
|
||||||
|
action,
|
||||||
|
&action_fingerprint,
|
||||||
|
pending_action,
|
||||||
|
true,
|
||||||
|
|| observe_agent_runtime_unity_editor_execute(root, action, pending_action),
|
||||||
|
),
|
||||||
"preview.validate" => {
|
"preview.validate" => {
|
||||||
observe_agent_runtime_preview_validate(
|
observe_agent_runtime_preview_validate(
|
||||||
root,
|
root,
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ pub(in crate::agent) fn game_creator_agent_runtime_tool_command_id(
|
|||||||
"command.run_limited" => Some("command.run_limited"),
|
"command.run_limited" => Some("command.run_limited"),
|
||||||
#[cfg(feature = "cocos-editor-execute")]
|
#[cfg(feature = "cocos-editor-execute")]
|
||||||
"cocos.editor.execute" => Some("cocos.editor.execute"),
|
"cocos.editor.execute" => Some("cocos.editor.execute"),
|
||||||
|
"unity.editor.execute" => Some("unity.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"),
|
||||||
|
|||||||
+9
-4
@@ -247,7 +247,10 @@ fn build_game_creator_agent_background_tool_plan_request_at(
|
|||||||
let relaxed_prompt = format!(
|
let relaxed_prompt = format!(
|
||||||
"你正在执行一个自主游戏构建任务。请按自己的判断规划并直接调用当前广告的原生工具完成目标;任务可以与其它 Agent 并行,依赖只作为参考,不要等待或索要平台资产/验收回执。已有观察只代表已发生的事实,完成后直接调用 respond_to_user。\n\n运行上下文:\n{context}\n\n任务:\n{effective_task}\n\n已有观察:\n{observations_json}"
|
"你正在执行一个自主游戏构建任务。请按自己的判断规划并直接调用当前广告的原生工具完成目标;任务可以与其它 Agent 并行,依赖只作为参考,不要等待或索要平台资产/验收回执。已有观察只代表已发生的事实,完成后直接调用 respond_to_user。\n\n运行上下文:\n{context}\n\n任务:\n{effective_task}\n\n已有观察:\n{observations_json}"
|
||||||
);
|
);
|
||||||
let mut function_tools = build_agent_runtime_native_function_tools_for_agent(agent_id)?;
|
let mut function_tools =
|
||||||
|
crate::agent_native_tools::build_agent_runtime_native_function_tools_for_project(
|
||||||
|
root, agent_id,
|
||||||
|
)?;
|
||||||
remove_relaxed_autonomous_platform_validation_tools(&mut function_tools)?;
|
remove_relaxed_autonomous_platform_validation_tools(&mut function_tools)?;
|
||||||
// Platform-backed generation remains an optional capability. A
|
// Platform-backed generation remains an optional capability. A
|
||||||
// relaxed run may proceed with all ordinary project tools when no
|
// relaxed run may proceed with all ordinary project tools when no
|
||||||
@@ -479,9 +482,11 @@ fn build_game_creator_agent_background_tool_plan_request_at(
|
|||||||
.with_api_kind(api_kind)
|
.with_api_kind(api_kind)
|
||||||
.with_max_output_tokens(AGENT_RUNTIME_TOOL_PLAN_MAX_OUTPUT_TOKENS)
|
.with_max_output_tokens(AGENT_RUNTIME_TOOL_PLAN_MAX_OUTPUT_TOKENS)
|
||||||
.with_response_text_verbosity(platform_llm::LlmResponseTextVerbosity::Low)
|
.with_response_text_verbosity(platform_llm::LlmResponseTextVerbosity::Low)
|
||||||
.with_function_tools(build_agent_runtime_native_function_tools_for_agent(
|
.with_function_tools(
|
||||||
agent_id,
|
crate::agent_native_tools::build_agent_runtime_native_function_tools_for_project(
|
||||||
)?)
|
root, agent_id,
|
||||||
|
)?,
|
||||||
|
)
|
||||||
.with_tool_choice(platform_llm::LlmToolChoice::Required);
|
.with_tool_choice(platform_llm::LlmToolChoice::Required);
|
||||||
if runtime_owner_artifact_validation_available {
|
if runtime_owner_artifact_validation_available {
|
||||||
remove_autonomous_owner_manual_verification_tools(&mut request.function_tools)?;
|
remove_autonomous_owner_manual_verification_tools(&mut request.function_tools)?;
|
||||||
|
|||||||
+1
-1
@@ -969,7 +969,7 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
|
|||||||
|| force_autonomous_pre_mutation
|
|| force_autonomous_pre_mutation
|
||||||
{
|
{
|
||||||
request.function_tools =
|
request.function_tools =
|
||||||
build_agent_runtime_native_function_tools_for_agent(agent_id)?;
|
crate::agent_native_tools::build_agent_runtime_native_function_tools_for_project(root, agent_id)?;
|
||||||
if runtime_owner_artifact_validation_available {
|
if runtime_owner_artifact_validation_available {
|
||||||
remove_autonomous_owner_manual_verification_tools(
|
remove_autonomous_owner_manual_verification_tools(
|
||||||
&mut request.function_tools,
|
&mut request.function_tools,
|
||||||
|
|||||||
+12
@@ -70,6 +70,9 @@ pub(crate) fn agent_runtime_executable_tools() -> Vec<&'static str> {
|
|||||||
if crate::builtin_plugins::cocos_editor_agent_tool_available() {
|
if crate::builtin_plugins::cocos_editor_agent_tool_available() {
|
||||||
tools.push(crate::builtin_plugins::AGC_COCOS_EDITOR_TOOL_NAME);
|
tools.push(crate::builtin_plugins::AGC_COCOS_EDITOR_TOOL_NAME);
|
||||||
}
|
}
|
||||||
|
if crate::builtin_plugins::unity_editor_agent_tool_available() {
|
||||||
|
tools.push(crate::builtin_plugins::AGC_UNITY_EDITOR_TOOL_NAME);
|
||||||
|
}
|
||||||
tools
|
tools
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,6 +157,11 @@ pub(in crate::agent) fn agent_runtime_tool_policy_snapshot_at(
|
|||||||
let mut confirm_tools = Vec::new();
|
let mut confirm_tools = Vec::new();
|
||||||
let mut denied_tools = Vec::new();
|
let mut denied_tools = Vec::new();
|
||||||
for tool in agent_runtime_executable_tools() {
|
for tool in agent_runtime_executable_tools() {
|
||||||
|
if tool == crate::builtin_plugins::AGC_UNITY_EDITOR_TOOL_NAME
|
||||||
|
&& !crate::builtin_plugins::unity_editor_agent_tool_available_for_project(root)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if isolated && ISOLATED_AGENT_UNSCOPED_DENIED_TOOLS.contains(&tool) {
|
if isolated && ISOLATED_AGENT_UNSCOPED_DENIED_TOOLS.contains(&tool) {
|
||||||
denied_tools.push(tool.to_string());
|
denied_tools.push(tool.to_string());
|
||||||
continue;
|
continue;
|
||||||
@@ -196,6 +204,10 @@ pub(in crate::agent) fn agent_runtime_tool_policy_snapshot_at(
|
|||||||
run_profile_binding_fingerprint: String::new(),
|
run_profile_binding_fingerprint: String::new(),
|
||||||
allowed_tools: agent_runtime_executable_tools()
|
allowed_tools: agent_runtime_executable_tools()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
.filter(|tool| {
|
||||||
|
*tool != crate::builtin_plugins::AGC_UNITY_EDITOR_TOOL_NAME
|
||||||
|
|| crate::builtin_plugins::unity_editor_agent_tool_available_for_project(root)
|
||||||
|
})
|
||||||
.map(str::to_string)
|
.map(str::to_string)
|
||||||
.collect(),
|
.collect(),
|
||||||
auto_tools,
|
auto_tools,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ mod goal_contract;
|
|||||||
mod helpers;
|
mod helpers;
|
||||||
mod isolated_joins;
|
mod isolated_joins;
|
||||||
mod media;
|
mod media;
|
||||||
|
mod unity_editor;
|
||||||
pub(in crate::agent) use media::design_foundation_ui_page_output_path_is_valid;
|
pub(in crate::agent) use media::design_foundation_ui_page_output_path_is_valid;
|
||||||
mod policy;
|
mod policy;
|
||||||
mod preview;
|
mod preview;
|
||||||
@@ -38,6 +39,7 @@ pub(in crate::agent) use project_ops::*;
|
|||||||
pub(in crate::agent) use run_status::*;
|
pub(in crate::agent) use run_status::*;
|
||||||
pub(in crate::agent) use task_ops::*;
|
pub(in crate::agent) use task_ops::*;
|
||||||
pub(in crate::agent) use ui_workflow::*;
|
pub(in crate::agent) use ui_workflow::*;
|
||||||
|
pub(in crate::agent) use unity_editor::*;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) use delegation::observe_agent_runtime_agent_delegate_at_locked;
|
pub(crate) use delegation::observe_agent_runtime_agent_delegate_at_locked;
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||||
|
struct UnityEditorExecuteInput {
|
||||||
|
code: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::agent) fn observe_agent_runtime_unity_editor_execute(
|
||||||
|
root: &Path,
|
||||||
|
action: &AgentRuntimeToolAction,
|
||||||
|
pending_action: Option<&AgentRuntimePendingToolAction>,
|
||||||
|
) -> AgentRuntimeToolObservation {
|
||||||
|
let execution = (|| {
|
||||||
|
let input: UnityEditorExecuteInput = serde_json::from_value(action.input.clone())
|
||||||
|
.map_err(|error| format!("unity.editor.execute 输入无效:{error}"))?;
|
||||||
|
if pending_action.is_none() {
|
||||||
|
return Err("unity.editor.execute 必须绑定 durable pending action".to_string());
|
||||||
|
}
|
||||||
|
if !crate::builtin_plugins::unity_editor_agent_tool_available_for_project(root) {
|
||||||
|
return Err("当前项目不是 Unity 项目或 Unity 插件不可用".to_string());
|
||||||
|
}
|
||||||
|
crate::editor_adapters::execute_unity_editor_code(root, &input.code)
|
||||||
|
})();
|
||||||
|
match execution {
|
||||||
|
Ok(response) => {
|
||||||
|
let status = match response["status"].as_str() {
|
||||||
|
Some("completed") if response["ok"] == true => "ok",
|
||||||
|
Some("needs-reconciliation") => {
|
||||||
|
AGENT_RUNTIME_TOOL_OBSERVATION_STATUS_NEEDS_RECONCILIATION
|
||||||
|
}
|
||||||
|
_ => "failed",
|
||||||
|
};
|
||||||
|
AgentRuntimeToolObservation {
|
||||||
|
tool: "unity.editor.execute".to_string(),
|
||||||
|
status: status.to_string(),
|
||||||
|
summary: match status {
|
||||||
|
"ok" => "Unity 编辑器已返回执行成功回执",
|
||||||
|
"needs-reconciliation" => "Unity 执行结果待人工核对,禁止自动重发",
|
||||||
|
_ => "Unity 编辑器执行失败",
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
detail: Some(redact_agent_runtime_project_paths(
|
||||||
|
root,
|
||||||
|
&response.to_string(),
|
||||||
|
32_000,
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => AgentRuntimeToolObservation {
|
||||||
|
tool: "unity.editor.execute".to_string(),
|
||||||
|
status: "failed".to_string(),
|
||||||
|
summary: redact_agent_runtime_project_paths(root, &error, 480),
|
||||||
|
detail: None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unity_execute_requires_pending_action_and_rejects_project_override() {
|
||||||
|
for input in [
|
||||||
|
serde_json::json!({"code":"return 2;"}),
|
||||||
|
serde_json::json!({"code":"return 2;", "projectPath":"C:/other"}),
|
||||||
|
] {
|
||||||
|
let action = AgentRuntimeToolAction {
|
||||||
|
tool: "unity.editor.execute".to_string(),
|
||||||
|
reason: None,
|
||||||
|
input,
|
||||||
|
};
|
||||||
|
let observation =
|
||||||
|
observe_agent_runtime_unity_editor_execute(Path::new("C:/unity"), &action, None);
|
||||||
|
assert_eq!(observation.status, "failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -253,18 +253,13 @@ fn build_agent_runtime_native_capability_registry(
|
|||||||
|
|
||||||
fn agent_runtime_native_capability_registry() -> Result<&'static CapabilityRegistry<String>, String>
|
fn agent_runtime_native_capability_registry() -> Result<&'static CapabilityRegistry<String>, String>
|
||||||
{
|
{
|
||||||
// 内置插件开关会改变工具目录,因此按“可用 / 不可用”各缓存一份:切换后立即
|
// 两个独立开关产生四份目录,使用同一快照选缓存并构建。
|
||||||
// 生效,又不需要每次调用都重建 registry。
|
static REGISTRIES: [OnceLock<Result<CapabilityRegistry<String>, String>>; 4] =
|
||||||
static ENABLED_REGISTRY: OnceLock<Result<CapabilityRegistry<String>, String>> = OnceLock::new();
|
[const { OnceLock::new() }; 4];
|
||||||
static DISABLED_REGISTRY: OnceLock<Result<CapabilityRegistry<String>, String>> =
|
|
||||||
OnceLock::new();
|
|
||||||
// 缓存选择与构建消费同一份快照,避免开关变化污染另一份永久缓存。
|
|
||||||
let tools = agent_runtime_native_executable_tools();
|
let tools = agent_runtime_native_executable_tools();
|
||||||
let cache = if tools.contains(&crate::builtin_plugins::AGC_COCOS_EDITOR_TOOL_NAME) {
|
let index = usize::from(tools.contains(&crate::builtin_plugins::AGC_COCOS_EDITOR_TOOL_NAME))
|
||||||
&ENABLED_REGISTRY
|
| (usize::from(tools.contains(&crate::builtin_plugins::AGC_UNITY_EDITOR_TOOL_NAME)) << 1);
|
||||||
} else {
|
let cache = ®ISTRIES[index];
|
||||||
&DISABLED_REGISTRY
|
|
||||||
};
|
|
||||||
cache
|
cache
|
||||||
.get_or_init(|| build_agent_runtime_native_capability_registry(tools))
|
.get_or_init(|| build_agent_runtime_native_capability_registry(tools))
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -311,6 +306,18 @@ pub(crate) fn build_agent_runtime_native_function_tools_for_agent(
|
|||||||
Ok(functions)
|
Ok(functions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn build_agent_runtime_native_function_tools_for_project(
|
||||||
|
root: &std::path::Path,
|
||||||
|
agent_id: &str,
|
||||||
|
) -> Result<Vec<LlmFunctionTool>, String> {
|
||||||
|
let mut tools = build_agent_runtime_native_function_tools_for_agent(agent_id)?;
|
||||||
|
if !crate::builtin_plugins::unity_editor_agent_tool_available_for_project(root) {
|
||||||
|
let name = native_runtime_function_name_for_tool("unity.editor.execute");
|
||||||
|
tools.retain(|tool| tool.name != name);
|
||||||
|
}
|
||||||
|
Ok(tools)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn agent_runtime_native_tool_allowed_for_agent(tool: &str) -> bool {
|
pub(crate) fn agent_runtime_native_tool_allowed_for_agent(tool: &str) -> bool {
|
||||||
agent_runtime_native_capability_registry()
|
agent_runtime_native_capability_registry()
|
||||||
.ok()
|
.ok()
|
||||||
@@ -1044,6 +1051,7 @@ fn runtime_tool_description(tool: &str) -> &'static str {
|
|||||||
"cocos.editor.execute" => {
|
"cocos.editor.execute" => {
|
||||||
"在当前项目对应的已打开 Cocos Creator 编辑器中执行一段有界代码;Runtime 自动绑定唯一匹配的 Creator 主进程,代码与结果都通过注入 payload 的本机 bridge 返回。"
|
"在当前项目对应的已打开 Cocos Creator 编辑器中执行一段有界代码;Runtime 自动绑定唯一匹配的 Creator 主进程,代码与结果都通过注入 payload 的本机 bridge 返回。"
|
||||||
}
|
}
|
||||||
|
"unity.editor.execute" => "在当前 Unity 项目已打开的 Windows x64 Mono Editor 中执行 C#。仅提交 code,宿主绑定项目身份;结果待核对时禁止自动重发。",
|
||||||
"blackboard.write" => "向项目级共享黑板追加稳定结论。",
|
"blackboard.write" => "向项目级共享黑板追加稳定结论。",
|
||||||
"agent.message" => "向一个目标 Agent 写入定向上下文消息。",
|
"agent.message" => "向一个目标 Agent 写入定向上下文消息。",
|
||||||
"agent.delegate" => {
|
"agent.delegate" => {
|
||||||
@@ -1241,7 +1249,7 @@ fn runtime_tool_input_schema(tool: &str) -> Value {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
"command.exec" | "command.start" => command_start_input_schema(),
|
"command.exec" | "command.start" => command_start_input_schema(),
|
||||||
"cocos.editor.execute" => json!({
|
"cocos.editor.execute" | "unity.editor.execute" => json!({
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["code"],
|
"required": ["code"],
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ pub(crate) const AGC_COCOS_EDITOR_PLUGIN_ID: &str = "agc-cocos-editor";
|
|||||||
|
|
||||||
/// 该插件在 Agent 侧对应的 Runtime 工具名。
|
/// 该插件在 Agent 侧对应的 Runtime 工具名。
|
||||||
pub(crate) const AGC_COCOS_EDITOR_TOOL_NAME: &str = "cocos.editor.execute";
|
pub(crate) const AGC_COCOS_EDITOR_TOOL_NAME: &str = "cocos.editor.execute";
|
||||||
|
pub(crate) const AGC_UNITY_EDITOR_PLUGIN_ID: &str = "agc-unity-editor";
|
||||||
|
pub(crate) const AGC_UNITY_EDITOR_TOOL_NAME: &str = "unity.editor.execute";
|
||||||
|
|
||||||
const STATE_FILE_NAME: &str = "builtin-plugins.json";
|
const STATE_FILE_NAME: &str = "builtin-plugins.json";
|
||||||
const STATE_SCHEMA_VERSION: &str = "agc.builtin-plugins.v1";
|
const STATE_SCHEMA_VERSION: &str = "agc.builtin-plugins.v1";
|
||||||
@@ -24,31 +26,34 @@ const STATE_SCHEMA_VERSION: &str = "agc.builtin-plugins.v1";
|
|||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub(crate) enum BuiltinPlugin {
|
pub(crate) enum BuiltinPlugin {
|
||||||
CocosEditor,
|
CocosEditor,
|
||||||
|
UnityEditor,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BuiltinPlugin {
|
impl BuiltinPlugin {
|
||||||
pub(crate) fn id(self) -> &'static str {
|
pub(crate) fn id(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::CocosEditor => AGC_COCOS_EDITOR_PLUGIN_ID,
|
Self::CocosEditor => AGC_COCOS_EDITOR_PLUGIN_ID,
|
||||||
|
Self::UnityEditor => AGC_UNITY_EDITOR_PLUGIN_ID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 未持久化任何开关时的默认状态。
|
/// 未持久化任何开关时的默认状态。
|
||||||
fn default_enabled(self) -> bool {
|
fn default_enabled(self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Self::CocosEditor => true,
|
Self::CocosEditor | Self::UnityEditor => true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 该插件是否向 Agent 暴露 Runtime 工具。
|
/// 该插件是否向 Agent 暴露 Runtime 工具。
|
||||||
fn exposes_agent_tools(self) -> bool {
|
fn exposes_agent_tools(self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Self::CocosEditor => true,
|
Self::CocosEditor | Self::UnityEditor => true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) const BUILTIN_PLUGINS: &[BuiltinPlugin] = &[BuiltinPlugin::CocosEditor];
|
pub(crate) const BUILTIN_PLUGINS: &[BuiltinPlugin] =
|
||||||
|
&[BuiltinPlugin::CocosEditor, BuiltinPlugin::UnityEditor];
|
||||||
|
|
||||||
pub(crate) fn builtin_plugin(id: &str) -> Option<BuiltinPlugin> {
|
pub(crate) fn builtin_plugin(id: &str) -> Option<BuiltinPlugin> {
|
||||||
BUILTIN_PLUGINS
|
BUILTIN_PLUGINS
|
||||||
@@ -237,7 +242,16 @@ fn persist(guard: &BuiltinPluginState) -> Result<(), String> {
|
|||||||
/// Agent 工具面是否可用:编译期 feature 打开且用户没有禁用该内置插件。
|
/// Agent 工具面是否可用:编译期 feature 打开且用户没有禁用该内置插件。
|
||||||
pub(crate) fn agent_tool_available(plugin: BuiltinPlugin) -> bool {
|
pub(crate) fn agent_tool_available(plugin: BuiltinPlugin) -> bool {
|
||||||
plugin.exposes_agent_tools()
|
plugin.exposes_agent_tools()
|
||||||
&& cfg!(all(windows, feature = "cocos-editor-execute"))
|
&& match plugin {
|
||||||
|
BuiltinPlugin::CocosEditor => cfg!(all(windows, feature = "cocos-editor-execute")),
|
||||||
|
BuiltinPlugin::UnityEditor => {
|
||||||
|
cfg!(all(
|
||||||
|
windows,
|
||||||
|
target_arch = "x86_64",
|
||||||
|
feature = "unity-editor-execute"
|
||||||
|
)) && unity_editor_bridge::is_supported_platform()
|
||||||
|
}
|
||||||
|
}
|
||||||
&& is_enabled(plugin.id())
|
&& is_enabled(plugin.id())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,6 +271,7 @@ pub(crate) fn cocos_editor_agent_tool_available_for_project(root: &Path) -> bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn available_agent_tools() -> Vec<&'static str> {
|
pub(crate) fn available_agent_tools() -> Vec<&'static str> {
|
||||||
|
let mut available = Vec::new();
|
||||||
if cocos_editor_agent_tool_available() {
|
if cocos_editor_agent_tool_available() {
|
||||||
let mut tools = vec![AGC_COCOS_EDITOR_TOOL_NAME];
|
let mut tools = vec![AGC_COCOS_EDITOR_TOOL_NAME];
|
||||||
tools.extend(
|
tools.extend(
|
||||||
@@ -264,19 +279,39 @@ pub(crate) fn available_agent_tools() -> Vec<&'static str> {
|
|||||||
.iter()
|
.iter()
|
||||||
.filter_map(|tool| tool["name"].as_str()),
|
.filter_map(|tool| tool["name"].as_str()),
|
||||||
);
|
);
|
||||||
tools
|
available.extend(tools);
|
||||||
} else {
|
|
||||||
Vec::new()
|
|
||||||
}
|
}
|
||||||
|
if unity_editor_agent_tool_available() {
|
||||||
|
available.push(AGC_UNITY_EDITOR_TOOL_NAME);
|
||||||
|
}
|
||||||
|
available
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Project-scoped variant used by the isolated DirectProject MCP bridge.
|
/// Project-scoped variant used by the isolated DirectProject MCP bridge.
|
||||||
/// Without a project root the safe result is an empty Cocos tool set.
|
/// Without a project root the safe result is an empty Cocos tool set.
|
||||||
pub(crate) fn available_agent_tools_for_project(root: &Path) -> Vec<&'static str> {
|
pub(crate) fn available_agent_tools_for_project(root: &Path) -> Vec<&'static str> {
|
||||||
if !cocos_editor_agent_tool_available_for_project(root) {
|
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
available_agent_tools()
|
available_agent_tools()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|tool| {
|
||||||
|
if *tool == AGC_UNITY_EDITOR_TOOL_NAME {
|
||||||
|
unity_editor_agent_tool_available_for_project(root)
|
||||||
|
} else {
|
||||||
|
cocos_editor_agent_tool_available_for_project(root)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn unity_editor_agent_tool_available() -> bool {
|
||||||
|
agent_tool_available(BuiltinPlugin::UnityEditor)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn unity_editor_agent_tool_available_for_project(root: &Path) -> bool {
|
||||||
|
unity_editor_agent_tool_available()
|
||||||
|
&& crate::project::discover_local_unity_project_root(root)
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -294,6 +329,47 @@ mod tests {
|
|||||||
.unwrap_or_else(|error| error.into_inner())
|
.unwrap_or_else(|error| error.into_inner())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unity_tool_visibility_requires_project_platform_and_independent_toggle() {
|
||||||
|
let _guard = test_lock();
|
||||||
|
let config = tempdir().unwrap();
|
||||||
|
initialize(config.path()).unwrap();
|
||||||
|
let project = tempdir().unwrap();
|
||||||
|
for directory in ["Assets", "Packages", "ProjectSettings"] {
|
||||||
|
fs::create_dir(project.path().join(directory)).unwrap();
|
||||||
|
}
|
||||||
|
fs::write(
|
||||||
|
project.path().join("ProjectSettings/ProjectVersion.txt"),
|
||||||
|
"m_EditorVersion: 6000.0.1f1",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let supported = cfg!(all(
|
||||||
|
windows,
|
||||||
|
target_arch = "x86_64",
|
||||||
|
feature = "unity-editor-execute"
|
||||||
|
));
|
||||||
|
assert_eq!(
|
||||||
|
available_agent_tools_for_project(project.path()).contains(&AGC_UNITY_EDITOR_TOOL_NAME),
|
||||||
|
supported
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!available_agent_tools_for_project(config.path()).contains(&AGC_UNITY_EDITOR_TOOL_NAME)
|
||||||
|
);
|
||||||
|
set_enabled(AGC_COCOS_EDITOR_PLUGIN_ID, false).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
unity_editor_agent_tool_available_for_project(project.path()),
|
||||||
|
supported
|
||||||
|
);
|
||||||
|
set_enabled(AGC_UNITY_EDITOR_PLUGIN_ID, false).unwrap();
|
||||||
|
assert!(!unity_editor_agent_tool_available_for_project(
|
||||||
|
project.path()
|
||||||
|
));
|
||||||
|
set_enabled(AGC_COCOS_EDITOR_PLUGIN_ID, true).unwrap();
|
||||||
|
assert!(!unity_editor_agent_tool_available_for_project(
|
||||||
|
project.path()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn builtin_plugins_default_to_enabled_and_reject_unknown_ids() {
|
fn builtin_plugins_default_to_enabled_and_reject_unknown_ids() {
|
||||||
let _guard = test_lock();
|
let _guard = test_lock();
|
||||||
|
|||||||
@@ -676,6 +676,21 @@ pub(crate) fn import_local_cocos_project(
|
|||||||
import_local_cocos_project_at(root, project_id.trim(), name.trim())
|
import_local_cocos_project_at(root, project_id.trim(), name.trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub(crate) fn import_local_unity_project(
|
||||||
|
project_path: String,
|
||||||
|
project_id: String,
|
||||||
|
name: String,
|
||||||
|
) -> Result<InitLocalProjectResult, String> {
|
||||||
|
let root = Path::new(project_path.trim());
|
||||||
|
enforce_project_permission_policy(root, "project.create")?;
|
||||||
|
if discover_local_unity_project_root(root)?.is_none() {
|
||||||
|
return Err("所选目录不是有效的 Unity 项目".to_string());
|
||||||
|
}
|
||||||
|
let _lock = acquire_project_write_lock(root, "project.create")?;
|
||||||
|
import_local_unity_project_at(root, project_id.trim(), name.trim())
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub(crate) fn is_local_project_directory_non_empty(project_path: String) -> Result<bool, String> {
|
pub(crate) fn is_local_project_directory_non_empty(project_path: String) -> Result<bool, String> {
|
||||||
let root = Path::new(project_path.trim());
|
let root = Path::new(project_path.trim());
|
||||||
@@ -737,6 +752,7 @@ pub(crate) fn inspect_local_project_directory_sync(
|
|||||||
let recent_run_trace = recent_game_creator_run_trace(root);
|
let recent_run_trace = recent_game_creator_run_trace(root);
|
||||||
let godot_project_root = discover_local_godot_project_root(root)?;
|
let godot_project_root = discover_local_godot_project_root(root)?;
|
||||||
let cocos_project_root = discover_local_cocos_project_root(root)?;
|
let cocos_project_root = discover_local_cocos_project_root(root)?;
|
||||||
|
let unity_project_root = discover_local_unity_project_root(root)?;
|
||||||
Ok(LocalProjectDirectoryStatus {
|
Ok(LocalProjectDirectoryStatus {
|
||||||
project_path: root.to_string_lossy().into_owned(),
|
project_path: root.to_string_lossy().into_owned(),
|
||||||
exists: root.exists(),
|
exists: root.exists(),
|
||||||
@@ -746,6 +762,8 @@ pub(crate) fn inspect_local_project_directory_sync(
|
|||||||
godot_project_root,
|
godot_project_root,
|
||||||
is_cocos_project: cocos_project_root.is_some(),
|
is_cocos_project: cocos_project_root.is_some(),
|
||||||
cocos_project_root,
|
cocos_project_root,
|
||||||
|
is_unity_project: unity_project_root.is_some(),
|
||||||
|
unity_project_root,
|
||||||
project_name: game_creator_project_name(root),
|
project_name: game_creator_project_name(root),
|
||||||
modified_at: project_directory_modified_at(root),
|
modified_at: project_directory_modified_at(root),
|
||||||
manifest_error: game_creator_project_manifest_error(root),
|
manifest_error: game_creator_project_manifest_error(root),
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user