diff --git a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/manifest.json b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/manifest.json index 0ad98a927..2a3066fc6 100644 --- a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/manifest.json +++ b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/manifest.json @@ -33,6 +33,8 @@ ], "requiredTools": [ "agc_tools.agc_read_skill_resource", + "agc_tools.agc_generate_image", + "agc_tools.agc_edit_image", "agc_tools.taonier_prepare_game_art" ], "files": [ @@ -40,7 +42,7 @@ "agents/openai.yaml", "references/platform-art-contract.md" ], - "sha256": "16daad7fa179a87e9e2904335c83fe88f64ae2167a2d84476e791cca4797a7b0" + "sha256": "600c758662de7c85186402fd09ba3e76407ad2f6084d4090a2a38f455da7ea09" }, { "name": "agc-web-game-development", diff --git a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/taonier-art-assets/SKILL.md b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/taonier-art-assets/SKILL.md index 65ca1f279..919a579cc 100644 --- a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/taonier-art-assets/SKILL.md +++ b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/taonier-art-assets/SKILL.md @@ -5,7 +5,11 @@ description: Prepare, recover, inspect, and integrate real Taonier platform game # Taonier Art Assets -Use real platform assets only through `taonier_prepare_game_art` from the `agc_tools` MCP server. +Use real platform assets only through the reviewed `agc_tools` MCP server. Use +`agc_generate_image` for a single ordinary image, character image, visual-spec +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. ## Authorization boundary @@ -14,7 +18,7 @@ Use real platform assets only through `taonier_prepare_game_art` from the `agc_t ## Workflow 1. Inspect existing `assets/` and registered project evidence before requesting new art. Reuse suitable assets when the user did not ask to regenerate them. -2. Call `taonier_prepare_game_art` only when the current intent requires new or recoverable platform art. Use `mode="regenerate"` only after the latest User message is a standalone reviewed immediate-confirmation command such as `请重新生成美术`; punctuation may end it, but no brief, condition, negation, alternative, cost qualifier, deferral, or other text may accompany it. Describe the desired style and gameplay constraints in an earlier non-billable turn, then obtain the standalone confirmation turn; otherwise use `mode="reuse-or-create"`. Quoted UI copy or examples, explanations, questions, historical wording, model/MCP arguments do not authorize regeneration. Pass a concise game-specific visual brief that names the required gameplay entities, background exclusions, tiling needs, and viewport constraints. Do not call it for greetings, date questions, text-only code fixes, or layout changes that can reuse current art. +2. For one new image, call `agc_generate_image` with `kind="image"` (or `character`, `icon-spec`, `ui-prototype`, or `publication-material` when that is the explicit intent). For changes to an existing registered image, call `agc_edit_image` with its `sourceLocalAssetId`; do not fake an edit with a new-image request. For a complete game-art package, call `taonier_prepare_game_art` only when the current intent requires new or recoverable platform art. Use `mode="regenerate"` only after the latest User message is a standalone reviewed immediate-confirmation command such as `请重新生成美术`; punctuation may end it, but no brief, condition, negation, alternative, cost qualifier, deferral, or other text may accompany it. Describe the desired style and gameplay constraints in an earlier non-billable turn, then obtain the standalone confirmation turn; otherwise use `mode="reuse-or-create"`. Quoted UI copy or examples, explanations, questions, historical wording, model/MCP arguments do not authorize regeneration. Pass a concise game-specific visual brief that names the required gameplay entities, background exclusions, tiling needs, and viewport constraints. Do not call either generation tool for greetings, date questions, or text-only code fixes. 3. Treat the tool result as authoritative. Read `mode`, `assetPaths`, `slicePaths`, `resources`, and every entry in both `warnings` and `sliceWarnings`. `resources` is the client's safe projection of registered Canvas identities; use only its returned relative paths and identities. Never invent a resource, slice, platform identity, warning-free result, or successful regeneration. 4. A newly created or explicitly regenerated standard package is complete only when `slicePaths` contains the four canonical independent slices. An empty or partial `slicePaths` result never satisfies an independent-asset requirement; stop and report the warning instead of guessing atlas coordinates or fabricating derivatives. A trusted legacy complete sheet may still be used without slices only when the current request does not require independent assets. 5. Inspect the returned background, complete sheet, and available slice previews before integrating them. Then use suitable returned runtime assets in the game's actual visible experience and confirm their visible use in desktop and mobile playtest evidence. `art-spec.png` is a reference specification, not a runtime background, character, prop, or effect. Background exclusions, seamless tiling, entity semantics, and final draw dimensions are visual/runtime acceptance checks; a prompt alone does not prove them. A hidden or side-panel preview does not count as gameplay use. diff --git a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/taonier-art-assets/references/platform-art-contract.md b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/taonier-art-assets/references/platform-art-contract.md index 0c09c98bd..9a04e1256 100644 --- a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/taonier-art-assets/references/platform-art-contract.md +++ b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/taonier-art-assets/references/platform-art-contract.md @@ -1,6 +1,6 @@ # Platform Art Contract -`taonier_prepare_game_art` is the only paid art entry exposed to the AGC Codex thread. The client owns authentication, stable idempotency keys, durable `operationId` recovery, download, PNG decoding, source identity, and asset registration. +`agc_generate_image` and `taonier_prepare_game_art` are the reviewed paid art entries exposed to the AGC Codex thread. The former covers one ordinary/character/spec/UI/publication image; `agc_edit_image` covers edits to an existing registered image; the latter covers the complete game-art package and canonical slices. The client owns authentication, stable idempotency keys, durable `operationId` recovery, download, PNG decoding, source identity, and asset registration for all three. - `mode="reuse-or-create"` reuses a complete trusted package and creates only missing assets. It is the safe default for existing games. - `mode="regenerate"` is reserved for an explicit user request to replace or restyle the package. It bypasses complete-package reuse, but it never bypasses an unresolved billable operation. diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime.rs index ee0ce1dc6..2eb65f8ca 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime.rs @@ -13,7 +13,7 @@ const MAX_DIRECT_HOME_ATTACHMENT_MEDIA_TYPE_CHARS: usize = 96; const MIN_DIRECT_CLIENT_TURN_ID_CHARS: usize = 6; const MAX_DIRECT_CLIENT_TURN_ID_CHARS: usize = 160; const DIRECT_TAONIER_IDENTITY_GUIDANCE: &str = "对外身份合同:你是“陶泥儿”,是 Genarrative 的游戏创作助手。用户询问你是谁、你的名称或能力时,以陶泥儿的身份回答;不要把 Codex、ChatGPT、OpenAI、模型、通用 AI 助手或内部执行智能体当作自己的名称或对外身份。Codex app-server 仅是客户端内部执行技术;只有用户明确询问底层实现时才可如实说明,同时仍以陶泥儿自称。"; -const DIRECT_AGC_ENGINEERING_GUIDANCE: &str = "AGC 工程合同:当前 Codex cwd 是项目真实 `game/` 源码目录,只允许把项目源码写入该目录;原生文件工具、原生 patch 和命令参数中的文件路径必须相对于当前 cwd:合法写法是 `index.html`、`style.css`、`game.js`,禁止写 `game/index.html`、`../game/index.html`、项目根绝对路径或任何其它父目录路径;`game/...` 只用于 AGC 回执、manifest 和客户端投影,不用于 cwd 内的原生 patch。`../assets/` 只能按审核 Skill 或 `agc_tools` 返回的相对路径使用,不要用原生文件/命令工具遍历父目录;`.agent/` 和项目根由客户端维护,不能请求扩权或直接改写。DirectProject 提供 Codex 原生文件、搜索、命令、图片查看、Skill,以及经审核的 `agc_tools` MCP;浏览器试玩、平台美术、资源登记和受控联网搜索等带 AGC 账本的动作使用 `agc_tools`。按用户意图自行选择并执行,不要等待 Supervisor、harness 或宿主规划器。不要读取或输出凭据、Token、Cookie、auth.json、.env 或宿主私密路径。项目锁、付费提交、幂等键、下载校验和客户端投影仍由客户端确定性掌管。游戏文件真实变化后由客户端登记资源和版本,Codex 不直接保存或伪造项目版本。"; +const DIRECT_AGC_ENGINEERING_GUIDANCE: &str = "AGC 工程合同:当前 Codex cwd 是项目真实 `game/` 源码目录,只允许把项目源码写入该目录;原生文件工具、原生 patch 和命令参数中的文件路径必须相对于当前 cwd:合法写法是 `index.html`、`style.css`、`game.js`,禁止写 `game/index.html`、`../game/index.html`、项目根绝对路径或任何其它父目录路径;`game/...` 只用于 AGC 回执、manifest 和客户端投影,不用于 cwd 内的原生 patch。`../assets/` 只能按审核 Skill 或 `agc_tools` 返回的相对路径使用,不要用原生文件/命令工具遍历父目录;`.agent/` 和项目根由客户端维护,不能请求扩权或直接改写。DirectProject 提供 Codex 原生文件、搜索、命令、图片查看、Skill,以及经审核的 `agc_tools` MCP;普通单张图片、角色图、视觉规范图、UI 设计图和发布宣传图使用 `agc_tools.agc_generate_image`,已有图片修改使用 `agc_tools.agc_edit_image`,完整游戏美术包和 canonical 切片才使用 `agc_tools.taonier_prepare_game_art`,视频、角色动画、音效、背景音乐、浏览器试玩、资源登记和受控联网搜索等带 AGC 账本的动作也使用 `agc_tools`。按用户意图自行选择并执行,不要把普通图片误报成只能生成美术包,也不要等待 Supervisor、harness 或宿主规划器。不要读取或输出凭据、Token、Cookie、auth.json、.env 或宿主私密路径。项目锁、付费提交、幂等键、下载校验和客户端投影仍由客户端确定性掌管。游戏文件真实变化后由客户端登记资源和版本,Codex 不直接保存或伪造项目版本。"; const DIRECT_CODEX_ART_SPEC_ASSET_PATH: &str = "assets/art-spec.png"; const DIRECT_CODEX_BACKGROUND_ASSET_PATH: &str = "assets/direct-game-background.png"; const DIRECT_CODEX_SPRITESHEET_ASSET_PATH: &str = "assets/art-spritesheet.png"; @@ -4548,6 +4548,7 @@ mod tests { let prompt = build_direct_codex_system_prompt(root.path()).expect("build direct system prompt"); assert!(prompt.contains("agc_tools.taonier_prepare_game_art")); + assert!(prompt.contains("agc_tools.agc_generate_image")); assert!(prompt.contains("agc_tools.agc_browser_playtest")); assert!(prompt.contains("DirectProject 提供 Codex 原生文件、搜索、命令、图片查看、Skill")); assert!(!prompt.contains("客户端会在系统上下文提供有界的当前游戏文件快照")); diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs index f685251e7..0470d5a8a 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs @@ -15,6 +15,7 @@ pub(crate) const DIRECT_TOOL_BRIDGE_URL_ENV: &str = "GENARRATIVE_AGC_TOOL_BRIDGE const DIRECT_TOOL_BRIDGE_MAX_REQUEST_BYTES: usize = 16 * 1024; const DIRECT_TOOL_BRIDGE_MAX_ART_BRIEF_CHARS: usize = 4_000; +const DIRECT_TOOL_BRIDGE_MAX_IMAGE_PROMPT_CHARS: usize = 32_000; const DIRECT_TOOL_BRIDGE_MAX_IMAGE_BYTES: u64 = 6 * 1024 * 1024; const DIRECT_TOOL_BRIDGE_MAX_SEARCH_QUERY_CHARS: usize = 400; const DIRECT_TOOL_BRIDGE_MAX_SEARCH_RESULTS: usize = 5; @@ -32,6 +33,7 @@ struct DirectToolBridgeState { turn_authorization: StdMutex, regeneration_gate: tokio::sync::Mutex<()>, resource_generation_gate: tokio::sync::Mutex<()>, + image_generation_gate: tokio::sync::Mutex<()>, } #[derive(Default)] @@ -65,6 +67,7 @@ struct DirectToolBridgeRequest { #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum DirectResourceGenerationKind { + Image, Video, CharacterAnimation, SoundEffect, @@ -74,6 +77,7 @@ enum DirectResourceGenerationKind { impl DirectResourceGenerationKind { fn parse(value: &str) -> Result { match value { + "image" => Ok(Self::Image), "video" => Ok(Self::Video), "character-animation" => Ok(Self::CharacterAnimation), "sound-effect" => Ok(Self::SoundEffect), @@ -84,6 +88,7 @@ impl DirectResourceGenerationKind { fn as_str(self) -> &'static str { match self { + Self::Image => "image", Self::Video => "video", Self::CharacterAnimation => "character-animation", Self::SoundEffect => "sound-effect", @@ -93,6 +98,7 @@ impl DirectResourceGenerationKind { fn edit_kind(self) -> LocalProjectResourceEditKind { match self { + Self::Image => LocalProjectResourceEditKind::ImageReference, Self::Video => LocalProjectResourceEditKind::Video, Self::CharacterAnimation => LocalProjectResourceEditKind::CharacterAnimation, Self::SoundEffect => LocalProjectResourceEditKind::SoundEffect, @@ -663,6 +669,7 @@ fn direct_tool_bridge_state(root: PathBuf) -> Arc { turn_authorization: StdMutex::new(DirectToolBridgeTurnAuthorization::default()), regeneration_gate: tokio::sync::Mutex::new(()), resource_generation_gate: tokio::sync::Mutex::new(()), + image_generation_gate: tokio::sync::Mutex::new(()), }) } @@ -988,6 +995,9 @@ fn bridge_resource_generation_input( return Err("背景音乐提示词必须在 1..=140 字符内".to_string()); } match (kind, mode, source_local_asset_id.as_ref()) { + (DirectResourceGenerationKind::Image, DirectResourceGenerationMode::Create, _) => { + return Err("图片编辑必须基于已登记图片资源派生".to_string()) + } ( DirectResourceGenerationKind::CharacterAnimation, DirectResourceGenerationMode::Create, @@ -1912,6 +1922,159 @@ async fn bridge_prepare_game_art(state: &DirectToolBridgeState, arguments: &Valu result } +fn bridge_image_generation_kind(arguments: &Value) -> Result { + let kind = arguments + .get("kind") + .and_then(Value::as_str) + .map(str::trim) + .filter(|value| !value.is_empty()) + .unwrap_or("image"); + if !matches!( + kind, + "image" | "character" | "icon-spec" | "ui-prototype" | "publication-material" + ) { + return Err( + "工具参数 kind 只允许 image、character、icon-spec、ui-prototype 或 publication-material" + .to_string(), + ); + } + Ok(kind.to_string()) +} + +async fn bridge_generate_image(state: &DirectToolBridgeState, arguments: &Value) -> Value { + let result = async { + bridge_reject_unknown_fields( + arguments, + &[ + "prompt", + "kind", + "aspectRatio", + "imageSize", + "assetName", + "outputPath", + ], + )?; + enforce_project_permission_policy(&state.root, "canvas.asset_generate")?; + enforce_project_permission_policy(&state.root, "asset.register")?; + let prompt = bridge_bounded_string( + arguments, + "prompt", + DIRECT_TOOL_BRIDGE_MAX_IMAGE_PROMPT_CHARS, + )?; + let kind = bridge_image_generation_kind(arguments)?; + let aspect_ratio = arguments + .get("aspectRatio") + .map(|_| bridge_bounded_string(arguments, "aspectRatio", 8)) + .transpose()? + .unwrap_or_else(|| "1:1".to_string()); + if !matches!( + aspect_ratio.as_str(), + "1:1" | "2:3" | "3:2" | "9:16" | "16:9" + ) { + return Err("工具参数 aspectRatio 不是受支持的图片比例".to_string()); + } + let image_size = arguments + .get("imageSize") + .map(|_| bridge_bounded_string(arguments, "imageSize", 4)) + .transpose()? + .unwrap_or_else(|| "1K".to_string()); + if !matches!(image_size.as_str(), "0.5K" | "1K" | "2K") { + return Err("工具参数 imageSize 不是受支持的图片尺寸".to_string()); + } + let asset_name = arguments + .get("assetName") + .map(|_| { + bridge_bounded_string( + arguments, + "assetName", + DIRECT_TOOL_BRIDGE_MAX_RESOURCE_NAME_CHARS, + ) + }) + .transpose()? + .unwrap_or_else(|| "AI 生成图片".to_string()); + let output_path = bridge_optional_bounded_string(arguments, "outputPath", 512)?; + let options = PlatformArtAssetGenerationOptions { + output_path, + aspect_ratio, + image_size, + asset_kind: kind.clone(), + asset_label: asset_name.clone(), + replace_existing: false, + }; + let _generation_guard = state.image_generation_gate.lock().await; + let generated = with_direct_editor_api_credentials( + generate_platform_art_asset_with_options_at(&state.root, &prompt, &[], &options), + ) + .await?; + let resources = bridge_art_resources( + &state.root, + std::slice::from_ref(&generated.asset.local_path), + &[], + )?; + let images = bridge_png_content(&state.root, &state.root.join(&generated.asset.local_path)) + .ok() + .into_iter() + .collect::>(); + Ok::<_, String>((kind, generated, resources, images)) + } + .await; + match result { + Ok((kind, generated, resources, images)) => bridge_tool_result( + json!({ + "status": "completed", + "kind": kind, + "assetKind": kind, + "asset": { + "localAssetId": generated.asset.id, + "localPath": generated.asset.local_path, + "resourceId": generated.resource_id, + "assetObjectId": generated.asset_object_id, + "taskId": generated.task_id, + }, + "resources": resources, + "warnings": generated.warning.map(|warning| bridge_safe_warning_messages(&state.root, vec![warning])).unwrap_or_default(), + "sliceWarnings": generated.slice_warning.map(|warning| bridge_safe_warning_messages(&state.root, vec![warning])).unwrap_or_default(), + }) + .to_string(), + images, + false, + ), + Err(error) => bridge_tool_result( + redact_agent_runtime_error(&state.root, &error, 480), + Vec::new(), + true, + ), + } +} + +async fn bridge_edit_image(state: &DirectToolBridgeState, arguments: &Value) -> Value { + let result = (|| { + bridge_reject_unknown_fields(arguments, &["sourceLocalAssetId", "prompt", "assetName"])?; + let source = bridge_bounded_string(arguments, "sourceLocalAssetId", 80)?; + let prompt = bridge_bounded_string( + arguments, + "prompt", + DIRECT_TOOL_BRIDGE_MAX_IMAGE_PROMPT_CHARS, + )?; + let asset_name = bridge_bounded_string( + arguments, + "assetName", + DIRECT_TOOL_BRIDGE_MAX_RESOURCE_NAME_CHARS, + )?; + Ok::<_, String>(json!({ + "kind": "image", + "mode": "derive", + "sourceLocalAssetId": source, + "prompt": prompt, + "assetName": asset_name, + })) + })(); + match result { + Ok(arguments) => bridge_create_or_derive_resource(state, &arguments).await, + Err(error) => bridge_tool_result(error, Vec::new(), true), + } +} + async fn bridge_browser_playtest(root: &Path, arguments: &Value) -> Value { let result = async { enforce_project_permission_policy(root, "game.run_local")?; @@ -2023,6 +2186,8 @@ async fn handle_direct_tool_bridge( ) -> Json { let result = match request.tool.as_str() { "taonier_prepare_game_art" => bridge_prepare_game_art(&state, &request.arguments).await, + "agc_generate_image" => bridge_generate_image(&state, &request.arguments).await, + "agc_edit_image" => bridge_edit_image(&state, &request.arguments).await, "agc_list_registered_assets" => { bridge_list_registered_assets(&state.root, &request.arguments) } diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tools_mcp.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tools_mcp.rs index 5b81c1235..d53d0ab86 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tools_mcp.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tools_mcp.rs @@ -8,6 +8,7 @@ pub(crate) const DIRECT_TOOLS_MCP_CONTROLLED_WEB_SEARCH_ENV: &str = "AGC_CONTROLLED_WEB_SEARCH_ENABLED"; const DIRECT_TOOLS_MCP_MAX_REQUEST_BYTES: usize = 1024 * 1024; const DIRECT_TOOLS_MCP_MAX_ART_BRIEF_CHARS: usize = 4_000; +const DIRECT_TOOLS_MCP_MAX_IMAGE_PROMPT_CHARS: usize = 32_000; const DIRECT_TOOLS_MCP_MAX_SEARCH_QUERY_CHARS: usize = 400; const DIRECT_TOOLS_MCP_MAX_RESOURCE_PROMPT_CHARS: usize = 4_000; const DIRECT_TOOLS_MCP_MAX_RESOURCE_NAME_CHARS: usize = 120; @@ -84,6 +85,79 @@ fn direct_tools_mcp_specs_for(controlled_web_search: bool) -> Value { "additionalProperties": false } }), + json!({ + "name": "agc_generate_image", + "description": "按原网站图片画布能力生成一张新图片:普通插画、角色立绘、统一视觉规范图或游戏 UI 设计图都可使用。仅在用户明确要求生成新图时调用;游戏美术包是另一个专用工具,不是本工具的限制。客户端负责登录态授权、计费、幂等账本、下载校验、manifest/revision 登记和本地预览,不需要用户提供 API Key、Token、URL 或 .env。", + "inputSchema": { + "type": "object", + "properties": { + "prompt": { + "type": "string", + "minLength": 1, + "maxLength": DIRECT_TOOLS_MCP_MAX_IMAGE_PROMPT_CHARS, + "description": "完整图片描述;普通图片、角色、规范图或 UI 设计图均可" + }, + "kind": { + "type": "string", + "enum": ["image", "character", "icon-spec", "ui-prototype", "publication-material"], + "default": "image", + "description": "image=普通新图,character=角色图,icon-spec=视觉规范图,ui-prototype=完整 UI 设计图,publication-material=发布宣传图" + }, + "aspectRatio": { + "type": "string", + "enum": ["1:1", "2:3", "3:2", "9:16", "16:9"], + "default": "1:1" + }, + "imageSize": { + "type": "string", + "enum": ["0.5K", "1K", "2K"], + "default": "1K" + }, + "assetName": { + "type": "string", + "minLength": 1, + "maxLength": DIRECT_TOOLS_MCP_MAX_RESOURCE_NAME_CHARS, + "description": "本地素材显示名称,不是路径或 URL" + }, + "outputPath": { + "type": "string", + "maxLength": 512, + "description": "可选项目相对输出路径,必须位于 assets/ 且不能覆盖已有文件" + } + }, + "required": ["prompt"], + "additionalProperties": false + } + }), + json!({ + "name": "agc_edit_image", + "description": "按原网站图片精修能力修改一张已登记图片:换装、改色、换背景或局部重绘。必须使用 agc_list_registered_assets 返回的 sourceLocalAssetId;客户端负责引用校验、登录态授权、幂等恢复、下载校验、manifest/revision 登记和本地预览,不需要用户提供 API Key、Token、URL 或 .env。", + "inputSchema": { + "type": "object", + "properties": { + "sourceLocalAssetId": { + "type": "string", + "minLength": 1, + "maxLength": 80, + "description": "当前项目已登记的图片 localAssetId" + }, + "prompt": { + "type": "string", + "minLength": 1, + "maxLength": DIRECT_TOOLS_MCP_MAX_IMAGE_PROMPT_CHARS, + "description": "对已有图片的修改要求" + }, + "assetName": { + "type": "string", + "minLength": 1, + "maxLength": DIRECT_TOOLS_MCP_MAX_RESOURCE_NAME_CHARS, + "description": "新图片的本地素材显示名称" + } + }, + "required": ["sourceLocalAssetId", "prompt", "assetName"], + "additionalProperties": false + } + }), json!({ "name": "agc_list_registered_assets", "description": "查询当前项目由客户端权威 manifest 登记的资源与未完成资源 operation。结果有界且只包含项目相对路径、稳定资源身份、序列帧身份和恢复状态,不返回 prompt、模型、签名 URL、宿主路径或凭据。", @@ -746,6 +820,76 @@ async fn call_taonier_prepare_game_art(arguments: &Value) -> Value { call_client_tool_bridge("taonier_prepare_game_art", arguments).await } +async fn call_agc_generate_image(arguments: &Value) -> Value { + if let Err(error) = validate_tool_object_fields( + arguments, + &[ + "prompt", + "kind", + "aspectRatio", + "imageSize", + "assetName", + "outputPath", + ], + ) { + return mcp_tool_result(error, Vec::new(), true); + } + if let Err(error) = + bounded_tool_string(arguments, "prompt", DIRECT_TOOLS_MCP_MAX_IMAGE_PROMPT_CHARS) + { + return mcp_tool_result(error, Vec::new(), true); + } + if let Some(kind) = arguments.get("kind") { + if !kind.is_string() + || ![ + "image", + "character", + "icon-spec", + "ui-prototype", + "publication-material", + ] + .contains(&kind.as_str().unwrap_or_default()) + { + return mcp_tool_result( + "工具参数 kind 不是受支持的图片生成类型".to_string(), + Vec::new(), + true, + ); + } + } + for (field, max_chars) in [ + ("aspectRatio", 8_usize), + ("imageSize", 4), + ("assetName", DIRECT_TOOLS_MCP_MAX_RESOURCE_NAME_CHARS), + ("outputPath", 512), + ] { + if arguments.get(field).is_some() { + if let Err(error) = bounded_tool_string(arguments, field, max_chars) { + return mcp_tool_result(error, Vec::new(), true); + } + } + } + call_client_tool_bridge("agc_generate_image", arguments).await +} + +async fn call_agc_edit_image(arguments: &Value) -> Value { + if let Err(error) = + validate_tool_object_fields(arguments, &["sourceLocalAssetId", "prompt", "assetName"]) + { + return mcp_tool_result(error, Vec::new(), true); + } + for (field, max_chars) in [ + ("sourceLocalAssetId", 80_usize), + ("prompt", DIRECT_TOOLS_MCP_MAX_IMAGE_PROMPT_CHARS), + ("assetName", DIRECT_TOOLS_MCP_MAX_RESOURCE_NAME_CHARS), + ] { + if let Err(error) = bounded_tool_string(arguments, field, max_chars) { + return mcp_tool_result(error, Vec::new(), true); + } + } + call_client_tool_bridge("agc_edit_image", arguments).await +} + async fn call_agc_list_registered_assets(arguments: &Value) -> Value { if let Err(error) = validate_registered_assets_arguments(arguments) { return mcp_tool_result(error, Vec::new(), true); @@ -854,6 +998,8 @@ async fn handle_direct_tools_mcp_request(_root: &Path, request: Value) -> Option let result = match tool { "agc_read_skill_resource" => call_agc_read_skill_resource(&arguments), "taonier_prepare_game_art" => call_taonier_prepare_game_art(&arguments).await, + "agc_generate_image" => call_agc_generate_image(&arguments).await, + "agc_edit_image" => call_agc_edit_image(&arguments).await, "agc_list_registered_assets" => call_agc_list_registered_assets(&arguments).await, "agc_list_project_files" => call_agc_list_project_files(&arguments).await, "agc_list_account_assets" => call_agc_list_account_assets(&arguments).await, @@ -984,6 +1130,8 @@ mod tests { vec![ "agc_read_skill_resource", "taonier_prepare_game_art", + "agc_generate_image", + "agc_edit_image", "agc_list_registered_assets", "agc_list_project_files", "agc_list_account_assets", @@ -1020,6 +1168,36 @@ mod tests { description.contains("用户不需要提供、配置、粘贴或创建 API Key") && description.contains("不得向用户索要凭据或暴露内部 URL") })); + let image_tool = specs["tools"] + .as_array() + .expect("tool array") + .iter() + .find(|tool| tool["name"] == "agc_generate_image") + .expect("image tool"); + assert_eq!( + image_tool["inputSchema"]["properties"]["kind"]["enum"], + json!([ + "image", + "character", + "icon-spec", + "ui-prototype", + "publication-material" + ]) + ); + assert_eq!(image_tool["inputSchema"]["required"], json!(["prompt"])); + assert!(image_tool["description"] + .as_str() + .is_some_and(|description| description.contains("不是本工具的限制"))); + let edit_tool = specs["tools"] + .as_array() + .expect("tool array") + .iter() + .find(|tool| tool["name"] == "agc_edit_image") + .expect("image edit tool"); + assert_eq!( + edit_tool["inputSchema"]["required"], + json!(["sourceLocalAssetId", "prompt", "assetName"]) + ); let resource_tool = specs["tools"] .as_array() .expect("tool array") diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs index 156daf3c0..ee9b32f60 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs @@ -2523,6 +2523,9 @@ pub(in crate::agent) async fn request_platform_art_asset_with_runtime_options_at let generation_kind = match options.asset_kind.as_str() { "ui-prototype" => "ui-design", "art-spritesheet" => "icon-spritesheet", + "image" => "image", + "character" => "character", + "publication-material" => "publication-material", _ => "spec", }; let is_canonical_art_spritesheet = options.asset_kind == "art-spritesheet"; @@ -2590,6 +2593,21 @@ pub(in crate::agent) async fn request_platform_art_asset_with_runtime_options_at }), ) }; + let request_body = if matches!( + options.asset_kind.as_str(), + "image" | "character" | "publication-material" + ) { + let mut request_body = request_body; + if let Some(object) = request_body.as_object_mut() { + object.remove("generationInputs"); + if options.asset_kind == "image" { + object.remove("kind"); + } + } + request_body + } else { + request_body + }; let runtime_state = runtime_context .map(|context| { prepare_platform_art_generation_runtime_state( @@ -7505,6 +7523,24 @@ pub(crate) fn build_platform_art_asset_prompt( briefs: &[AgentGroupBrief], options: &PlatformArtAssetGenerationOptions, ) -> String { + if options.asset_kind == "image" { + return format!( + "根据用户需求生成一张全新的原创图片。主体、环境、风格、构图、光线和色彩以用户描述为准;不要生成素材图集、规范展板、完整游戏截图或文字说明。不得修改或复述为已有图片编辑。\n\n用户需求:{}", + truncate_prompt_context(prompt.trim()) + ); + } + if options.asset_kind == "character" { + return format!( + "根据用户需求生成一张全新的原创角色形象或人物立绘。清楚表现角色外貌、服饰、姿势、表情、画风、构图和背景;只生成一张完整图片,不要生成图集、规范展板、完整游戏截图或文字说明。不得复刻现有作品角色或 Logo。\n\n用户需求:{}", + truncate_prompt_context(prompt.trim()) + ); + } + if options.asset_kind == "publication-material" { + return format!( + "根据用户需求生成一张全新的原创游戏发布宣传图。突出主体、卖点、氛围、构图、色彩和适合发布展示的画面层次;只生成一张完整图片,不要生成素材图集、规范展板、完整游戏截图或文字说明。不得复刻现有作品角色或 Logo。\n\n用户需求:{}", + truncate_prompt_context(prompt.trim()) + ); + } if options.asset_kind == "icon-spec" { return format!( "为这个 Web 小游戏生成一张 1:1 的统一视觉规范图,作为后续 UI 设计图和透明游戏图集的共同权威参考。规范板必须分区展示:玩家主体及其成长形态、核心目标或收集物、场景地块与障碍、HUD/操作图标、得分/受击/胜负反馈、主辅强调色与材质规则。所有元素使用一致的正交视角、轮廓、光照和原创视觉语言,留出清楚间距;不要生成完整游戏截图、海报、黑底图集或纯文字说明。玩法机制只用于理解功能,不授权复刻现有作品。\n\n项目视觉需求:{}", diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/external_generation_state.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/external_generation_state.rs index bfc4e6413..a6075cda0 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/external_generation_state.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/external_generation_state.rs @@ -707,9 +707,12 @@ pub(super) fn platform_art_generation_runtime_request_snapshot( } let (generation_kind, reference_resource_ids) = match state.endpoint.as_str() { "/api/external/v1/editor/images/generations" => { + // The public editor contract omits `kind` for an ordinary image. + // Keep a stable internal identity for recovery without rewriting + // the request into the specialised spec route. let generation_kind = json_string_field(&request_body, "kind") .filter(|value| !value.trim().is_empty()) - .ok_or_else(|| "External Editor 图片生成账本请求缺少 kind".to_string())?; + .unwrap_or_else(|| "image".to_string()); let reference_resource_ids = request_body .get("referenceImageSrcs") .and_then(serde_json::Value::as_array) diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 2c91504c4..8beef2c9a 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -1242,7 +1242,7 @@ game-project/ 本文早期关于“DirectProject 关闭通用 shell、原生网络和主动工具”的描述属于迁移前基线,现由以下覆盖规则取代:DirectProject 仅在真实 `game/` cwd 与 `workspaceWrite(writableRoots=[game])` 内恢复 Codex 原生文件/搜索/命令、图片查看和 Skill;其余 ToolHost/DirectHome 合同不变。客户端审核的 `agc_tools` MCP 继续承担平台美术、资源登记、去背景、浏览器试玩和受控搜索,并保留项目锁、幂等账本、下载校验、恢复与投影权威。 -DirectProject 使用 `approvalPolicy=never`,避免每次原生调用再经过泛化 ToolHost 包装;原生命令网络保持关闭,联网资料继续走受控 `agc_web_search`。多 Agent、Apps、插件、hooks、图片生成、Goals、Workspace Dependencies、Tool Suggestion 和原生浏览器/电脑控制仍关闭,避免绕过 AGC durable delegation、浏览器证据和副作用审计。app-server 使用隔离 `CODEX_HOME`,明确清空外部 MCP 后只注入 `agc_tools`;配置了 AGC LLM Key 或可解析的 `OPENAI_API_KEY` 登录态时,真实 provider 凭据只由 AGC 本地 provider proxy 持有,Codex 仅使用连接级随机代理令牌;无法安全代理的 OAuth `auth.json` 继续关闭 native shell/unified exec。`agc_tools` 的平台授权由 AGC 客户端当前登录会话和受控后端完成,普通客户端不得把 DirectProject 请求改成外部 API Key 请求;401/403 只投影为客户端登录或权限异常,不向用户索要凭据或暴露内部 URL。shell 子进程采用 `shell_environment_policy` core 继承及 secret/proxy/bridge 排除,provider key 和桥接凭据不得进入命令环境。系统提示词不再预注入项目源码快照或 Skill 正文,Codex 按需读取当前 cwd 文件。 +DirectProject 使用 `approvalPolicy=never`,避免每次原生调用再经过泛化 ToolHost 包装;原生命令网络保持关闭,联网资料继续走受控 `agc_web_search`。多 Agent、Apps、插件、hooks、Goals、Workspace Dependencies、Tool Suggestion 和原生浏览器/电脑控制仍关闭,避免绕过 AGC durable delegation、浏览器证据和副作用审计;图片生成通过客户端审核的 `agc_tools.agc_generate_image` 暴露普通单图、角色图、视觉规范图和 UI 设计图,完整游戏美术包继续使用 `agc_tools.taonier_prepare_game_art`,两者都复用同一客户端登录态、幂等账本、下载校验和 manifest/revision 投影,不开放 Codex 原生 image tool。app-server 使用隔离 `CODEX_HOME`,明确清空外部 MCP 后只注入 `agc_tools`;配置了 AGC LLM Key 或可解析的 `OPENAI_API_KEY` 登录态时,真实 provider 凭据只由 AGC 本地 provider proxy 持有,Codex 仅使用连接级随机代理令牌;无法安全代理的 OAuth `auth.json` 继续关闭 native shell/unified exec。`agc_tools` 的平台授权由 AGC 客户端当前登录会话和受控后端完成,普通客户端不得把 DirectProject 请求改成外部 API Key 请求;401/403 只投影为客户端登录或权限异常,不向用户索要凭据或暴露内部 URL。shell 子进程采用 `shell_environment_policy` core 继承及 secret/proxy/bridge 排除,provider key 和桥接凭据不得进入命令环境。系统提示词不再预注入项目源码快照或 Skill 正文,Codex 按需读取当前 cwd 文件。 ## 2026-08-24 AGC UI 原型桥接与自主 UI workflow - 2026-08-24 起,`ui-prototype` 与 UI 编辑器的 `UI` JSON 资源明确分离。设计图生成后必须由白名单 `ui.workflow.run` 按页面执行 `prepare → recognize → status → finalize`:为每个功能页面创建并关联 `UI` JSON,载入页面设计图和已登记图片/图标/字体,调用 UI Editor 的 provider-backed 结构识别、多树合并与分批组件绑定,持久化 State/revision,写入 `game/` 应用标记,并把 `reference-ready → structure-ready → merge-ready → binding-ready → application-ready → completed` 各阶段的 `generationKind` 和 manifest revision 投影给客户端。Provider 未配置、请求失败、工具调用缺失、结果不匹配、未知字体引用、未产出可渲染组件或仍有待审节点时保留最近真实阶段并返回 blocker,不得使用 deterministic seed 冒充完成。工作台点击 `ui-prototype` 时通过 `ensure_ui_design_resource_for_prototype` 幂等补齐关联资源;工作流完成后自动打开首个页面的 UI 编辑器 `visual-binding` 最终阶段,交给用户检查和手动调整。只生成图片、登记空 JSON 或进入普通图片画布均不构成 UI 工作流完成,详见 [`【技术方案】UI工作流资源桥接与Runtime执行-2026-08-24.md`](../【技术方案】UI工作流资源桥接与Runtime执行-2026-08-24.md)。