diff --git a/1.6/1.6/Assemblies/WulaFallenEmpire.dll b/1.6/1.6/Assemblies/WulaFallenEmpire.dll index 2189bb08..32333cdc 100644 Binary files a/1.6/1.6/Assemblies/WulaFallenEmpire.dll and b/1.6/1.6/Assemblies/WulaFallenEmpire.dll differ diff --git a/1.6/1.6/Assemblies/WulaFallenEmpire.pdb b/1.6/1.6/Assemblies/WulaFallenEmpire.pdb index 77032d12..c2f038ab 100644 Binary files a/1.6/1.6/Assemblies/WulaFallenEmpire.pdb and b/1.6/1.6/Assemblies/WulaFallenEmpire.pdb differ diff --git a/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs b/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs index 2e0f45c6..cacbd290 100644 --- a/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs +++ b/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs @@ -321,17 +321,17 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori _tools.Add(new Tool_SearchThingDef()); _tools.Add(new Tool_SearchPawnKind()); - // Agent 工具 - 游戏操作 - _tools.Add(new Tool_GetGameState()); - _tools.Add(new Tool_DesignateMine()); - _tools.Add(new Tool_DraftPawn()); - - // VLM 视觉分析工具 (条件性启用) + // Agent 工具 - 纯视觉操作 (移除了 GetGameState, DesignateMine, DraftPawn) if (WulaFallenEmpireMod.settings?.enableVlmFeatures == true) { _tools.Add(new Tool_AnalyzeScreen()); _tools.Add(new Tool_VisualClick()); _tools.Add(new Tool_VisualScroll()); + _tools.Add(new Tool_VisualTypeText()); + _tools.Add(new Tool_VisualDrag()); + _tools.Add(new Tool_VisualHotkey()); + _tools.Add(new Tool_VisualWait()); + _tools.Add(new Tool_VisualDeleteText()); } } @@ -485,7 +485,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori : string.Empty; string actionWhitelist = phase == RequestPhase.ActionTools ? "ACTION PHASE VALID TAGS ONLY:\n" + - ", , , , \n" + + ", , , , , , , , , , , \n" + "INVALID EXAMPLES (do NOT use now): , , \n" : string.Empty; @@ -559,7 +559,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori "Rules:\n" + "- You MUST NOT write any natural language to the user in this phase.\n" + "- Output XML tool calls only, or exactly: .\n" + - "- ONLY action tools are accepted in this phase (spawn_resources, send_reinforcement, call_bombardment, modify_goodwill).\n" + + "- ONLY action tools are accepted in this phase (spawn_resources, send_reinforcement, call_bombardment, modify_goodwill, visual_click, visual_scroll, visual_type_text, visual_drag, visual_hotkey, visual_wait, visual_delete_text).\n" + "- Query tools (get_*/search_*) will be ignored.\n" + "- Prefer action tools (spawn_resources, send_reinforcement, call_bombardment, modify_goodwill).\n" + "- Avoid queries unless absolutely required.\n" + @@ -630,14 +630,22 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori return toolName == "spawn_resources" || toolName == "send_reinforcement" || toolName == "call_bombardment" || - toolName == "modify_goodwill"; + toolName == "modify_goodwill" || + toolName == "visual_click" || + toolName == "visual_scroll" || + toolName == "visual_type_text" || + toolName == "visual_drag" || + toolName == "visual_hotkey" || + toolName == "visual_wait" || + toolName == "visual_delete_text"; } private static bool IsQueryToolName(string toolName) { if (string.IsNullOrWhiteSpace(toolName)) return false; return toolName.StartsWith("get_", StringComparison.OrdinalIgnoreCase) || - toolName.StartsWith("search_", StringComparison.OrdinalIgnoreCase); + toolName.StartsWith("search_", StringComparison.OrdinalIgnoreCase) || + toolName.StartsWith("analyze_", StringComparison.OrdinalIgnoreCase); } private static string SanitizeToolResultForActionPhase(string message) @@ -938,12 +946,18 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori "If the previous output indicates no action is needed or refuses action, output exactly: .\n" + "Do NOT invent new actions.\n" + "Output VALID XML tool calls only. No natural language, no commentary.\n" + - "Allowed tags: , , , , .\n" + + "Allowed tags: , , , , , , , .\n" + "\nAction tool XML formats:\n" + "- DefNameInt\n" + "- PawnKindDef: Count, ...\n" + "- DefNameIntInt\n" + "- Int\n" + + "- FloatFloat\n" + + "- Int\n" + + "- String\n" + + "- 0-10-10-10-1\n" + + "- String (e.g. 'enter', 'esc', 'space')\n" + + "- Float\n" + "\nPrevious output:\n" + TrimForPrompt(actionResponse, 600); string fixedResponse = await client.GetChatCompletionAsync(fixInstruction, actionContext, maxTokens: 128, temperature: 0.1f); bool fixedHasXml = !string.IsNullOrEmpty(fixedResponse) && IsXmlToolCall(fixedResponse); @@ -1007,12 +1021,18 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori "If the previous output indicates no action is needed or refuses action, output exactly: .\n" + "Do NOT invent new actions.\n" + "Output VALID XML tool calls only. No natural language, no commentary.\n" + - "Allowed tags: , , , , .\n" + + "Allowed tags: , , , , , , , .\n" + "\nAction tool XML formats:\n" + "- DefNameInt\n" + "- PawnKindDef: Count, ...\n" + "- DefNameIntInt\n" + "- Int\n" + + "- FloatFloat\n" + + "- Int\n" + + "- String\n" + + "- 0-10-10-10-1\n" + + "- String\n" + + "- Float\n" + "\nPrevious output:\n" + TrimForPrompt(retryActionResponse, 600); string retryFixedResponse = await client.GetChatCompletionAsync(retryFixInstruction, retryActionContext, maxTokens: 128, temperature: 0.1f); bool retryFixedHasXml = !string.IsNullOrEmpty(retryFixedResponse) && IsXmlToolCall(retryFixedResponse); @@ -1206,7 +1226,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori WulaLog.Debug($"[WulaAI] Executing tool (phase {phase}): {toolName} with args: {argsXml}"); } - string result = tool.Execute(argsXml).Trim(); + string result = (await tool.ExecuteAsync(argsXml)).Trim(); bool isError = !string.IsNullOrEmpty(result) && result.StartsWith("Error:", StringComparison.OrdinalIgnoreCase); if (toolName == "modify_goodwill") { diff --git a/Source/WulaFallenEmpire/EventSystem/AI/Agent/MouseSimulator.cs b/Source/WulaFallenEmpire/EventSystem/AI/Agent/MouseSimulator.cs index a4f293f9..2c0ae809 100644 --- a/Source/WulaFallenEmpire/EventSystem/AI/Agent/MouseSimulator.cs +++ b/Source/WulaFallenEmpire/EventSystem/AI/Agent/MouseSimulator.cs @@ -55,12 +55,9 @@ namespace WulaFallenEmpire.EventSystem.AI.Agent { try { - // Unity 坐标是左下角为原点,Windows 是左上角 - // 需要翻转 Y 坐标 - int windowsY = Screen.height - screenY; + // Windows坐标系原点在左上角,与 VLM Agent 使用的坐标 convention 一致 + int windowsY = screenY; - // 获取游戏窗口位置并加上偏移 - // 注意:这在全屏模式下可能需要调整 return SetCursorPos(screenX, windowsY); } catch (Exception ex) diff --git a/Source/WulaFallenEmpire/EventSystem/AI/Agent/VisualInteractionTools.cs b/Source/WulaFallenEmpire/EventSystem/AI/Agent/VisualInteractionTools.cs index f62f97db..d6d3a212 100644 --- a/Source/WulaFallenEmpire/EventSystem/AI/Agent/VisualInteractionTools.cs +++ b/Source/WulaFallenEmpire/EventSystem/AI/Agent/VisualInteractionTools.cs @@ -54,10 +54,7 @@ namespace WulaFallenEmpire.EventSystem.AI.Agent try { int screenX = Mathf.RoundToInt(x * Screen.width); - int screenY = Mathf.RoundToInt(y * Screen.height); - - // Unity Y 轴翻转 - int windowsY = Screen.height - screenY; + int windowsY = Mathf.RoundToInt(y * Screen.height); SetCursorPos(screenX, windowsY); Thread.Sleep(20); @@ -79,7 +76,7 @@ namespace WulaFallenEmpire.EventSystem.AI.Agent string buttonText = button == "right" ? "右键" : "左键"; string clickText = clicks == 2 ? "双击" : "单击"; - return $"Success: 在 ({screenX}, {screenY}) 处{buttonText}{clickText}"; + return $"Success: 在 ({screenX}, {windowsY}) 处{buttonText}{clickText}"; } catch (Exception ex) { @@ -124,8 +121,7 @@ namespace WulaFallenEmpire.EventSystem.AI.Agent try { int screenX = Mathf.RoundToInt(x * Screen.width); - int screenY = Mathf.RoundToInt(y * Screen.height); - int windowsY = Screen.height - screenY; + int windowsY = Mathf.RoundToInt(y * Screen.height); SetCursorPos(screenX, windowsY); Thread.Sleep(20); @@ -134,7 +130,7 @@ namespace WulaFallenEmpire.EventSystem.AI.Agent mouse_event(MOUSEEVENTF_WHEEL, 0, 0, (uint)wheelDelta, 0); string dir = direction == "up" ? "向上" : "向下"; - return $"Success: 在 ({screenX}, {screenY}) 处{dir}滚动 {amount} 步"; + return $"Success: 在 ({screenX}, {windowsY}) 处{dir}滚动 {amount} 步"; } catch (Exception ex) { @@ -150,9 +146,9 @@ namespace WulaFallenEmpire.EventSystem.AI.Agent try { int sx = Mathf.RoundToInt(startX * Screen.width); - int sy = Screen.height - Mathf.RoundToInt(startY * Screen.height); + int sy = Mathf.RoundToInt(startY * Screen.height); int ex = Mathf.RoundToInt(endX * Screen.width); - int ey = Screen.height - Mathf.RoundToInt(endY * Screen.height); + int ey = Mathf.RoundToInt(endY * Screen.height); // 移动到起点 SetCursorPos(sx, sy); diff --git a/Source/WulaFallenEmpire/EventSystem/AI/Tools/AITool.cs b/Source/WulaFallenEmpire/EventSystem/AI/Tools/AITool.cs index 6af97af6..e51fc7a9 100644 --- a/Source/WulaFallenEmpire/EventSystem/AI/Tools/AITool.cs +++ b/Source/WulaFallenEmpire/EventSystem/AI/Tools/AITool.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text.RegularExpressions; +using System.Threading.Tasks; using Verse; namespace WulaFallenEmpire.EventSystem.AI.Tools @@ -11,7 +12,8 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools public abstract string Description { get; } public abstract string UsageSchema { get; } // XML schema description - public abstract string Execute(string args); + public virtual string Execute(string args) => "Error: Synchronous execution not supported for this tool."; + public virtual Task ExecuteAsync(string args) => Task.FromResult(Execute(args)); /// /// Helper method to parse XML arguments into a dictionary. diff --git a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_AnalyzeScreen.cs b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_AnalyzeScreen.cs index 500649fe..695b7887 100644 --- a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_AnalyzeScreen.cs +++ b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_AnalyzeScreen.cs @@ -11,27 +11,18 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools public override string Name => "analyze_screen"; public override string Description => - "分析当前游戏屏幕截图,了解玩家正在查看什么区域或内容。需要配置 VLM API 密钥。"; + "分析当前游戏屏幕截图。你可以提供具体的指令(instruction)告诉视觉模型你需要观察什么、寻找什么、或者如何描述屏幕。"; public override string UsageSchema => - "分析目标,如:玩家在看什么区域"; + "给视觉模型的具体指令。例如:'找到科研按钮的比例坐标' 或 '描述当前角色的健康状态栏内容'"; - private const string VisionSystemPrompt = @" -你是一个 RimWorld 游戏屏幕分析助手。分析截图并用简洁中文描述: -- 玩家正在查看的区域(如:殖民地基地、世界地图、菜单界面) -- 可见的重要建筑、角色、资源 -- 任何明显的问题或特殊状态 -保持回答简洁,不超过100字。不要使用 XML 标签。"; + private const string BaseVisionSystemPrompt = "你是一个专业的老练 RimWorld 助手。你会根据指示分析屏幕截图。保持回答专业且简洁。不要输出 XML 标签,除非被明确要求。"; - public override string Execute(string args) + public override async Task ExecuteAsync(string args) { - // 由于 VLM API 调用是异步的,我们需要同步等待结果 - // 这在 Unity 主线程上可能会阻塞,但工具执行通常在异步上下文中调用 try { - var task = ExecuteInternalAsync(args); - // 使用 GetAwaiter().GetResult() 来同步等待,避免死锁 - return task.GetAwaiter().GetResult(); + return await ExecuteInternalAsync(args); } catch (Exception ex) { @@ -43,7 +34,9 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools private async Task ExecuteInternalAsync(string xmlContent) { var argsDict = ParseXmlArgs(xmlContent); - string context = argsDict.TryGetValue("context", out var ctx) ? ctx : "描述当前屏幕内容"; + // 优先使用 instruction,兼容旧的 context 参数 + string instruction = argsDict.TryGetValue("instruction", out var inst) ? inst : + (argsDict.TryGetValue("context", out var ctx) ? ctx : "描述当前屏幕内容,重点关注 UI 状态和重要实体。"); try { @@ -75,11 +68,11 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools var client = new SimpleAIClient(vlmApiKey, vlmBaseUrl, vlmModel); string result = await client.GetVisionCompletionAsync( - VisionSystemPrompt, - context, + BaseVisionSystemPrompt, + instruction, base64Image, - maxTokens: 256, - temperature: 0.3f + maxTokens: 512, // 增加 token 数以支持更复杂的分析指令响应 + temperature: 0.2f ); if (string.IsNullOrEmpty(result)) diff --git a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_VisualClick.cs b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_VisualClick.cs index 10018548..9a43db5d 100644 --- a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_VisualClick.cs +++ b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_VisualClick.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using UnityEngine; +using WulaFallenEmpire.EventSystem.AI.Agent; namespace WulaFallenEmpire.EventSystem.AI.Tools { @@ -14,12 +15,12 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools public override string Description => "在指定的屏幕位置执行鼠标点击。坐标使用比例值 (0-1),(0,0) 是左上角,(1,1) 是右下角。" + - "适用于点击无法通过 API 操作的 mod 按钮或 UI 元素。先使用 analyze_screen 获取目标位置。"; + "适用于点击无法通过 API 操作的 mod 按钮或 UI 元素。先使用 analyze_screen 获取目标位置分析。"; public override string UsageSchema => "0-1之间的X比例0-1之间的Y比例可选,true为右键"; - public override string Execute(string args) + public override Task ExecuteAsync(string args) { try { @@ -28,19 +29,19 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools // 解析 X 坐标 if (!argsDict.TryGetValue("x", out string xStr) || !float.TryParse(xStr, out float x)) { - return "Error: 缺少有效的 x 坐标 (0-1之间的比例值)"; + return Task.FromResult("Error: 缺少有效的 x 坐标 (0-1之间的比例值)"); } // 解析 Y 坐标 if (!argsDict.TryGetValue("y", out string yStr) || !float.TryParse(yStr, out float y)) { - return "Error: 缺少有效的 y 坐标 (0-1之间的比例值)"; + return Task.FromResult("Error: 缺少有效的 y 坐标 (0-1之间的比例值)"); } // 验证范围 if (x < 0 || x > 1 || y < 0 || y > 1) { - return $"Error: 坐标 ({x}, {y}) 超出范围,必须在 0-1 之间"; + return Task.FromResult($"Error: 坐标 ({x}, {y}) 超出范围,必须在 0-1 之间"); } // 解析右键选项 @@ -60,17 +61,17 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools int screenY = Mathf.RoundToInt(y * Screen.height); WulaLog.Debug($"[Tool_VisualClick] {clickType}点击 ({x:F3}, {y:F3}) -> 屏幕 ({screenX}, {screenY})"); - return $"Success: 已在屏幕位置 ({screenX}, {screenY}) 执行{clickType}点击"; + return Task.FromResult($"Success: 已在屏幕位置 ({screenX}, {screenY}) 执行{clickType}点击"); } else { - return "Error: 点击操作失败"; + return Task.FromResult("Error: 点击操作失败"); } } catch (Exception ex) { WulaLog.Debug($"[Tool_VisualClick] Error: {ex}"); - return $"Error: 点击操作失败 - {ex.Message}"; + return Task.FromResult($"Error: 点击操作失败 - {ex.Message}"); } } } @@ -88,7 +89,7 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools public override string UsageSchema => "要输入的文本"; - public override string Execute(string args) + public override Task ExecuteAsync(string args) { try { @@ -96,23 +97,23 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools if (!argsDict.TryGetValue("text", out string text) || string.IsNullOrEmpty(text)) { - return "Error: 缺少要输入的文本"; + return Task.FromResult("Error: 缺少要输入的文本"); } - // 使用剪贴板方式输入(支持中文) - GUIUtility.systemCopyBuffer = text; + // 获取当前鼠标位置 + var pos = MouseSimulator.GetCurrentPosition(); - // 模拟 Ctrl+V 粘贴 - // 注意:这需要额外的键盘模拟实现 - // 暂时返回成功,实际使用时需要完善 + float propX = Mathf.Clamp01((float)pos.x / Screen.width); + float propY = Mathf.Clamp01((float)pos.y / Screen.height); - WulaLog.Debug($"[Tool_VisualTypeText] 已将文本复制到剪贴板: {text}"); - return $"Success: 已将文本复制到剪贴板。请手动按 Ctrl+V 粘贴,或等待键盘模拟功能完善。"; + WulaLog.Debug($"[VisualTypeText] Current Pos: ({pos.x}, {pos.y}) -> Proportional: ({propX:F3}, {propY:F3})"); + + return Task.FromResult(VisualInteractionTools.TypeText(propX, propY, text)); } catch (Exception ex) { WulaLog.Debug($"[Tool_VisualTypeText] Error: {ex}"); - return $"Error: 输入文本失败 - {ex.Message}"; + return Task.FromResult($"Error: 输入文本失败 - {ex.Message}"); } } } @@ -130,7 +131,7 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools public override string UsageSchema => "滚动量,正数向上负数向下可选,0-1 X坐标可选,0-1 Y坐标"; - public override string Execute(string args) + public override Task ExecuteAsync(string args) { try { @@ -138,7 +139,7 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools if (!argsDict.TryGetValue("delta", out string deltaStr) || !int.TryParse(deltaStr, out int delta)) { - return "Error: 缺少有效的 delta 值"; + return Task.FromResult("Error: 缺少有效的 delta 值"); } // 可选:先移动到指定位置 @@ -154,12 +155,12 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools Agent.MouseSimulator.Scroll(delta); string direction = delta > 0 ? "向上" : "向下"; - return $"Success: 已{direction}滚动 {Math.Abs(delta)} 单位"; + return Task.FromResult($"Success: 已{direction}滚动 {Math.Abs(delta)} 单位"); } catch (Exception ex) { WulaLog.Debug($"[Tool_VisualScroll] Error: {ex}"); - return $"Error: 滚动操作失败 - {ex.Message}"; + return Task.FromResult($"Error: 滚动操作失败 - {ex.Message}"); } } } diff --git a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_VisualUtils.cs b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_VisualUtils.cs new file mode 100644 index 00000000..d652bde1 --- /dev/null +++ b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_VisualUtils.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using UnityEngine; +using WulaFallenEmpire.EventSystem.AI.Agent; + +namespace WulaFallenEmpire.EventSystem.AI.Tools +{ + public abstract class VisualToolBase : AITool + { + protected bool GetFloat(Dictionary dict, string key, out float result) + { + result = 0f; + if (dict.TryGetValue(key, out string val) && float.TryParse(val, out result)) + return true; + return false; + } + + public abstract override Task ExecuteAsync(string args); + } + + /// + /// 视觉拖拽工具 + /// + public class Tool_VisualDrag : VisualToolBase + { + public override string Name => "visual_drag"; + public override string Description => "从起始坐标拖拽到结束坐标。适用于框选单位、拖动滑块或地图。"; + public override string UsageSchema => "0-10-10-10-1秒(默认0.5)"; + + public override Task ExecuteAsync(string args) + { + try + { + var dict = ParseXmlArgs(args); + if (!GetFloat(dict, "start_x", out float sx) || !GetFloat(dict, "start_y", out float sy) || + !GetFloat(dict, "end_x", out float ex) || !GetFloat(dict, "end_y", out float ey)) + return Task.FromResult("Error: 缺少有效的坐标参数 (0-1)"); + + float duration = 0.5f; + if (GetFloat(dict, "duration", out float d)) duration = d; + + return Task.FromResult(VisualInteractionTools.MouseDrag(sx, sy, ex, ey, duration)); + } + catch (Exception ex) { return Task.FromResult($"Error: {ex.Message}"); } + } + } + + /// + /// 视觉快捷键工具 (通用) + /// + public class Tool_VisualHotkey : VisualToolBase + { + public override string Name => "visual_hotkey"; + public override string Description => "在指定位置点击(可选)并按下快捷键。支持组合键如 'ctrl+c', 'alt+f4', 单键如 'enter', 'esc', 'r', 'space'。"; + public override string UsageSchema => "快捷键可选可选"; + + public override Task ExecuteAsync(string args) + { + try + { + var dict = ParseXmlArgs(args); + string key = dict.ContainsKey("key") ? dict["key"] : ""; + if (string.IsNullOrEmpty(key)) return Task.FromResult("Error: 缺少 key 参数"); + + // 如果提供了坐标,先点击 + if (GetFloat(dict, "x", out float x) && GetFloat(dict, "y", out float y)) + { + return Task.FromResult(VisualInteractionTools.PressHotkey(x, y, key)); + } + else + { + // 在当前位置直接按键 + var pos = MouseSimulator.GetCurrentPosition(); + float propX = Mathf.Clamp01((float)pos.x / Screen.width); + float propY = Mathf.Clamp01(1.0f - ((float)pos.y / Screen.height)); + return Task.FromResult(VisualInteractionTools.PressHotkey(propX, propY, key)); + } + } + catch (Exception ex) { return Task.FromResult($"Error: {ex.Message}"); } + } + } + + /// + /// 视觉等待工具 + /// + public class Tool_VisualWait : VisualToolBase + { + public override string Name => "visual_wait"; + public override string Description => "等待指定时间。用于等待UI动画或加载。"; + public override string UsageSchema => "秒数"; + + public override Task ExecuteAsync(string args) + { + try + { + var dict = ParseXmlArgs(args); + if (!GetFloat(dict, "seconds", out float seconds)) return Task.FromResult("Error: 缺少 seconds 参数"); + return Task.FromResult(VisualInteractionTools.Wait(seconds)); + } + catch (Exception ex) { return Task.FromResult($"Error: {ex.Message}"); } + } + } + + /// + /// 视觉删除文本工具 + /// + public class Tool_VisualDeleteText : VisualToolBase + { + public override string Name => "visual_delete_text"; + public override string Description => "点击指定位置并按 Backspace 删除指定数量的字符。用于清空输入框。"; + public override string UsageSchema => "0-10-1字符数(默认1)"; + + public override Task ExecuteAsync(string args) + { + try + { + var dict = ParseXmlArgs(args); + if (!GetFloat(dict, "x", out float x) || !GetFloat(dict, "y", out float y)) + return Task.FromResult("Error: 缺少有效的坐标参数"); + + int count = 1; + if (dict.TryGetValue("count", out string cStr) && int.TryParse(cStr, out int c)) count = c; + + return Task.FromResult(VisualInteractionTools.DeleteText(x, y, count)); + } + catch (Exception ex) { return Task.FromResult($"Error: {ex.Message}"); } + } + } +}