Merge remote-tracking branch 'refs/remotes/origin/master' into feat/gptimage2to2.5
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 7m17s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 7m25s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 7m34s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 7m37s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m3s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 3m23s
Project CI / Frontend tests (pull_request) Failing after 14m1s
Project CI / Repository checks (pull_request) Successful in 12m35s
Project CI / Native shell tests (pull_request) Successful in 17m2s
Project CI / Backend tests (pull_request) Successful in 17m18s
Project CI / AI game creator shell web tests (pull_request) Failing after 14m48s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 7m17s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 7m25s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 7m34s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 7m37s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m3s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 3m23s
Project CI / Frontend tests (pull_request) Failing after 14m1s
Project CI / Repository checks (pull_request) Successful in 12m35s
Project CI / Native shell tests (pull_request) Successful in 17m2s
Project CI / Backend tests (pull_request) Successful in 17m18s
Project CI / AI game creator shell web tests (pull_request) Failing after 14m48s
This commit is contained in:
@@ -23,3 +23,6 @@
|
||||
*.meta text
|
||||
*.anim text
|
||||
*.controller text
|
||||
|
||||
# Rust ts-rs 生成的共享契约:保留在仓库中供 TS 消费,但不作为手写源文件统计。
|
||||
packages/shared/src/contracts/generated/** linguist-generated=true
|
||||
|
||||
@@ -221,8 +221,7 @@ jobs:
|
||||
- name: Run AI game creator shell agent-run smoke
|
||||
run: npm run check:native-shells:agc-rust-smoke
|
||||
|
||||
# AGC 壳依赖的共享 / 平台 crate 测试用的是 server-rs workspace 与两个无锁独立 crate
|
||||
# 的 manifest,属另一套依赖图,因此单独一个 job 预热、单独跑。
|
||||
# AGC 壳依赖的共享 / 平台和编辑器插件 crate 各自预热独立 manifest,再运行对应测试。
|
||||
ai-game-creator-shell-rust-crates:
|
||||
name: AI game creator shell Rust crates
|
||||
runs-on: genarrative-ci
|
||||
@@ -263,13 +262,15 @@ jobs:
|
||||
# `cargo test --manifest-path` 单独跑这两个 crate。不在这里预热的话,这两条测试
|
||||
# 会在测试阶段自己 `Updating crates.io index`,crates.io 一抖动整条 job 就红
|
||||
# (见 #327 / PR #316 run 1950)。
|
||||
# 两个 crate 都没有提交 Cargo.lock,所以这里只能做不带锁标志的 fetch:
|
||||
# Cocos 插件也使用独立且未提交的锁文件,一并预热。
|
||||
# 这些 crate 都没有提交 Cargo.lock,所以这里只能做不带锁标志的 fetch:
|
||||
# 加锁标志会因为缺少锁文件直接失败。生成的 Cargo.lock 落在两个 crate 目录内,
|
||||
# 已被各自的 .gitignore 忽略,只留在容器里;随后的测试阶段因此能用锁定版本
|
||||
# 解析,不再触碰 registry index。
|
||||
for manifest_path in \
|
||||
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
|
||||
if cargo fetch \
|
||||
--target x86_64-unknown-linux-gnu \
|
||||
@@ -284,6 +285,23 @@ jobs:
|
||||
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
|
||||
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/NOTICE.md
|
||||
/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/.llm-drafts/
|
||||
/apps/ai-game-creator-shell/game-creator.config.local.json
|
||||
|
||||
+10
-1
@@ -1,5 +1,14 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"semi": true,
|
||||
"trailingComma": "all"
|
||||
"trailingComma": "all",
|
||||
"overrides": [
|
||||
{
|
||||
"files": "packages/shared/src/contracts/generated/**/*.ts",
|
||||
"options": {
|
||||
"printWidth": 1000,
|
||||
"singleQuote": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -676,11 +676,11 @@ async function runSelfTest() {
|
||||
designFoundationAssetCall?.arguments?.input?.outputPath ===
|
||||
'assets/ui-prototype.png' &&
|
||||
designFoundationAssetCall.arguments.input.aspectRatio === '16:9' &&
|
||||
designFoundationAssetCall.arguments.input.assetKind === 'ui-prototype' &&
|
||||
designFoundationAssetCall.arguments.input.assetKind === 'ui-design' &&
|
||||
artAssetPlanAssetCall?.arguments?.input?.outputPath ===
|
||||
'assets/art-spritesheet.png' &&
|
||||
artAssetPlanAssetCall.arguments.input.aspectRatio === '1:1' &&
|
||||
artAssetPlanAssetCall.arguments.input.assetKind === 'art-spritesheet',
|
||||
artAssetPlanAssetCall.arguments.input.assetKind === 'icon-spritesheet',
|
||||
'self-test-visual-assets-invalid',
|
||||
);
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ const requiredFormalArtifactSpecs = [
|
||||
{ path: 'game/balance.json', kind: 'json' },
|
||||
{ path: 'assets/manifest.art.json', kind: 'json' },
|
||||
{ path: 'assets/manifest.audio.json', kind: 'json' },
|
||||
{ path: 'game/index.html', kind: 'game-entry' },
|
||||
{ path: 'game/index.html', kind: 'file' },
|
||||
{ path: 'exports/README.md', kind: 'file' },
|
||||
];
|
||||
const editorImageArtifactSpecs = [
|
||||
|
||||
@@ -105,6 +105,7 @@ export const agcReleasePathPatterns = [
|
||||
'packages/',
|
||||
'server-rs/crates/',
|
||||
'plugins/agc-cocos-editor/',
|
||||
'plugins/agc-unity-editor/',
|
||||
'apps/desktop-shell/src-tauri/icons/',
|
||||
'package.json',
|
||||
'package-lock.json',
|
||||
|
||||
@@ -344,7 +344,11 @@ test('Windows remains the default and explicit Windows overrides macOS environme
|
||||
context,
|
||||
{
|
||||
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'));
|
||||
const configIndex = command.lastIndexOf('--config');
|
||||
const config = JSON.parse(
|
||||
|
||||
@@ -19,6 +19,6 @@ export function withDefaultCargoFeatures(argv, features) {
|
||||
|
||||
export function defaultEditorFeatures(target) {
|
||||
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'),
|
||||
[
|
||||
'build',
|
||||
'--features=cocos-editor-execute',
|
||||
'--features=cocos-editor-execute,unity-editor-execute',
|
||||
'--target',
|
||||
'x86_64-pc-windows-msvc',
|
||||
],
|
||||
|
||||
@@ -143,7 +143,6 @@ const allowedUncalledTauriCommands = [
|
||||
'reload_agc_plugin',
|
||||
'call_agc_plugin',
|
||||
'read_agc_plugin_panel',
|
||||
'set_agc_plugin_project_path',
|
||||
'set_agc_plugin_enabled',
|
||||
];
|
||||
const sourceExtensions = new Set([
|
||||
|
||||
@@ -290,7 +290,7 @@ function deterministicArtManifest(
|
||||
{
|
||||
id: 'garden-guardians-spritesheet',
|
||||
path: 'assets/art-spritesheet.png',
|
||||
kind: 'art-spritesheet',
|
||||
kind: 'icon-spritesheet',
|
||||
usage: ['defenders', 'enemies', 'battlefield-ui'],
|
||||
source: 'canvas',
|
||||
status: 'ready',
|
||||
@@ -710,7 +710,7 @@ function canvasAssetCall(agentId) {
|
||||
outputPath: 'assets/ui-prototype.png',
|
||||
aspectRatio: '16:9',
|
||||
imageSize: '2K',
|
||||
assetKind: 'ui-prototype',
|
||||
assetKind: 'ui-design',
|
||||
assetLabel: '游戏横屏界面原型图',
|
||||
replaceExisting: false,
|
||||
});
|
||||
@@ -721,7 +721,7 @@ function canvasAssetCall(agentId) {
|
||||
outputPath: 'assets/art-spritesheet.png',
|
||||
aspectRatio: '1:1',
|
||||
imageSize: '1K',
|
||||
assetKind: 'art-spritesheet',
|
||||
assetKind: 'icon-spritesheet',
|
||||
assetLabel: '游戏首版核心美术素材',
|
||||
replaceExisting: false,
|
||||
sliceMode: 'connected-components',
|
||||
@@ -830,12 +830,12 @@ function missingGeneratedVisualAssetObservation(context, agentId) {
|
||||
},
|
||||
'design-foundation': {
|
||||
path: 'assets/ui-prototype.png',
|
||||
kind: 'ui-prototype',
|
||||
kind: 'ui-design',
|
||||
summary: '策划界面原型图尚未按正式视觉流程生成并登记,不能完成任务',
|
||||
},
|
||||
'art-asset-plan': {
|
||||
path: 'assets/art-spritesheet.png',
|
||||
kind: 'art-spritesheet',
|
||||
kind: 'icon-spritesheet',
|
||||
summary: '首版美术素材图尚未按正式视觉流程生成并登记,不能完成任务',
|
||||
},
|
||||
}[agentId];
|
||||
@@ -2667,7 +2667,7 @@ function createDeterministicCanvasFixture(apiKey) {
|
||||
asset: {
|
||||
assetId: `asset-${sliceImageId}`,
|
||||
assetObjectId: sliceAssetObjectId,
|
||||
assetKind: 'art-spritesheet-slice',
|
||||
assetKind: 'icon',
|
||||
projectId,
|
||||
taskId,
|
||||
},
|
||||
@@ -2707,7 +2707,7 @@ function createDeterministicCanvasFixture(apiKey) {
|
||||
spritesheetAsset: {
|
||||
assetId: `asset-${imageId}`,
|
||||
assetObjectId,
|
||||
assetKind: 'art-spritesheet',
|
||||
assetKind: 'icon-spritesheet',
|
||||
projectId,
|
||||
taskId,
|
||||
},
|
||||
@@ -2749,7 +2749,7 @@ function createDeterministicCanvasFixture(apiKey) {
|
||||
const assetObjectId = `asset-object-${imageId}`;
|
||||
const resourceId = `resource-${imageId}`;
|
||||
const assetKind =
|
||||
typeof body?.assetKind === 'string' ? body.assetKind : 'game-art';
|
||||
typeof body?.assetKind === 'string' ? body.assetKind : 'image';
|
||||
images.set(imageId, {
|
||||
...image,
|
||||
objectKey,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { withDefaultCargoFeatures } from './cargo-features.mjs';
|
||||
import {
|
||||
defaultEditorFeatures,
|
||||
withDefaultCargoFeatures,
|
||||
} from './cargo-features.mjs';
|
||||
import {
|
||||
readAgcDevEndpoint,
|
||||
resolveAgcDevEndpoint,
|
||||
@@ -47,9 +50,8 @@ function buildTauriArguments(argv, devUrl = readAgcDevEndpoint().url) {
|
||||
];
|
||||
}
|
||||
|
||||
// `agc_cocos_execute` 与 Cocos 编辑器适配器只在 `cocos-editor-execute` feature 下
|
||||
// 注册。开发构建默认在 Windows 打开它,否则 Agent 的工具清单里根本没有该工具,
|
||||
// 只能退化成改写脚本。可用 AGC_DEV_CARGO_FEATURES(逗号分隔)覆盖,传空串即关闭。
|
||||
// 开发和发行构建使用同一平台编辑器 feature 集合。
|
||||
// 可用 AGC_DEV_CARGO_FEATURES(逗号分隔)覆盖,传空串即关闭。
|
||||
function readDevCargoFeatures(env = process.env) {
|
||||
const override = env.AGC_DEV_CARGO_FEATURES;
|
||||
if (override !== undefined) {
|
||||
@@ -58,7 +60,7 @@ function readDevCargoFeatures(env = process.env) {
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
return process.platform === 'win32' ? ['cocos-editor-execute'] : [];
|
||||
return defaultEditorFeatures(process.platform);
|
||||
}
|
||||
|
||||
function withDevCargoFeatures(argv, features = readDevCargoFeatures()) {
|
||||
|
||||
+13
@@ -1793,6 +1793,7 @@ dependencies = [
|
||||
"ttf-parser",
|
||||
"typed_floats",
|
||||
"unicode-normalization",
|
||||
"unity-editor-bridge",
|
||||
"url",
|
||||
"uuid",
|
||||
"windows-sys 0.61.2",
|
||||
@@ -3931,6 +3932,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5016,6 +5018,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"ts-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6407,6 +6410,16 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||
|
||||
[[package]]
|
||||
name = "unity-editor-bridge"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"editor-adapter-api",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unsafe-libyaml"
|
||||
version = "0.2.11"
|
||||
|
||||
@@ -12,6 +12,7 @@ template-library-fixtures = []
|
||||
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"]
|
||||
unity-editor-execute = []
|
||||
|
||||
[build-dependencies]
|
||||
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" }
|
||||
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" }
|
||||
unity-editor-bridge = { path = "../../../plugins/agc-unity-editor/native/unity-editor-bridge" }
|
||||
base64 = "0.22"
|
||||
axum = "0.8"
|
||||
chromiumoxide = "0.9.1"
|
||||
@@ -56,7 +58,7 @@ platform-agent = { path = "../../../server-rs/crates/platform-agent" }
|
||||
portable-pty = "0.9"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "multipart", "native-tls", "stream"] }
|
||||
regex = "1"
|
||||
shared-contracts = { path = "../../../server-rs/crates/shared-contracts", default-features = false }
|
||||
shared-contracts = { path = "../../../server-rs/crates/shared-contracts", default-features = false, features = ["ts-bindings"] }
|
||||
tauri = { version = "2.11.2", features = [] }
|
||||
tauri-plugin-dialog = "2.7.1"
|
||||
tauri-plugin-http = { version = "2.5.9", default-features = false, features = ["charset", "cookies", "http2", "rustls-tls"] }
|
||||
|
||||
@@ -196,6 +196,7 @@ fn main() {
|
||||
);
|
||||
let manifest_path = manifest_dir.join("prompts/runtime/manifest.json");
|
||||
stage_bundled_codex_cli(&manifest_dir);
|
||||
prepare_unity_editor_helper(&manifest_dir);
|
||||
stage_plugin_workspace(&manifest_dir);
|
||||
stage_cocos_editor_payload(&manifest_dir);
|
||||
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))]
|
||||
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/` 工作区里的插件包随包映射到应用资源目录。
|
||||
///
|
||||
/// 只复制插件运行需要的清单、入口、面板和 native payload,不复制 native 源码、
|
||||
@@ -313,9 +415,15 @@ fn stage_plugin_workspace(manifest_dir: &std::path::Path) {
|
||||
for relative in [
|
||||
std::path::PathBuf::from("src"),
|
||||
std::path::PathBuf::from("panels"),
|
||||
std::path::PathBuf::from("skills"),
|
||||
std::path::PathBuf::from("native/payload"),
|
||||
std::path::PathBuf::from("dotnet/publish/win-x64"),
|
||||
] {
|
||||
if relative == std::path::Path::new("native/payload") && !target.contains("windows") {
|
||||
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;
|
||||
}
|
||||
copy_plugin_tree(&plugin_root.join(&relative), &destination.join(&relative));
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
"agents/openai.yaml",
|
||||
"references/platform-art-contract.md"
|
||||
],
|
||||
"sha256": "47ac742d9b88e5d6cd9833484ab212152578e58ae27f7add312fd1d78183385c"
|
||||
"sha256": "6668bf1aa69601bcc65c97fdcd879c699c3139befcd70805d95614997f6e44e9"
|
||||
},
|
||||
{
|
||||
"name": "agc-web-game-development",
|
||||
|
||||
+2
-2
@@ -19,13 +19,13 @@ image, UI design image, or publication material; use `agc_edit_image` for an
|
||||
edit of an existing registered image; use `taonier_prepare_game_art` only for
|
||||
the complete game-art package and its canonical slices.
|
||||
|
||||
With `agc_generate_image`, `kind="character"` and `kind="art-spritesheet"`
|
||||
With `agc_generate_image`, `kind="character"` and `kind="icon-spritesheet"`
|
||||
generate the subject on a solid-colour background and automatically matte it
|
||||
away afterwards, producing transparent-background results; write the prompt
|
||||
for the subject only, never for a scene. `kind="image"` keeps the rendered
|
||||
frame without extra processing.
|
||||
|
||||
When `agc_generate_image` is used with `kind="art-spritesheet"`, `sliceMode` is
|
||||
When `agc_generate_image` is used with `kind="icon-spritesheet"`, `sliceMode` is
|
||||
required and has no default, so decide it explicitly:
|
||||
|
||||
- Use `sliceMode="grid"` with `gridX` and `gridY` (1-32 each) only when the user
|
||||
|
||||
@@ -1198,7 +1198,7 @@ mod tests {
|
||||
"tool": "agc_generate_image",
|
||||
"arguments": {
|
||||
"prompt": prompt,
|
||||
"kind": "art-spritesheet",
|
||||
"kind": "icon-spritesheet",
|
||||
"sliceMode": "connected-components",
|
||||
"sliceCount": 8,
|
||||
"screenColor": "#CFEFFF"
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
use super::model::{DirectCodexUserContentPart, DirectCodexUserItem};
|
||||
use super::model::{DirectCodexUserContentPart, DirectCodexUserItem, DirectCodexUserMessageItem};
|
||||
use super::validation::validate_direct_codex_user_item;
|
||||
use crate::agent::{read_manifest_for_project, sanitize_attachment_local_path};
|
||||
use crate::ui_editor::persistence::{
|
||||
generate_ui_design_code_at, GenerateUiDesignCodeInput, UI_DESIGN_DOC_ASSET_KIND,
|
||||
UI_DESIGN_DOC_MEDIA_TYPE,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use std::path::Path;
|
||||
|
||||
@@ -111,29 +115,165 @@ pub(crate) fn direct_codex_user_item_to_prompt(
|
||||
item: &DirectCodexUserItem,
|
||||
) -> Result<String, String> {
|
||||
let wire = direct_codex_user_item_to_wire_input(root, item)?;
|
||||
wire.as_array()
|
||||
let DirectCodexUserItem::Message(message) = item;
|
||||
let mut prompt = wire
|
||||
.as_array()
|
||||
.ok_or_else(|| "DirectProject user item wire input 不是数组".to_string())
|
||||
.map(|parts| {
|
||||
.and_then(|parts| {
|
||||
parts
|
||||
.iter()
|
||||
.filter_map(|part| part.get("text").and_then(Value::as_str))
|
||||
.collect::<String>()
|
||||
})
|
||||
.and_then(|prompt| {
|
||||
if prompt.trim().is_empty() {
|
||||
Err("DirectProject user item 不能转换为空 prompt".to_string())
|
||||
} else {
|
||||
Ok(prompt)
|
||||
.map(|part| {
|
||||
part.get("text")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| "DirectProject user item wire part 缺少 text".to_string())
|
||||
})
|
||||
.collect::<Result<String, String>>()
|
||||
})?;
|
||||
if let Some(code_context) = render_ui_design_code_context(root, message)? {
|
||||
prompt.push('\n');
|
||||
prompt.push_str(&code_context);
|
||||
}
|
||||
if prompt.trim().is_empty() {
|
||||
return Err("DirectProject user item 不能转换为空 prompt".to_string());
|
||||
}
|
||||
Ok(prompt)
|
||||
}
|
||||
|
||||
/// 本轮 prompt 的 UI 设计文档引用上下文:复用 UI Editor 代码导出,把带文档注释的
|
||||
/// JS 片段路径交给模型;生成失败只追加原始错误,不阻断本轮引用,其它引用继续处理。
|
||||
///
|
||||
/// 只在生成本轮 prompt 时展开:历史 item 回读走 `direct_codex_user_item_to_response_item`
|
||||
/// 的纯投影,不得在这里产生项目写副作用。
|
||||
fn render_ui_design_code_context(
|
||||
root: &Path,
|
||||
message: &DirectCodexUserMessageItem,
|
||||
) -> Result<Option<String>, String> {
|
||||
let referenced_ids = message
|
||||
.content
|
||||
.iter()
|
||||
.filter_map(|part| match part {
|
||||
DirectCodexUserContentPart::AgcResourceReference { resource_id } => {
|
||||
Some(resource_id.trim())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if referenced_ids.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
let manifest = read_manifest_for_project(root)?;
|
||||
let mut lines = Vec::new();
|
||||
for resource_id in referenced_ids {
|
||||
let is_ui_design_doc = manifest.assets.iter().any(|asset| {
|
||||
asset.id == resource_id
|
||||
&& asset.kind == UI_DESIGN_DOC_ASSET_KIND
|
||||
&& asset.media_type == UI_DESIGN_DOC_MEDIA_TYPE
|
||||
});
|
||||
if !is_ui_design_doc {
|
||||
continue;
|
||||
}
|
||||
lines.push(
|
||||
match generate_ui_design_code_at(GenerateUiDesignCodeInput {
|
||||
project_path: root.to_string_lossy().into_owned(),
|
||||
expected_project_id: manifest.project_id.clone(),
|
||||
asset_id: resource_id.to_string(),
|
||||
}) {
|
||||
Ok(result) => format!("请先阅读生成的带有文档的代码片段: {}", result.relative_path),
|
||||
Err(error) => format!("生成代码遇到错误{error}"),
|
||||
},
|
||||
);
|
||||
}
|
||||
if lines.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(lines.join("\n")))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::direct_codex_user_item_to_response_item;
|
||||
use super::{direct_codex_user_item_to_prompt, direct_codex_user_item_to_response_item};
|
||||
use crate::ui_editor::persistence::UI_DESIGN_DOC_MEDIA_TYPE;
|
||||
use serde_json::json;
|
||||
use shared_contracts::game_creation_app::{
|
||||
GameCreationAppAssetKind, GameCreationAppAssetSource, GameCreationAppAssetSourceKind,
|
||||
};
|
||||
use std::path::Path;
|
||||
|
||||
const PROMPT_CONTEXT_PROJECT_ID: &str = "direct-prompt-context-project";
|
||||
|
||||
fn prompt_context_project() -> tempfile::TempDir {
|
||||
let directory = tempfile::tempdir().expect("temp project");
|
||||
crate::init_local_game_project_at(
|
||||
directory.path(),
|
||||
PROMPT_CONTEXT_PROJECT_ID,
|
||||
"Direct prompt 上下文测试",
|
||||
)
|
||||
.expect("init project");
|
||||
directory
|
||||
}
|
||||
|
||||
fn register_fixture_asset(
|
||||
root: &Path,
|
||||
relative_path: &str,
|
||||
kind: GameCreationAppAssetKind,
|
||||
media_type: &str,
|
||||
) -> String {
|
||||
let absolute_path = crate::resolve_local_project_path(root, relative_path)
|
||||
.expect("resolve fixture asset path");
|
||||
std::fs::create_dir_all(absolute_path.parent().expect("fixture asset parent"))
|
||||
.expect("create fixture asset parent");
|
||||
std::fs::write(&absolute_path, b"{}").expect("write fixture asset file");
|
||||
crate::assets::register_local_asset_at(
|
||||
root,
|
||||
relative_path,
|
||||
kind,
|
||||
media_type,
|
||||
"prompt-context-test",
|
||||
GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Generated,
|
||||
canvas_project_id: None,
|
||||
resource_id: Some("prompt-context".to_string()),
|
||||
asset_object_id: None,
|
||||
task_id: None,
|
||||
prompt: None,
|
||||
model: None,
|
||||
generation_route: None,
|
||||
generation_kind: None,
|
||||
reference_resource_ids: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("register fixture asset")
|
||||
.id
|
||||
}
|
||||
|
||||
fn ui_design_doc_fixture(initialize_state: bool) -> (tempfile::TempDir, String) {
|
||||
let directory = prompt_context_project();
|
||||
let asset_id = register_fixture_asset(
|
||||
directory.path(),
|
||||
"ui/design.json",
|
||||
GameCreationAppAssetKind::UiDesignDoc,
|
||||
UI_DESIGN_DOC_MEDIA_TYPE,
|
||||
);
|
||||
if initialize_state {
|
||||
crate::ui_editor::persistence::initialize_ui_design_state_at(
|
||||
directory.path(),
|
||||
PROMPT_CONTEXT_PROJECT_ID,
|
||||
&asset_id,
|
||||
)
|
||||
.expect("initialize UI design state");
|
||||
}
|
||||
(directory, asset_id)
|
||||
}
|
||||
|
||||
fn user_item_with_resource_reference(asset_id: &str) -> serde_json::Value {
|
||||
json!({
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"id": "turn-1:user",
|
||||
"content": [{"type": "agc_resource_reference", "resourceId": asset_id}],
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standard_response_item_passes_through_without_agc_private_parts() {
|
||||
let item = json!({
|
||||
@@ -174,4 +314,81 @@ mod tests {
|
||||
.expect_err("history item without type must fail");
|
||||
assert!(error.contains("缺少 type"), "{error}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui_design_doc_reference_appends_generated_code_context() {
|
||||
let (project, asset_id) = ui_design_doc_fixture(true);
|
||||
let item: super::DirectCodexUserItem =
|
||||
serde_json::from_value(user_item_with_resource_reference(&asset_id))
|
||||
.expect("canonical user item");
|
||||
let prompt =
|
||||
direct_codex_user_item_to_prompt(project.path(), &item).expect("prompt projection");
|
||||
assert!(
|
||||
prompt.contains(&format!(
|
||||
"[素材引用 resourceId={asset_id};项目路径=ui/design.json]"
|
||||
)),
|
||||
"{prompt}"
|
||||
);
|
||||
assert!(
|
||||
prompt.contains("请先阅读生成的带有文档的代码片段: ui/generated-"),
|
||||
"{prompt}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_projection_never_writes_generated_ui_design_code() {
|
||||
let (project, asset_id) = ui_design_doc_fixture(true);
|
||||
let item = user_item_with_resource_reference(&asset_id);
|
||||
direct_codex_user_item_to_response_item(project.path(), &item).expect("history projection");
|
||||
let generated_root =
|
||||
crate::resolve_local_project_path(project.path(), "ui").expect("resolve ui directory");
|
||||
let generated_files = std::fs::read_dir(generated_root)
|
||||
.expect("read ui directory")
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter(|entry| {
|
||||
entry
|
||||
.file_name()
|
||||
.to_string_lossy()
|
||||
.starts_with("generated-")
|
||||
})
|
||||
.count();
|
||||
assert_eq!(
|
||||
generated_files, 0,
|
||||
"历史回读只做纯投影,不得生成 UI 设计代码"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui_design_generation_failure_keeps_reference_and_reports_error() {
|
||||
let (project, asset_id) = ui_design_doc_fixture(false);
|
||||
let item: super::DirectCodexUserItem =
|
||||
serde_json::from_value(user_item_with_resource_reference(&asset_id))
|
||||
.expect("canonical user item");
|
||||
let prompt =
|
||||
direct_codex_user_item_to_prompt(project.path(), &item).expect("prompt projection");
|
||||
assert!(prompt.contains("素材引用 resourceId="), "{prompt}");
|
||||
assert!(prompt.contains("生成代码遇到错误"), "{prompt}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn other_asset_kind_reference_does_not_generate_ui_design_code() {
|
||||
let project = prompt_context_project();
|
||||
let asset_id = register_fixture_asset(
|
||||
project.path(),
|
||||
"assets/hero.png",
|
||||
GameCreationAppAssetKind::Character,
|
||||
"image/png",
|
||||
);
|
||||
let item: super::DirectCodexUserItem =
|
||||
serde_json::from_value(user_item_with_resource_reference(&asset_id))
|
||||
.expect("canonical user item");
|
||||
let prompt =
|
||||
direct_codex_user_item_to_prompt(project.path(), &item).expect("prompt projection");
|
||||
assert!(prompt.contains("素材引用 resourceId="), "{prompt}");
|
||||
assert!(
|
||||
!prompt.contains("请先阅读生成的带有文档的代码片段"),
|
||||
"{prompt}"
|
||||
);
|
||||
assert!(!prompt.contains("生成代码遇到错误"), "{prompt}");
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1197,9 +1197,9 @@ fn bridge_registered_resource(
|
||||
"mediaType": asset.media_type,
|
||||
// Agent 与 UI 必须看到同一个口径:UI 栏目走 TS 的 `gameCreationAppAssetCategory`
|
||||
// (落盘值 + 按 kind 派生 + 读时自愈),这里走 Rust 的同构实现。
|
||||
// 直接透传落盘 `asset.category` 会让 `kind:"ui"` 的资产在 UI 显示「UI 交互」、
|
||||
// 直接透传落盘 `asset.category` 会让 UI 设计资产在 UI 显示「UI 交互」、
|
||||
// 在 Agent 侧读到 `unclassified`(真机 55 条分歧)。
|
||||
"category": game_creation_app_asset_effective_category(&asset.kind, asset.category),
|
||||
"category": game_creation_app_asset_effective_category(asset.kind, asset.category),
|
||||
"tags": asset.tags,
|
||||
"canvasProjectId": asset.source.canvas_project_id,
|
||||
"resourceId": asset.source.resource_id,
|
||||
@@ -1240,7 +1240,9 @@ fn bridge_list_registered_assets(root: &Path, arguments: &Value) -> Value {
|
||||
arguments,
|
||||
"kind",
|
||||
DIRECT_TOOL_BRIDGE_MAX_RESOURCE_KIND_CHARS,
|
||||
)?;
|
||||
)?
|
||||
.map(|kind| bridge_asset_list_kind_filter(&kind))
|
||||
.transpose()?;
|
||||
let asset_id = bridge_optional_bounded_string(
|
||||
arguments,
|
||||
"assetId",
|
||||
@@ -1260,7 +1262,7 @@ fn bridge_list_registered_assets(root: &Path, arguments: &Value) -> Value {
|
||||
let mut assets = manifest
|
||||
.assets
|
||||
.iter()
|
||||
.filter(|asset| kind.as_ref().is_none_or(|kind| asset.kind == *kind))
|
||||
.filter(|asset| kind.is_none_or(|kind| asset.kind == kind))
|
||||
.filter(|asset| {
|
||||
asset_id
|
||||
.as_ref()
|
||||
@@ -1326,6 +1328,24 @@ fn bridge_list_registered_assets(root: &Path, arguments: &Value) -> Value {
|
||||
}
|
||||
}
|
||||
|
||||
/// `asset.list` 的 `kind` 过滤:只接受 canonical 值,认不出的值直接报错。
|
||||
///
|
||||
/// 以前认不出的值会收口成 `unknown` 再参与等值过滤,结果是**静默返回空列表**:调用方(模型)
|
||||
/// 会以为项目里没有这类资产,而不是"你传的 kind 不合法",于是继续按错误前提往下走。
|
||||
/// `unknown` 本身仍是合法输入(确有 kind 未知的资产),只拒绝"既不是 canonical、也不是字面
|
||||
/// `unknown`"的原值。
|
||||
fn bridge_asset_list_kind_filter(raw: &str) -> Result<GameCreationAppAssetKind, String> {
|
||||
let kind = GameCreationAppAssetKind::parse_with_context(raw, "asset.list.kind");
|
||||
if kind == GameCreationAppAssetKind::Unknown
|
||||
&& raw.trim() != GameCreationAppAssetKind::Unknown.as_str()
|
||||
{
|
||||
return Err(format!(
|
||||
"kind 不是已知的 manifest 资源 kind:{raw};请改用 canonical kind(如 image、scene、character、icon、icon-spritesheet、character-animation、audio、video、document)"
|
||||
));
|
||||
}
|
||||
Ok(kind)
|
||||
}
|
||||
|
||||
fn bridge_project_file_class(path: &str) -> (&'static str, Option<&'static str>) {
|
||||
let extension = Path::new(path)
|
||||
.extension()
|
||||
@@ -1896,7 +1916,7 @@ async fn bridge_create_or_derive_resource(
|
||||
source_asset_id: source_asset.as_ref().map(|asset| asset.id.clone()),
|
||||
source_path: source_asset.as_ref().map(|asset| asset.local_path.clone()),
|
||||
source_media_type: source_asset.as_ref().map(|asset| asset.media_type.clone()),
|
||||
source_subtype: source_asset.as_ref().map(|asset| asset.kind.clone()),
|
||||
source_subtype: source_asset.as_ref().map(|asset| asset.kind.to_string()),
|
||||
producer_task_id: source_asset
|
||||
.as_ref()
|
||||
.and_then(|asset| asset.source.task_id.clone()),
|
||||
@@ -2006,7 +2026,7 @@ async fn bridge_remove_background(state: &DirectToolBridgeState, arguments: &Val
|
||||
source_asset_id: Some(source_asset_id.clone()),
|
||||
source_path: Some(source_asset.local_path.clone()),
|
||||
source_media_type: Some(source_asset.media_type.clone()),
|
||||
source_subtype: Some(source_asset.kind.clone()),
|
||||
source_subtype: Some(source_asset.kind.to_string()),
|
||||
producer_task_id: source_asset.source.task_id.clone(),
|
||||
source_version_id: None,
|
||||
prompt: "去除背景".to_string(),
|
||||
@@ -2174,35 +2194,37 @@ async fn bridge_prepare_game_art(state: &DirectToolBridgeState, arguments: &Valu
|
||||
result
|
||||
}
|
||||
|
||||
fn bridge_image_generation_kind(arguments: &Value) -> Result<String, String> {
|
||||
fn bridge_image_generation_kind(arguments: &Value) -> Result<GameCreationAppAssetKind, String> {
|
||||
let kind = arguments
|
||||
.get("kind")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or("image");
|
||||
normalize_platform_art_asset_generation_kind(kind)
|
||||
.map(str::to_string)
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"工具参数 kind 只允许 {}",
|
||||
PLATFORM_ART_ASSET_GENERATION_KINDS.join("、")
|
||||
)
|
||||
})
|
||||
normalize_platform_art_asset_generation_kind(kind).ok_or_else(|| {
|
||||
format!(
|
||||
"工具参数 kind 只允许 {}",
|
||||
PLATFORM_ART_ASSET_GENERATION_KINDS
|
||||
.iter()
|
||||
.map(|kind| kind.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join("、")
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// 切分模式没有默认值:图集必须显式声明,且声明必须与 kind 和网格参数自洽。
|
||||
fn validate_generate_image_slice_declaration(
|
||||
kind: &str,
|
||||
kind: GameCreationAppAssetKind,
|
||||
slice_mode: Option<&str>,
|
||||
grid_x: Option<u32>,
|
||||
grid_y: Option<u32>,
|
||||
slice_count: Option<usize>,
|
||||
) -> Result<(), String> {
|
||||
if kind == "art-spritesheet" {
|
||||
if kind == GameCreationAppAssetKind::IconSpritesheet {
|
||||
if slice_mode.is_none() {
|
||||
return Err(
|
||||
"kind=art-spritesheet 必须显式声明 sliceMode,没有默认值:需求要求等分网格、固定槽位或指定行列数时传 sliceMode=grid 并提供 gridX/gridY;自由排布、数量不定或只要求一张图集时传 sliceMode=connected-components"
|
||||
"kind=icon-spritesheet 必须显式声明 sliceMode,没有默认值:需求要求等分网格、固定槽位或指定行列数时传 sliceMode=grid 并提供 gridX/gridY;自由排布、数量不定或只要求一张图集时传 sliceMode=connected-components"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
@@ -2215,19 +2237,19 @@ fn validate_generate_image_slice_declaration(
|
||||
}
|
||||
if slice_mode.is_some() || grid_x.is_some() || grid_y.is_some() || slice_count.is_some() {
|
||||
return Err(format!(
|
||||
"工具参数 sliceMode/gridX/gridY/sliceCount 仅对 kind=art-spritesheet 生效,当前 kind={kind}"
|
||||
"工具参数 sliceMode/gridX/gridY/sliceCount 仅对 kind=icon-spritesheet 生效,当前 kind={kind}"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 抠图纯色背景只服务 character 与 art-spritesheet 链路;格式校验收口为
|
||||
/// 抠图纯色背景只服务 character 与 icon-spritesheet 链路;格式校验收口为
|
||||
/// `auto` 或 `#RRGGBB`(服务端另有支持色板,客户端不复制),`auto`/空串归一为
|
||||
/// None(服务端自动决策),hex 统一大写后透传。其它 kind 携带该字段直接拒绝,
|
||||
/// 避免服务端静默忽略造成“已生效”的误解。
|
||||
fn normalize_generate_image_screen_color(
|
||||
arguments: &Value,
|
||||
kind: &str,
|
||||
kind: GameCreationAppAssetKind,
|
||||
) -> Result<Option<String>, String> {
|
||||
let Some(value) = arguments.get("screenColor") else {
|
||||
return Ok(None);
|
||||
@@ -2235,9 +2257,12 @@ fn normalize_generate_image_screen_color(
|
||||
if value.is_null() {
|
||||
return Ok(None);
|
||||
}
|
||||
if !matches!(kind, "character" | "art-spritesheet") {
|
||||
if !matches!(
|
||||
kind,
|
||||
GameCreationAppAssetKind::Character | GameCreationAppAssetKind::IconSpritesheet
|
||||
) {
|
||||
return Err(format!(
|
||||
"工具参数 screenColor 仅对 kind=character 和 kind=art-spritesheet 生效,当前 kind={kind}"
|
||||
"工具参数 screenColor 仅对 kind=character 和 kind=icon-spritesheet 生效,当前 kind={kind}"
|
||||
));
|
||||
}
|
||||
let raw = value
|
||||
@@ -2364,18 +2389,18 @@ async fn bridge_generate_image(state: &DirectToolBridgeState, arguments: &Value)
|
||||
})
|
||||
.transpose()?;
|
||||
validate_generate_image_slice_declaration(
|
||||
kind.as_str(),
|
||||
kind,
|
||||
slice_mode.as_deref(),
|
||||
grid_x,
|
||||
grid_y,
|
||||
slice_count,
|
||||
)?;
|
||||
let screen_color = normalize_generate_image_screen_color(arguments, kind.as_str())?;
|
||||
let screen_color = normalize_generate_image_screen_color(arguments, kind)?;
|
||||
let options = PlatformArtAssetGenerationOptions {
|
||||
output_path,
|
||||
aspect_ratio,
|
||||
image_size,
|
||||
asset_kind: kind.clone(),
|
||||
asset_kind: kind,
|
||||
asset_label: asset_name.clone(),
|
||||
replace_existing: false,
|
||||
slice_count,
|
||||
@@ -2583,6 +2608,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"))]
|
||||
async fn bridge_cocos_execute(state: &DirectToolBridgeState, arguments: &Value) -> Value {
|
||||
bridge_cocos_call(state, arguments, None).await
|
||||
@@ -2594,6 +2664,9 @@ async fn bridge_cocos_call(
|
||||
arguments: &Value,
|
||||
operation: Option<&str>,
|
||||
) -> 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) {
|
||||
return bridge_tool_result(
|
||||
"当前项目不是 Cocos Creator 项目或 Cocos 插件不可用,agc_cocos_execute 不可用"
|
||||
@@ -2780,6 +2853,8 @@ async fn handle_direct_tool_bridge(
|
||||
"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,
|
||||
#[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"))]
|
||||
operation if cocos_editor_bridge::is_cocos_operation(operation) => {
|
||||
bridge_cocos_call(&state, &request.arguments, Some(operation)).await
|
||||
@@ -2864,14 +2939,19 @@ pub(crate) async fn start_direct_tool_bridge(
|
||||
mod tests {
|
||||
#[test]
|
||||
fn generate_image_slice_declaration_is_explicit_and_self_consistent() {
|
||||
let missing =
|
||||
validate_generate_image_slice_declaration("art-spritesheet", None, None, None, None)
|
||||
.expect_err("art-spritesheet without sliceMode must fail closed");
|
||||
let missing = validate_generate_image_slice_declaration(
|
||||
GameCreationAppAssetKind::IconSpritesheet,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.expect_err("icon-spritesheet without sliceMode must fail closed");
|
||||
assert!(missing.contains("没有默认值"), "{missing}");
|
||||
assert!(missing.contains("connected-components"), "{missing}");
|
||||
|
||||
assert!(validate_generate_image_slice_declaration(
|
||||
"art-spritesheet",
|
||||
GameCreationAppAssetKind::IconSpritesheet,
|
||||
Some("connected-components"),
|
||||
None,
|
||||
None,
|
||||
@@ -2879,7 +2959,7 @@ mod tests {
|
||||
)
|
||||
.is_ok());
|
||||
assert!(validate_generate_image_slice_declaration(
|
||||
"art-spritesheet",
|
||||
GameCreationAppAssetKind::IconSpritesheet,
|
||||
Some("grid"),
|
||||
Some(3),
|
||||
Some(2),
|
||||
@@ -2887,7 +2967,7 @@ mod tests {
|
||||
)
|
||||
.is_ok());
|
||||
let grid_with_count = validate_generate_image_slice_declaration(
|
||||
"art-spritesheet",
|
||||
GameCreationAppAssetKind::IconSpritesheet,
|
||||
Some("grid"),
|
||||
Some(2),
|
||||
Some(2),
|
||||
@@ -2897,58 +2977,80 @@ mod tests {
|
||||
assert!(grid_with_count.contains("gridX×gridY"), "{grid_with_count}");
|
||||
|
||||
let wrong_kind = validate_generate_image_slice_declaration(
|
||||
"image",
|
||||
GameCreationAppAssetKind::Image,
|
||||
Some("connected-components"),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.expect_err("slice declaration must stay scoped to art-spritesheet");
|
||||
.expect_err("slice declaration must stay scoped to icon-spritesheet");
|
||||
assert!(
|
||||
wrong_kind.contains("仅对 kind=art-spritesheet 生效"),
|
||||
wrong_kind.contains("仅对 kind=icon-spritesheet 生效"),
|
||||
"{wrong_kind}"
|
||||
);
|
||||
let wrong_kind_count =
|
||||
validate_generate_image_slice_declaration("image", None, None, None, Some(8))
|
||||
.expect_err("sliceCount-only violation must be rejected");
|
||||
let wrong_kind_count = validate_generate_image_slice_declaration(
|
||||
GameCreationAppAssetKind::Image,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(8),
|
||||
)
|
||||
.expect_err("sliceCount-only violation must be rejected");
|
||||
assert!(
|
||||
wrong_kind_count.contains("sliceCount"),
|
||||
"sliceCount-only violation must name sliceCount: {wrong_kind_count}"
|
||||
);
|
||||
assert!(validate_generate_image_slice_declaration("image", None, None, None, None).is_ok());
|
||||
assert!(validate_generate_image_slice_declaration(
|
||||
GameCreationAppAssetKind::Image,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None
|
||||
)
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_image_screen_color_is_normalized_and_kind_gated() {
|
||||
// 省略与显式 null 等价,且不触发 kind 门禁。
|
||||
assert_eq!(
|
||||
normalize_generate_image_screen_color(&json!({}), "image").expect("omitted"),
|
||||
normalize_generate_image_screen_color(&json!({}), GameCreationAppAssetKind::Image)
|
||||
.expect("omitted"),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_generate_image_screen_color(&json!({"screenColor": null}), "image")
|
||||
.expect("null"),
|
||||
normalize_generate_image_screen_color(
|
||||
&json!({"screenColor": null}),
|
||||
GameCreationAppAssetKind::Image
|
||||
)
|
||||
.expect("null"),
|
||||
None
|
||||
);
|
||||
// auto 家族归一为 None(服务端自动决策),大小写与空白不敏感。
|
||||
for raw in ["auto", "AUTO", " auto ", ""] {
|
||||
assert_eq!(
|
||||
normalize_generate_image_screen_color(&json!({"screenColor": raw}), "character")
|
||||
.expect("auto variants"),
|
||||
normalize_generate_image_screen_color(
|
||||
&json!({"screenColor": raw}),
|
||||
GameCreationAppAssetKind::Character
|
||||
)
|
||||
.expect("auto variants"),
|
||||
None,
|
||||
"{raw}"
|
||||
);
|
||||
}
|
||||
// hex 统一大写透传;色板白名单由服务端权威校验,客户端只守格式。
|
||||
assert_eq!(
|
||||
normalize_generate_image_screen_color(&json!({"screenColor": "#cfefff"}), "character")
|
||||
.expect("lowercase hex"),
|
||||
normalize_generate_image_screen_color(
|
||||
&json!({"screenColor": "#cfefff"}),
|
||||
GameCreationAppAssetKind::Character
|
||||
)
|
||||
.expect("lowercase hex"),
|
||||
Some("#CFEFFF".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_generate_image_screen_color(
|
||||
&json!({"screenColor": " #A0BBA0 "}),
|
||||
"art-spritesheet"
|
||||
GameCreationAppAssetKind::IconSpritesheet
|
||||
)
|
||||
.expect("padded hex"),
|
||||
Some("#A0BBA0".to_string())
|
||||
@@ -2956,19 +3058,24 @@ mod tests {
|
||||
// 非 auto/非 hex、非字符串一律拒绝。
|
||||
for bad in [json!("green"), json!("#GGGGGG"), json!("#FFF"), json!(12)] {
|
||||
assert!(
|
||||
normalize_generate_image_screen_color(&json!({"screenColor": bad}), "character")
|
||||
.is_err(),
|
||||
normalize_generate_image_screen_color(
|
||||
&json!({"screenColor": bad}),
|
||||
GameCreationAppAssetKind::Character
|
||||
)
|
||||
.is_err(),
|
||||
"{bad}"
|
||||
);
|
||||
}
|
||||
// 其它 kind 携带该字段直接拒绝,即使取值合法。
|
||||
let gated =
|
||||
normalize_generate_image_screen_color(&json!({"screenColor": "#CFEFFF"}), "image")
|
||||
.expect_err("screenColor must stay scoped to character/art-spritesheet");
|
||||
let gated = normalize_generate_image_screen_color(
|
||||
&json!({"screenColor": "#CFEFFF"}),
|
||||
GameCreationAppAssetKind::Image,
|
||||
)
|
||||
.expect_err("screenColor must stay scoped to character/icon-spritesheet");
|
||||
assert!(gated.contains("kind=character"), "{gated}");
|
||||
assert!(normalize_generate_image_screen_color(
|
||||
&json!({"screenColor": "auto"}),
|
||||
"ui-prototype"
|
||||
GameCreationAppAssetKind::UiDesign
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
@@ -3737,12 +3844,14 @@ mod tests {
|
||||
fn bridge_art_resource_exposes_only_the_safe_identity_projection() {
|
||||
let asset = GameCreationAppAssetManifestEntry {
|
||||
id: "local-art-1".to_string(),
|
||||
kind: "art-spritesheet-slice".to_string(),
|
||||
kind: GameCreationAppAssetKind::Icon,
|
||||
media_type: "image/png".to_string(),
|
||||
local_path: "assets/art-spritesheet-slices/player.png".to_string(),
|
||||
image_sequence_frames: None,
|
||||
image_sequence_duration_ms: None,
|
||||
category: game_creation_app_asset_category_for_kind("art-spritesheet-slice"),
|
||||
category: game_creation_app_asset_category_for_kind(
|
||||
GameCreationAppAssetKind::IconSpritesheet,
|
||||
),
|
||||
tags: Vec::new(),
|
||||
source: GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Canvas,
|
||||
@@ -3794,7 +3903,7 @@ mod tests {
|
||||
fn bridge_registered_resource_exposes_formal_sequence_without_private_generation_fields() {
|
||||
let asset = GameCreationAppAssetManifestEntry {
|
||||
id: "animation-1".to_string(),
|
||||
kind: "character-animation".to_string(),
|
||||
kind: GameCreationAppAssetKind::CharacterAnimation,
|
||||
media_type: "video/mp4".to_string(),
|
||||
local_path: "assets/edits/animation.mp4".to_string(),
|
||||
image_sequence_frames: Some(vec![
|
||||
@@ -3807,7 +3916,9 @@ mod tests {
|
||||
},
|
||||
]),
|
||||
image_sequence_duration_ms: Some(4_000),
|
||||
category: game_creation_app_asset_category_for_kind("character-animation"),
|
||||
category: game_creation_app_asset_category_for_kind(
|
||||
GameCreationAppAssetKind::CharacterAnimation,
|
||||
),
|
||||
tags: Vec::new(),
|
||||
source: GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Canvas,
|
||||
@@ -3845,7 +3956,7 @@ mod tests {
|
||||
fn bridge_registered_resource_keeps_explicit_manifest_classification() {
|
||||
let asset = GameCreationAppAssetManifestEntry {
|
||||
id: "spec-1".to_string(),
|
||||
kind: "spec".to_string(),
|
||||
kind: GameCreationAppAssetKind::Spec,
|
||||
media_type: "application/json".to_string(),
|
||||
local_path: "assets/specs/hero.json".to_string(),
|
||||
image_sequence_frames: None,
|
||||
@@ -3881,13 +3992,13 @@ mod tests {
|
||||
///
|
||||
/// UI 栏目走 TS 的 `gameCreationAppAssetCategory`(落盘值 + 按 kind 派生 + 读时自愈),
|
||||
/// Agent 走 Rust 的同构实现 `game_creation_app_asset_effective_category`。直接透传落盘
|
||||
/// `category` 会让 `kind:"ui"` 的资产在 UI 显示「UI 交互」、在 Agent 侧读到
|
||||
/// `unclassified`——真机 122 条资产里有 55 条这样分叉。
|
||||
/// `category` 会让 `ui-design` 的资产在 UI 显示「UI 交互」、在 Agent 侧读到
|
||||
/// `unclassified`,两侧分类口径必须一致。
|
||||
#[test]
|
||||
fn bridge_registered_resource_projects_effective_category_not_raw_persisted_value() {
|
||||
let asset = GameCreationAppAssetManifestEntry {
|
||||
id: "ui-1".to_string(),
|
||||
kind: "ui".to_string(),
|
||||
id: "ui-design-1".to_string(),
|
||||
kind: GameCreationAppAssetKind::UiDesign,
|
||||
media_type: "application/json".to_string(),
|
||||
local_path: "assets/UI 设计 1.json".to_string(),
|
||||
image_sequence_frames: None,
|
||||
@@ -3910,7 +4021,7 @@ mod tests {
|
||||
};
|
||||
|
||||
let projection = bridge_registered_resource(&asset, false);
|
||||
assert_eq!(projection["kind"], "ui");
|
||||
assert_eq!(projection["kind"], "ui-design");
|
||||
assert_eq!(projection["category"], "ui-interaction");
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,14 @@ 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")) {
|
||||
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 询问绑定的宿主;失败时不广告可选插件工具。
|
||||
if let Ok(result) = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(5),
|
||||
@@ -96,10 +103,22 @@ async fn direct_tools_mcp_specs() -> Value {
|
||||
.iter()
|
||||
.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 值)对应的提示词上限。
|
||||
@@ -128,7 +147,16 @@ fn resource_tool_prompt_schema_max_chars() -> usize {
|
||||
.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![
|
||||
json!({
|
||||
"name": "client.session.info",
|
||||
@@ -247,7 +275,7 @@ fn direct_tools_mcp_specs_for(controlled_web_search: bool, _cocos_editor_availab
|
||||
"type": "string",
|
||||
"enum": PLATFORM_ART_ASSET_GENERATION_KINDS,
|
||||
"default": "image",
|
||||
"description": "image=普通新图(不做额外处理),character=角色图(纯色底生成后自动抠图,产出透明背景立绘,prompt 只描述角色主体),spec/icon-spec=统一视觉规范图(spec 是服务端同义词,客户端统一登记为 icon-spec),ui-prototype=完整 UI 设计图,art-spritesheet=透明游戏素材图集(纯色底生成后自动抠图并切片,项目须已有 icon-spec 规范图),publication-material=发布宣传图"
|
||||
"description": "image=普通新图(不做额外处理),character=角色图(纯色底生成后自动抠图,产出透明背景立绘,prompt 只描述角色主体),icon-spec=统一视觉规范图,ui-design=完整 UI 设计图,icon-spritesheet=透明游戏素材图集(纯色底生成后自动抠图并切片,项目须已有 icon-spec 规范图),publication-material=发布宣传图"
|
||||
},
|
||||
"aspectRatio": {
|
||||
"type": "string",
|
||||
@@ -273,7 +301,7 @@ fn direct_tools_mcp_specs_for(controlled_web_search: bool, _cocos_editor_availab
|
||||
"sliceMode": {
|
||||
"type": "string",
|
||||
"enum": ["connected-components", "grid"],
|
||||
"description": "仅 kind=art-spritesheet 生效,且必填、没有默认值:需求明确要求等分网格、固定槽位或指定行列数时传 grid,并用 gridX/gridY 传入来自需求本身的行列数;自由排布、数量不定或只要求一张图集时传 connected-components,需要约束素材张数时用 sliceCount。省略、与 kind 不匹配或与 gridX/gridY 互相矛盾时客户端直接拒绝,不会替你选择"
|
||||
"description": "仅 kind=icon-spritesheet 生效,且必填、没有默认值:需求明确要求等分网格、固定槽位或指定行列数时传 grid,并用 gridX/gridY 传入来自需求本身的行列数;自由排布、数量不定或只要求一张图集时传 connected-components,需要约束素材张数时用 sliceCount。省略、与 kind 不匹配或与 gridX/gridY 互相矛盾时客户端直接拒绝,不会替你选择"
|
||||
},
|
||||
"gridX": {
|
||||
"type": "integer",
|
||||
@@ -291,11 +319,11 @@ fn direct_tools_mcp_specs_for(controlled_web_search: bool, _cocos_editor_availab
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 256,
|
||||
"description": "只与 kind=art-spritesheet 且 sliceMode=connected-components 同时提供,用于约束目标素材张数;省略时按图像内容自动识别"
|
||||
"description": "只与 kind=icon-spritesheet 且 sliceMode=connected-components 同时提供,用于约束目标素材张数;省略时按图像内容自动识别"
|
||||
},
|
||||
"screenColor": {
|
||||
"type": "string",
|
||||
"description": "抠图纯色背景,仅 kind=character(角色形象)和 kind=art-spritesheet(图标素材)生效,其它 kind 携带会被拒绝。生成时把主体置于该纯色背景上,回图后据此抠除背景。取值只能是 auto 或下列色板 hex 之一,传值只填 hex 本身、不要附带色名:#CFEFFF(浅雾蓝)、#B0C2E0(浅钢蓝)、#FFD6C2(暖浅桃色)、#E6D8FF(淡薰衣草紫)、#F4D8E8(浅粉灰)、#7FB3FF(中度天蓝)、#FFF2A8(浅柠黄)、#CFFFE1(淡薄荷绿)、#D8DEE8(浅中性灰)、#D8D2E8(淡灰紫)、#A8F7F0(高对比浅青)、#A0BBA0(灰竹绿);auto 时由服务端自动选色。手动指定时不能与角色或素材本体的颜色接近"
|
||||
"description": "抠图纯色背景,仅 kind=character(角色形象)和 kind=icon-spritesheet(图标素材)生效,其它 kind 携带会被拒绝。生成时把主体置于该纯色背景上,回图后据此抠除背景。取值只能是 auto 或下列色板 hex 之一,传值只填 hex 本身、不要附带色名:#CFEFFF(浅雾蓝)、#B0C2E0(浅钢蓝)、#FFD6C2(暖浅桃色)、#E6D8FF(淡薰衣草紫)、#F4D8E8(浅粉灰)、#7FB3FF(中度天蓝)、#FFF2A8(浅柠黄)、#CFFFE1(淡薄荷绿)、#D8DEE8(浅中性灰)、#D8D2E8(淡灰紫)、#A8F7F0(高对比浅青)、#A0BBA0(灰竹绿);auto 时由服务端自动选色。手动指定时不能与角色或素材本体的颜色接近"
|
||||
}
|
||||
},
|
||||
"required": ["prompt"],
|
||||
@@ -341,7 +369,7 @@ fn direct_tools_mcp_specs_for(controlled_web_search: bool, _cocos_editor_availab
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 80,
|
||||
"description": "可选的 manifest 资源 kind 精确过滤,例如 video、character-animation、sound-effect、background-music 或 art-spritesheet-slice"
|
||||
"description": "可选的 manifest 资源 kind 精确过滤,必须是 canonical kind(例如 video、character-animation、sound-effect、background-music、icon-spritesheet);传非 canonical 值会被拒绝并报错,不会静默返回空列表"
|
||||
},
|
||||
"assetId": {
|
||||
"type": "string",
|
||||
@@ -568,6 +596,14 @@ fn direct_tools_mcp_specs_for(controlled_web_search: bool, _cocos_editor_availab
|
||||
.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 {
|
||||
tools.push(json!({
|
||||
"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
|
||||
}
|
||||
|
||||
#[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 {
|
||||
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,
|
||||
#[cfg(all(windows, feature = "cocos-editor-execute"))]
|
||||
"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"))]
|
||||
operation if cocos_editor_bridge::is_cocos_operation(operation) => {
|
||||
call_client_tool_bridge(operation, &arguments).await
|
||||
@@ -2523,7 +2579,7 @@ mod tests {
|
||||
// 目录外的 kind 必须在本地拒绝:既不下发 bridge,也不产生任何计费副作用。
|
||||
let unsupported_kind = call_agc_generate_image(&json!({
|
||||
"prompt": "像素月光主角",
|
||||
"kind": "game-art"
|
||||
"kind": "future-kind"
|
||||
}))
|
||||
.await;
|
||||
assert_eq!(unsupported_kind["isError"], json!(true));
|
||||
@@ -2533,7 +2589,7 @@ mod tests {
|
||||
|
||||
let oversized_prompt = call_agc_generate_image(&json!({
|
||||
"prompt": "x".repeat(DIRECT_TOOLS_MCP_MAX_IMAGE_PROMPT_CHARS + 1),
|
||||
"kind": "art-spritesheet"
|
||||
"kind": "icon-spritesheet"
|
||||
}))
|
||||
.await;
|
||||
assert_eq!(oversized_prompt["isError"], json!(true));
|
||||
@@ -2897,12 +2953,12 @@ mod tests {
|
||||
fn tool_chain_image_asset(id: &str, local_path: &str) -> GameCreationAppAssetManifestEntry {
|
||||
GameCreationAppAssetManifestEntry {
|
||||
id: id.to_string(),
|
||||
kind: "image".to_string(),
|
||||
kind: GameCreationAppAssetKind::Image,
|
||||
media_type: "image/png".to_string(),
|
||||
local_path: local_path.to_string(),
|
||||
image_sequence_frames: None,
|
||||
image_sequence_duration_ms: None,
|
||||
category: game_creation_app_asset_category_for_kind("image"),
|
||||
category: game_creation_app_asset_category_for_kind(GameCreationAppAssetKind::Image),
|
||||
tags: Vec::new(),
|
||||
source: GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Canvas,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+8
-4
@@ -31,9 +31,9 @@ fn is_direct_platform_art_generation_runtime_context(
|
||||
context: &PlatformArtGenerationRuntimeContext,
|
||||
) -> bool {
|
||||
let asset_kind = match context.run_id.as_str() {
|
||||
"art-spec" => "icon-spec",
|
||||
"game-background" => "game-background",
|
||||
"art-spritesheet" => "art-spritesheet",
|
||||
"art-spec" => GameCreationAppAssetKind::IconSpec,
|
||||
"game-background" => GameCreationAppAssetKind::Scene,
|
||||
"art-spritesheet" => GameCreationAppAssetKind::IconSpritesheet,
|
||||
_ => return false,
|
||||
};
|
||||
context.agent_id == "direct-codex-art"
|
||||
@@ -42,7 +42,11 @@ fn is_direct_platform_art_generation_runtime_context(
|
||||
&& context.source == "direct-codex"
|
||||
&& context.action_id == format!("direct-taonier-{}", context.run_id)
|
||||
&& context.action_fingerprint
|
||||
== format!("direct-taonier-art-v1:{asset_kind}:{}", context.run_id)
|
||||
== format!(
|
||||
"direct-taonier-art-v1:{}:{}",
|
||||
asset_kind.as_str(),
|
||||
context.run_id
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user