This commit is contained in:
2025-12-27 21:58:51 +08:00
parent 9d4a7c5e8e
commit 8f17366bd3
6 changed files with 1093 additions and 0 deletions

View File

@@ -0,0 +1,278 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using RimWorld;
using UnityEngine;
using Verse;
namespace WulaFallenEmpire.EventSystem.AI.Agent
{
/// <summary>
/// 自主 Agent 循环 - 持续观察游戏并做出决策
/// 用户只需给出开放式指令如"帮我挖铁"或"帮我玩殖民地"
/// </summary>
public class AutonomousAgentLoop : GameComponent
{
public static AutonomousAgentLoop Instance { get; private set; }
// Agent 状态
private bool _isRunning;
private string _currentObjective;
private float _lastDecisionTime;
private int _decisionCount;
private readonly List<string> _actionHistory = new List<string>();
// 配置
private const float DecisionIntervalSeconds = 3f; // 每 3 秒决策一次
private const int MaxActionsPerObjective = 100;
// 事件
public event Action<string> OnDecisionMade;
public event Action<string> OnObjectiveComplete;
public event Action<string> OnError;
public bool IsRunning => _isRunning;
public string CurrentObjective => _currentObjective;
public int DecisionCount => _decisionCount;
public AutonomousAgentLoop(Game game)
{
Instance = this;
}
/// <summary>
/// 开始执行开放式目标
/// </summary>
public void StartObjective(string objective)
{
if (string.IsNullOrWhiteSpace(objective))
{
OnError?.Invoke("目标不能为空");
return;
}
_currentObjective = objective;
_isRunning = true;
_decisionCount = 0;
_actionHistory.Clear();
_lastDecisionTime = Time.realtimeSinceStartup;
WulaLog.Debug($"[AgentLoop] Started objective: {objective}");
Messages.Message($"AI Agent 开始执行: {objective}", MessageTypeDefOf.NeutralEvent);
// 立即执行第一次决策
_ = ExecuteDecisionCycleAsync();
}
/// <summary>
/// 停止 Agent
/// </summary>
public void Stop()
{
if (!_isRunning) return;
_isRunning = false;
WulaLog.Debug($"[AgentLoop] Stopped after {_decisionCount} decisions");
Messages.Message($"AI Agent 已停止,执行了 {_decisionCount} 次决策", MessageTypeDefOf.NeutralEvent);
}
public override void GameComponentTick()
{
if (!_isRunning) return;
// 检查是否到达决策间隔
if (Time.realtimeSinceStartup - _lastDecisionTime < DecisionIntervalSeconds) return;
// 检查是否超过最大操作次数
if (_decisionCount >= MaxActionsPerObjective)
{
Messages.Message($"AI Agent: 已达到最大操作次数 ({MaxActionsPerObjective}),暂停执行", MessageTypeDefOf.CautionInput);
Stop();
return;
}
_lastDecisionTime = Time.realtimeSinceStartup;
// 异步执行决策
_ = ExecuteDecisionCycleAsync();
}
/// <summary>
/// 执行一次决策循环: Observe → Think → Act
/// </summary>
private async Task ExecuteDecisionCycleAsync()
{
try
{
// 1. Observe - 收集游戏状态
var gameState = StateObserver.CaptureState();
string stateText = gameState.ToPromptText();
// 2. 构建决策提示词
string prompt = BuildDecisionPrompt(stateText);
// 3. Think - 调用 AI 获取决策
var settings = WulaFallenEmpireMod.settings;
if (settings == null || string.IsNullOrEmpty(settings.apiKey))
{
OnError?.Invoke("API Key 未配置");
Stop();
return;
}
var client = new SimpleAIClient(settings.apiKey, settings.baseUrl, settings.model);
// 使用 VLM 如果启用且配置了
string decision;
if (settings.enableVlmFeatures && !string.IsNullOrEmpty(settings.vlmApiKey))
{
// 使用 VLM 分析屏幕
string base64Image = ScreenCaptureUtility.CaptureScreenAsBase64();
var vlmClient = new SimpleAIClient(settings.vlmApiKey, settings.vlmBaseUrl, settings.vlmModel);
decision = await vlmClient.GetVisionCompletionAsync(
GetAgentSystemPrompt(),
prompt,
base64Image,
maxTokens: 512,
temperature: 0.3f
);
}
else
{
// 纯文本模式
var messages = new List<(string role, string message)>
{
("user", prompt)
};
decision = await client.GetChatCompletionAsync(GetAgentSystemPrompt(), messages, 512, 0.3f);
}
if (string.IsNullOrEmpty(decision))
{
WulaLog.Debug("[AgentLoop] Empty decision received");
return;
}
_decisionCount++;
WulaLog.Debug($"[AgentLoop] Decision #{_decisionCount}: {decision.Substring(0, Math.Min(100, decision.Length))}...");
// 4. Act - 执行决策
ExecuteDecision(decision);
// 5. 记录历史
_actionHistory.Add($"[{_decisionCount}] {decision.Substring(0, Math.Min(50, decision.Length))}");
if (_actionHistory.Count > 20)
{
_actionHistory.RemoveAt(0);
}
OnDecisionMade?.Invoke(decision);
// 6. 检查是否完成目标
if (decision.Contains("<objective_complete") || decision.Contains("目标已完成"))
{
OnObjectiveComplete?.Invoke(_currentObjective);
Stop();
}
}
catch (Exception ex)
{
WulaLog.Debug($"[AgentLoop] Error in decision cycle: {ex.Message}");
OnError?.Invoke(ex.Message);
}
}
private string BuildDecisionPrompt(string gameStateText)
{
var sb = new StringBuilder();
sb.AppendLine("# 当前任务");
sb.AppendLine($"**目标**: {_currentObjective}");
sb.AppendLine($"**已执行决策次数**: {_decisionCount}");
sb.AppendLine();
sb.AppendLine(gameStateText);
if (_actionHistory.Count > 0)
{
sb.AppendLine();
sb.AppendLine("# 最近操作历史");
foreach (var action in _actionHistory)
{
sb.AppendLine($"- {action}");
}
}
sb.AppendLine();
sb.AppendLine("# 请决定下一步操作");
sb.AppendLine("分析当前状态,输出一个 XML 工具调用来推进目标。");
sb.AppendLine("如果目标已完成,输出 <objective_complete/>。");
sb.AppendLine("如果不需要操作(等待中),输出 <no_action/>。");
return sb.ToString();
}
private string GetAgentSystemPrompt()
{
return @"你是一个自主 RimWorld 游戏 AI Agent。你的任务是独立完成用户给出的开放式目标。
# 核心原则
1. **自主决策**: 不要等待用户指示,主动分析情况并采取行动
2. **循序渐进**: 每次只执行一个操作,观察结果后再决定下一步
3. **问题应对**: 遇到障碍时自己想办法解决
4. **目标导向**: 始终围绕目标推进,避免无关操作
# 可用工具
- get_game_state: 获取详细游戏状态
- designate_mine: <designate_mine><x>X坐标</x><z>Z坐标</z><radius>可选半径</radius></designate_mine> 标记采矿
- draft_pawn: <draft_pawn><pawn_name>名字</pawn_name><draft>true/false</draft></draft_pawn> 征召殖民者
- analyze_screen: <analyze_screen><context>分析目标</context></analyze_screen> 分析屏幕需要VLM
- visual_click: <visual_click><x>0-1比例</x><y>0-1比例</y></visual_click> 模拟点击
# 输出格式
直接输出一个 XML 工具调用,不要解释。
如果目标已完成: <objective_complete/>
如果需要等待: <no_action/>
# 注意事项
- 坐标使用游戏内整数坐标,不是屏幕比例
- 优先使用 API 工具designate_mine 等),视觉工具用于 mod 内容
- 保持简洁高效";
}
private void ExecuteDecision(string decision)
{
// 解析并执行 AI 的决策
// 从 AIIntelligenceCore 借用工具执行逻辑
var core = AIIntelligenceCore.Instance;
if (core == null)
{
WulaLog.Debug("[AgentLoop] AIIntelligenceCore not available");
return;
}
// 提取工具调用并执行
// 暂时使用简单的正则匹配,实际应整合 AIIntelligenceCore 的解析逻辑
if (decision.Contains("<no_action") || decision.Contains("<objective_complete"))
{
// 不需要执行
return;
}
// 委托给 AIIntelligenceCore 执行工具
// TODO: 整合更完善的工具执行逻辑
WulaLog.Debug($"[AgentLoop] Executing: {decision}");
}
public override void ExposeData()
{
base.ExposeData();
Scribe_Values.Look(ref _currentObjective, "agentObjective", "");
Scribe_Values.Look(ref _isRunning, "agentRunning", false);
Scribe_Values.Look(ref _decisionCount, "agentDecisionCount", 0);
}
}
}

