|
|
|
@@ -48,7 +48,7 @@ use axum::routing::post;
|
|
|
|
|
use axum::{Json, Router};
|
|
|
|
|
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
|
|
|
|
|
use serde::Deserialize;
|
|
|
|
|
use serde_json::{json, Value};
|
|
|
|
|
use serde_json::{json, Map, Value};
|
|
|
|
|
use std::collections::BTreeMap;
|
|
|
|
|
use std::io::Read;
|
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
@@ -797,7 +797,7 @@ impl AccountAssetImportField {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn import_account_asset_string_array(
|
|
|
|
|
arguments: &Value,
|
|
|
|
|
arguments: &Map<String, Value>,
|
|
|
|
|
field: AccountAssetImportField,
|
|
|
|
|
) -> Result<Vec<String>, ImportAccountAssetsError> {
|
|
|
|
|
let Some(value) = arguments.get(field.key()).filter(|value| !value.is_null()) else {
|
|
|
|
@@ -856,7 +856,7 @@ fn import_account_asset_string_array(
|
|
|
|
|
pub(in crate::agent) fn account_asset_import_inputs(
|
|
|
|
|
arguments: &Value,
|
|
|
|
|
) -> Result<(Vec<String>, Vec<String>), ImportAccountAssetsError> {
|
|
|
|
|
ToolArgumentsRejection::check(arguments, &["assetIds", "localPaths"])?;
|
|
|
|
|
let arguments = ToolArgumentsRejection::check(arguments, &["assetIds", "localPaths"])?;
|
|
|
|
|
let asset_ids =
|
|
|
|
|
import_account_asset_string_array(arguments, AccountAssetImportField::AssetIds)?;
|
|
|
|
|
let local_paths =
|
|
|
|
@@ -962,7 +962,7 @@ impl ToolTextField<CreateOrDeriveResourceError> for ResourceGenerationTextField
|
|
|
|
|
pub(in crate::agent) fn resource_generation_input(
|
|
|
|
|
arguments: &Value,
|
|
|
|
|
) -> Result<DirectResourceGenerationInput, CreateOrDeriveResourceError> {
|
|
|
|
|
ToolArgumentsRejection::check(
|
|
|
|
|
let arguments = ToolArgumentsRejection::check(
|
|
|
|
|
arguments,
|
|
|
|
|
&["kind", "mode", "sourceLocalAssetId", "prompt", "assetName"],
|
|
|
|
|
)?;
|
|
|
|
@@ -1199,7 +1199,7 @@ pub(in crate::agent) struct ListRegisteredAssetsInput {
|
|
|
|
|
pub(in crate::agent) fn list_registered_assets_input(
|
|
|
|
|
arguments: &Value,
|
|
|
|
|
) -> Result<ListRegisteredAssetsInput, ListRegisteredAssetsError> {
|
|
|
|
|
ToolArgumentsRejection::check(
|
|
|
|
|
let arguments = ToolArgumentsRejection::check(
|
|
|
|
|
arguments,
|
|
|
|
|
&[
|
|
|
|
|
"kind",
|
|
|
|
@@ -1555,7 +1555,8 @@ pub(in crate::agent) struct ListProjectFilesInput {
|
|
|
|
|
pub(in crate::agent) fn list_project_files_input(
|
|
|
|
|
arguments: &Value,
|
|
|
|
|
) -> Result<ListProjectFilesInput, ListProjectFilesError> {
|
|
|
|
|
ToolArgumentsRejection::check(arguments, &["path", "query", "kind", "offset", "limit"])?;
|
|
|
|
|
let arguments =
|
|
|
|
|
ToolArgumentsRejection::check(arguments, &["path", "query", "kind", "offset", "limit"])?;
|
|
|
|
|
let path = optional_text(arguments, ListProjectFilesTextField::Path)?;
|
|
|
|
|
if let Some(raw) = path.as_deref() {
|
|
|
|
|
let slash_normalized = raw.replace('\\', "/");
|
|
|
|
@@ -1763,7 +1764,7 @@ fn bridge_file_content_changed(root: &Path, path: &str, content: &[u8]) -> Optio
|
|
|
|
|
pub(in crate::agent) fn write_file_input(
|
|
|
|
|
arguments: &Value,
|
|
|
|
|
) -> Result<(String, String), WriteFileError> {
|
|
|
|
|
ToolArgumentsRejection::check(arguments, &["path", "content"])?;
|
|
|
|
|
let arguments = ToolArgumentsRejection::check(arguments, &["path", "content"])?;
|
|
|
|
|
let raw_path = arguments
|
|
|
|
|
.get("path")
|
|
|
|
|
.and_then(Value::as_str)
|
|
|
|
@@ -1999,7 +2000,8 @@ pub(in crate::agent) struct ListAccountAssetsInput {
|
|
|
|
|
pub(in crate::agent) fn list_account_assets_input(
|
|
|
|
|
arguments: &Value,
|
|
|
|
|
) -> Result<ListAccountAssetsInput, ListAccountAssetsError> {
|
|
|
|
|
ToolArgumentsRejection::check(arguments, &["folderId", "query", "offset", "limit"])?;
|
|
|
|
|
let arguments =
|
|
|
|
|
ToolArgumentsRejection::check(arguments, &["folderId", "query", "offset", "limit"])?;
|
|
|
|
|
let folder_id = optional_text(arguments, AccountAssetTextField::FolderId)?;
|
|
|
|
|
let query =
|
|
|
|
|
optional_text(arguments, AccountAssetTextField::Query)?.map(|value| value.to_lowercase());
|
|
|
|
@@ -2387,7 +2389,7 @@ pub(in crate::agent) struct RemoveBackgroundInput {
|
|
|
|
|
pub(in crate::agent) fn remove_background_input(
|
|
|
|
|
arguments: &Value,
|
|
|
|
|
) -> Result<RemoveBackgroundInput, RemoveBackgroundError> {
|
|
|
|
|
ToolArgumentsRejection::check(
|
|
|
|
|
let arguments = ToolArgumentsRejection::check(
|
|
|
|
|
arguments,
|
|
|
|
|
&[
|
|
|
|
|
"sourceLocalAssetId",
|
|
|
|
@@ -2721,14 +2723,14 @@ fn prepare_game_art_request(
|
|
|
|
|
pub(in crate::agent) fn prepare_game_art_input(
|
|
|
|
|
arguments: &Value,
|
|
|
|
|
) -> Result<(String, DirectTaonierArtPreparationMode), PrepareGameArtError> {
|
|
|
|
|
ToolArgumentsRejection::check(arguments, &["brief", "mode"])?;
|
|
|
|
|
let arguments = ToolArgumentsRejection::check(arguments, &["brief", "mode"])?;
|
|
|
|
|
let brief = required_text(arguments, PrepareGameArtTextField::Brief)?;
|
|
|
|
|
Ok((brief, art_preparation_mode(arguments)?))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// `mode` 缺省为 `reuse-or-create`;给了就必须是受支持的模式名。
|
|
|
|
|
fn art_preparation_mode(
|
|
|
|
|
arguments: &Value,
|
|
|
|
|
arguments: &Map<String, Value>,
|
|
|
|
|
) -> Result<DirectTaonierArtPreparationMode, PrepareGameArtError> {
|
|
|
|
|
match arguments.get("mode").filter(|value| !value.is_null()) {
|
|
|
|
|
None => Ok(DirectTaonierArtPreparationMode::ReuseOrCreate),
|
|
|
|
@@ -2894,7 +2896,7 @@ impl ToolTextField<GenerateImageError> for GenerateImageTextField {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn generate_image_grid_axis(
|
|
|
|
|
arguments: &Value,
|
|
|
|
|
arguments: &Map<String, Value>,
|
|
|
|
|
axis: &'static str,
|
|
|
|
|
) -> Result<Option<u32>, GenerateImageError> {
|
|
|
|
|
let Some(value) = arguments.get(axis) else {
|
|
|
|
@@ -2908,7 +2910,7 @@ fn generate_image_grid_axis(
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn bridge_image_generation_kind(
|
|
|
|
|
arguments: &Value,
|
|
|
|
|
arguments: &Map<String, Value>,
|
|
|
|
|
) -> Result<GameCreationAppAssetKind, GenerateImageError> {
|
|
|
|
|
// 缺省与显式 null 用 schema 声明的默认值;给了别的类型不许静默当 image。
|
|
|
|
|
let kind = match arguments.get("kind") {
|
|
|
|
@@ -2955,7 +2957,7 @@ fn validate_generate_image_slice_declaration(
|
|
|
|
|
/// None(服务端自动决策),hex 统一大写后透传。其它 kind 携带该字段直接拒绝,
|
|
|
|
|
/// 避免服务端静默忽略造成“已生效”的误解。
|
|
|
|
|
fn normalize_generate_image_screen_color(
|
|
|
|
|
arguments: &Value,
|
|
|
|
|
arguments: &Map<String, Value>,
|
|
|
|
|
kind: GameCreationAppAssetKind,
|
|
|
|
|
) -> Result<Option<String>, GenerateImageError> {
|
|
|
|
|
let Some(value) = arguments.get("screenColor") else {
|
|
|
|
@@ -3012,7 +3014,7 @@ pub(in crate::agent) struct GenerateImageInput {
|
|
|
|
|
pub(in crate::agent) fn generate_image_input(
|
|
|
|
|
arguments: &Value,
|
|
|
|
|
) -> Result<GenerateImageInput, GenerateImageError> {
|
|
|
|
|
ToolArgumentsRejection::check(arguments, GENERATE_IMAGE_ARGUMENTS)?;
|
|
|
|
|
let arguments = ToolArgumentsRejection::check(arguments, GENERATE_IMAGE_ARGUMENTS)?;
|
|
|
|
|
let prompt = required_text(arguments, GenerateImageTextField::Prompt)?;
|
|
|
|
|
let kind = bridge_image_generation_kind(arguments)?;
|
|
|
|
|
let aspect_ratio = optional_text(arguments, GenerateImageTextField::AspectRatio)?
|
|
|
|
@@ -3254,7 +3256,8 @@ impl ToolTextField<EditImageError> for EditImageTextField {
|
|
|
|
|
///
|
|
|
|
|
/// 工具桥与 MCP 预检共用这一份入参规则;失败文案只写在 [`EditImageError`] 上。
|
|
|
|
|
pub(in crate::agent) fn edit_image_arguments(arguments: &Value) -> Result<Value, EditImageError> {
|
|
|
|
|
ToolArgumentsRejection::check(arguments, &["sourceLocalAssetId", "prompt", "assetName"])?;
|
|
|
|
|
let arguments =
|
|
|
|
|
ToolArgumentsRejection::check(arguments, &["sourceLocalAssetId", "prompt", "assetName"])?;
|
|
|
|
|
let source = required_text(arguments, EditImageTextField::SourceLocalAssetId)?;
|
|
|
|
|
let prompt = required_text(arguments, EditImageTextField::Prompt)?;
|
|
|
|
|
let asset_name = required_text(arguments, EditImageTextField::AssetName)?;
|
|
|
|
@@ -3324,7 +3327,7 @@ async fn bridge_environment_check(
|
|
|
|
|
root: &Path,
|
|
|
|
|
arguments: &Value,
|
|
|
|
|
) -> Result<Value, EnvironmentCheckError> {
|
|
|
|
|
ToolArgumentsRejection::check(arguments, &[])?;
|
|
|
|
|
let arguments = ToolArgumentsRejection::check(arguments, &[])?;
|
|
|
|
|
enforce_project_permission_policy_rejection(root, "game.run_local")?;
|
|
|
|
|
let result = crate::environment_check::check_environment(root).await;
|
|
|
|
|
if result["status"] != "ready" {
|
|
|
|
@@ -3480,7 +3483,7 @@ impl ToolTextField<WebSearchError> for WebSearchTextField {
|
|
|
|
|
pub(in crate::agent) fn web_search_input(
|
|
|
|
|
arguments: &Value,
|
|
|
|
|
) -> Result<(String, usize), WebSearchError> {
|
|
|
|
|
ToolArgumentsRejection::check(arguments, &["query", "maxResults"])?;
|
|
|
|
|
let arguments = ToolArgumentsRejection::check(arguments, &["query", "maxResults"])?;
|
|
|
|
|
let query = required_text(arguments, WebSearchTextField::Query)?;
|
|
|
|
|
let max_results = match arguments.get("maxResults") {
|
|
|
|
|
None => 3u64,
|
|
|
|
@@ -3652,7 +3655,7 @@ pub(in crate::agent) fn editor_execute_code_input(
|
|
|
|
|
arguments: &Value,
|
|
|
|
|
editor: EditorKind,
|
|
|
|
|
) -> Result<String, EditorExecuteError> {
|
|
|
|
|
ToolArgumentsRejection::check(arguments, &["code"])?;
|
|
|
|
|
let arguments = ToolArgumentsRejection::check(arguments, &["code"])?;
|
|
|
|
|
let code = arguments
|
|
|
|
|
.get("code")
|
|
|
|
|
.and_then(Value::as_str)
|
|
|
|
@@ -3698,7 +3701,7 @@ fn prepare_editor_execute(
|
|
|
|
|
pub(in crate::agent) fn cocos_execute_code_input(
|
|
|
|
|
arguments: &Value,
|
|
|
|
|
) -> Result<String, CocosExecuteError> {
|
|
|
|
|
ToolArgumentsRejection::check(arguments, &["code"])?;
|
|
|
|
|
let arguments = ToolArgumentsRejection::check(arguments, &["code"])?;
|
|
|
|
|
let code = arguments
|
|
|
|
|
.get("code")
|
|
|
|
|
.and_then(Value::as_str)
|
|
|
|
@@ -4611,13 +4614,13 @@ mod tests {
|
|
|
|
|
fn generate_image_screen_color_is_normalized_and_kind_gated() {
|
|
|
|
|
// 省略与显式 null 等价,且不触发 kind 门禁。
|
|
|
|
|
assert_eq!(
|
|
|
|
|
normalize_generate_image_screen_color(&json!({}), GameCreationAppAssetKind::Image)
|
|
|
|
|
normalize_generate_image_screen_color(obj(&json!({})), GameCreationAppAssetKind::Image)
|
|
|
|
|
.expect("omitted"),
|
|
|
|
|
None
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
normalize_generate_image_screen_color(
|
|
|
|
|
&json!({"screenColor": null}),
|
|
|
|
|
obj(&json!({"screenColor": null})),
|
|
|
|
|
GameCreationAppAssetKind::Image
|
|
|
|
|
)
|
|
|
|
|
.expect("null"),
|
|
|
|
@@ -4627,7 +4630,7 @@ mod tests {
|
|
|
|
|
for raw in ["auto", "AUTO", " auto ", ""] {
|
|
|
|
|
assert_eq!(
|
|
|
|
|
normalize_generate_image_screen_color(
|
|
|
|
|
&json!({"screenColor": raw}),
|
|
|
|
|
obj(&json!({"screenColor": raw})),
|
|
|
|
|
GameCreationAppAssetKind::Character
|
|
|
|
|
)
|
|
|
|
|
.expect("auto variants"),
|
|
|
|
@@ -4638,7 +4641,7 @@ mod tests {
|
|
|
|
|
// hex 统一大写透传;色板白名单由服务端权威校验,客户端只守格式。
|
|
|
|
|
assert_eq!(
|
|
|
|
|
normalize_generate_image_screen_color(
|
|
|
|
|
&json!({"screenColor": "#cfefff"}),
|
|
|
|
|
obj(&json!({"screenColor": "#cfefff"})),
|
|
|
|
|
GameCreationAppAssetKind::Character
|
|
|
|
|
)
|
|
|
|
|
.expect("lowercase hex"),
|
|
|
|
@@ -4646,7 +4649,7 @@ mod tests {
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
normalize_generate_image_screen_color(
|
|
|
|
|
&json!({"screenColor": " #A0BBA0 "}),
|
|
|
|
|
obj(&json!({"screenColor": " #A0BBA0 "})),
|
|
|
|
|
GameCreationAppAssetKind::IconSpritesheet
|
|
|
|
|
)
|
|
|
|
|
.expect("padded hex"),
|
|
|
|
@@ -4656,7 +4659,7 @@ mod tests {
|
|
|
|
|
for bad in [json!("green"), json!("#GGGGGG"), json!("#FFF"), json!(12)] {
|
|
|
|
|
assert!(
|
|
|
|
|
normalize_generate_image_screen_color(
|
|
|
|
|
&json!({"screenColor": bad}),
|
|
|
|
|
obj(&json!({"screenColor": bad})),
|
|
|
|
|
GameCreationAppAssetKind::Character
|
|
|
|
|
)
|
|
|
|
|
.is_err(),
|
|
|
|
@@ -4665,14 +4668,14 @@ mod tests {
|
|
|
|
|
}
|
|
|
|
|
// 其它 kind 携带该字段直接拒绝,即使取值合法。
|
|
|
|
|
let gated = normalize_generate_image_screen_color(
|
|
|
|
|
&json!({"screenColor": "#CFEFFF"}),
|
|
|
|
|
obj(&json!({"screenColor": "#CFEFFF"})),
|
|
|
|
|
GameCreationAppAssetKind::Image,
|
|
|
|
|
)
|
|
|
|
|
.expect_err("screenColor must stay scoped to character/icon-spritesheet")
|
|
|
|
|
.to_user_msg();
|
|
|
|
|
assert!(gated.contains("kind=character"), "{gated}");
|
|
|
|
|
assert!(normalize_generate_image_screen_color(
|
|
|
|
|
&json!({"screenColor": "auto"}),
|
|
|
|
|
obj(&json!({"screenColor": "auto"})),
|
|
|
|
|
GameCreationAppAssetKind::UiDesign
|
|
|
|
|
)
|
|
|
|
|
.is_err());
|
|
|
|
@@ -4702,6 +4705,10 @@ mod tests {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
fn obj(value: &Value) -> &Map<String, Value> {
|
|
|
|
|
value.as_object().expect("测试入参必须是对象")
|
|
|
|
|
}
|
|
|
|
|
use std::io::{Cursor, Read, Write};
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
@@ -4802,9 +4809,11 @@ mod tests {
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn bridge_argument_bounds_are_deterministic() {
|
|
|
|
|
assert!(required_text(&json!({ "brief": "" }), PrepareGameArtTextField::Brief).is_err());
|
|
|
|
|
assert!(
|
|
|
|
|
required_text(obj(&json!({ "brief": "" })), PrepareGameArtTextField::Brief).is_err()
|
|
|
|
|
);
|
|
|
|
|
assert!(required_text(
|
|
|
|
|
&json!({ "brief": "字".repeat(DIRECT_TOOL_BRIDGE_MAX_ART_BRIEF_CHARS + 1) }),
|
|
|
|
|
obj(&json!({ "brief": "字".repeat(DIRECT_TOOL_BRIDGE_MAX_ART_BRIEF_CHARS + 1) })),
|
|
|
|
|
PrepareGameArtTextField::Brief
|
|
|
|
|
)
|
|
|
|
|
.is_err());
|
|
|
|
@@ -4819,7 +4828,7 @@ mod tests {
|
|
|
|
|
DirectTaonierArtPreparationMode::Regenerate
|
|
|
|
|
);
|
|
|
|
|
assert!(DirectTaonierArtPreparationMode::from_tool_value(Some("force")).is_err());
|
|
|
|
|
assert!(art_preparation_mode(&json!({ "mode": 1 })).is_err());
|
|
|
|
|
assert!(art_preparation_mode(obj(&json!({ "mode": 1 }))).is_err());
|
|
|
|
|
assert!(resource_generation_input(&json!({
|
|
|
|
|
"kind": "video",
|
|
|
|
|
"mode": "create",
|
|
|
|
@@ -4929,10 +4938,12 @@ mod tests {
|
|
|
|
|
"{interior_newline}"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let whitespace_only =
|
|
|
|
|
required_text(&json!({ "brief": "\n\t" }), PrepareGameArtTextField::Brief)
|
|
|
|
|
.expect_err("纯空白仍然按空值处理")
|
|
|
|
|
.to_user_msg();
|
|
|
|
|
let whitespace_only = required_text(
|
|
|
|
|
obj(&json!({ "brief": "\n\t" })),
|
|
|
|
|
PrepareGameArtTextField::Brief,
|
|
|
|
|
)
|
|
|
|
|
.expect_err("纯空白仍然按空值处理")
|
|
|
|
|
.to_user_msg();
|
|
|
|
|
assert!(
|
|
|
|
|
whitespace_only.contains("brief 不能为空"),
|
|
|
|
|
"{whitespace_only}"
|
|
|
|
@@ -4940,7 +4951,7 @@ mod tests {
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
required_text(
|
|
|
|
|
&json!({ "brief": " 美术包 " }),
|
|
|
|
|
obj(&json!({ "brief": " 美术包 " })),
|
|
|
|
|
PrepareGameArtTextField::Brief
|
|
|
|
|
)
|
|
|
|
|
.expect("首尾普通空格仍正常裁剪"),
|
|
|
|
@@ -4985,9 +4996,9 @@ mod tests {
|
|
|
|
|
oversized_not_text.chars().count()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let inline_not_text = art_preparation_mode(&json!({
|
|
|
|
|
let inline_not_text = art_preparation_mode(obj(&json!({
|
|
|
|
|
"mode": vec!["需要回显但必须截断的模式值"; 40]
|
|
|
|
|
}))
|
|
|
|
|
})))
|
|
|
|
|
.expect_err("mode 不是字符串时必须被拒绝")
|
|
|
|
|
.to_user_msg();
|
|
|
|
|
assert!(
|
|
|
|
@@ -4999,18 +5010,20 @@ mod tests {
|
|
|
|
|
"内联判类型的字段回显也要限长:{inline_not_text}"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let brief_not_text = required_text(&json!({ "brief": 5 }), PrepareGameArtTextField::Brief)
|
|
|
|
|
.expect_err("brief 不是字符串时必须被拒绝")
|
|
|
|
|
.to_user_msg();
|
|
|
|
|
let brief_not_text =
|
|
|
|
|
required_text(obj(&json!({ "brief": 5 })), PrepareGameArtTextField::Brief)
|
|
|
|
|
.expect_err("brief 不是字符串时必须被拒绝")
|
|
|
|
|
.to_user_msg();
|
|
|
|
|
assert!(
|
|
|
|
|
brief_not_text.contains("brief 必须是字符串"),
|
|
|
|
|
"{brief_not_text}"
|
|
|
|
|
);
|
|
|
|
|
assert!(!brief_not_text.contains("不能为空"), "{brief_not_text}");
|
|
|
|
|
|
|
|
|
|
let empty_brief = required_text(&json!({ "brief": "" }), PrepareGameArtTextField::Brief)
|
|
|
|
|
.expect_err("空 brief 仍然按空值处理")
|
|
|
|
|
.to_user_msg();
|
|
|
|
|
let empty_brief =
|
|
|
|
|
required_text(obj(&json!({ "brief": "" })), PrepareGameArtTextField::Brief)
|
|
|
|
|
.expect_err("空 brief 仍然按空值处理")
|
|
|
|
|
.to_user_msg();
|
|
|
|
|
assert!(empty_brief.contains("brief 不能为空"), "{empty_brief}");
|
|
|
|
|
|
|
|
|
|
let aspect_ratio_not_text = match generate_image_input(&json!({
|
|
|
|
|