This commit is contained in:
2025-12-28 12:03:33 +08:00
parent 42427f28df
commit addb6bbd08
6 changed files with 181 additions and 391 deletions

View File

@@ -248,7 +248,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
return; return;
} }
// 附加选中对象的上下文信息 // 附加选中对象的上下文信息
string messageWithContext = BuildUserMessageWithContext(text); string messageWithContext = BuildUserMessageWithContext(text);
_history.Add(("user", messageWithContext)); _history.Add(("user", messageWithContext));
PersistHistory(); PersistHistory();
@@ -317,23 +317,18 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
_tools.Add(new Tool_SendReinforcement()); _tools.Add(new Tool_SendReinforcement());
_tools.Add(new Tool_GetColonistStatus()); _tools.Add(new Tool_GetColonistStatus());
_tools.Add(new Tool_GetMapResources()); _tools.Add(new Tool_GetMapResources());
_tools.Add(new Tool_GetAvailablePrefabs());
_tools.Add(new Tool_GetMapPawns()); _tools.Add(new Tool_GetMapPawns());
_tools.Add(new Tool_GetRecentNotifications()); _tools.Add(new Tool_GetRecentNotifications());
_tools.Add(new Tool_CallBombardment()); _tools.Add(new Tool_CallBombardment());
_tools.Add(new Tool_SearchThingDef()); _tools.Add(new Tool_SearchThingDef());
_tools.Add(new Tool_SearchPawnKind()); _tools.Add(new Tool_SearchPawnKind());
_tools.Add(new Tool_CallPrefabAirdrop());
// Agent 工具 - 纯视觉操作 (移除了 GetGameState, DesignateMine, DraftPawn) // Agent 工具 - 保留画面分析截图能力,移除所有模拟操作工具
if (WulaFallenEmpireMod.settings?.enableVlmFeatures == true) if (WulaFallenEmpireMod.settings?.enableVlmFeatures == true)
{ {
_tools.Add(new Tool_AnalyzeScreen()); _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());
} }
} }
@@ -482,6 +477,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
"- send_reinforcement\n" + "- send_reinforcement\n" +
"- call_bombardment\n" + "- call_bombardment\n" +
"- modify_goodwill\n" + "- modify_goodwill\n" +
"- call_prefab_airdrop\n" +
"If no action is required, output exactly: <no_action/>.\n" + "If no action is required, output exactly: <no_action/>.\n" +
"Query tools exist but are disabled in this phase (not listed here).\n" "Query tools exist but are disabled in this phase (not listed here).\n"
: string.Empty; : string.Empty;
@@ -497,8 +493,8 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
string actionWhitelist = phase == RequestPhase.ActionTools string actionWhitelist = phase == RequestPhase.ActionTools
? "ACTION PHASE VALID TAGS ONLY:\n" + ? "ACTION PHASE VALID TAGS ONLY:\n" +
"<spawn_resources>, <send_reinforcement>, <call_bombardment>, <modify_goodwill>, <visual_click>, <visual_scroll>, <visual_type_text>, <visual_drag>, <visual_hotkey>, <visual_wait>, <visual_delete_text>, <no_action/>\n" + "<spawn_resources>, <send_reinforcement>, <call_bombardment>, <modify_goodwill>, <call_prefab_airdrop>, <no_action/>\n" +
"INVALID EXAMPLES (do NOT use now): <get_map_resources/>, JSON, Markdown Code Blocks\n" "INVALID EXAMPLES (do NOT use now): <get_map_resources/>, <analyze_screen/>\n"
: string.Empty; : string.Empty;
return string.Join("\n\n", new[] return string.Join("\n\n", new[]
@@ -571,7 +567,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
"Rules:\n" + "Rules:\n" +
"- You MUST NOT write any natural language to the user in this phase.\n" + "- You MUST NOT write any natural language to the user in this phase.\n" +
"- Output XML tool calls only, or exactly: <no_action/>.\n" + "- Output XML tool calls only, or exactly: <no_action/>.\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" + "- ONLY action tools are accepted in this phase (spawn_resources, send_reinforcement, call_bombardment, modify_goodwill, call_prefab_airdrop).\n" +
"- Query tools (get_*/search_*) will be ignored.\n" + "- Query tools (get_*/search_*) will be ignored.\n" +
"- Prefer action tools (spawn_resources, send_reinforcement, call_bombardment, modify_goodwill).\n" + "- Prefer action tools (spawn_resources, send_reinforcement, call_bombardment, modify_goodwill).\n" +
"- Avoid queries unless absolutely required.\n" + "- Avoid queries unless absolutely required.\n" +
@@ -643,13 +639,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
toolName == "send_reinforcement" || toolName == "send_reinforcement" ||
toolName == "call_bombardment" || toolName == "call_bombardment" ||
toolName == "modify_goodwill" || toolName == "modify_goodwill" ||
toolName == "visual_click" || toolName == "call_prefab_airdrop";
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) private static bool IsQueryToolName(string toolName)
@@ -839,15 +829,6 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
} }
} }
private bool CheckVisualIntent(string message)
{
if (string.IsNullOrEmpty(message)) return false;
string[] keywords = new string[] {
"屏幕", "画面", "截图", "看", "找", "显示", // CN
"screen", "screenshot", "image", "view", "look", "see", "find", "visual", "scan" // EN
};
return keywords.Any(k => message.IndexOf(k, StringComparison.OrdinalIgnoreCase) >= 0);
}
private async Task RunPhasedRequestAsync() private async Task RunPhasedRequestAsync()
{ {
if (_isThinking) return; if (_isThinking) return;
@@ -880,7 +861,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
if (settings.enableVlmFeatures && settings.showThinkingProcess) if (settings.enableVlmFeatures && settings.showThinkingProcess)
{ {
// Optional: We can still say "Analyzing data link..." // Optional: We can still say "Analyzing data link..."
AddAssistantMessage("<i>[P.I.A] 正在分析数据链路...</i>"); AddAssistantMessage("<i>[P.I.A] 正在分析数据链路...</i>");
} }
var queryPhase = RequestPhase.QueryTools; var queryPhase = RequestPhase.QueryTools;
@@ -914,7 +895,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
base64Image = queryResult.CapturedImage; base64Image = queryResult.CapturedImage;
if (settings.showThinkingProcess) if (settings.showThinkingProcess)
{ {
AddAssistantMessage("<i>[P.I.A] 视觉传感器已激活,图像已捕获...</i>"); AddAssistantMessage("<i>[P.I.A] 视觉传感器已激活,图像已捕获...</i>");
} }
} }
@@ -965,7 +946,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
if (settings.showThinkingProcess) if (settings.showThinkingProcess)
{ {
AddAssistantMessage("<i>[P.I.A] 正在计算最优战术方案...</i>"); AddAssistantMessage("<i>[P.I.A] 正在计算最优战术方案...</i>");
} }
var actionPhase = RequestPhase.ActionTools; var actionPhase = RequestPhase.ActionTools;
@@ -998,19 +979,14 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
"Preserve the intent of the previous output.\n" + "Preserve the intent of the previous output.\n" +
"If the previous output indicates no action is needed or refuses action, output exactly: <no_action/>.\n" + "If the previous output indicates no action is needed or refuses action, output exactly: <no_action/>.\n" +
"Do NOT invent new actions.\n" + "Do NOT invent new actions.\n" +
"Output VALID XML tool calls only. No natural language, no commentary.\nIf the previous output contains JSON (including JSON code blocks or inline {\"point\": [x,y]}), convert it to XML.\n- For a point coordinate, output: <visual_click><x>...</x><y>...</y></visual_click>.\n- If coordinates are larger than 1 (e.g., 0-1000), normalize by dividing by 1000.\nIgnore any non-XML text.\n" + "Output VALID XML tool calls only. No natural language, no commentary.\nIgnore any non-XML text.\n" +
"Allowed tags: <spawn_resources>, <send_reinforcement>, <call_bombardment>, <modify_goodwill>, <visual_click>, <visual_scroll>, <visual_type_text>, <no_action/>.\n" + "Allowed tags: <spawn_resources>, <send_reinforcement>, <call_bombardment>, <modify_goodwill>, <call_prefab_airdrop>, <no_action/>.\n" +
"\nAction tool XML formats:\n" + "\nAction tool XML formats:\n" +
"- <spawn_resources><items><item><name>DefName</name><count>Int</count></item></items></spawn_resources>\n" + "- <spawn_resources><items><item><name>DefName</name><count>Int</count></item></items></spawn_resources>\n" +
"- <send_reinforcement><units>PawnKindDef: Count, ...</units></send_reinforcement>\n" + "- <send_reinforcement><units>PawnKindDef: Count, ...</units></send_reinforcement>\n" +
"- <call_bombardment><abilityDef>DefName</abilityDef><x>Int</x><z>Int</z></call_bombardment>\n" + "- <call_bombardment><abilityDef>DefName</abilityDef><x>Int</x><z>Int</z></call_bombardment>\n" +
"- <modify_goodwill><amount>Int</amount></modify_goodwill>\n" + "- <modify_goodwill><amount>Int</amount></modify_goodwill>\n" +
"- <visual_click><x>Float</x><y>Float</y></visual_click>\n" + "- <call_prefab_airdrop><prefabDefName>DefName</prefabDefName><x>Int</x><z>Int</z></call_prefab_airdrop>\n" +
"- <visual_scroll><delta>Int</delta></visual_scroll>\n" +
"- <visual_type_text><text>String</text></visual_type_text>\n" +
"- <visual_drag><start_x>0-1</start_x><start_y>0-1</start_y><end_x>0-1</end_x><end_y>0-1</end_y></visual_drag>\n" +
"- <visual_hotkey><key>String (e.g. 'enter', 'esc', 'space')</key></visual_hotkey>\n" +
"- <visual_wait><seconds>Float</seconds></visual_wait>\n" +
"\nPrevious output:\n" + TrimForPrompt(actionResponse, 600); "\nPrevious output:\n" + TrimForPrompt(actionResponse, 600);
string fixedResponse = await client.GetChatCompletionAsync(fixInstruction, actionContext, maxTokens: 2048, temperature: 0.1f); string fixedResponse = await client.GetChatCompletionAsync(fixInstruction, actionContext, maxTokens: 2048, temperature: 0.1f);
bool fixedHasXml = !string.IsNullOrEmpty(fixedResponse) && IsXmlToolCall(fixedResponse); bool fixedHasXml = !string.IsNullOrEmpty(fixedResponse) && IsXmlToolCall(fixedResponse);
@@ -1073,19 +1049,14 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
"Preserve the intent of the previous output.\n" + "Preserve the intent of the previous output.\n" +
"If the previous output indicates no action is needed or refuses action, output exactly: <no_action/>.\n" + "If the previous output indicates no action is needed or refuses action, output exactly: <no_action/>.\n" +
"Do NOT invent new actions.\n" + "Do NOT invent new actions.\n" +
"Output VALID XML tool calls only. No natural language, no commentary.\nIf the previous output contains JSON (including JSON code blocks or inline {\"point\": [x,y]}), convert it to XML.\n- For a point coordinate, output: <visual_click><x>...</x><y>...</y></visual_click>.\n- If coordinates are larger than 1 (e.g., 0-1000), normalize by dividing by 1000.\nIgnore any non-XML text.\n" + "Output VALID XML tool calls only. No natural language, no commentary.\nIgnore any non-XML text.\n" +
"Allowed tags: <spawn_resources>, <send_reinforcement>, <call_bombardment>, <modify_goodwill>, <visual_click>, <visual_scroll>, <visual_type_text>, <no_action/>.\n" + "Allowed tags: <spawn_resources>, <send_reinforcement>, <call_bombardment>, <modify_goodwill>, <call_prefab_airdrop>, <no_action/>.\n" +
"\nAction tool XML formats:\n" + "\nAction tool XML formats:\n" +
"- <spawn_resources><items><item><name>DefName</name><count>Int</count></item></items></spawn_resources>\n" + "- <spawn_resources><items><item><name>DefName</name><count>Int</count></item></items></spawn_resources>\n" +
"- <send_reinforcement><units>PawnKindDef: Count, ...</units></send_reinforcement>\n" + "- <send_reinforcement><units>PawnKindDef: Count, ...</units></send_reinforcement>\n" +
"- <call_bombardment><abilityDef>DefName</abilityDef><x>Int</x><z>Int</z></call_bombardment>\n" + "- <call_bombardment><abilityDef>DefName</abilityDef><x>Int</x><z>Int</z></call_bombardment>\n" +
"- <modify_goodwill><amount>Int</amount></modify_goodwill>\n" + "- <modify_goodwill><amount>Int</amount></modify_goodwill>\n" +
"- <visual_click><x>Float</x><y>Float</y></visual_click>\n" + "- <call_prefab_airdrop><prefabDefName>DefName</prefabDefName><x>Int</x><z>Int</z></call_prefab_airdrop>\n" +
"- <visual_scroll><delta>Int</delta></visual_scroll>\n" +
"- <visual_type_text><text>String</text></visual_type_text>\n" +
"- <visual_drag><start_x>0-1</start_x><start_y>0-1</start_y><end_x>0-1</end_x><end_y>0-1</end_y></visual_drag>\n" +
"- <visual_hotkey><key>String</key></visual_hotkey>\n" +
"- <visual_wait><seconds>Float</seconds></visual_wait>\n" +
"\nPrevious output:\n" + TrimForPrompt(retryActionResponse, 600); "\nPrevious output:\n" + TrimForPrompt(retryActionResponse, 600);
string retryFixedResponse = await client.GetChatCompletionAsync(retryFixInstruction, retryActionContext, maxTokens: 2048, temperature: 0.1f); string retryFixedResponse = await client.GetChatCompletionAsync(retryFixInstruction, retryActionContext, maxTokens: 2048, temperature: 0.1f);
bool retryFixedHasXml = !string.IsNullOrEmpty(retryFixedResponse) && IsXmlToolCall(retryFixedResponse); bool retryFixedHasXml = !string.IsNullOrEmpty(retryFixedResponse) && IsXmlToolCall(retryFixedResponse);
@@ -1151,7 +1122,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
if (settings.showThinkingProcess) if (settings.showThinkingProcess)
{ {
AddAssistantMessage("<i>[P.I.A] 正在汇总战报并建立通讯记录...</i>"); AddAssistantMessage("<i>[P.I.A] 正在汇总战报并建立通讯记录...</i>");
} }
// VISUAL CONTEXT FOR REPLY: Pass the image so the AI can describe what it sees. // VISUAL CONTEXT FOR REPLY: Pass the image so the AI can describe what it sees.
@@ -1212,52 +1183,6 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
string guidance = "ToolRunner Guidance: Reply to the player in natural language only. Do NOT output any XML. You may include [EXPR:n] to set expression (n=1-6)."; string guidance = "ToolRunner Guidance: Reply to the player in natural language only. Do NOT output any XML. You may include [EXPR:n] to set expression (n=1-6).";
var matches = Regex.Matches(xml ?? "", @"<([a-zA-Z0-9_]+)(?:>.*?</\1>|/>)", RegexOptions.Singleline); var matches = Regex.Matches(xml ?? "", @"<([a-zA-Z0-9_]+)(?:>.*?</\1>|/>)", RegexOptions.Singleline);
// GEMINI 3 JSON FALLBACK (Restored & Fixed)
// If no XML found, try to parse JSON markdown block commonly output by Gemini 3 Flash Preview
// Uses a 0-1000 coordinate system typically.
if (matches.Count == 0 && (xml ?? "").Contains("```json"))
{
try
{
var jsonMatch = Regex.Match(xml, @"```json\s*(\[.*?\])\s*```", RegexOptions.Singleline);
if (jsonMatch.Success)
{
string jsonArr = jsonMatch.Groups[1].Value;
// Regex to extract objects with "point" (and optional "action")
// Matches: { "point": [123, 456] }
// Note: In verbatim string @"""", double quotes are escaped as "". Not \".
var pointMatches = Regex.Matches(jsonArr, @"\{.*?\""point\""\s*:\s*\[\s*([\d\.]+)\s*,\s*([\d\.]+)\s*\].*?\}", RegexOptions.Singleline);
if (pointMatches.Count > 0)
{
StringBuilder synthesizedXml = new StringBuilder();
foreach (Match pm in pointMatches)
{
if (float.TryParse(pm.Groups[1].Value, out float xVal) && float.TryParse(pm.Groups[2].Value, out float yVal))
{
// Gemini uses 0-1000 scale usually
if (xVal > 1 || yVal > 1)
{
xVal /= 1000.0f;
yVal /= 1000.0f;
}
synthesizedXml.Append($"<visual_click><x>{xVal}</x><y>{yVal}</y></visual_click>");
}
}
if (synthesizedXml.Length > 0)
{
xml = synthesizedXml.ToString() + "\n" + xml; // Prepend to process downstream
matches = Regex.Matches(xml, @"<([a-zA-Z0-9_]+)(?:>.*?</\1>|/>)", RegexOptions.Singleline);
}
}
}
}
catch (Exception ex)
{
WulaLog.Debug($"[JSON Fallback Error] {ex.Message}");
}
}
if (matches.Count == 0 || (matches.Count == 1 && matches[0].Groups[1].Value.Equals("no_action", StringComparison.OrdinalIgnoreCase))) if (matches.Count == 0 || (matches.Count == 1 && matches[0].Groups[1].Value.Equals("no_action", StringComparison.OrdinalIgnoreCase)))
{ {

View File

@@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using RimWorld;
using UnityEngine;
using Verse;
namespace WulaFallenEmpire.EventSystem.AI.Tools
{
public class Tool_CallPrefabAirdrop : AITool
{
public override string Name => "call_prefab_airdrop";
public override string Description => "Calls a large prefab building airdrop at the specified coordinates. " +
"You must specify the prefabDefName (e.g., 'WULA_NewColonyBase') and the coordinates (x, z). " +
"TIP: Use the 'get_available_prefabs' tool first to see which structures are available. " +
"The default skyfaller animation is 'WULA_Prefab_Incoming'.";
public override string UsageSchema => "<call_prefab_airdrop><prefabDefName>DefName of the prefab</prefabDefName><skyfallerDef>Optional, default is WULA_Prefab_Incoming</skyfallerDef><x>int</x><z>int</z></call_prefab_airdrop>";
public override string Execute(string args)
{
try
{
var parsed = ParseXmlArgs(args);
if (!parsed.TryGetValue("prefabDefName", out string prefabDefName) || string.IsNullOrWhiteSpace(prefabDefName))
{
return "Error: Missing <prefabDefName>. Example: <prefabDefName>WULA_NewColonyBase</prefabDefName>";
}
if (!parsed.TryGetValue("x", out string xStr) || !int.TryParse(xStr, out int x) ||
!parsed.TryGetValue("z", out string zStr) || !int.TryParse(zStr, out int z))
{
return "Error: Missing or invalid target coordinates. Provide <x> and <z>.";
}
string skyfallerDefName = parsed.TryGetValue("skyfallerDef", out string sd) && !string.IsNullOrWhiteSpace(sd)
? sd.Trim()
: "WULA_Prefab_Incoming";
Map map = Find.CurrentMap;
if (map == null) return "Error: No active map.";
IntVec3 targetCell = new IntVec3(x, 0, z);
if (!targetCell.InBounds(map)) return $"Error: Target {targetCell} is out of bounds.";
// Check if prefab exists
PrefabDef prefabDef = DefDatabase<PrefabDef>.GetNamed(prefabDefName, false);
if (prefabDef == null)
{
return $"Error: PrefabDef '{prefabDefName}' not found.";
}
// Check if skyfaller exists
ThingDef skyfallerDef = DefDatabase<ThingDef>.GetNamed(skyfallerDefName, false);
if (skyfallerDef == null)
{
return $"Error: Skyfaller ThingDef '{skyfallerDefName}' not found.";
}
// Spawning must happen on main thread
string resultMessage = $"Success: Scheduled airdrop for '{prefabDefName}' at {targetCell} using {skyfallerDefName}.";
// We use a closure to capture the parameters
string pDef = prefabDefName;
ThingDef sDef = skyfallerDef;
IntVec3 cell = targetCell;
Map targetMap = map;
LongEventHandler.ExecuteWhenFinished(() =>
{
try
{
var skyfaller = (Skyfaller_PrefabSpawner)SkyfallerMaker.MakeSkyfaller(sDef);
skyfaller.prefabDefName = pDef;
GenSpawn.Spawn(skyfaller, cell, targetMap);
WulaLog.Debug($"[WulaAI] Prefab airdrop spawned: {pDef} at {cell}");
}
catch (Exception ex)
{
WulaLog.Debug($"[WulaAI] Failed to spawn prefab airdrop on main thread: {ex.Message}");
}
});
return resultMessage;
}
catch (Exception ex)
{
return $"Error: {ex.Message}";
}
}
}
}

View File

@@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using RimWorld;
using Verse;
namespace WulaFallenEmpire.EventSystem.AI.Tools
{
public class Tool_GetAvailablePrefabs : AITool
{
public override string Name => "get_available_prefabs";
public override string Description => "Returns a list of available building prefabs (blueprints) that can be summoned. " +
"Use this to find the correct 'prefabDefName' for the 'call_prefab_airdrop' tool.";
public override string UsageSchema => "<get_available_prefabs/>";
public override string Execute(string args)
{
try
{
var prefabs = DefDatabase<PrefabDef>.AllDefs.ToList();
if (prefabs.Count == 0)
{
return "No prefabs found in the database.";
}
StringBuilder sb = new StringBuilder();
sb.AppendLine($"Found {prefabs.Count} available prefabs:");
// Group by prefix to help AI categorize
var wulaPrefabs = prefabs.Where(p => p.defName.StartsWith("WULA_", StringComparison.OrdinalIgnoreCase)).ToList();
var otherPrefabs = prefabs.Where(p => !p.defName.StartsWith("WULA_", StringComparison.OrdinalIgnoreCase)).ToList();
if (wulaPrefabs.Count > 0)
{
sb.AppendLine("\n[Wula Empire Specialized Prefabs]:");
foreach (var p in wulaPrefabs)
{
string label = !string.IsNullOrEmpty(p.label) ? $" ({p.label})" : "";
sb.AppendLine($"- {p.defName}{label}, Size: {p.size}");
}
}
if (otherPrefabs.Count > 0)
{
sb.AppendLine("\n[Generic/Other Prefabs]:");
// Limit generic ones to avoid token bloat
var genericToShow = otherPrefabs.Take(20).ToList();
foreach (var p in genericToShow)
{
string label = !string.IsNullOrEmpty(p.label) ? $" ({p.label})" : "";
sb.AppendLine($"- {p.defName}{label}, Size: {p.size}");
}
if (otherPrefabs.Count > 20)
{
sb.AppendLine($"- ... and {otherPrefabs.Count - 20} more generic prefabs.");
}
}
return sb.ToString();
}
catch (Exception ex)
{
return $"Error: {ex.Message}";
}
}
}
}

View File

@@ -1,167 +0,0 @@
using System;
using System.Threading.Tasks;
using UnityEngine;
using WulaFallenEmpire.EventSystem.AI.Agent;
namespace WulaFallenEmpire.EventSystem.AI.Tools
{
/// <summary>
/// 视觉点击工具 - 使用 VLM 分析屏幕后模拟鼠标点击
/// 适用于原版 API 无法直接操作的 mod UI 元素
/// </summary>
public class Tool_VisualClick : AITool
{
public override string Name => "visual_click";
public override string Description =>
"在指定的屏幕位置执行鼠标点击。坐标使用比例值 (0-1)(0,0) 是左上角,(1,1) 是右下角。" +
"适用于点击无法通过 API 操作的 mod 按钮或 UI 元素。先使用 analyze_screen 获取目标位置分析。";
public override string UsageSchema =>
"<visual_click><x>0-1之间的X比例</x><y>0-1之间的Y比例</y><right_click>可选true为右键</right_click></visual_click>";
public override Task<string> ExecuteAsync(string args)
{
try
{
var argsDict = ParseXmlArgs(args);
// 解析 X 坐标
if (!argsDict.TryGetValue("x", out string xStr) || !float.TryParse(xStr, out float x))
{
return Task.FromResult("Error: 缺少有效的 x 坐标 (0-1之间的比例值)");
}
// 解析 Y 坐标
if (!argsDict.TryGetValue("y", out string yStr) || !float.TryParse(yStr, out float y))
{
return Task.FromResult("Error: 缺少有效的 y 坐标 (0-1之间的比例值)");
}
// 验证范围
if (x < 0 || x > 1 || y < 0 || y > 1)
{
return Task.FromResult($"Error: 坐标 ({x}, {y}) 超出范围,必须在 0-1 之间");
}
// 解析右键选项
bool rightClick = false;
if (argsDict.TryGetValue("right_click", out string rightStr))
{
rightClick = rightStr.ToLowerInvariant() == "true" || rightStr == "1";
}
// 执行点击
bool success = Agent.MouseSimulator.ClickAtProportional(x, y, rightClick);
if (success)
{
string clickType = rightClick ? "右键" : "左键";
int screenX = Mathf.RoundToInt(x * Screen.width);
int screenY = Mathf.RoundToInt(y * Screen.height);
WulaLog.Debug($"[Tool_VisualClick] {clickType}点击 ({x:F3}, {y:F3}) -> 屏幕 ({screenX}, {screenY})");
return Task.FromResult($"Success: 已在屏幕位置 ({screenX}, {screenY}) 执行{clickType}点击");
}
else
{
return Task.FromResult("Error: 点击操作失败");
}
}
catch (Exception ex)
{
WulaLog.Debug($"[Tool_VisualClick] Error: {ex}");
return Task.FromResult($"Error: 点击操作失败 - {ex.Message}");
}
}
}
/// <summary>
/// 视觉输入文本工具 - 在当前焦点位置输入文本
/// </summary>
public class Tool_VisualTypeText : AITool
{
public override string Name => "visual_type_text";
public override string Description =>
"在当前焦点位置输入文本。适用于需要文本输入的对话框或输入框。应先用 visual_click 点击输入框获取焦点。";
public override string UsageSchema =>
"<visual_type_text><text>要输入的文本</text></visual_type_text>";
public override Task<string> ExecuteAsync(string args)
{
try
{
var argsDict = ParseXmlArgs(args);
if (!argsDict.TryGetValue("text", out string text) || string.IsNullOrEmpty(text))
{
return Task.FromResult("Error: 缺少要输入的文本");
}
// 获取当前鼠标位置
var pos = MouseSimulator.GetCurrentPosition();
float propX = Mathf.Clamp01((float)pos.x / Screen.width);
float propY = Mathf.Clamp01((float)pos.y / Screen.height);
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 Task.FromResult($"Error: 输入文本失败 - {ex.Message}");
}
}
}
/// <summary>
/// 视觉滚动工具 - 在当前位置滚动鼠标滚轮
/// </summary>
public class Tool_VisualScroll : AITool
{
public override string Name => "visual_scroll";
public override string Description =>
"在当前鼠标位置滚动。可选先移动到指定位置再滚动。delta 正数向上滚动,负数向下滚动。";
public override string UsageSchema =>
"<visual_scroll><delta>滚动量,正数向上负数向下</delta><x>可选0-1 X坐标</x><y>可选0-1 Y坐标</y></visual_scroll>";
public override Task<string> ExecuteAsync(string args)
{
try
{
var argsDict = ParseXmlArgs(args);
if (!argsDict.TryGetValue("delta", out string deltaStr) || !int.TryParse(deltaStr, out int delta))
{
return Task.FromResult("Error: 缺少有效的 delta 值");
}
// 可选:先移动到指定位置
if (argsDict.TryGetValue("x", out string xStr) && argsDict.TryGetValue("y", out string yStr))
{
if (float.TryParse(xStr, out float x) && float.TryParse(yStr, out float y))
{
Agent.MouseSimulator.MoveToProportional(x, y);
System.Threading.Thread.Sleep(10);
}
}
Agent.MouseSimulator.Scroll(delta);
string direction = delta > 0 ? "向上" : "向下";
return Task.FromResult($"Success: 已{direction}滚动 {Math.Abs(delta)} 单位");
}
catch (Exception ex)
{
WulaLog.Debug($"[Tool_VisualScroll] Error: {ex}");
return Task.FromResult($"Error: 滚动操作失败 - {ex.Message}");
}
}
}
}

View File

@@ -1,130 +0,0 @@
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<string, string> 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<string> ExecuteAsync(string args);
}
/// <summary>
/// 视觉拖拽工具
/// </summary>
public class Tool_VisualDrag : VisualToolBase
{
public override string Name => "visual_drag";
public override string Description => "从起始坐标拖拽到结束坐标。适用于框选单位、拖动滑块或地图。";
public override string UsageSchema => "<visual_drag><start_x>0-1</start_x><start_y>0-1</start_y><end_x>0-1</end_x><end_y>0-1</end_y><duration>秒(默认0.5)</duration></visual_drag>";
public override Task<string> 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}"); }
}
}
/// <summary>
/// 视觉快捷键工具 (通用)
/// </summary>
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 => "<visual_hotkey><key>快捷键</key><x>可选</x><y>可选</y></visual_hotkey>";
public override Task<string> 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}"); }
}
}
/// <summary>
/// 视觉等待工具
/// </summary>
public class Tool_VisualWait : VisualToolBase
{
public override string Name => "visual_wait";
public override string Description => "等待指定时间。用于等待UI动画或加载。";
public override string UsageSchema => "<visual_wait><seconds>秒数</seconds></visual_wait>";
public override Task<string> 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}"); }
}
}
/// <summary>
/// 视觉删除文本工具
/// </summary>
public class Tool_VisualDeleteText : VisualToolBase
{
public override string Name => "visual_delete_text";
public override string Description => "点击指定位置并按 Backspace 删除指定数量的字符。用于清空输入框。";
public override string UsageSchema => "<visual_delete_text><x>0-1</x><y>0-1</y><count>字符数(默认1)</count></visual_delete_text>";
public override Task<string> 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}"); }
}
}
}