View File

@@ -0,0 +1,354 @@
using System;
using System.Runtime.InteropServices;
using System.Threading;
using UnityEngine;
namespace WulaFallenEmpire.EventSystem.AI.Agent
{
/// <summary>
/// 纯视觉交互工具集 - 仿照 Python VLM Agent
/// 当没有原生 API 可用时AI 可以通过这些工具操作任何界面
/// </summary>
public static class VisualInteractionTools
{
// 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 void keybd_event(byte bVk, byte bScan, uint dwFlags, int dwExtraInfo);
[DllImport("user32.dll")]
private static extern short VkKeyScan(char ch);
// 鼠标事件标志
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;
// 键盘事件标志
private const uint KEYEVENTF_KEYDOWN = 0x0000;
private const uint KEYEVENTF_KEYUP = 0x0002;
// 虚拟键码
private const byte VK_CONTROL = 0x11;
private const byte VK_SHIFT = 0x10;
private const byte VK_ALT = 0x12;
private const byte VK_RETURN = 0x0D;
private const byte VK_BACK = 0x08;
private const byte VK_ESCAPE = 0x1B;
private const byte VK_TAB = 0x09;
private const byte VK_LWIN = 0x5B;
private const byte VK_F4 = 0x73;
/// <summary>
/// 1. 鼠标点击 - 在比例坐标处点击
/// </summary>
public static string MouseClick(float x, float y, string button = "left", int clicks = 1)
{
try
{
int screenX = Mathf.RoundToInt(x * Screen.width);
int screenY = Mathf.RoundToInt(y * Screen.height);
// Unity Y 轴翻转
int windowsY = Screen.height - screenY;
SetCursorPos(screenX, windowsY);
Thread.Sleep(20);
for (int i = 0; i < clicks; i++)
{
if (button == "right")
{
mouse_event(MOUSEEVENTF_RIGHTDOWN, 0, 0, 0, 0);
mouse_event(MOUSEEVENTF_RIGHTUP, 0, 0, 0, 0);
}
else
{
mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0);
mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
}
if (i < clicks - 1) Thread.Sleep(50);
}
string buttonText = button == "right" ? "右键" : "左键";
string clickText = clicks == 2 ? "双击" : "单击";
return $"Success: 在 ({screenX}, {screenY}) 处{buttonText}{clickText}";
}
catch (Exception ex)
{
return $"Error: 点击失败 - {ex.Message}";
}
}
/// <summary>
/// 2. 输入文本 - 在指定位置点击后输入文本(通过剪贴板)
/// </summary>
public static string TypeText(float x, float y, string text)
{
try
{
// 先点击
MouseClick(x, y);
Thread.Sleep(100);
// 通过剪贴板输入
GUIUtility.systemCopyBuffer = text;
Thread.Sleep(50);
// Ctrl+V 粘贴
keybd_event(VK_CONTROL, 0, KEYEVENTF_KEYDOWN, 0);
keybd_event(0x56, 0, KEYEVENTF_KEYDOWN, 0); // V
keybd_event(0x56, 0, KEYEVENTF_KEYUP, 0);
keybd_event(VK_CONTROL, 0, KEYEVENTF_KEYUP, 0);
return $"Success: 在 ({x:F3}, {y:F3}) 处输入文本: {text}";
}
catch (Exception ex)
{
return $"Error: 输入文本失败 - {ex.Message}";
}
}
/// <summary>
/// 3. 滚动窗口 - 在指定位置滚动
/// </summary>
public static string ScrollWindow(float x, float y, string direction = "up", int amount = 3)
{
try
{
int screenX = Mathf.RoundToInt(x * Screen.width);
int screenY = Mathf.RoundToInt(y * Screen.height);
int windowsY = Screen.height - screenY;
SetCursorPos(screenX, windowsY);
Thread.Sleep(20);
int wheelDelta = (direction == "up" ? 1 : -1) * 120 * amount;
mouse_event(MOUSEEVENTF_WHEEL, 0, 0, (uint)wheelDelta, 0);
string dir = direction == "up" ? "向上" : "向下";
return $"Success: 在 ({screenX}, {screenY}) 处{dir}滚动 {amount} 步";
}
catch (Exception ex)
{
return $"Error: 滚动失败 - {ex.Message}";
}
}
/// <summary>
/// 4. 鼠标拖拽 - 从起点拖到终点
/// </summary>
public static string MouseDrag(float startX, float startY, float endX, float endY, float durationSec = 0.5f)
{
try
{
int sx = Mathf.RoundToInt(startX * Screen.width);
int sy = Screen.height - Mathf.RoundToInt(startY * Screen.height);
int ex = Mathf.RoundToInt(endX * Screen.width);
int ey = Screen.height - Mathf.RoundToInt(endY * Screen.height);
// 移动到起点
SetCursorPos(sx, sy);
Thread.Sleep(50);
// 按下
mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0);
// 平滑移动
int steps = Mathf.Max(5, Mathf.RoundToInt(durationSec * 20));
int delayMs = Mathf.RoundToInt(durationSec * 1000 / steps);
for (int i = 1; i <= steps; i++)
{
float t = (float)i / steps;
int cx = Mathf.RoundToInt(Mathf.Lerp(sx, ex, t));
int cy = Mathf.RoundToInt(Mathf.Lerp(sy, ey, t));
SetCursorPos(cx, cy);
Thread.Sleep(delayMs);
}
// 释放
mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
return $"Success: 从 ({startX:F3}, {startY:F3}) 拖拽到 ({endX:F3}, {endY:F3})";
}
catch (Exception ex)
{
return $"Error: 拖拽失败 - {ex.Message}";
}
}
/// <summary>
/// 5. 等待 - 暂停指定秒数
/// </summary>
public static string Wait(float seconds)
{
try
{
Thread.Sleep(Mathf.RoundToInt(seconds * 1000));
return $"Success: 等待了 {seconds} 秒";
}
catch (Exception ex)
{
return $"Error: 等待失败 - {ex.Message}";
}
}
/// <summary>
/// 6. 按下回车键
/// </summary>
public static string PressEnter()
{
try
{
keybd_event(VK_RETURN, 0, KEYEVENTF_KEYDOWN, 0);
keybd_event(VK_RETURN, 0, KEYEVENTF_KEYUP, 0);
return "Success: 按下回车键";
}
catch (Exception ex)
{
return $"Error: 按键失败 - {ex.Message}";
}
}
/// <summary>
/// 7. 按下 Escape 键
/// </summary>
public static string PressEscape()
{
try
{
keybd_event(VK_ESCAPE, 0, KEYEVENTF_KEYDOWN, 0);
keybd_event(VK_ESCAPE, 0, KEYEVENTF_KEYUP, 0);
return "Success: 按下 Escape 键";
}
catch (Exception ex)
{
return $"Error: 按键失败 - {ex.Message}";
}
}
/// <summary>
/// 8. 删除文本 - 按 Backspace 删除指定数量字符
/// </summary>
public static string DeleteText(float x, float y, int count = 1)
{
try
{
MouseClick(x, y);
Thread.Sleep(100);
for (int i = 0; i < count; i++)
{
keybd_event(VK_BACK, 0, KEYEVENTF_KEYDOWN, 0);
keybd_event(VK_BACK, 0, KEYEVENTF_KEYUP, 0);
Thread.Sleep(20);
}
return $"Success: 删除了 {count} 个字符";
}
catch (Exception ex)
{
return $"Error: 删除失败 - {ex.Message}";
}
}
/// <summary>
/// 9. 执行快捷键 - 如 Ctrl+C, Alt+F4 等
/// </summary>
public static string PressHotkey(float x, float y, string hotkey)
{
try
{
// 先点击获取焦点
MouseClick(x, y);
Thread.Sleep(100);
// 解析快捷键
var keys = hotkey.ToLowerInvariant().Replace("+", " ").Replace("-", " ").Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
// 按下修饰键
foreach (var key in keys)
{
byte vk = GetVirtualKeyCode(key);
if (vk != 0)
{
keybd_event(vk, 0, KEYEVENTF_KEYDOWN, 0);
}
}
Thread.Sleep(50);
// 释放修饰键(逆序)
for (int i = keys.Length - 1; i >= 0; i--)
{
byte vk = GetVirtualKeyCode(keys[i]);
if (vk != 0)
{
keybd_event(vk, 0, KEYEVENTF_KEYUP, 0);
}
}
return $"Success: 执行快捷键 {hotkey}";
}
catch (Exception ex)
{
return $"Error: 快捷键失败 - {ex.Message}";
}
}
/// <summary>
/// 10. 关闭窗口 - Alt+F4
/// </summary>
public static string CloseWindow(float x, float y)
{
try
{
MouseClick(x, y);
Thread.Sleep(100);
keybd_event(VK_ALT, 0, KEYEVENTF_KEYDOWN, 0);
keybd_event(VK_F4, 0, KEYEVENTF_KEYDOWN, 0);
keybd_event(VK_F4, 0, KEYEVENTF_KEYUP, 0);
keybd_event(VK_ALT, 0, KEYEVENTF_KEYUP, 0);
return "Success: 关闭窗口";
}
catch (Exception ex)
{
return $"Error: 关闭窗口失败 - {ex.Message}";
}
}
private static byte GetVirtualKeyCode(string keyName)
{
return keyName.ToLowerInvariant() switch
{
"ctrl" or "control" => VK_CONTROL,
"shift" => VK_SHIFT,
"alt" => VK_ALT,
"enter" or "return" => VK_RETURN,
"esc" or "escape" => VK_ESCAPE,
"tab" => VK_TAB,
"backspace" or "back" => VK_BACK,
"win" or "windows" => VK_LWIN,
"f4" => VK_F4,
// 字母键
"a" => 0x41, "b" => 0x42, "c" => 0x43, "d" => 0x44, "e" => 0x45,
"f" => 0x46, "g" => 0x47, "h" => 0x48, "i" => 0x49, "j" => 0x4A,
"k" => 0x4B, "l" => 0x4C, "m" => 0x4D, "n" => 0x4E, "o" => 0x4F,
"p" => 0x50, "q" => 0x51, "r" => 0x52, "s" => 0x53, "t" => 0x54,
"u" => 0x55, "v" => 0x56, "w" => 0x57, "x" => 0x58, "y" => 0x59,
"z" => 0x5A,
_ => 0
};
}
}
}

