diff --git a/1.6/1.6/Assemblies/WulaFallenEmpire.dll b/1.6/1.6/Assemblies/WulaFallenEmpire.dll
index ba13b5df..476dd34e 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 40221daf..dfd5ff62 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 0240a9c4..2e0f45c6 100644
--- a/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs
+++ b/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs
@@ -321,10 +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 视觉分析工具 (条件性启用)
if (WulaFallenEmpireMod.settings?.enableVlmFeatures == true)
{
_tools.Add(new Tool_AnalyzeScreen());
+ _tools.Add(new Tool_VisualClick());
+ _tools.Add(new Tool_VisualScroll());
}
}
diff --git a/Source/WulaFallenEmpire/EventSystem/AI/Agent/GameStateSnapshot.cs b/Source/WulaFallenEmpire/EventSystem/AI/Agent/GameStateSnapshot.cs
new file mode 100644
index 00000000..d2308fc7
--- /dev/null
+++ b/Source/WulaFallenEmpire/EventSystem/AI/Agent/GameStateSnapshot.cs
@@ -0,0 +1,181 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using RimWorld;
+using RimWorld.Planet;
+using Verse;
+
+namespace WulaFallenEmpire.EventSystem.AI.Agent
+{
+ ///
+ /// 游戏状态快照 - 包含 AI 决策所需的所有游戏信息
+ ///
+ public class GameStateSnapshot
+ {
+ // 时间信息
+ public int Hour;
+ public string Season;
+ public int DayOfQuadrum;
+ public int Year;
+ public float GameSpeedMultiplier;
+
+ // 殖民者信息
+ public List Colonists = new List();
+ public List Prisoners = new List();
+ public List Animals = new List();
+
+ // 资源统计
+ public Dictionary Resources = new Dictionary();
+
+ // 建筑和蓝图
+ public int TotalBuildings;
+ public int PendingBlueprints;
+ public int ConstructionFrames;
+
+ // 威胁
+ public List Threats = new List();
+
+ // 最近事件
+ public List RecentMessages = new List();
+
+ // 地图信息
+ public string BiomeName;
+ public float OutdoorTemperature;
+ public string Weather;
+
+ ///
+ /// 生成给 VLM 的文本描述
+ ///
+ public string ToPromptText()
+ {
+ var sb = new StringBuilder();
+
+ // 时间
+ sb.AppendLine($"# 当前游戏状态");
+ sb.AppendLine();
+ sb.AppendLine($"## 时间");
+ sb.AppendLine($"- {Season},第 {DayOfQuadrum} 天,{Hour}:00");
+ sb.AppendLine($"- 第 {Year} 年");
+ sb.AppendLine();
+
+ // 环境
+ sb.AppendLine($"## 环境");
+ sb.AppendLine($"- 生物群系: {BiomeName}");
+ sb.AppendLine($"- 室外温度: {OutdoorTemperature:F1}°C");
+ sb.AppendLine($"- 天气: {Weather}");
+ sb.AppendLine();
+
+ // 殖民者
+ sb.AppendLine($"## 殖民者 ({Colonists.Count}人)");
+ foreach (var c in Colonists.Take(10))
+ {
+ string status = c.CurrentJob ?? "空闲";
+ string health = c.HealthPercent >= 0.9f ? "健康" :
+ c.HealthPercent >= 0.5f ? "受伤" : "重伤";
+ string mood = c.MoodPercent >= 0.7f ? "😊" :
+ c.MoodPercent >= 0.4f ? "😐" : "😞";
+ sb.AppendLine($"- {c.Name}: {health} {c.HealthPercent:P0}, 心情{mood} {c.MoodPercent:P0}, {status}");
+ }
+ if (Colonists.Count > 10)
+ {
+ sb.AppendLine($"- ... 还有 {Colonists.Count - 10} 人");
+ }
+ sb.AppendLine();
+
+ // 资源
+ sb.AppendLine($"## 主要资源");
+ var importantResources = new[] { "Silver", "Steel", "Plasteel", "ComponentIndustrial", "Gold", "MealSimple", "MealFine", "MedicineHerbal", "MedicineIndustrial", "WoodLog" };
+ foreach (var res in importantResources)
+ {
+ if (Resources.TryGetValue(res, out int count) && count > 0)
+ {
+ string label = GetResourceLabel(res);
+ sb.AppendLine($"- {label}: {count}");
+ }
+ }
+ sb.AppendLine();
+
+ // 建造
+ if (PendingBlueprints > 0 || ConstructionFrames > 0)
+ {
+ sb.AppendLine($"## 建造进度");
+ if (PendingBlueprints > 0) sb.AppendLine($"- 待建蓝图: {PendingBlueprints}");
+ if (ConstructionFrames > 0) sb.AppendLine($"- 建造中: {ConstructionFrames}");
+ sb.AppendLine();
+ }
+
+ // 威胁
+ if (Threats.Count > 0)
+ {
+ sb.AppendLine($"## ⚠️ 威胁");
+ foreach (var t in Threats)
+ {
+ sb.AppendLine($"- {t.Description} (距离: {t.Distance:F0})");
+ }
+ sb.AppendLine();
+ }
+ else
+ {
+ sb.AppendLine($"## 威胁: 暂无");
+ sb.AppendLine();
+ }
+
+ // 最近消息
+ if (RecentMessages.Count > 0)
+ {
+ sb.AppendLine($"## 最近事件");
+ foreach (var msg in RecentMessages.Skip(Math.Max(0, RecentMessages.Count - 5)))
+ {
+ sb.AppendLine($"- {msg}");
+ }
+ }
+
+ return sb.ToString();
+ }
+
+ private static string GetResourceLabel(string defName)
+ {
+ return defName switch
+ {
+ "Silver" => "银",
+ "Steel" => "钢铁",
+ "Plasteel" => "玻璃钢",
+ "ComponentIndustrial" => "零部件",
+ "Gold" => "金",
+ "MealSimple" => "简单食物",
+ "MealFine" => "精致食物",
+ "MedicineHerbal" => "草药",
+ "MedicineIndustrial" => "医药",
+ "WoodLog" => "木材",
+ _ => defName
+ };
+ }
+ }
+
+ ///
+ /// 殖民者/Pawn 快照
+ ///
+ public class PawnSnapshot
+ {
+ public string Name;
+ public float HealthPercent;
+ public float MoodPercent;
+ public bool IsDrafted;
+ public bool IsDowned;
+ public string CurrentJob;
+ public int X;
+ public int Z;
+ }
+
+ ///
+ /// 威胁快照
+ ///
+ public class ThreatSnapshot
+ {
+ public string Description;
+ public float Distance;
+ public int Count;
+ public bool IsHostile;
+ }
+}
diff --git a/Source/WulaFallenEmpire/EventSystem/AI/Agent/MouseSimulator.cs b/Source/WulaFallenEmpire/EventSystem/AI/Agent/MouseSimulator.cs
new file mode 100644
index 00000000..a4f293f9
--- /dev/null
+++ b/Source/WulaFallenEmpire/EventSystem/AI/Agent/MouseSimulator.cs
@@ -0,0 +1,227 @@
+using System;
+using System.Runtime.InteropServices;
+using UnityEngine;
+
+namespace WulaFallenEmpire.EventSystem.AI.Agent
+{
+ ///
+ /// 模拟鼠标输入工具 - 用于 VLM 视觉操作模式
+ /// 支持移动鼠标、点击、拖拽等操作
+ ///
+ public static class MouseSimulator
+ {
+ // Windows API 导入
+ [DllImport("user32.dll")]
+ private static extern bool SetCursorPos(int X, int Y);
+
+ [DllImport("user32.dll")]
+ private static extern void mouse_event(uint dwFlags, uint dx, uint dy, uint dwData, int dwExtraInfo);
+
+ [DllImport("user32.dll")]
+ private static extern bool GetCursorPos(out POINT lpPoint);
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct POINT
+ {
+ public int X;
+ public int Y;
+ }
+
+ // 鼠标事件标志
+ private const uint MOUSEEVENTF_LEFTDOWN = 0x0002;
+ private const uint MOUSEEVENTF_LEFTUP = 0x0004;
+ private const uint MOUSEEVENTF_RIGHTDOWN = 0x0008;
+ private const uint MOUSEEVENTF_RIGHTUP = 0x0010;
+ private const uint MOUSEEVENTF_WHEEL = 0x0800;
+
+ ///
+ /// 将比例坐标 (0-1) 转换为屏幕坐标
+ ///
+ public static (int x, int y) ProportionalToScreen(float propX, float propY)
+ {
+ int screenWidth = Screen.width;
+ int screenHeight = Screen.height;
+
+ int x = Mathf.Clamp(Mathf.RoundToInt(propX * screenWidth), 0, screenWidth - 1);
+ int y = Mathf.Clamp(Mathf.RoundToInt(propY * screenHeight), 0, screenHeight - 1);
+
+ return (x, y);
+ }
+
+ ///
+ /// 移动鼠标到指定屏幕坐标
+ ///
+ public static bool MoveTo(int screenX, int screenY)
+ {
+ try
+ {
+ // Unity 坐标是左下角为原点,Windows 是左上角
+ // 需要翻转 Y 坐标
+ int windowsY = Screen.height - screenY;
+
+ // 获取游戏窗口位置并加上偏移
+ // 注意:这在全屏模式下可能需要调整
+ return SetCursorPos(screenX, windowsY);
+ }
+ catch (Exception ex)
+ {
+ WulaLog.Debug($"[MouseSimulator] MoveTo failed: {ex.Message}");
+ return false;
+ }
+ }
+
+ ///
+ /// 移动鼠标到比例坐标 (0-1)
+ ///
+ public static bool MoveToProportional(float propX, float propY)
+ {
+ var (x, y) = ProportionalToScreen(propX, propY);
+ return MoveTo(x, y);
+ }
+
+ ///
+ /// 在当前位置执行左键点击
+ ///
+ public static void LeftClick()
+ {
+ try
+ {
+ mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0);
+ mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
+ }
+ catch (Exception ex)
+ {
+ WulaLog.Debug($"[MouseSimulator] LeftClick failed: {ex.Message}");
+ }
+ }
+
+ ///
+ /// 在当前位置执行右键点击
+ ///
+ public static void RightClick()
+ {
+ try
+ {
+ mouse_event(MOUSEEVENTF_RIGHTDOWN, 0, 0, 0, 0);
+ mouse_event(MOUSEEVENTF_RIGHTUP, 0, 0, 0, 0);
+ }
+ catch (Exception ex)
+ {
+ WulaLog.Debug($"[MouseSimulator] RightClick failed: {ex.Message}");
+ }
+ }
+
+ ///
+ /// 在指定屏幕坐标执行左键点击
+ ///
+ public static bool ClickAt(int screenX, int screenY, bool rightClick = false)
+ {
+ try
+ {
+ if (!MoveTo(screenX, screenY))
+ {
+ return false;
+ }
+
+ // 短暂延迟确保鼠标位置更新
+ System.Threading.Thread.Sleep(10);
+
+ if (rightClick)
+ {
+ RightClick();
+ }
+ else
+ {
+ LeftClick();
+ }
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ WulaLog.Debug($"[MouseSimulator] ClickAt failed: {ex.Message}");
+ return false;
+ }
+ }
+
+ ///
+ /// 在比例坐标 (0-1) 执行点击
+ ///
+ public static bool ClickAtProportional(float propX, float propY, bool rightClick = false)
+ {
+ var (x, y) = ProportionalToScreen(propX, propY);
+ return ClickAt(x, y, rightClick);
+ }
+
+ ///
+ /// 滚动鼠标滚轮
+ ///
+ public static void Scroll(int delta)
+ {
+ try
+ {
+ // delta 为正向上滚动,为负向下滚动
+ // Windows 使用 WHEEL_DELTA = 120 作为一个单位
+ uint wheelDelta = (uint)(delta * 120);
+ mouse_event(MOUSEEVENTF_WHEEL, 0, 0, wheelDelta, 0);
+ }
+ catch (Exception ex)
+ {
+ WulaLog.Debug($"[MouseSimulator] Scroll failed: {ex.Message}");
+ }
+ }
+
+ ///
+ /// 执行拖拽操作
+ ///
+ public static bool Drag(int startX, int startY, int endX, int endY, int durationMs = 200)
+ {
+ try
+ {
+ // 移动到起始位置
+ MoveTo(startX, startY);
+ System.Threading.Thread.Sleep(20);
+
+ // 按下左键
+ mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0);
+
+ // 平滑移动到目标位置
+ int steps = Math.Max(5, durationMs / 20);
+ for (int i = 1; i <= steps; i++)
+ {
+ float t = (float)i / steps;
+ int x = Mathf.RoundToInt(Mathf.Lerp(startX, endX, t));
+ int y = Mathf.RoundToInt(Mathf.Lerp(startY, endY, t));
+ MoveTo(x, y);
+ System.Threading.Thread.Sleep(durationMs / steps);
+ }
+
+ // 释放左键
+ mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ WulaLog.Debug($"[MouseSimulator] Drag failed: {ex.Message}");
+ return false;
+ }
+ }
+
+ ///
+ /// 获取当前鼠标位置
+ ///
+ public static (int x, int y) GetCurrentPosition()
+ {
+ try
+ {
+ if (GetCursorPos(out POINT point))
+ {
+ return (point.X, point.Y);
+ }
+ }
+ catch { }
+ return (0, 0);
+ }
+ }
+}
diff --git a/Source/WulaFallenEmpire/EventSystem/AI/Agent/OmniRealtimeClient.cs b/Source/WulaFallenEmpire/EventSystem/AI/Agent/OmniRealtimeClient.cs
new file mode 100644
index 00000000..9b9e1163
--- /dev/null
+++ b/Source/WulaFallenEmpire/EventSystem/AI/Agent/OmniRealtimeClient.cs
@@ -0,0 +1,523 @@
+using System;
+using System.Collections.Generic;
+using System.Net.WebSockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using UnityEngine;
+using Verse;
+
+namespace WulaFallenEmpire.EventSystem.AI.Agent
+{
+ ///
+ /// Qwen-Omni-Realtime WebSocket 客户端
+ /// 用于实时流式图像输入和文本输出
+ ///
+ public class OmniRealtimeClient : IDisposable
+ {
+ private ClientWebSocket _webSocket;
+ private CancellationTokenSource _cancellationSource;
+ private readonly string _apiKey;
+ private readonly string _model;
+ private bool _isConnected;
+ private bool _isDisposed;
+
+ private readonly Queue _pendingResponses = new Queue();
+ private readonly StringBuilder _currentResponse = new StringBuilder();
+
+ // 事件
+ public event Action OnTextDelta;
+ public event Action OnTextComplete;
+ public event Action OnError;
+ public event Action OnConnected;
+ public event Action OnDisconnected;
+
+ // WebSocket 端点
+ private const string WS_ENDPOINT_CN = "wss://dashscope.aliyuncs.com/api-ws/v1/realtime";
+ private const string WS_ENDPOINT_INTL = "wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime";
+
+ public bool IsConnected => _isConnected && _webSocket?.State == WebSocketState.Open;
+
+ public OmniRealtimeClient(string apiKey, string model = "qwen3-omni-flash-realtime")
+ {
+ _apiKey = apiKey;
+ _model = model;
+ }
+
+ ///
+ /// 建立 WebSocket 连接
+ ///
+ public async Task ConnectAsync(bool useInternational = false)
+ {
+ if (_isConnected) return;
+
+ try
+ {
+ _webSocket = new ClientWebSocket();
+ _cancellationSource = new CancellationTokenSource();
+
+ // 设置认证头
+ _webSocket.Options.SetRequestHeader("Authorization", $"Bearer {_apiKey}");
+
+ // 构建连接 URL
+ string endpoint = useInternational ? WS_ENDPOINT_INTL : WS_ENDPOINT_CN;
+ string url = $"{endpoint}?model={_model}";
+
+ WulaLog.Debug($"[OmniRealtime] Connecting to {url}");
+
+ await _webSocket.ConnectAsync(new Uri(url), _cancellationSource.Token);
+
+ if (_webSocket.State == WebSocketState.Open)
+ {
+ _isConnected = true;
+ WulaLog.Debug("[OmniRealtime] Connected successfully");
+
+ // 启动接收循环
+ _ = ReceiveLoopAsync();
+
+ // 配置会话(仅文本输出,禁用 VAD)
+ await ConfigureSessionAsync();
+
+ OnConnected?.Invoke();
+ }
+ }
+ catch (Exception ex)
+ {
+ WulaLog.Debug($"[OmniRealtime] Connection failed: {ex.Message}");
+ OnError?.Invoke($"连接失败: {ex.Message}");
+ _isConnected = false;
+ }
+ }
+
+ ///
+ /// 配置会话参数
+ ///
+ private async Task ConfigureSessionAsync()
+ {
+ var sessionConfig = new
+ {
+ event_id = GenerateEventId(),
+ type = "session.update",
+ session = new
+ {
+ // 仅输出文本(不需要音频)
+ modalities = new[] { "text" },
+ // 系统指令
+ instructions = @"你是一个 RimWorld 游戏 AI 代理。你可以看到游戏屏幕截图。
+分析屏幕内容,识别重要元素(殖民者、资源、威胁、建筑等)。
+根据观察做出决策,输出 XML 格式的工具调用。
+可用工具: designate_mine, draft_pawn, visual_click, get_game_state 等。
+如果不需要操作,输出 。
+保持简洁,直接输出工具调用,不要解释。",
+ // 禁用 VAD(手动模式,因为我们不使用音频)
+ turn_detection = (object)null
+ }
+ };
+
+ await SendEventAsync(sessionConfig);
+ }
+
+ ///
+ /// 发送图像到服务端
+ ///
+ public async Task SendImageAsync(string base64Image)
+ {
+ if (!IsConnected)
+ {
+ WulaLog.Debug("[OmniRealtime] Not connected, cannot send image");
+ return;
+ }
+
+ try
+ {
+ // 发送图像
+ var imageEvent = new
+ {
+ event_id = GenerateEventId(),
+ type = "input_image_buffer.append",
+ image = base64Image
+ };
+
+ await SendEventAsync(imageEvent);
+ WulaLog.Debug($"[OmniRealtime] Sent image ({base64Image.Length} chars)");
+ }
+ catch (Exception ex)
+ {
+ WulaLog.Debug($"[OmniRealtime] Failed to send image: {ex.Message}");
+ }
+ }
+
+ ///
+ /// 发送文本消息并请求响应
+ ///
+ public async Task SendTextAndRequestResponseAsync(string text)
+ {
+ if (!IsConnected) return;
+
+ try
+ {
+ // 对于手动模式,需要发送 conversation.item.create 和 response.create
+ var itemEvent = new
+ {
+ event_id = GenerateEventId(),
+ type = "conversation.item.create",
+ item = new
+ {
+ type = "message",
+ role = "user",
+ content = new[]
+ {
+ new { type = "input_text", text = text }
+ }
+ }
+ };
+
+ await SendEventAsync(itemEvent);
+
+ // 请求响应
+ var responseEvent = new
+ {
+ event_id = GenerateEventId(),
+ type = "response.create"
+ };
+
+ await SendEventAsync(responseEvent);
+ }
+ catch (Exception ex)
+ {
+ WulaLog.Debug($"[OmniRealtime] Failed to send text: {ex.Message}");
+ }
+ }
+
+ ///
+ /// 发送图像并请求分析
+ ///
+ public async Task SendImageAndRequestAnalysisAsync(string base64Image, string prompt = "分析当前游戏画面,决定下一步操作")
+ {
+ if (!IsConnected) return;
+
+ try
+ {
+ // 先发送图像
+ await SendImageAsync(base64Image);
+
+ // 提交输入并请求响应
+ var commitEvent = new
+ {
+ event_id = GenerateEventId(),
+ type = "input_audio_buffer.commit" // 这会同时提交图像缓冲区
+ };
+ await SendEventAsync(commitEvent);
+
+ // 发送文本提示
+ await SendTextAndRequestResponseAsync(prompt);
+ }
+ catch (Exception ex)
+ {
+ WulaLog.Debug($"[OmniRealtime] Failed to send image for analysis: {ex.Message}");
+ }
+ }
+
+ ///
+ /// 接收消息循环
+ ///
+ private async Task ReceiveLoopAsync()
+ {
+ var buffer = new byte[8192];
+ var messageBuffer = new StringBuilder();
+
+ try
+ {
+ while (_webSocket?.State == WebSocketState.Open && !_cancellationSource.IsCancellationRequested)
+ {
+ var segment = new ArraySegment(buffer);
+ var result = await _webSocket.ReceiveAsync(segment, _cancellationSource.Token);
+
+ if (result.MessageType == WebSocketMessageType.Close)
+ {
+ WulaLog.Debug("[OmniRealtime] Server closed connection");
+ break;
+ }
+
+ if (result.MessageType == WebSocketMessageType.Text)
+ {
+ string chunk = Encoding.UTF8.GetString(buffer, 0, result.Count);
+ messageBuffer.Append(chunk);
+
+ if (result.EndOfMessage)
+ {
+ ProcessServerEvent(messageBuffer.ToString());
+ messageBuffer.Clear();
+ }
+ }
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ // 正常取消
+ }
+ catch (Exception ex)
+ {
+ WulaLog.Debug($"[OmniRealtime] Receive error: {ex.Message}");
+ OnError?.Invoke($"接收错误: {ex.Message}");
+ }
+ finally
+ {
+ _isConnected = false;
+ OnDisconnected?.Invoke();
+ }
+ }
+
+ ///
+ /// 处理服务端事件
+ ///
+ private void ProcessServerEvent(string json)
+ {
+ try
+ {
+ // 简单解析 JSON 获取事件类型和内容
+ // 注意:这里使用简单的字符串解析,生产环境应使用 JSON 库
+
+ string eventType = ExtractJsonValue(json, "type");
+
+ switch (eventType)
+ {
+ case "session.created":
+ case "session.updated":
+ WulaLog.Debug($"[OmniRealtime] Session event: {eventType}");
+ break;
+
+ case "response.text.delta":
+ string textDelta = ExtractJsonValue(json, "delta");
+ if (!string.IsNullOrEmpty(textDelta))
+ {
+ _currentResponse.Append(textDelta);
+ OnTextDelta?.Invoke(textDelta);
+ }
+ break;
+
+ case "response.text.done":
+ string completeText = _currentResponse.ToString();
+ _currentResponse.Clear();
+ OnTextComplete?.Invoke(completeText);
+ WulaLog.Debug($"[OmniRealtime] Response complete: {completeText.Substring(0, Math.Min(100, completeText.Length))}...");
+ break;
+
+ case "response.audio_transcript.delta":
+ // 音频转录的文本增量(如果启用了音频输出)
+ string transcriptDelta = ExtractJsonValue(json, "delta");
+ if (!string.IsNullOrEmpty(transcriptDelta))
+ {
+ _currentResponse.Append(transcriptDelta);
+ OnTextDelta?.Invoke(transcriptDelta);
+ }
+ break;
+
+ case "response.audio_transcript.done":
+ string transcript = _currentResponse.ToString();
+ _currentResponse.Clear();
+ OnTextComplete?.Invoke(transcript);
+ break;
+
+ case "error":
+ string errorMsg = ExtractJsonValue(json, "message") ?? json;
+ WulaLog.Debug($"[OmniRealtime] Error: {errorMsg}");
+ OnError?.Invoke(errorMsg);
+ break;
+
+ case "response.done":
+ // 响应完成
+ break;
+
+ default:
+ // 其他事件
+ if (Prefs.DevMode)
+ {
+ WulaLog.Debug($"[OmniRealtime] Event: {eventType}");
+ }
+ break;
+ }
+ }
+ catch (Exception ex)
+ {
+ WulaLog.Debug($"[OmniRealtime] Failed to process event: {ex.Message}");
+ }
+ }
+
+ private async Task SendEventAsync(object eventObj)
+ {
+ if (_webSocket?.State != WebSocketState.Open) return;
+
+ string json = ToSimpleJson(eventObj);
+ byte[] data = Encoding.UTF8.GetBytes(json);
+
+ await _webSocket.SendAsync(
+ new ArraySegment(data),
+ WebSocketMessageType.Text,
+ true,
+ _cancellationSource.Token
+ );
+ }
+
+ private static string GenerateEventId()
+ {
+ return $"event_{Guid.NewGuid():N}".Substring(0, 24);
+ }
+
+ ///
+ /// 简单的对象转 JSON(避免依赖外部库)
+ ///
+ private static string ToSimpleJson(object obj)
+ {
+ // 对于复杂对象,建议使用 Newtonsoft.Json 或 System.Text.Json
+ // 这里使用简化实现
+ var sb = new StringBuilder();
+ SerializeObject(sb, obj);
+ return sb.ToString();
+ }
+
+ private static void SerializeObject(StringBuilder sb, object obj)
+ {
+ if (obj == null)
+ {
+ sb.Append("null");
+ return;
+ }
+
+ var type = obj.GetType();
+
+ if (obj is string str)
+ {
+ sb.Append('"');
+ sb.Append(str.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\n", "\\n").Replace("\r", "\\r"));
+ sb.Append('"');
+ }
+ else if (obj is bool b)
+ {
+ sb.Append(b ? "true" : "false");
+ }
+ else if (obj is int || obj is long || obj is float || obj is double)
+ {
+ sb.Append(obj.ToString());
+ }
+ else if (type.IsArray)
+ {
+ sb.Append('[');
+ var arr = (Array)obj;
+ for (int i = 0; i < arr.Length; i++)
+ {
+ if (i > 0) sb.Append(',');
+ SerializeObject(sb, arr.GetValue(i));
+ }
+ sb.Append(']');
+ }
+ else if (type.IsClass)
+ {
+ sb.Append('{');
+ bool first = true;
+ foreach (var prop in type.GetProperties())
+ {
+ var value = prop.GetValue(obj);
+ if (value == null) continue;
+
+ if (!first) sb.Append(',');
+ first = false;
+
+ sb.Append('"');
+ sb.Append(prop.Name);
+ sb.Append("\":");
+ SerializeObject(sb, value);
+ }
+ // 匿名类型使用字段
+ foreach (var field in type.GetFields())
+ {
+ var value = field.GetValue(obj);
+ if (value == null) continue;
+
+ if (!first) sb.Append(',');
+ first = false;
+
+ sb.Append('"');
+ sb.Append(field.Name);
+ sb.Append("\":");
+ SerializeObject(sb, value);
+ }
+ sb.Append('}');
+ }
+ }
+
+ private static string ExtractJsonValue(string json, string key)
+ {
+ // 简单提取 JSON 值
+ string pattern = $"\"{key}\":";
+ int idx = json.IndexOf(pattern);
+ if (idx < 0) return null;
+
+ idx += pattern.Length;
+ while (idx < json.Length && char.IsWhiteSpace(json[idx])) idx++;
+
+ if (idx >= json.Length) return null;
+
+ if (json[idx] == '"')
+ {
+ // 字符串值
+ idx++;
+ int end = idx;
+ while (end < json.Length && json[end] != '"')
+ {
+ if (json[end] == '\\') end++; // 跳过转义字符
+ end++;
+ }
+ return json.Substring(idx, end - idx).Replace("\\n", "\n").Replace("\\\"", "\"");
+ }
+ else
+ {
+ // 其他值
+ int end = idx;
+ while (end < json.Length && json[end] != ',' && json[end] != '}' && json[end] != ']')
+ {
+ end++;
+ }
+ return json.Substring(idx, end - idx).Trim();
+ }
+ }
+
+ ///
+ /// 断开连接
+ ///
+ public async Task DisconnectAsync()
+ {
+ if (!_isConnected) return;
+
+ try
+ {
+ _cancellationSource?.Cancel();
+
+ if (_webSocket?.State == WebSocketState.Open)
+ {
+ await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Client disconnect", CancellationToken.None);
+ }
+ }
+ catch (Exception ex)
+ {
+ WulaLog.Debug($"[OmniRealtime] Disconnect error: {ex.Message}");
+ }
+ finally
+ {
+ _isConnected = false;
+ _webSocket?.Dispose();
+ _webSocket = null;
+ }
+ }
+
+ public void Dispose()
+ {
+ if (_isDisposed) return;
+ _isDisposed = true;
+
+ _cancellationSource?.Cancel();
+ _cancellationSource?.Dispose();
+ _webSocket?.Dispose();
+ }
+ }
+}
diff --git a/Source/WulaFallenEmpire/EventSystem/AI/Agent/StateObserver.cs b/Source/WulaFallenEmpire/EventSystem/AI/Agent/StateObserver.cs
new file mode 100644
index 00000000..69d0b2ea
--- /dev/null
+++ b/Source/WulaFallenEmpire/EventSystem/AI/Agent/StateObserver.cs
@@ -0,0 +1,285 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using RimWorld;
+using RimWorld.Planet;
+using Verse;
+using Verse.AI;
+
+namespace WulaFallenEmpire.EventSystem.AI.Agent
+{
+ ///
+ /// 游戏状态观察器 - 收集当前游戏状态用于 AI 决策
+ ///
+ public static class StateObserver
+ {
+ ///
+ /// 捕获当前游戏状态快照
+ ///
+ public static GameStateSnapshot CaptureState()
+ {
+ var snapshot = new GameStateSnapshot();
+
+ try
+ {
+ Map map = Find.CurrentMap ?? Find.Maps?.FirstOrDefault();
+ if (map == null)
+ {
+ WulaLog.Debug("[StateObserver] No map available");
+ return snapshot;
+ }
+
+ // 时间信息
+ CaptureTimeInfo(snapshot);
+
+ // 环境信息
+ CaptureEnvironmentInfo(snapshot, map);
+
+ // 殖民者信息
+ CaptureColonistInfo(snapshot, map);
+
+ // 资源统计
+ CaptureResourceInfo(snapshot, map);
+
+ // 建筑信息
+ CaptureBuildingInfo(snapshot, map);
+
+ // 威胁检测
+ CaptureThreatInfo(snapshot, map);
+
+ // 最近消息
+ CaptureRecentMessages(snapshot);
+ }
+ catch (Exception ex)
+ {
+ WulaLog.Debug($"[StateObserver] Error capturing state: {ex.Message}");
+ }
+
+ return snapshot;
+ }
+
+ private static void CaptureTimeInfo(GameStateSnapshot snapshot)
+ {
+ try
+ {
+ var tickManager = Find.TickManager;
+ if (tickManager == null) return;
+
+ int ticksAbs = Find.TickManager.TicksAbs;
+ int tile = Find.CurrentMap?.Tile ?? 0;
+ float longitude = Find.WorldGrid.LongLatOf(tile).x;
+ snapshot.Hour = GenDate.HourOfDay(ticksAbs, longitude);
+ snapshot.DayOfQuadrum = GenDate.DayOfQuadrum(ticksAbs, longitude) + 1;
+ snapshot.Year = GenDate.Year(ticksAbs, longitude);
+ snapshot.Season = GenDate.Season(ticksAbs, Find.WorldGrid.LongLatOf(tile)).Label();
+ snapshot.GameSpeedMultiplier = Find.TickManager.TickRateMultiplier;
+ }
+ catch (Exception ex)
+ {
+ WulaLog.Debug($"[StateObserver] Error capturing time: {ex.Message}");
+ }
+ }
+
+ private static void CaptureEnvironmentInfo(GameStateSnapshot snapshot, Map map)
+ {
+ try
+ {
+ snapshot.BiomeName = map.Biome?.label ?? "Unknown";
+ snapshot.OutdoorTemperature = map.mapTemperature?.OutdoorTemp ?? 20f;
+ snapshot.Weather = map.weatherManager?.curWeather?.label ?? "Unknown";
+ }
+ catch (Exception ex)
+ {
+ WulaLog.Debug($"[StateObserver] Error capturing environment: {ex.Message}");
+ }
+ }
+
+ private static void CaptureColonistInfo(GameStateSnapshot snapshot, Map map)
+ {
+ try
+ {
+ var colonists = map.mapPawns?.FreeColonists;
+ if (colonists == null) return;
+
+ foreach (var pawn in colonists)
+ {
+ if (pawn == null || pawn.Dead) continue;
+
+ var pawnSnapshot = new PawnSnapshot
+ {
+ Name = pawn.LabelShortCap,
+ HealthPercent = pawn.health?.summaryHealth?.SummaryHealthPercent ?? 1f,
+ MoodPercent = pawn.needs?.mood?.CurLevelPercentage ?? 0.5f,
+ IsDrafted = pawn.Drafted,
+ IsDowned = pawn.Downed,
+ CurrentJob = GetJobDescription(pawn),
+ X = pawn.Position.x,
+ Z = pawn.Position.z
+ };
+
+ snapshot.Colonists.Add(pawnSnapshot);
+ }
+
+ // 囚犯
+ var prisoners = map.mapPawns?.PrisonersOfColony;
+ if (prisoners != null)
+ {
+ foreach (var pawn in prisoners)
+ {
+ if (pawn == null || pawn.Dead) continue;
+ snapshot.Prisoners.Add(new PawnSnapshot
+ {
+ Name = pawn.LabelShortCap,
+ HealthPercent = pawn.health?.summaryHealth?.SummaryHealthPercent ?? 1f,
+ IsDowned = pawn.Downed
+ });
+ }
+ }
+
+ // 驯养动物
+ var animals = map.mapPawns?.SpawnedColonyAnimals;
+ if (animals != null)
+ {
+ snapshot.Animals.AddRange(animals.Take(20).Select(a => new PawnSnapshot
+ {
+ Name = a.LabelShortCap,
+ HealthPercent = a.health?.summaryHealth?.SummaryHealthPercent ?? 1f
+ }));
+ }
+ }
+ catch (Exception ex)
+ {
+ WulaLog.Debug($"[StateObserver] Error capturing colonists: {ex.Message}");
+ }
+ }
+
+ private static string GetJobDescription(Pawn pawn)
+ {
+ try
+ {
+ if (pawn.Drafted) return "已征召";
+ if (pawn.Downed) return "倒地";
+ if (pawn.InMentalState) return $"精神崩溃: {pawn.MentalStateDef?.label ?? "未知"}";
+
+ var job = pawn.CurJob;
+ if (job == null) return "空闲";
+
+ // 返回简短的工作描述
+ string jobLabel = job.def?.reportString ?? job.def?.label ?? "工作中";
+
+ // 清理一些常见的格式
+ if (job.targetA.Thing != null)
+ {
+ jobLabel = $"{job.def?.label}: {job.targetA.Thing.LabelShortCap}";
+ }
+ else
+ {
+ jobLabel = job.def?.label ?? "工作中";
+ }
+
+ return jobLabel;
+ }
+ catch
+ {
+ return "未知";
+ }
+ }
+
+ private static void CaptureResourceInfo(GameStateSnapshot snapshot, Map map)
+ {
+ try
+ {
+ var resourceCounter = map.resourceCounter;
+ if (resourceCounter == null) return;
+
+ foreach (var kvp in resourceCounter.AllCountedAmounts)
+ {
+ if (kvp.Value > 0)
+ {
+ snapshot.Resources[kvp.Key.defName] = kvp.Value;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ WulaLog.Debug($"[StateObserver] Error capturing resources: {ex.Message}");
+ }
+ }
+
+ private static void CaptureBuildingInfo(GameStateSnapshot snapshot, Map map)
+ {
+ try
+ {
+ // 统计建筑
+ snapshot.TotalBuildings = map.listerBuildings?.allBuildingsColonist?.Count ?? 0;
+
+ // 统计蓝图
+ snapshot.PendingBlueprints = map.listerThings?.ThingsInGroup(ThingRequestGroup.Blueprint)?.Count ?? 0;
+
+ // 统计建造中的框架
+ snapshot.ConstructionFrames = map.listerThings?.ThingsInGroup(ThingRequestGroup.BuildingFrame)?.Count ?? 0;
+ }
+ catch (Exception ex)
+ {
+ WulaLog.Debug($"[StateObserver] Error capturing buildings: {ex.Message}");
+ }
+ }
+
+ private static void CaptureThreatInfo(GameStateSnapshot snapshot, Map map)
+ {
+ try
+ {
+ // 检测敌对 pawns
+ var hostilePawns = map.mapPawns?.AllPawnsSpawned?
+ .Where(p => p != null && !p.Dead && p.HostileTo(Faction.OfPlayer))
+ .ToList();
+
+ if (hostilePawns == null || hostilePawns.Count == 0) return;
+
+ // 按派系分组
+ var threatGroups = hostilePawns.GroupBy(p => p.Faction?.Name ?? "野生动物");
+
+ foreach (var group in threatGroups)
+ {
+ IntVec3 colonyCenter = map.IsPlayerHome ? map.mapPawns.FreeColonists.FirstOrDefault()?.Position ?? IntVec3.Zero : IntVec3.Zero;
+ float minDistance = group.Min(p => (p.Position - colonyCenter).LengthHorizontal);
+
+ snapshot.Threats.Add(new ThreatSnapshot
+ {
+ Description = $"{group.Count()} 个 {group.Key}",
+ Count = group.Count(),
+ Distance = minDistance,
+ IsHostile = true
+ });
+ }
+ }
+ catch (Exception ex)
+ {
+ WulaLog.Debug($"[StateObserver] Error capturing threats: {ex.Message}");
+ }
+ }
+
+ private static void CaptureRecentMessages(GameStateSnapshot snapshot)
+ {
+ try
+ {
+ var messages = Find.Archive?.ArchivablesListForReading;
+ if (messages == null) return;
+
+ // 获取最近 5 条消息
+ var allMessages = messages.OfType().ToList();
+ var recentMessages = allMessages
+ .Skip(Math.Max(0, allMessages.Count - 5))
+ .Select(m => m.text?.ToString() ?? "")
+ .Where(s => !string.IsNullOrEmpty(s))
+ .ToList();
+
+ snapshot.RecentMessages.AddRange(recentMessages);
+ }
+ catch (Exception ex)
+ {
+ WulaLog.Debug($"[StateObserver] Error capturing messages: {ex.Message}");
+ }
+ }
+ }
+}
diff --git a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_DesignateMine.cs b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_DesignateMine.cs
new file mode 100644
index 00000000..da98b7e4
--- /dev/null
+++ b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_DesignateMine.cs
@@ -0,0 +1,139 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using RimWorld;
+using Verse;
+
+namespace WulaFallenEmpire.EventSystem.AI.Tools
+{
+ ///
+ /// 采矿指令工具 - 在指定位置或区域添加采矿标记
+ ///
+ public class Tool_DesignateMine : AITool
+ {
+ public override string Name => "designate_mine";
+
+ public override string Description =>
+ "在指定坐标添加采矿标记。可以指定单个格子或矩形区域。只能标记可采矿的岩石。";
+
+ public override string UsageSchema =>
+ "整数X坐标整数Z坐标可选,整数半径,默认0表示单格";
+
+ public override string Execute(string args)
+ {
+ try
+ {
+ var argsDict = ParseXmlArgs(args);
+
+ // 解析坐标
+ if (!argsDict.TryGetValue("x", out string xStr) || !int.TryParse(xStr, out int x))
+ {
+ return "Error: 缺少有效的 x 坐标";
+ }
+ if (!argsDict.TryGetValue("z", out string zStr) || !int.TryParse(zStr, out int z))
+ {
+ return "Error: 缺少有效的 z 坐标";
+ }
+
+ int radius = 0;
+ if (argsDict.TryGetValue("radius", out string radiusStr))
+ {
+ int.TryParse(radiusStr, out radius);
+ }
+ radius = Math.Max(0, Math.Min(10, radius)); // 限制半径 0-10
+
+ // 获取地图
+ Map map = Find.CurrentMap;
+ if (map == null)
+ {
+ return "Error: 没有活动的地图";
+ }
+
+ IntVec3 center = new IntVec3(x, 0, z);
+ if (!center.InBounds(map))
+ {
+ return $"Error: 坐标 ({x}, {z}) 超出地图边界";
+ }
+
+ // 收集要标记的格子
+ List cellsToMark = new List();
+
+ if (radius == 0)
+ {
+ cellsToMark.Add(center);
+ }
+ else
+ {
+ // 矩形区域
+ for (int dx = -radius; dx <= radius; dx++)
+ {
+ for (int dz = -radius; dz <= radius; dz++)
+ {
+ IntVec3 cell = new IntVec3(x + dx, 0, z + dz);
+ if (cell.InBounds(map))
+ {
+ cellsToMark.Add(cell);
+ }
+ }
+ }
+ }
+
+ int successCount = 0;
+ int alreadyMarkedCount = 0;
+ int notMineableCount = 0;
+
+ foreach (var cell in cellsToMark)
+ {
+ // 检查是否已有采矿标记
+ if (map.designationManager.DesignationAt(cell, DesignationDefOf.Mine) != null)
+ {
+ alreadyMarkedCount++;
+ continue;
+ }
+
+ // 检查是否可采矿
+ Mineable mineable = cell.GetFirstMineable(map);
+ if (mineable == null)
+ {
+ notMineableCount++;
+ continue;
+ }
+
+ // 添加采矿标记
+ map.designationManager.AddDesignation(new Designation(cell, DesignationDefOf.Mine));
+ successCount++;
+ }
+
+ // 生成结果报告
+ if (successCount > 0)
+ {
+ string result = $"Success: 已标记 {successCount} 个格子进行采矿";
+ if (alreadyMarkedCount > 0)
+ {
+ result += $",{alreadyMarkedCount} 个已有标记";
+ }
+ if (notMineableCount > 0)
+ {
+ result += $",{notMineableCount} 个不可采矿";
+ }
+
+ Messages.Message($"AI: 标记了 {successCount} 处采矿", MessageTypeDefOf.NeutralEvent);
+ return result;
+ }
+ else if (alreadyMarkedCount > 0)
+ {
+ return $"Info: 该区域 {alreadyMarkedCount} 个格子已有采矿标记";
+ }
+ else
+ {
+ return $"Error: 坐标 ({x}, {z}) 附近没有可采矿的岩石";
+ }
+ }
+ catch (Exception ex)
+ {
+ WulaLog.Debug($"[Tool_DesignateMine] Error: {ex}");
+ return $"Error: 采矿指令失败 - {ex.Message}";
+ }
+ }
+ }
+}
diff --git a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_DraftPawn.cs b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_DraftPawn.cs
new file mode 100644
index 00000000..1a55b079
--- /dev/null
+++ b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_DraftPawn.cs
@@ -0,0 +1,105 @@
+using System;
+using RimWorld;
+using Verse;
+
+namespace WulaFallenEmpire.EventSystem.AI.Tools
+{
+ ///
+ /// 征召殖民者工具 - 将殖民者置于征召状态以便直接控制
+ ///
+ public class Tool_DraftPawn : AITool
+ {
+ public override string Name => "draft_pawn";
+
+ public override string Description =>
+ "征召或解除征召殖民者。征召后可以直接控制殖民者移动和攻击。";
+
+ public override string UsageSchema =>
+ "殖民者名字true征召/false解除";
+
+ public override string Execute(string args)
+ {
+ try
+ {
+ var argsDict = ParseXmlArgs(args);
+
+ // 解析殖民者名字
+ if (!argsDict.TryGetValue("pawn_name", out string pawnName) || string.IsNullOrWhiteSpace(pawnName))
+ {
+ // 尝试其他常见参数名
+ if (!argsDict.TryGetValue("name", out pawnName) || string.IsNullOrWhiteSpace(pawnName))
+ {
+ return "Error: 缺少殖民者名字 (pawn_name)";
+ }
+ }
+
+ // 解析征召状态
+ bool draft = true;
+ if (argsDict.TryGetValue("draft", out string draftStr))
+ {
+ draft = draftStr.ToLowerInvariant() != "false" && draftStr != "0";
+ }
+
+ // 获取地图
+ Map map = Find.CurrentMap;
+ if (map == null)
+ {
+ return "Error: 没有活动的地图";
+ }
+
+ // 查找殖民者
+ Pawn targetPawn = null;
+ foreach (var pawn in map.mapPawns.FreeColonists)
+ {
+ if (pawn.LabelShortCap.Equals(pawnName, StringComparison.OrdinalIgnoreCase) ||
+ pawn.Name?.ToStringShort?.Equals(pawnName, StringComparison.OrdinalIgnoreCase) == true ||
+ pawn.LabelCap.ToString().IndexOf(pawnName, StringComparison.OrdinalIgnoreCase) >= 0)
+ {
+ targetPawn = pawn;
+ break;
+ }
+ }
+
+ if (targetPawn == null)
+ {
+ return $"Error: 找不到殖民者 '{pawnName}'";
+ }
+
+ // 检查是否可以征召
+ if (targetPawn.Downed)
+ {
+ return $"Error: {targetPawn.LabelShortCap} 已倒地,无法征召";
+ }
+
+ if (targetPawn.Dead)
+ {
+ return $"Error: {targetPawn.LabelShortCap} 已死亡";
+ }
+
+ if (targetPawn.drafter == null)
+ {
+ return $"Error: {targetPawn.LabelShortCap} 无法被征召";
+ }
+
+ // 执行征召/解除
+ bool wasDrafted = targetPawn.Drafted;
+ targetPawn.drafter.Drafted = draft;
+
+ string action = draft ? "征召" : "解除征召";
+
+ if (wasDrafted == draft)
+ {
+ return $"Info: {targetPawn.LabelShortCap} 已经处于{(draft ? "征召" : "非征召")}状态";
+ }
+
+ Messages.Message($"AI: {action}了 {targetPawn.LabelShortCap}", targetPawn, MessageTypeDefOf.NeutralEvent);
+ return $"Success: 已{action} {targetPawn.LabelShortCap},当前位置 ({targetPawn.Position.x}, {targetPawn.Position.z})";
+ }
+ catch (Exception ex)
+ {
+ WulaLog.Debug($"[Tool_DraftPawn] Error: {ex}");
+ return $"Error: 征召操作失败 - {ex.Message}";
+ }
+ }
+ }
+}
diff --git a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_GetGameState.cs b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_GetGameState.cs
new file mode 100644
index 00000000..c84a6947
--- /dev/null
+++ b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_GetGameState.cs
@@ -0,0 +1,45 @@
+using System;
+
+namespace WulaFallenEmpire.EventSystem.AI.Tools
+{
+ ///
+ /// 获取当前游戏状态工具 - 让 AI 了解殖民地当前情况
+ ///
+ public class Tool_GetGameState : AITool
+ {
+ public override string Name => "get_game_state";
+
+ public override string Description =>
+ "获取当前游戏状态的详细报告,包括殖民者状态、资源、建筑进度、威胁等信息。在做出任何操作决策前应先调用此工具了解当前情况。";
+
+ public override string UsageSchema =>
+ "";
+
+ public override string Execute(string args)
+ {
+ try
+ {
+ var snapshot = Agent.StateObserver.CaptureState();
+
+ if (snapshot == null)
+ {
+ return "Error: 无法捕获游戏状态,可能没有活动的地图。";
+ }
+
+ string stateText = snapshot.ToPromptText();
+
+ if (string.IsNullOrWhiteSpace(stateText))
+ {
+ return "Error: 游戏状态为空。";
+ }
+
+ return stateText;
+ }
+ catch (Exception ex)
+ {
+ WulaLog.Debug($"[Tool_GetGameState] Error: {ex}");
+ return $"Error: 获取游戏状态失败 - {ex.Message}";
+ }
+ }
+ }
+}
diff --git a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_VisualClick.cs b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_VisualClick.cs
new file mode 100644
index 00000000..10018548
--- /dev/null
+++ b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_VisualClick.cs
@@ -0,0 +1,166 @@
+using System;
+using System.Threading.Tasks;
+using UnityEngine;
+
+namespace WulaFallenEmpire.EventSystem.AI.Tools
+{
+ ///
+ /// 视觉点击工具 - 使用 VLM 分析屏幕后模拟鼠标点击
+ /// 适用于原版 API 无法直接操作的 mod UI 元素
+ ///
+ 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 =>
+ "0-1之间的X比例0-1之间的Y比例可选,true为右键";
+
+ public override string Execute(string args)
+ {
+ try
+ {
+ var argsDict = ParseXmlArgs(args);
+
+ // 解析 X 坐标
+ if (!argsDict.TryGetValue("x", out string xStr) || !float.TryParse(xStr, out float x))
+ {
+ return "Error: 缺少有效的 x 坐标 (0-1之间的比例值)";
+ }
+
+ // 解析 Y 坐标
+ if (!argsDict.TryGetValue("y", out string yStr) || !float.TryParse(yStr, out float y))
+ {
+ return "Error: 缺少有效的 y 坐标 (0-1之间的比例值)";
+ }
+
+ // 验证范围
+ if (x < 0 || x > 1 || y < 0 || y > 1)
+ {
+ return $"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 $"Success: 已在屏幕位置 ({screenX}, {screenY}) 执行{clickType}点击";
+ }
+ else
+ {
+ return "Error: 点击操作失败";
+ }
+ }
+ catch (Exception ex)
+ {
+ WulaLog.Debug($"[Tool_VisualClick] Error: {ex}");
+ return $"Error: 点击操作失败 - {ex.Message}";
+ }
+ }
+ }
+
+ ///
+ /// 视觉输入文本工具 - 在当前焦点位置输入文本
+ ///
+ public class Tool_VisualTypeText : AITool
+ {
+ public override string Name => "visual_type_text";
+
+ public override string Description =>
+ "在当前焦点位置输入文本。适用于需要文本输入的对话框或输入框。应先用 visual_click 点击输入框获取焦点。";
+
+ public override string UsageSchema =>
+ "要输入的文本";
+
+ public override string Execute(string args)
+ {
+ try
+ {
+ var argsDict = ParseXmlArgs(args);
+
+ if (!argsDict.TryGetValue("text", out string text) || string.IsNullOrEmpty(text))
+ {
+ return "Error: 缺少要输入的文本";
+ }
+
+ // 使用剪贴板方式输入(支持中文)
+ GUIUtility.systemCopyBuffer = text;
+
+ // 模拟 Ctrl+V 粘贴
+ // 注意:这需要额外的键盘模拟实现
+ // 暂时返回成功,实际使用时需要完善
+
+ WulaLog.Debug($"[Tool_VisualTypeText] 已将文本复制到剪贴板: {text}");
+ return $"Success: 已将文本复制到剪贴板。请手动按 Ctrl+V 粘贴,或等待键盘模拟功能完善。";
+ }
+ catch (Exception ex)
+ {
+ WulaLog.Debug($"[Tool_VisualTypeText] Error: {ex}");
+ return $"Error: 输入文本失败 - {ex.Message}";
+ }
+ }
+ }
+
+ ///
+ /// 视觉滚动工具 - 在当前位置滚动鼠标滚轮
+ ///
+ public class Tool_VisualScroll : AITool
+ {
+ public override string Name => "visual_scroll";
+
+ public override string Description =>
+ "在当前鼠标位置滚动。可选先移动到指定位置再滚动。delta 正数向上滚动,负数向下滚动。";
+
+ public override string UsageSchema =>
+ "滚动量,正数向上负数向下可选,0-1 X坐标可选,0-1 Y坐标";
+
+ public override string Execute(string args)
+ {
+ try
+ {
+ var argsDict = ParseXmlArgs(args);
+
+ if (!argsDict.TryGetValue("delta", out string deltaStr) || !int.TryParse(deltaStr, out int delta))
+ {
+ return "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 $"Success: 已{direction}滚动 {Math.Abs(delta)} 单位";
+ }
+ catch (Exception ex)
+ {
+ WulaLog.Debug($"[Tool_VisualScroll] Error: {ex}");
+ return $"Error: 滚动操作失败 - {ex.Message}";
+ }
+ }
+ }
+}