View File

@@ -12,6 +12,7 @@ namespace WulaFallenEmpire
{
public static WulaFallenEmpireSettings settings;
public static bool _showApiKey = false;
public static bool _showVlmApiKey = false;
private string _maxContextTokensBuffer;
public WulaFallenEmpireMod(ModContentPack content) : base(content)
@@ -67,6 +68,37 @@ namespace WulaFallenEmpire
listingStandard.GapLine();
listingStandard.CheckboxLabeled("Wula_EnableDebugLogs".Translate(), ref settings.enableDebugLogs, "Wula_EnableDebugLogsDesc".Translate());
// VLM 设置部分
listingStandard.GapLine();
listingStandard.Label("<color=cyan>VLM (视觉模型) 设置</color>");
listingStandard.CheckboxLabeled("启用 VLM 视觉功能", ref settings.enableVlmFeatures, "启用后 AI 可以「看到」游戏屏幕并分析");
if (settings.enableVlmFeatures)
{
listingStandard.Label("VLM API Key:");
Rect vlmKeyRect = listingStandard.GetRect(30f);
Rect vlmPasswordRect = new Rect(vlmKeyRect.x, vlmKeyRect.y, vlmKeyRect.width - toggleWidth - 5f, vlmKeyRect.height);
Rect vlmToggleRect = new Rect(vlmKeyRect.xMax - toggleWidth, vlmKeyRect.y, toggleWidth, vlmKeyRect.height);
if (_showVlmApiKey)
{
settings.vlmApiKey = Widgets.TextField(vlmPasswordRect, settings.vlmApiKey ?? "");
}
else
{
settings.vlmApiKey = GUI.PasswordField(vlmPasswordRect, settings.vlmApiKey ?? "", '•');
}
Widgets.CheckboxLabeled(vlmToggleRect, "Show", ref _showVlmApiKey);
listingStandard.Gap(listingStandard.verticalSpacing);
listingStandard.Label("VLM Base URL:");
settings.vlmBaseUrl = listingStandard.TextEntry(settings.vlmBaseUrl ?? "https://dashscope.aliyuncs.com/compatible-mode/v1");
listingStandard.Label("VLM Model:");
settings.vlmModel = listingStandard.TextEntry(settings.vlmModel ?? "qwen-vl-max");
}
listingStandard.GapLine();
listingStandard.Label("Translation tools");
Rect exportRect = listingStandard.GetRect(30f);

View File

@@ -0,0 +1,429 @@
# RimWorld AI Agent 开发文档
> 本文档用于移交给 Codex 继续开发
---
## 1. 项目概述
### 目标
创建一个**完全自主的 AI Agent**,能够自动玩 RimWorld 游戏。用户只需给出开放式指令(如"帮我挖点铁"或"帮我玩10分钟"AI 即可独立决策并操作游戏。
### 技术栈
- **语言**: C# (.NET Framework 4.8)
- **框架**: RimWorld Mod (Verse/RimWorld API)
- **AI 后端**: 阿里云百炼 (DashScope) API
- **VLM 模型**: Qwen-VL / Qwen-Omni-Realtime
### 核心设计
```
用户指令 → AIIntelligenceCore → [被动模式 | 主动模式] → 工具执行 → 游戏操作
```
---
## 2. 架构
### 2.1 模式切换设计
```
┌────────────────────────────────────────────┐
│ AIIntelligenceCore │
│ ┌─────────────┐ ┌─────────────────┐ │
│ │ 被动模式 │◄──►│ 主动模式 │ │
│ │ (聊天对话) │ │ (Agent循环) │ │
│ └─────────────┘ └────────┬────────┘ │
└──────────────────────────────┼────────────┘
┌─────────────────────┐
│ AutonomousAgentLoop │
│ Observe → Think │
│ → Act │
└─────────────────────┘
```
**模式切换触发条件(待实现)**:
- 用户说"帮我玩X分钟" → 切换到主动模式
- 主动模式任务完成 → 自动切回被动模式
- 用户说"停止" → 强制切回被动模式
### 2.2 文件结构
```
Source/WulaFallenEmpire/EventSystem/AI/
├── AIIntelligenceCore.cs # 核心AI控制器已有
├── SimpleAIClient.cs # HTTP API 客户端(已有)
├── ScreenCaptureUtility.cs # 截屏工具(已有)
├── Agent/ # ★ 新增目录
│ ├── AutonomousAgentLoop.cs # 主动模式循环
│ ├── StateObserver.cs # 游戏状态收集器
│ ├── GameStateSnapshot.cs # 状态数据结构
│ ├── VisualInteractionTools.cs # 视觉交互工具(10个)
│ ├── MouseSimulator.cs # 鼠标模拟
│ └── OmniRealtimeClient.cs # WebSocket流式连接
├── Tools/ # AI工具
│ ├── AITool.cs # 工具基类(已有)
│ ├── Tool_GetGameState.cs # ★ 新增
│ ├── Tool_DesignateMine.cs # ★ 新增
│ ├── Tool_DraftPawn.cs # ★ 新增
│ ├── Tool_VisualClick.cs # ★ 新增
│ └── ... (其他原有工具)
└── UI/
├── Dialog_AIConversation.cs # 对话UI已有
└── Overlay_WulaLink.cs # 悬浮UI已有
```
---
## 3. 已完成组件
### 3.1 StateObserver (状态观察器)
**路径**: [Agent/StateObserver.cs](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/Agent/StateObserver.cs)
**功能**: 收集当前游戏状态,生成给 VLM 的文本描述
**API**:
```csharp
public static class StateObserver
{
// 捕获当前游戏状态快照
public static GameStateSnapshot CaptureState();
}
```
**收集内容**:
- 时间(小时、季节、年份)
- 环境(生物群系、温度、天气)
- 殖民者(名字、健康、心情、当前工作、位置)
- 资源(钢铁、银、食物、医药等)
- 建筑进度(蓝图、建造框架)
- 威胁(敌对派系、距离)
- 最近消息
---
### 3.2 GameStateSnapshot (状态数据结构)
**路径**: [Agent/GameStateSnapshot.cs](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/Agent/GameStateSnapshot.cs)
**功能**: 存储游戏状态数据
**关键方法**:
```csharp
public class GameStateSnapshot
{
public List<PawnSnapshot> Colonists;
public Dictionary<string, int> Resources;
public List<ThreatSnapshot> Threats;
// 生成给VLM的文本描述
public string ToPromptText();
}
```
---
### 3.3 VisualInteractionTools (视觉交互工具集)
**路径**: [Agent/VisualInteractionTools.cs](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/Agent/VisualInteractionTools.cs)
**功能**: 10个纯视觉交互工具使用 Windows API 模拟输入
| 方法 | 功能 | 参数 |
|------|------|------|
| [MouseClick(x, y, button, clicks)](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/Agent/VisualInteractionTools.cs#49-89) | 鼠标点击 | 0-1比例坐标 |
| [TypeText(x, y, text)](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/Agent/VisualInteractionTools.cs#90-118) | 输入文本 | 通过剪贴板 |
| [ScrollWindow(x, y, direction, amount)](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/Agent/VisualInteractionTools.cs#119-144) | 滚动 | up/down |
| [MouseDrag(sx, sy, ex, ey, duration)](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/Agent/VisualInteractionTools.cs#145-187) | 拖拽 | 起止坐标 |
| [Wait(seconds)](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/Agent/VisualInteractionTools.cs#188-203) | 等待 | 秒数 |
| [PressEnter()](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/Agent/VisualInteractionTools.cs#204-220) | 按回车 | 无 |
| [PressEscape()](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/Agent/VisualInteractionTools.cs#221-237) | 按ESC | 无 |
| [DeleteText(x, y, count)](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/Agent/VisualInteractionTools.cs#238-262) | 删除 | 字符数 |
| [PressHotkey(x, y, hotkey)](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/Agent/VisualInteractionTools.cs#263-306) | 快捷键 | 如"ctrl+c" |
| [CloseWindow(x, y)](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/Agent/VisualInteractionTools.cs#307-329) | 关闭窗口 | Alt+F4 |
---
### 3.4 AutonomousAgentLoop (自主Agent循环)
**路径**: [Agent/AutonomousAgentLoop.cs](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/Agent/AutonomousAgentLoop.cs)
**功能**: 主动模式的核心循环
**状态**:
- `IsRunning`: 是否运行中
- `CurrentObjective`: 当前目标
- `DecisionCount`: 已执行决策次数
**关键API**:
```csharp
public class AutonomousAgentLoop : GameComponent
{
public static AutonomousAgentLoop Instance;
// 开始执行目标
public void StartObjective(string objective);
// 停止Agent
public void Stop();
// 事件
public event Action<string> OnDecisionMade;
public event Action<string> OnObjectiveComplete;
}
```
**待完成**: [ExecuteDecision()](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/Agent/AutonomousAgentLoop.cs#244-269) 方法需要整合工具执行逻辑
---
### 3.5 原生API工具
| 工具 | 路径 | 功能 | 参数格式 |
|------|------|------|----------|
| [Tool_GetGameState](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_GetGameState.cs#8-45) | Tools/ | 获取游戏状态 | `<get_game_state/>` |
| [Tool_DesignateMine](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_DesignateMine.cs#12-139) | Tools/ | 采矿指令 | `<designate_mine><x>数字</x><z>数字</z><radius>可选</radius></designate_mine>` |
| [Tool_DraftPawn](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_DraftPawn.cs#10-105) | Tools/ | 征召殖民者 | `<draft_pawn><pawn_name>名字</pawn_name><draft>true/false</draft></draft_pawn>` |
---
## 4. 待完成任务
### 4.1 模式切换整合 (高优先级)
**目标**: 在 [AIIntelligenceCore](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs#16-1364) 中实现被动/主动模式切换
**实现思路**:
```csharp
// AIIntelligenceCore 中添加
private bool _isAgentMode = false;
public void ProcessUserMessage(string message)
{
// 检测是否触发主动模式
if (IsAgentTrigger(message, out string objective, out float duration))
{
_isAgentMode = true;
AutonomousAgentLoop.Instance.StartObjective(objective);
// 设置定时器duration后自动停止
}
else
{
// 正常对话处理
RunConversation(message);
}
}
private bool IsAgentTrigger(string msg, out string obj, out float dur)
{
// 匹配模式:
// "帮我玩10分钟" → obj="管理殖民地", dur=600
// "帮我挖点铁" → obj="采集铁矿", dur=0(无限)
// ...
}
```
---
### 4.2 工具执行整合 (高优先级)
**目标**: 让 `AutonomousAgentLoop.ExecuteDecision()` 能够执行工具
**当前状态**: 方法体是空的 TODO
**实现思路**:
```csharp
private void ExecuteDecision(string decision)
{
// 1. 检查特殊标记
if (decision.Contains("<no_action")) return;
if (decision.Contains("<objective_complete"))
{
OnObjectiveComplete?.Invoke(_currentObjective);
Stop();
return;
}
// 2. 获取工具实例
var core = AIIntelligenceCore.Instance;
var tools = core.GetAvailableTools();
// 3. 解析XML工具调用复用AIIntelligenceCore的逻辑
foreach (var tool in tools)
{
if (decision.Contains($"<{tool.Name}"))
{
string result = tool.Execute(decision);
WulaLog.Debug($"Tool {tool.Name}: {result}");
_lastToolResult = result;
break;
}
}
}
```
---
### 4.3 AI待办清单 (中优先级)
**目标**: AI 维护自己的待办清单,持久化到游戏存档
**设计**:
```csharp
public class AgentTodoList : IExposable
{
public List<TodoItem> Items = new List<TodoItem>();
public void ExposeData()
{
Scribe_Collections.Look(ref Items, "todoItems", LookMode.Deep);
}
}
public class TodoItem : IExposable
{
public string Description;
public bool IsComplete;
public int Priority;
public int CreatedTick;
}
```
---
### 4.4 Qwen-Omni-Realtime 测试 (低优先级)
**目标**: 测试 WebSocket 流式连接
**已完成**: [OmniRealtimeClient.cs](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/Agent/OmniRealtimeClient.cs) 基础实现
**待测试**:
- WebSocket 连接建立
- 图片发送 (`input_image_buffer.append`)
- 文本接收 (`response.text.delta`)
---
## 5. 关键接口参考
### 5.1 AITool 基类
```csharp
public abstract class AITool
{
public abstract string Name { get; }
public abstract string Description { get; }
public abstract string UsageSchema { get; }
public abstract string Execute(string args);
// 解析XML参数
protected Dictionary<string, string> ParseXmlArgs(string xmlContent);
}
```
### 5.2 SimpleAIClient API
```csharp
public class SimpleAIClient
{
public SimpleAIClient(string apiKey, string baseUrl, string model);
// 文本对话
public Task<string> GetChatCompletionAsync(
string systemPrompt,
List<(string role, string message)> messages,
int maxTokens = 1024,
float temperature = 0.7f
);
// VLM 视觉分析
public Task<string> GetVisionCompletionAsync(
string systemPrompt,
string userPrompt,
string base64Image,
int maxTokens = 1024,
float temperature = 0.7f
);
}
```
### 5.3 设置 (WulaFallenEmpireSettings)
```csharp
public class WulaFallenEmpireSettings : ModSettings
{
// 主模型
public string apiKey;
public string baseUrl = "https://dashscope.aliyuncs.com/compatible-mode/v1";
public string model = "qwen-turbo";
// VLM 模型
public string vlmApiKey;
public string vlmBaseUrl;
public string vlmModel = "qwen-vl-max";
public bool enableVlmFeatures = false;
}
```
---
## 6. 开发指南
### 6.1 添加新工具
1. 在 [Tools/](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs#310-337) 创建新类继承 [AITool](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/Tools/AITool.cs#8-41)
2. 实现 [Name](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs#636-642), [Description](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/Agent/StateObserver.cs#156-187), `UsageSchema`, [Execute](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_VisualClick.cs#133-165)
3. 在 `AIIntelligenceCore.InitializeTools()` 中注册
```csharp
// 示例Tool_BuildWall.cs
public class Tool_BuildWall : AITool
{
public override string Name => "build_wall";
public override string Description => "在指定位置放置墙壁蓝图";
public override string UsageSchema => "<build_wall><x>X</x><z>Z</z><stuff>材料</stuff></build_wall>";
public override string Execute(string args)
{
var dict = ParseXmlArgs(args);
// 实现建造逻辑
// GenConstruct.PlaceBlueprintForBuild(...)
return "Success: 墙壁蓝图已放置";
}
}
```
### 6.2 调试技巧
- 使用 `WulaLog.Debug()` 输出日志
- 检查 RimWorld 的 `Player.log` 文件
- 在开发者模式下 `Prefs.DevMode = true` 显示更多信息
### 6.3 常见问题
**Q: .NET Framework 4.8 兼容性问题**
- 不支持 `TakeLast()` → 使用 `Skip(list.Count - n)`
- 不支持 `string.Contains(x, StringComparison)` → 使用 `IndexOf`
**Q: Unity 主线程限制**
- 异步操作结果需要回到主线程执行
- 使用 `LongEventHandler.ExecuteWhenFinished(() => { ... })`
---
## 7. 参考资源
- [RimWorld Modding Wiki](https://rimworldwiki.com/wiki/Modding)
- [Harmony Patching](https://harmony.pardeike.net/)
- [阿里云百炼 API](https://help.aliyun.com/zh/model-studio/)
- [Qwen-Omni 文档](https://help.aliyun.com/zh/model-studio/user-guide/qwen-omni)
---
**文档版本**: v1.0
**更新时间**: 2025-12-27
**作者**: Gemini AI Agent