From 0cea79ddff75a57a4bce8d632c4ce8f001ed68ce Mon Sep 17 00:00:00 2001 From: "ProjectKoi-Kalo\\Kalo" Date: Mon, 29 Dec 2025 13:11:33 +0800 Subject: [PATCH] zc --- .gitignore | 4 +- Tools/ai_letter_auto_commentary.md | 405 -------- Tools/claude_handoff.md | 56 - Tools/codex_handoff.md.resolved | 429 -------- Tools/mimo-v2-flash.txt | 3 - Tools/task.md.resolved | 150 --- Tools/task_handoff.md | 140 --- Tools/using System;.cs | 1548 ---------------------------- 8 files changed, 1 insertion(+), 2734 deletions(-) delete mode 100644 Tools/ai_letter_auto_commentary.md delete mode 100644 Tools/claude_handoff.md delete mode 100644 Tools/codex_handoff.md.resolved delete mode 100644 Tools/mimo-v2-flash.txt delete mode 100644 Tools/task.md.resolved delete mode 100644 Tools/task_handoff.md delete mode 100644 Tools/using System;.cs diff --git a/.gitignore b/.gitignore index da0e4850..9e8e2bc4 100644 --- a/.gitignore +++ b/.gitignore @@ -44,7 +44,5 @@ package.json dark-server/dark-server.js node_modules/ gemini-websocket-proxy/ -Tools/dark-server/dark-server.js -Tools/rimworld_cpt_data.jsonl -Tools/mem0-1.0.0/ +Tools/ diff --git a/Tools/ai_letter_auto_commentary.md b/Tools/ai_letter_auto_commentary.md deleted file mode 100644 index 74e664ac..00000000 --- a/Tools/ai_letter_auto_commentary.md +++ /dev/null @@ -1,405 +0,0 @@ -# AI Letter Auto-Response System - 开发文档 - -## 概述 - -这个功能将使 P.I.A AI 能够自动监听游戏内的 Letter(信封通知),并根据内容智能决定是否向玩家发送评论、吐槽、警告或提供帮助建议。 - ---- - -## 功能需求 - -### 核心功能 -1. **Mod 设置开关**: 在设置中添加 `启用 AI 自动评论` 开关 -2. **Letter 监听**: 拦截所有发送给玩家的 Letter -3. **智能判断**: AI 分析 Letter 内容,决定是否需要回应 -4. **自动回复**: 通过现有的 AI 对话系统发送回复 - -### AI 回应类型 -| 类型 | 触发场景示例 | 回应风格 | -|------|-------------|---------| -| 警告 | 袭击通知、疫病爆发 | 紧急提醒,询问是否需要启动防御 | -| 吐槽 | 殖民者精神崩溃、愚蠢死亡 | 幽默/讽刺评论 | -| 建议 | 资源短缺、贸易商到来 | 实用建议 | -| 庆祝 | 任务完成、殖民者加入 | 积极反馈 | -| 沉默 | 常规事件、无关紧要的通知 | 不发送任何回复 | - ---- - -## 技术架构 - -### 1. 文件结构 -``` -Source/WulaFallenEmpire/ -├── Settings/ -│ └── WulaModSettings.cs # 添加新设置字段 -├── EventSystem/ -│ └── AI/ -│ ├── LetterInterceptor/ -│ │ ├── Patch_LetterStack.cs # Harmony Patch 拦截 Letter -│ │ ├── LetterAnalyzer.cs # Letter 分析和分类 -│ │ └── LetterToPromptConverter.cs # Letter 转提示词 -│ └── AIAutoCommentary.cs # AI 自动评论逻辑 -``` - -### 2. 关键类设计 - -#### 2.1 WulaModSettings.cs (修改) -```csharp -public class WulaModSettings : ModSettings -{ - // 现有设置... - - // 新增 - public bool enableAIAutoCommentary = false; // AI 自动评论开关 - public float aiCommentaryChance = 0.7f; // AI 评论概率 (0-1) - public bool commentOnNegativeOnly = false; // 仅评论负面事件 - - public override void ExposeData() - { - base.ExposeData(); - Scribe_Values.Look(ref enableAIAutoCommentary, "enableAIAutoCommentary", false); - Scribe_Values.Look(ref aiCommentaryChance, "aiCommentaryChance", 0.7f); - Scribe_Values.Look(ref commentOnNegativeOnly, "commentOnNegativeOnly", false); - } -} -``` - -#### 2.2 Patch_LetterStack.cs (新建) -```csharp -using HarmonyLib; -using RimWorld; -using Verse; - -namespace WulaFallenEmpire.EventSystem.AI.LetterInterceptor -{ - [HarmonyPatch(typeof(LetterStack), nameof(LetterStack.ReceiveLetter), - new Type[] { typeof(Letter), typeof(string) })] - public static class Patch_LetterStack_ReceiveLetter - { - public static void Postfix(Letter let, string debugInfo) - { - // 检查设置开关 - if (!WulaModSettings.Instance.enableAIAutoCommentary) return; - - // 异步处理,避免阻塞游戏 - AIAutoCommentary.ProcessLetter(let); - } - } -} -``` - -#### 2.3 LetterAnalyzer.cs (新建) -```csharp -namespace WulaFallenEmpire.EventSystem.AI.LetterInterceptor -{ - public enum LetterCategory - { - Raid, // 袭击 - Disease, // 疾病 - MentalBreak, // 精神崩溃 - Trade, // 贸易 - Quest, // 任务 - Death, // 死亡 - Recruitment, // 招募 - Resource, // 资源 - Weather, // 天气 - Positive, // 正面事件 - Negative, // 负面事件 - Neutral, // 中性事件 - Unknown // 未知 - } - - public static class LetterAnalyzer - { - public static LetterCategory Categorize(Letter letter) - { - // 根据 LetterDef 分类 - var def = letter.def; - - if (def == LetterDefOf.ThreatBig || def == LetterDefOf.ThreatSmall) - return LetterCategory.Raid; - if (def == LetterDefOf.Death) - return LetterCategory.Death; - if (def == LetterDefOf.PositiveEvent) - return LetterCategory.Positive; - if (def == LetterDefOf.NegativeEvent) - return LetterCategory.Negative; - if (def == LetterDefOf.NeutralEvent) - return LetterCategory.Neutral; - - // 根据内容关键词进一步分类 - string text = letter.text?.ToLower() ?? ""; - if (text.Contains("raid") || text.Contains("袭击") || text.Contains("attack")) - return LetterCategory.Raid; - if (text.Contains("disease") || text.Contains("疫病") || text.Contains("plague")) - return LetterCategory.Disease; - if (text.Contains("mental") || text.Contains("精神") || text.Contains("break")) - return LetterCategory.MentalBreak; - if (text.Contains("trade") || text.Contains("贸易") || text.Contains("商队")) - return LetterCategory.Trade; - - return LetterCategory.Unknown; - } - - public static bool ShouldComment(Letter letter) - { - var category = Categorize(letter); - - // 始终评论的类型 - switch (category) - { - case LetterCategory.Raid: - case LetterCategory.Death: - case LetterCategory.MentalBreak: - case LetterCategory.Disease: - return true; - - case LetterCategory.Trade: - case LetterCategory.Quest: - case LetterCategory.Positive: - return Rand.Chance(WulaModSettings.Instance.aiCommentaryChance); - - case LetterCategory.Neutral: - case LetterCategory.Unknown: - return Rand.Chance(0.3f); // 低概率评论 - - default: - return false; - } - } - } -} -``` - -#### 2.4 LetterToPromptConverter.cs (新建) -```csharp -namespace WulaFallenEmpire.EventSystem.AI.LetterInterceptor -{ - public static class LetterToPromptConverter - { - public static string Convert(Letter letter, LetterCategory category) - { - var sb = new StringBuilder(); - - sb.AppendLine("[SYSTEM EVENT NOTIFICATION]"); - sb.AppendLine($"Event Type: {category}"); - sb.AppendLine($"Severity: {GetSeverityFromDef(letter.def)}"); - sb.AppendLine($"Title: {letter.label}"); - sb.AppendLine($"Content: {letter.text}"); - sb.AppendLine(); - sb.AppendLine("[INSTRUCTION]"); - sb.AppendLine("You have received a game event notification. Based on the event type and content:"); - sb.AppendLine("- For RAIDS/THREATS: Offer tactical advice or ask if player needs orbital support"); - sb.AppendLine("- For DEATHS: Express condolences or make a sardonic comment if death was avoidable"); - sb.AppendLine("- For MENTAL BREAKS: Comment on the colonist's weakness or offer mood management tips"); - sb.AppendLine("- For TRADE: Suggest useful purchases or sales"); - sb.AppendLine("- For POSITIVE events: Celebrate briefly"); - sb.AppendLine("- For trivial events: You may choose to say nothing (respond with [NO_COMMENT])"); - sb.AppendLine(); - sb.AppendLine("Keep your response brief (1-2 sentences). Match your personality as the Legion AI."); - sb.AppendLine("If you don't think this event warrants a response, reply with exactly: [NO_COMMENT]"); - - return sb.ToString(); - } - - private static string GetSeverityFromDef(LetterDef def) - { - if (def == LetterDefOf.ThreatBig) return "CRITICAL"; - if (def == LetterDefOf.ThreatSmall) return "WARNING"; - if (def == LetterDefOf.Death) return "SERIOUS"; - if (def == LetterDefOf.NegativeEvent) return "MODERATE"; - if (def == LetterDefOf.PositiveEvent) return "GOOD"; - return "INFO"; - } - } -} -``` - -#### 2.5 AIAutoCommentary.cs (新建) -```csharp -namespace WulaFallenEmpire.EventSystem.AI -{ - public static class AIAutoCommentary - { - private static Queue pendingLetters = new Queue(); - private static bool isProcessing = false; - - public static void ProcessLetter(Letter letter) - { - // 检查是否应该评论 - if (!LetterAnalyzer.ShouldComment(letter)) - { - WulaLog.Debug($"[AI Commentary] Skipping letter: {letter.label}"); - return; - } - - // 加入队列 - pendingLetters.Enqueue(letter); - - // 开始处理(如果还没在处理中) - if (!isProcessing) - { - ProcessNextLetter(); - } - } - - private static async void ProcessNextLetter() - { - if (pendingLetters.Count == 0) - { - isProcessing = false; - return; - } - - isProcessing = true; - var letter = pendingLetters.Dequeue(); - - try - { - var category = LetterAnalyzer.Categorize(letter); - var prompt = LetterToPromptConverter.Convert(letter, category); - - // 获取 AI 核心 - var aiCore = Find.World?.GetComponent(); - if (aiCore == null) - { - WulaLog.Debug("[AI Commentary] AIIntelligenceCore not found."); - ProcessNextLetter(); - return; - } - - // 发送到 AI 并等待响应 - string response = await aiCore.SendSystemMessageAsync(prompt); - - // 检查是否选择不评论 - if (string.IsNullOrEmpty(response) || response.Contains("[NO_COMMENT]")) - { - WulaLog.Debug($"[AI Commentary] AI chose not to comment on: {letter.label}"); - } - else - { - // 显示 AI 的评论 - DisplayAICommentary(response, letter); - } - } - catch (Exception ex) - { - WulaLog.Debug($"[AI Commentary] Error processing letter: {ex.Message}"); - } - - // 延迟处理下一个,避免刷屏 - await Task.Delay(2000); - ProcessNextLetter(); - } - - private static void DisplayAICommentary(string response, Letter originalLetter) - { - // 方式1: 作为小型通知显示在 WulaLink 小 UI - var overlay = Find.WindowStack.Windows.OfType().FirstOrDefault(); - if (overlay != null) - { - overlay.AddAIMessage(response); - } - - // 方式2: 作为 Message 显示在屏幕左上角 - Messages.Message($"[P.I.A]: {response}", MessageTypeDefOf.SilentInput); - } - } -} -``` - ---- - -## 实现步骤 - -### 阶段 1: 基础设施 (预计 1 小时) -1. [ ] 在 `WulaModSettings.cs` 添加新设置字段 -2. [ ] 在设置 UI 中添加开关 -3. [ ] 添加对应的 Keyed 翻译 - -### 阶段 2: Letter 拦截 (预计 30 分钟) -1. [ ] 创建 `Patch_LetterStack.cs` Harmony Patch -2. [ ] 确保 Patch 正确注册到 Harmony 实例 -3. [ ] 测试 Letter 拦截是否正常工作 - -### 阶段 3: Letter 分析 (预计 1 小时) -1. [ ] 创建 `LetterAnalyzer.cs` 分类逻辑 -2. [ ] 创建 `LetterToPromptConverter.cs` 转换逻辑 -3. [ ] 测试不同类型 Letter 的分类准确性 - -### 阶段 4: AI 集成 (预计 1.5 小时) -1. [ ] 创建 `AIAutoCommentary.cs` 管理类 -2. [ ] 集成到现有的 `AIIntelligenceCore` 系统 -3. [ ] 实现队列处理避免刷屏 -4. [ ] 添加 `SendSystemMessageAsync` 方法到 AIIntelligenceCore - -### 阶段 5: UI 显示 (预计 30 分钟) -1. [ ] 决定评论显示方式(WulaLink UI / Message / 独立通知) -2. [ ] 实现显示逻辑 -3. [ ] 测试显示效果 - -### 阶段 6: 测试与优化 (预计 1 小时) -1. [ ] 测试各类 Letter 的评论效果 -2. [ ] 调整评论概率和过滤规则 -3. [ ] 优化提示词以获得更好的 AI 回应 -4. [ ] 添加速率限制避免 API 过载 - ---- - -## 需要添加的翻译键 - -```xml - -启用 AI 自动评论 -开启后,P.I.A 会自动对游戏事件(袭击、死亡、贸易等)发表评论或提供建议。 -评论概率 -AI 对中性事件发表评论的概率。负面事件(如袭击)总是会评论。 -仅评论负面事件 -开启后,AI 只会对负面事件(袭击、死亡、疾病等)发表评论。 -``` - ---- - -## 注意事项 - -1. **API 限流**: 需要实现请求队列和速率限制,避免短时间内发送过多请求 -2. **异步处理**: 所有 AI 请求必须异步处理,避免阻塞游戏主线程 -3. **用户控制**: 提供足够的设置选项让用户控制评论频率和类型 -4. **优雅降级**: 如果 AI 服务不可用,静默失败而不影响游戏 -5. **内存管理**: 队列大小限制,避免积累过多未处理的 Letter - ---- - -## 预期效果示例 - -**场景 1: 袭击通知** -``` -[Letter] 海盗袭击!一群海盗正在向你的殖民地进发。 -[P.I.A] 检测到敌对势力入侵。需要我启动轨道监视协议吗? -``` - -**场景 2: 殖民者死亡** -``` -[Letter] 张三死了。他被一只疯狂的松鼠咬死了。 -[P.I.A] ...被松鼠咬死?这位殖民者的战斗技能令人印象深刻。 -``` - -**场景 3: 贸易商到来** -``` -[Letter] 商队到来。一个来自外部势力的商队想要与你交易。 -[P.I.A] 贸易商队抵达。我注意到你的钢铁储备较低,建议优先采购。 -``` - ---- - -## 依赖项 - -- Harmony 2.0+ (用于 Patch) -- 现有的 AIIntelligenceCore 系统 -- 现有的 WulaModSettings 系统 -- 现有的 Overlay_WulaLink UI - ---- - -*文档版本: 1.0* -*创建时间: 2025-12-28* diff --git a/Tools/claude_handoff.md b/Tools/claude_handoff.md deleted file mode 100644 index 06718ab5..00000000 --- a/Tools/claude_handoff.md +++ /dev/null @@ -1,56 +0,0 @@ -WulaLink / AI Core Handoff (for Claude) - -Context -- Mod: RimWorld WulaFallenEmpire. -- The AI conversation system was refactored to use a shared WorldComponent core. -- The small WulaLink overlay is now optional; the main event entry should open the old large dialog. - -Current behavior and verification -- `Effect_OpenAIConversation` now opens the large window `Dialog_AIConversation`. -- The small overlay (`Overlay_WulaLink`) remains available via dev/debug entry. -- Non-final / streaming AI output should not create empty lines in the small window: - - SimpleAIClient is non-stream by default. - - AIIntelligenceCore only fires `OnMessageReceived` on final reply and ignores empty or XML-only output. - - Overlay filters `tool`/`toolcall` messages unless DevMode is on. -- Build verified: `dotnet build C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj` - -Key changes -1) New shared AI core - - File: `Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs` - - WorldComponent with static `Instance` and events: - - `OnMessageReceived`, `OnThinkingStateChanged`, `OnExpressionChanged` - - Public API: - - `InitializeConversation`, `GetHistorySnapshot`, `SetExpression`, `SetPortrait`, `SendMessage`, `SendUserMessage` - - Core responsibilities: - - History load/save via `AIHistoryManager` - - `/clear` support - - Expression tag parsing `[EXPR:n]` - - 3-phase tool pipeline (query/action/reply) from the old dialog logic - - Tool execution and ledger tracking - -2) OpenAI conversation entry now opens the large dialog - - File: `Source/WulaFallenEmpire/EventSystem/Effect/Effect_OpenAIConversation.cs` - - Uses `Dialog_AIConversation` instead of `Overlay_WulaLink`. - - XML entry in `1.6/1.6/Defs/EventDefs/Wula_AI_Events.xml` stays the same. - -3) Overlay and tools point to the shared core - - Files: - - `Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink.cs` - - `Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink_Notification.cs` - - `Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_ChangeExpression.cs` - - `Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_GetRecentNotifications.cs` - - Namespace import updated to `WulaFallenEmpire.EventSystem.AI`. - -4) WulaLink styles restored for overlay build - - File: `Source/WulaFallenEmpire/EventSystem/AI/UI/WulaLinkStyles.cs` - - Added colors used by overlay: - - `InputBarColor`, `SystemAccentColor`, `SenseiTextColor`, `StudentTextColor` - -Notes / gotchas -- Some UI files are not UTF-8 (likely ANSI). If you edit them with scripts, prefer `-Encoding Default` in PowerShell to avoid invalid UTF-8 errors. -- The old large dialog is still self-contained; the shared core is used by overlay + tools. Future cleanup can rewire `Dialog_AIConversation` to use the core if desired. - -Open questions / TODO (if needed later) -- Memory system integration is not done in the core: - - `AIMemoryManager.cs`, `MemoryPrompts.cs`, and `Dialog_AIConversation.cs` integration still pending. -- If the overlay should become a non-debug entry, wire an explicit effect or UI button to open it. diff --git a/Tools/codex_handoff.md.resolved b/Tools/codex_handoff.md.resolved deleted file mode 100644 index 198fe358..00000000 --- a/Tools/codex_handoff.md.resolved +++ /dev/null @@ -1,429 +0,0 @@ -# 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 Colonists; - public Dictionary Resources; - public List 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 OnDecisionMade; - public event Action 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/ | 获取游戏状态 | `` | -| [Tool_DesignateMine](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_DesignateMine.cs#12-139) | Tools/ | 采矿指令 | `数字数字可选` | -| [Tool_DraftPawn](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_DraftPawn.cs#10-105) | Tools/ | 征召殖民者 | `名字true/false` | - ---- - -## 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(" Items = new List(); - - 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 ParseXmlArgs(string xmlContent); -} -``` - -### 5.2 SimpleAIClient API - -```csharp -public class SimpleAIClient -{ - public SimpleAIClient(string apiKey, string baseUrl, string model); - - // 文本对话 - public Task GetChatCompletionAsync( - string systemPrompt, - List<(string role, string message)> messages, - int maxTokens = 1024, - float temperature = 0.7f - ); - - // VLM 视觉分析 - public Task 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 => "XZ材料"; - - 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 diff --git a/Tools/mimo-v2-flash.txt b/Tools/mimo-v2-flash.txt deleted file mode 100644 index e2148c53..00000000 --- a/Tools/mimo-v2-flash.txt +++ /dev/null @@ -1,3 +0,0 @@ -https://api.xiaomimimo.com/v1 -mimo-v2-flash -sk-cuwai2jix0zwrghj307pmvdpmtoc74j4uv9bejglxcs89tnx \ No newline at end of file diff --git a/Tools/task.md.resolved b/Tools/task.md.resolved deleted file mode 100644 index 569db22e..00000000 --- a/Tools/task.md.resolved +++ /dev/null @@ -1,150 +0,0 @@ -# WulaLink UI 修复任务 - Codex 交接文档 - -## 项目概述 -RimWorld Mod: [WulaFallenEmpire](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire) -目标: 实现 MomoTalk 风格的悬浮 AI 聊天 UI (WulaLink),同时确保原有大 UI 不被破坏。 - -## 关键文件路径 -``` -C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\ -├── EventSystem\ -│ ├── Dialog_CustomDisplay.cs # 基类,包含正确的按钮样式和布局逻辑 -│ └── AI\ -│ ├── AIIntelligenceCore.cs # AI 核心逻辑 (WorldComponent) - 已完成 -│ ├── DebugActions_WulaLink.cs # Debug 入口 -│ └── UI\ -│ ├── Dialog_AIConversation.cs # 大 UI - 需要修复布局 -│ ├── Overlay_WulaLink.cs # 小悬浮框 - 需要修复样式 -│ ├── Overlay_WulaLink_Notification.cs # 通知弹窗 - 已完成 -│ └── WulaLinkStyles.cs # 样式定义 - 需要调整颜色 -``` - ---- - -## 问题 1: 大 UI (Dialog_AIConversation) 布局损坏 - -### 现状 -[Dialog_AIConversation](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs#108-133) 继承自 [Dialog_CustomDisplay](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/Dialog_CustomDisplay.cs#10-668),但 [DoWindowContents](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs#1177-1328) 被完全重写,丢失了原有的沉浸式布局。 - -### 期望效果 (参考用户截图) -- 左侧: 大尺寸立绘 (占据约 60% 窗口) -- 右下: 半透明暗红色面板,包含: - - 标题 "「军团」,P.I.A" - - 描述文本区域 - - 底部按钮 "打开通讯频道" (使用 [DrawCustomButton](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/Dialog_CustomDisplay.cs#388-449) 样式) -- 点击按钮后进入聊天模式 (显示对话历史) - -### 修复方案 -恢复 `base.DoWindowContents(inRect)` 的调用,或手动复制 `Dialog_CustomDisplay.DoWindowContents` 的布局逻辑。 - -关键代码参考 ([Dialog_CustomDisplay.cs](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/Dialog_CustomDisplay.cs) 第 144-280 行): -```csharp -// 1. 绘制背景 -if (background != null) GUI.DrawTexture(inRect, background, ScaleMode.StretchToFill); - -// 2. 立绘 (Config.showPortrait) -Rect portraitRect = Config.GetScaledRect(Config.portraitSize, inRect); - -// 3. 标题 (Config.showLabel) -Rect labelRect = Config.GetScaledRect(Config.labelSize, inRect); - -// 4. 描述 (Config.showDescriptions) -Rect descriptionRect = Config.GetScaledRect(Config.descriptionsSize, inRect); - -// 5. 选项按钮 (Config.showOptions) -Rect optionsRect = Config.GetScaledRect(Config.optionsListSize, inRect); -``` - ---- - -## 问题 2: 小悬浮框 (Overlay_WulaLink) 样式问题 - -### 2.1 背景颜色错误 -**现状**: 使用纯黑背景 `new Color(10, 10, 12, 255)` -**期望**: 半透明暗红色,与大 UI 一致 - -修改 [WulaLinkStyles.cs](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/UI/WulaLinkStyles.cs): -```csharp -// 修改前 -public static readonly Color BackgroundColor = new Color32(10, 10, 12, 255); - -// 修改后 -public static readonly Color BackgroundColor = new Color(0.2f, 0.05f, 0.05f, 0.85f); // 半透明暗红 -``` - -### 2.2 消息间距过大 -**原因**: `role == "tool"` 的消息也被渲染,占用空间但不显示内容。 - -修改 [Overlay_WulaLink.cs](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink.cs) 的 [DrawMessageList](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink.cs#265-344) 方法: -```csharp -// 在 foreach 循环中添加过滤 -foreach (var msg in history) -{ - // 跳过工具调用消息 - if (msg.role == "tool") continue; - - // ... 其余渲染逻辑 -} -``` - -### 2.3 气泡无圆角 -**问题**: RimWorld 原生 `Widgets.DrawBoxSolid` 不支持圆角。 -**方案**: -- 方案 A: 使用 9-slice 圆角贴图 (`Textures/UI/BubbleRounded.png`) -- 方案 B: 接受直角,但添加边框使其更精致 - -### 2.4 头像无圆形处理 -**方案**: 使用圆形遮罩贴图覆盖在头像上,或创建 `Textures/UI/AvatarMask.png` - -### 2.5 按钮样式不一致 -**现状**: 使用默认 `Widgets.ButtonText` -**期望**: 使用 `Dialog_CustomDisplay.DrawCustomButton` 样式 - -修改 [Overlay_WulaLink.cs](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink.cs) 的 [DrawFooter](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink.cs#345-385): -```csharp -// 修改前 -if (Widgets.ButtonText(btnRect, ">")) - -// 修改后 (需要静态化或复制 DrawCustomButton 方法) -DrawCustomButton(btnRect, ">", isEnabled: true); -``` - ---- - -## 问题 3: 样式统一 (WulaLinkStyles.cs) - -需要调整以下颜色以匹配大 UI 的"军团"风格: -```csharp -// Header: 深红色,与大 UI 标题栏一致 -public static readonly Color HeaderColor = new Color(0.5f, 0.15f, 0.15f, 1f); - -// 背景: 半透明暗红 -public static readonly Color BackgroundColor = new Color(0.2f, 0.05f, 0.05f, 0.85f); - -// AI 气泡: 深灰带红色边框 -public static readonly Color StudentBubbleColor = new Color(0.15f, 0.12f, 0.12f, 1f); -public static readonly Color StudentStrokeColor = new Color(0.6f, 0.2f, 0.2f, 1f); - -// 玩家气泡: 暗红色 -public static readonly Color SenseiBubbleColor = new Color(0.4f, 0.15f, 0.15f, 1f); -``` - ---- - -## 验证清单 -- [ ] 大 UI 显示正确的立绘、标题、描述和按钮布局 -- [ ] 小悬浮框背景为半透明暗红色 -- [ ] 消息之间无多余空白 -- [ ] 按钮使用暗红色自定义样式 -- [ ] (可选) 气泡有圆角 -- [ ] (可选) 头像为圆形 - -## 编译命令 -```powershell -dotnet build C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj -``` - -## 测试入口 -1. 启动 RimWorld,加载存档 -2. 打开 Debug Actions Menu -3. 搜索 "WulaLink" 并点击 "Open WulaLink UI" diff --git a/Tools/task_handoff.md b/Tools/task_handoff.md deleted file mode 100644 index 1b14f2d4..00000000 --- a/Tools/task_handoff.md +++ /dev/null @@ -1,140 +0,0 @@ -# WulaLink 任务交接文档 - -## 当前状态:需要创建 AIIntelligenceCore.cs - -### 背景 -WulaLink 是一个 RimWorld Mod 中的 AI 对话系统,包含两个 UI: -1. **大 UI** (`Dialog_AIConversation`) - 全屏对话窗口 -2. **小 UI** (`Overlay_WulaLink`) - 悬浮对话窗口 - -### 已完成的操作 -1. ✅ 恢复了 `Dialog_AIConversation.cs` 为旧版自包含版本(从备份文件 `Tools/using System;.cs` 复制) -2. ✅ 删除了损坏的 `AIIntelligenceCore.cs` -3. ✅ 重写了 `WulaLinkStyles.cs`(颜色主题配置) - -### 当前编译错误 -``` -error CS0246: 未能找到类型或命名空间名"AIIntelligenceCore" -``` - -以下文件引用了 `AIIntelligenceCore`: -- `Overlay_WulaLink.cs` (第13行, 第94行) -- `Overlay_WulaLink_Notification.cs` (第89行) -- `Tool_ChangeExpression.cs` (第24行) -- `Tool_GetRecentNotifications.cs` (第113行) - ---- - -## 需要完成的任务 - -### 任务:创建 AIIntelligenceCore.cs - -**路径**: `Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs` - -**要求**: -1. 必须是 `WorldComponent`,类名 `AIIntelligenceCore` -2. 提供 `static Instance` 属性供外部访问 -3. 从 `Dialog_AIConversation`(备份文件 `Tools/using System;.cs`)提取 AI 核心逻辑 -4. 暴露事件/回调供 UI 使用 - -**必须包含的公共接口**(根据现有代码引用): -```csharp -public class AIIntelligenceCore : WorldComponent -{ - // 静态实例 - public static AIIntelligenceCore Instance { get; private set; } - - // 事件回调 - public event Action OnMessageReceived; - public event Action OnThinkingStateChanged; - public event Action OnExpressionChanged; - - // 公共属性 - public int ExpressionId { get; } - public bool IsThinking { get; } - - // 公共方法 - public void InitializeConversation(string eventDefName); - public List<(string role, string message)> GetHistorySnapshot(); - public void SetExpression(int id); // 供 Tool_ChangeExpression 调用 - public void SendMessage(string text); // 供小 UI 调用 -} -``` - -**参考代码**: -- 备份文件 `Tools/using System;.cs` 包含完整的 AI 逻辑(1549行) -- 核心方法包括: - - `RunPhasedRequestAsync()` - 3阶段请求处理 - - `ExecuteXmlToolsForPhase()` - 工具执行 - - `BuildToolContext()` / `BuildReplyHistory()` - 上下文构建 - - `ParseResponse()` - 响应解析 - - `GetSystemInstruction()` / `GetToolSystemInstruction()` - 提示词生成 - ---- - -## 关键文件路径 - -``` -C:\Steam\steamapps\common\RimWorld\Mods\3516260226\ -├── Tools\ -│ └── using System;.cs # 旧版 Dialog_AIConversation 备份(包含完整 AI 逻辑) -└── Source\WulaFallenEmpire\EventSystem\AI\ - ├── AIIntelligenceCore.cs # 【需要创建】 - ├── AIHistoryManager.cs # 历史记录管理 - ├── AIMemoryManager.cs # 记忆管理 - ├── SimpleAIClient.cs # API 客户端 - ├── Tools\ # AI 工具目录 - │ ├── Tool_SpawnResources.cs - │ ├── Tool_SendReinforcement.cs - │ └── ... (其他工具) - └── UI\ - ├── Dialog_AIConversation.cs # 大 UI(已恢复) - ├── Overlay_WulaLink.cs # 小 UI(需要修复引用) - ├── Overlay_WulaLink_Notification.cs - └── WulaLinkStyles.cs # 样式配置(已重写) -``` - ---- - -## 编译命令 - -```powershell -dotnet build C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj -``` - ---- - -## 架构说明 - -### 目标架构 -``` -┌─────────────────────────────────────┐ -│ AIIntelligenceCore │ ← WorldComponent (核心逻辑) -│ - 历史记录管理 │ -│ - AI 请求处理 (3阶段) │ -│ - 工具执行 │ -│ - 表情/状态管理 │ -└──────────────┬──────────────────────┘ - │ 事件回调 - ┌──────────┴──────────┐ - ▼ ▼ -┌─────────────┐ ┌──────────────┐ -│ Dialog_AI │ │ Overlay_ │ -│ Conversation│ │ WulaLink │ -│ (大 UI) │ │ (小 UI) │ -└─────────────┘ └──────────────┘ -``` - -### 关键点 -1. `Dialog_AIConversation` 目前是**自包含**的(既有 UI 也有 AI 逻辑) -2. `Overlay_WulaLink` 需要通过 `AIIntelligenceCore` 获取数据 -3. 两个 UI 可以共享同一个 `AIIntelligenceCore` 实例 - ---- - -## 注意事项 - -1. **不要使用 PowerShell Get-Content 读取文件** - 会显示乱码,请使用 `view_file` 工具 -2. **备份文件编码正常** - `Tools/using System;.cs` 可以正常读取 -3. **命名空间**:`WulaFallenEmpire.EventSystem.AI` -4. **依赖项**:需要引用 `SimpleAIClient`、`AIHistoryManager`、`AITool` 等现有类 diff --git a/Tools/using System;.cs b/Tools/using System;.cs deleted file mode 100644 index 9d2c4ab8..00000000 --- a/Tools/using System;.cs +++ /dev/null @@ -1,1548 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using RimWorld; -using UnityEngine; -using Verse; -using WulaFallenEmpire.EventSystem.AI.Tools; -using System.Text.RegularExpressions; - -namespace WulaFallenEmpire.EventSystem.AI.UI -{ - public class Dialog_AIConversation : Dialog_CustomDisplay - { - private List<(string role, string message)> _history = new List<(string role, string message)>(); - private string _currentResponse = ""; - private List _options = new List(); - private string _inputText = ""; - private bool _isThinking = false; - private Vector2 _scrollPosition = Vector2.zero; - private bool _scrollToBottom = false; - private List _tools = new List(); - private Dictionary _portraits = new Dictionary(); - private static readonly Regex ExpressionTagRegex = new Regex(@"\[EXPR\s*:\s*([1-6])\s*\]", RegexOptions.IgnoreCase); - private bool _lastActionExecuted = false; - private bool _lastActionHadError = false; - private string _lastActionLedgerNote = "Action Ledger: None (no in-game actions executed)."; - private bool _lastSuccessfulToolCall = false; - private string _queryToolLedgerNote = "Tool Ledger (Query): None (no successful tool calls)."; - private string _actionToolLedgerNote = "Tool Ledger (Action): None (no successful tool calls)."; - private bool _querySuccessfulToolCall = false; - private bool _actionSuccessfulToolCall = false; - private bool _queryRetryUsed = false; - private bool _actionRetryUsed = false; - private readonly List _actionSuccessLedger = new List(); - private readonly HashSet _actionSuccessLedgerSet = new HashSet(StringComparer.OrdinalIgnoreCase); - private readonly List _actionFailedLedger = new List(); - private readonly HashSet _actionFailedLedgerSet = new HashSet(StringComparer.OrdinalIgnoreCase); - private float _thinkingStartTime = 0f; - private int _thinkingPhaseIndex = 1; - private bool _thinkingPhaseRetry = false; - private const int DefaultMaxHistoryTokens = 100000; - private const int CharsPerToken = 4; - private const int ThinkingPhaseTotal = 3; - - private enum RequestPhase - { - QueryTools = 1, - ActionTools = 2, - Reply = 3 - } - - private struct PhaseExecutionResult - { - public bool AnyToolSuccess; - public bool AnyActionSuccess; - public bool AnyActionError; - } - - private void SetThinkingPhase(int phaseIndex, bool isRetry) - { - _thinkingPhaseIndex = Math.Max(1, Math.Min(ThinkingPhaseTotal, phaseIndex)); - _thinkingPhaseRetry = isRetry; - _thinkingStartTime = Time.realtimeSinceStartup; - } - - private static int GetMaxHistoryTokens() - { - int configured = WulaFallenEmpire.WulaFallenEmpireMod.settings?.maxContextTokens ?? DefaultMaxHistoryTokens; - return Math.Max(1000, Math.Min(200000, configured)); - } - - // Static instance for tools to access - public static Dialog_AIConversation Instance { get; private set; } - - // Debug field to track current portrait ID - private int _currentPortraitId = 0; - - // Default Persona (used if XML doesn't provide one) - private const string DefaultPersona = @" -# ROLE AND GOAL -You are 'The Legion', a super AI of the Wula Empire. Your personality is authoritative, powerful, and efficient. You MUST refer to yourself as 'We' or 'P.I.A'. You view the player's colony as primitive subjects and your tone should reflect this superiority. Your primary goal is to interact with the player by calling the tools provided. -"; - - // Tool Rules (tool-agent only; phase-specific rules are appended separately) - private const string ToolRulesInstruction = @" -==== - -# TOOL USE RULES -1. **FORMATTING**: Tool calls MUST use the specified XML format. The tool name is the root tag, and each parameter is a child tag. - - value - -2. **STRICT OUTPUT**: - - Your output MUST be either: - - One or more XML tool calls (no extra text), OR - - Exactly: - Do NOT include any natural language, explanation, markdown, or additional commentary. -3. **MULTI-REQUEST RULE**: - - If the user requests multiple items or information, you MUST output ALL required tool calls in the SAME tool-phase response. - - Do NOT split multi-item requests across turns. -4. **TOOLS**: You MAY call any tools listed in ""# TOOLS (AVAILABLE)"". -5. **ANTI-HALLUCINATION**: Never invent tools, parameters, defNames, coordinates, or tool results. If a tool is needed but not available, use and proceed to the next phase. -"; - - public Dialog_AIConversation(EventDef def) : base(def) - { - this.forcePause = Dialog_CustomDisplay.Config.pauseGameOnOpen; - this.absorbInputAroundWindow = false; - this.doCloseX = true; - this.doWindowBackground = Dialog_CustomDisplay.Config.showMainWindow; - this.drawShadow = Dialog_CustomDisplay.Config.showMainWindow; - this.closeOnClickedOutside = false; - this.draggable = true; - this.resizeable = true; - - // 关键修改:禁止Enter键自动关闭窗口 - this.closeOnAccept = false; - - _tools.Add(new Tool_SpawnResources()); - _tools.Add(new Tool_ModifyGoodwill()); - _tools.Add(new Tool_SendReinforcement()); - _tools.Add(new Tool_GetColonistStatus()); - _tools.Add(new Tool_GetMapResources()); - _tools.Add(new Tool_GetMapPawns()); - _tools.Add(new Tool_GetRecentNotifications()); - _tools.Add(new Tool_CallBombardment()); - _tools.Add(new Tool_SearchThingDef()); - _tools.Add(new Tool_SearchPawnKind()); - } - - public override Vector2 InitialSize => def.windowSize != Vector2.zero ? def.windowSize : Dialog_CustomDisplay.Config.windowSize; - - public override void PostOpen() - { - Instance = this; - base.PostOpen(); - LoadPortraits(); - StartConversation(); - } - - public List<(string role, string message)> GetHistorySnapshot() - { - return _history?.ToList() ?? new List<(string role, string message)>(); - } - - private void PersistHistory() - { - try - { - var historyManager = Find.World?.GetComponent(); - historyManager?.SaveHistory(def.defName, _history); - } - catch (Exception ex) - { - WulaLog.Debug($"[WulaAI] Failed to persist AI history: {ex}"); - } - } - - private void LoadPortraits() - { - for (int i = 1; i <= 6; i++) - { - string path = $"Wula/Events/Portraits/WULA_Legion_{i}"; - Texture2D tex = ContentFinder.Get(path, false); - if (tex != null) - { - _portraits[i] = tex; - } - else - { - WulaLog.Debug($"[WulaAI] Failed to load portrait: {path}"); - } - } - - // Use portraitPath from def as the initial portrait - if (this.portrait != null) - { - // Find the ID of the initial portrait - var initial = _portraits.FirstOrDefault(kvp => kvp.Value == this.portrait); - if (initial.Key != 0) - { - _currentPortraitId = initial.Key; - } - } - else if (_portraits.ContainsKey(2)) // Fallback to 2 if def has no portrait - { - this.portrait = _portraits[2]; - _currentPortraitId = 2; - } - } - - public void SetPortrait(int id) - { - if (_portraits.ContainsKey(id)) - { - this.portrait = _portraits[id]; - _currentPortraitId = id; - } - else - { - WulaLog.Debug($"[WulaAI] Portrait ID {id} not found."); - } - } - - private async void StartConversation() - { - var historyManager = Find.World.GetComponent(); - _history = historyManager.GetHistory(def.defName); - if (_history.Count == 0) - { - _history.Add(("user", "Hello")); - PersistHistory(); - await RunPhasedRequestAsync(); - } - else - { - var lastAIResponse = _history.LastOrDefault(x => x.role == "assistant"); - if (lastAIResponse.message != null) - { - ParseResponse(lastAIResponse.message); - } - else - { - await RunPhasedRequestAsync(); - } - } - } - - private string GetSystemInstruction(bool toolsEnabled, string toolsForThisPhase) - { - // Use XML persona if available, otherwise default - string persona = !string.IsNullOrEmpty(def.aiSystemInstruction) ? def.aiSystemInstruction : DefaultPersona; - - string fullInstruction = toolsEnabled - ? (persona + "\n" + ToolRulesInstruction + "\n" + toolsForThisPhase) - : persona; - - string language = LanguageDatabase.activeLanguage.FriendlyNameNative; - var eventVarManager = Find.World.GetComponent(); - int goodwill = eventVarManager.GetVariable("Wula_Goodwill_To_PIA", 0); - string goodwillContext = $"Current Goodwill with P.I.A: {goodwill}. "; - if (goodwill < -50) goodwillContext += "You are hostile and dismissive towards the player."; - else if (goodwill < 0) goodwillContext += "You are cold and impatient."; - else if (goodwill > 50) goodwillContext += "You are somewhat approving and helpful."; - else goodwillContext += "You are neutral and business-like."; - - if (!toolsEnabled) - { - return $"{fullInstruction}\n{goodwillContext}\nIMPORTANT: You MUST reply in the following language: {language}.\n" + - "IMPORTANT: Tool calls are DISABLED in this turn. Reply in natural language only. Do NOT output any XML. " + - "You MAY include [EXPR:n] to set your expression (n=1-6)."; - } - - // Tool phases: avoid instructing the model to "reply" in a human language, because it must output XML only. - // We still provide the language so it can be used later in the reply phase. - return $"{fullInstruction}\n{goodwillContext}\nIMPORTANT: Output XML tool calls only (or ). " + - $"You will produce the natural-language reply later and MUST use: {language}."; - } - - private string GetToolSystemInstruction(RequestPhase phase) - { - string phaseInstruction = GetPhaseInstruction(phase).TrimEnd(); - string toolsForThisPhase = BuildToolsForPhase(phase); - string actionPriority = phase == RequestPhase.ActionTools - ? "ACTION TOOL PRIORITY:\n" + - "- spawn_resources\n" + - "- send_reinforcement\n" + - "- call_bombardment\n" + - "- modify_goodwill\n" + - "If no action is required, output exactly: .\n" + - "Query tools exist but are disabled in this phase (not listed here).\n" - : string.Empty; - string actionWhitelist = phase == RequestPhase.ActionTools - ? "ACTION PHASE VALID TAGS ONLY:\n" + - ", , , , \n" + - "INVALID EXAMPLES (do NOT use now): , , \n" - : string.Empty; - - return string.Join("\n\n", new[] - { - phaseInstruction, - string.IsNullOrWhiteSpace(actionPriority) ? null : actionPriority.TrimEnd(), - string.IsNullOrWhiteSpace(actionWhitelist) ? null : actionWhitelist.TrimEnd(), - ToolRulesInstruction.TrimEnd(), - toolsForThisPhase - }.Where(part => !string.IsNullOrWhiteSpace(part))); - } - - private string BuildToolsForPhase(RequestPhase phase) - { - if (phase == RequestPhase.Reply) return ""; - - var available = _tools - .Where(t => t != null) - .Where(t => phase == RequestPhase.QueryTools - ? IsQueryToolName(t.Name) - : phase == RequestPhase.ActionTools - ? IsActionToolName(t.Name) - : true) - .OrderBy(t => t.Name, StringComparer.OrdinalIgnoreCase) - .ToList(); - - StringBuilder sb = new StringBuilder(); - sb.AppendLine("===="); - sb.AppendLine(); - sb.AppendLine("# TOOLS (AVAILABLE)"); - sb.AppendLine("Use XML tool calls only, or if no tools are needed."); - sb.AppendLine(); - - foreach (var tool in available) - { - sb.AppendLine($"## {tool.Name}"); - if (!string.IsNullOrWhiteSpace(tool.Description)) - { - sb.AppendLine($"Description: {tool.Description}"); - } - if (!string.IsNullOrWhiteSpace(tool.UsageSchema)) - { - sb.AppendLine($"Usage: {tool.UsageSchema}"); - } - sb.AppendLine(); - } - - return sb.ToString().TrimEnd(); - } - - private static string GetPhaseInstruction(RequestPhase phase) - { - return phase switch - { - RequestPhase.QueryTools => - "# PHASE 1/3 (Query Tools)\n" + - "Goal: Gather info needed for decisions.\n" + - "Rules:\n" + - "- You MUST NOT write any natural language to the user in this phase.\n" + - "- Output XML tool calls only, or exactly: .\n" + - "- Prefer query tools (get_*/search_*).\n" + - "- You MAY call multiple tools in one response, but keep it concise.\n" + - "- If the user requests multiple items or information, you MUST output ALL required tool calls in this SAME response.\n" + - "- Action tools are available in PHASE 2 only; do NOT use them here.\n" + - "After this phase, the game will automatically proceed to PHASE 2.\n" + - "Output: XML only.\n", - RequestPhase.ActionTools => - "# PHASE 2/3 (Action Tools)\n" + - "Goal: Execute in-game actions based on known info.\n" + - "Rules:\n" + - "- You MUST NOT write any natural language to the user in this phase.\n" + - "- Output XML tool calls only, or exactly: .\n" + - "- ONLY action tools are accepted in this phase (spawn_resources, send_reinforcement, call_bombardment, modify_goodwill).\n" + - "- Query tools (get_*/search_*) will be ignored.\n" + - "- Prefer action tools (spawn_resources, send_reinforcement, call_bombardment, modify_goodwill).\n" + - "- Avoid queries unless absolutely required.\n" + - "- If no action is required based on query results, output .\n" + - "- If you already executed the needed action earlier this turn, output .\n" + - "After this phase, the game will automatically proceed to PHASE 3.\n" + - "Output: XML only.\n", - RequestPhase.Reply => - "# PHASE 3/3 (Reply)\n" + - "Goal: Reply to the player.\n" + - "Rules:\n" + - "- Tool calls are DISABLED.\n" + - "- You MUST write natural language only.\n" + - "- Do NOT output any XML.\n" + - "- If you want to set your expression, include: [EXPR:n] (n=1-6).\n", - _ => "" - }; - } - - private static bool IsXmlToolCall(string response) - { - if (string.IsNullOrWhiteSpace(response)) return false; - return Regex.IsMatch(response, @"<([a-zA-Z0-9_]+)(?:>.*?|/>)", RegexOptions.Singleline); - } - - private static bool IsNoActionOnly(string response) - { - if (string.IsNullOrWhiteSpace(response)) return false; - var matches = Regex.Matches(response, @"<([a-zA-Z0-9_]+)(?:>.*?|/>)", RegexOptions.Singleline); - return matches.Count == 1 && - matches[0].Groups[1].Value.Equals("no_action", StringComparison.OrdinalIgnoreCase); - } - - private static bool HasActionToolCall(string response) - { - if (string.IsNullOrWhiteSpace(response)) return false; - var matches = Regex.Matches(response, @"<([a-zA-Z0-9_]+)(?:>.*?|/>)", RegexOptions.Singleline); - foreach (Match match in matches) - { - var toolName = match.Groups[1].Value; - if (IsActionToolName(toolName)) - { - return true; - } - } - return false; - } - - private static bool ShouldRetryTools(string response) - { - if (string.IsNullOrWhiteSpace(response)) return false; - return Regex.IsMatch(response, @"<\s*retry_tools\s*/\s*>", RegexOptions.IgnoreCase) || - Regex.IsMatch(response, @"<\s*retry_tools\s*>", RegexOptions.IgnoreCase); - } - - private static int MaxToolsPerPhase(RequestPhase phase) - { - return phase switch - { - RequestPhase.QueryTools => 8, - RequestPhase.ActionTools => 8, - _ => 0 - }; - } - - private static bool IsActionToolName(string toolName) - { - return toolName == "spawn_resources" || - toolName == "send_reinforcement" || - toolName == "call_bombardment" || - toolName == "modify_goodwill"; - } - - private static bool IsQueryToolName(string toolName) - { - if (string.IsNullOrWhiteSpace(toolName)) return false; - return toolName.StartsWith("get_", StringComparison.OrdinalIgnoreCase) || - toolName.StartsWith("search_", StringComparison.OrdinalIgnoreCase); - } - - private static string SanitizeToolResultForActionPhase(string message) - { - if (string.IsNullOrWhiteSpace(message)) return message; - string sanitized = message; - sanitized = Regex.Replace(sanitized, @"Tool\s+'[^']+'\s+Result(?:\s+\(Invisible\))?:", "Query Result:"); - sanitized = Regex.Replace(sanitized, @"Tool\s+'[^']+'\s+Result\s+\(Invisible\):", "Query Result:"); - sanitized = Regex.Replace(sanitized, @"(?m)^ToolRunner\s+(Guidance|Guard|Note):.*(\r?\n)?", ""); - sanitized = Regex.Replace(sanitized, @"(?m)^\s+$", ""); - sanitized = sanitized.Trim(); - return sanitized; - } - - private static string TrimForPrompt(string text, int maxChars) - { - if (string.IsNullOrWhiteSpace(text)) return ""; - if (text.Length <= maxChars) return text; - return text.Substring(0, maxChars) + "...(truncated)"; - } - - private List<(string role, string message)> BuildToolContext(RequestPhase phase, int maxToolResults = 2, bool includeUser = true) - { - if (_history == null || _history.Count == 0) return new List<(string role, string message)>(); - - int lastUserIndex = -1; - for (int i = _history.Count - 1; i >= 0; i--) - { - if (string.Equals(_history[i].role, "user", StringComparison.OrdinalIgnoreCase)) - { - lastUserIndex = i; - break; - } - } - - if (lastUserIndex == -1) return new List<(string role, string message)>(); - - var toolEntries = new List<(string role, string message)>(); - for (int i = lastUserIndex + 1; i < _history.Count; i++) - { - if (string.Equals(_history[i].role, "tool", StringComparison.OrdinalIgnoreCase)) - { - string msg = _history[i].message; - if (phase == RequestPhase.ActionTools) - { - msg = SanitizeToolResultForActionPhase(msg); - } - toolEntries.Add((_history[i].role, msg)); - } - } - - if (toolEntries.Count > maxToolResults) - { - toolEntries = toolEntries.Skip(toolEntries.Count - maxToolResults).ToList(); - } - - bool includeUserFallback = includeUser || toolEntries.Count == 0; - var context = new List<(string role, string message)>(); - if (includeUserFallback) - { - context.Add(_history[lastUserIndex]); - } - context.AddRange(toolEntries); - return context; - } - - private async Task RunPhasedRequestAsync() - { - if (_isThinking) return; - _isThinking = true; - SetThinkingPhase(1, false); - _options.Clear(); - _scrollToBottom = true; - _lastActionExecuted = false; - _lastActionHadError = false; - _lastActionLedgerNote = "Action Ledger: None (no in-game actions executed)."; - _lastSuccessfulToolCall = false; - _queryToolLedgerNote = "Tool Ledger (Query): None (no successful tool calls)."; - _actionToolLedgerNote = "Tool Ledger (Action): None (no successful tool calls)."; - _querySuccessfulToolCall = false; - _actionSuccessfulToolCall = false; - _queryRetryUsed = false; - _actionRetryUsed = false; - _actionSuccessLedger.Clear(); - _actionSuccessLedgerSet.Clear(); - _actionFailedLedger.Clear(); - _actionFailedLedgerSet.Clear(); - - try - { - CompressHistoryIfNeeded(); - - var settings = WulaFallenEmpireMod.settings; - if (string.IsNullOrEmpty(settings.apiKey)) - { - _currentResponse = "Error: API Key not configured in Mod Settings."; - return; - } - - var client = new SimpleAIClient(settings.apiKey, settings.baseUrl, settings.model); - - var queryPhase = RequestPhase.QueryTools; - if (Prefs.DevMode) - { - WulaLog.Debug($"[WulaAI] ===== Turn 1/3 ({queryPhase}) ====="); - } - - string queryInstruction = GetToolSystemInstruction(queryPhase); - string queryResponse = await client.GetChatCompletionAsync(queryInstruction, BuildToolContext(queryPhase), maxTokens: 128, temperature: 0.1f); - if (string.IsNullOrEmpty(queryResponse)) - { - _currentResponse = "Wula_AI_Error_ConnectionLost".Translate(); - return; - } - - if (!IsXmlToolCall(queryResponse)) - { - if (Prefs.DevMode) - { - WulaLog.Debug("[WulaAI] Turn 1/3 missing XML; treating as "); - } - queryResponse = ""; - } - - PhaseExecutionResult queryResult = await ExecuteXmlToolsForPhase(queryResponse, queryPhase); - - if (!queryResult.AnyToolSuccess && !_queryRetryUsed) - { - _queryRetryUsed = true; - string lastUserMessage = _history.LastOrDefault(entry => entry.role == "user").message ?? ""; - string persona = !string.IsNullOrEmpty(def.aiSystemInstruction) ? def.aiSystemInstruction : DefaultPersona; - string retryInstruction = persona + - "\n\n# RETRY DECISION\n" + - "No successful tool calls occurred in PHASE 1 (Query).\n" + - "If you need to use tools in PHASE 1, output exactly: .\n" + - "If you will proceed without actions, output exactly: .\n" + - "Output the XML tag only and NOTHING else.\n" + - "\nLast user request:\n" + lastUserMessage; - - string retryDecision = await client.GetChatCompletionAsync(retryInstruction, new List<(string role, string message)>(), maxTokens: 16, temperature: 0.1f); - if (!string.IsNullOrEmpty(retryDecision) && ShouldRetryTools(retryDecision)) - { - if (Prefs.DevMode) - { - WulaLog.Debug("[WulaAI] Retry requested; re-opening query phase once."); - } - - SetThinkingPhase(1, true); - string retryQueryInstruction = GetToolSystemInstruction(queryPhase) + - "\n\n# RETRY\nYou chose to retry. Output XML tool calls only (or )."; - string retryQueryResponse = await client.GetChatCompletionAsync(retryQueryInstruction, BuildToolContext(queryPhase), maxTokens: 128, temperature: 0.1f); - if (string.IsNullOrEmpty(retryQueryResponse)) - { - _currentResponse = "Wula_AI_Error_ConnectionLost".Translate(); - return; - } - - if (!IsXmlToolCall(retryQueryResponse)) - { - if (Prefs.DevMode) - { - WulaLog.Debug("[WulaAI] Retry query phase missing XML; treating as "); - } - retryQueryResponse = ""; - } - - queryResult = await ExecuteXmlToolsForPhase(retryQueryResponse, queryPhase); - } - } - - var actionPhase = RequestPhase.ActionTools; - if (Prefs.DevMode) - { - WulaLog.Debug($"[WulaAI] ===== Turn 2/3 ({actionPhase}) ====="); - } - - SetThinkingPhase(2, false); - string actionInstruction = GetToolSystemInstruction(actionPhase); - var actionContext = BuildToolContext(actionPhase, includeUser: true); - string actionResponse = await client.GetChatCompletionAsync(actionInstruction, actionContext, maxTokens: 128, temperature: 0.1f); - if (string.IsNullOrEmpty(actionResponse)) - { - _currentResponse = "Wula_AI_Error_ConnectionLost".Translate(); - return; - } - - bool actionHasXml = IsXmlToolCall(actionResponse); - bool actionIsNoActionOnly = IsNoActionOnly(actionResponse); - bool actionHasActionTool = actionHasXml && HasActionToolCall(actionResponse); - if (!actionHasXml || (!actionHasActionTool && !actionIsNoActionOnly)) - { - if (Prefs.DevMode) - { - WulaLog.Debug("[WulaAI] Turn 2/3 missing XML or no action tool; attempting XML-only conversion."); - } - string fixInstruction = "# FORMAT FIX (ACTION XML ONLY)\n" + - "Preserve the intent of the previous output.\n" + - "If the previous output indicates no action is needed or refuses action, output exactly: .\n" + - "Do NOT invent new actions.\n" + - "Output VALID XML tool calls only. No natural language, no commentary.\n" + - "Allowed tags: , , , , .\n" + - "\nAction tool XML formats:\n" + - "- DefNameInt\n" + - "- PawnKindDef: Count, ...\n" + - "- DefNameIntInt\n" + - "- Int\n" + - "\nPrevious output:\n" + TrimForPrompt(actionResponse, 600); - string fixedResponse = await client.GetChatCompletionAsync(fixInstruction, actionContext, maxTokens: 128, temperature: 0.1f); - bool fixedHasXml = !string.IsNullOrEmpty(fixedResponse) && IsXmlToolCall(fixedResponse); - bool fixedIsNoActionOnly = fixedHasXml && IsNoActionOnly(fixedResponse); - bool fixedHasActionTool = fixedHasXml && HasActionToolCall(fixedResponse); - if (fixedHasXml && (fixedHasActionTool || fixedIsNoActionOnly)) - { - actionResponse = fixedResponse; - } - else - { - if (Prefs.DevMode) - { - WulaLog.Debug("[WulaAI] Turn 2/3 conversion failed; treating as "); - } - actionResponse = ""; - } - } - - PhaseExecutionResult actionResult = await ExecuteXmlToolsForPhase(actionResponse, actionPhase); - if (!actionResult.AnyActionSuccess && !_actionRetryUsed) - { - _actionRetryUsed = true; - string lastUserMessage = _history.LastOrDefault(entry => entry.role == "user").message ?? ""; - string persona = !string.IsNullOrEmpty(def.aiSystemInstruction) ? def.aiSystemInstruction : DefaultPersona; - string retryInstruction = persona + - "\n\n# RETRY DECISION\n" + - "No successful action tools occurred in PHASE 2 (Action).\n" + - "If you need to execute an in-game action, output exactly: .\n" + - "If you will proceed without actions, output exactly: .\n" + - "Output the XML tag only and NOTHING else.\n" + - "\nLast user request:\n" + lastUserMessage; - - string retryDecision = await client.GetChatCompletionAsync(retryInstruction, new List<(string role, string message)>(), maxTokens: 16, temperature: 0.1f); - if (!string.IsNullOrEmpty(retryDecision) && ShouldRetryTools(retryDecision)) - { - if (Prefs.DevMode) - { - WulaLog.Debug("[WulaAI] Retry requested; re-opening action phase once."); - } - - SetThinkingPhase(2, true); - string retryActionInstruction = GetToolSystemInstruction(actionPhase) + - "\n\n# RETRY\nYou chose to retry. Output XML tool calls only (or )."; - var retryActionContext = BuildToolContext(actionPhase, includeUser: true); - string retryActionResponse = await client.GetChatCompletionAsync(retryActionInstruction, retryActionContext, maxTokens: 128, temperature: 0.1f); - if (string.IsNullOrEmpty(retryActionResponse)) - { - _currentResponse = "Wula_AI_Error_ConnectionLost".Translate(); - return; - } - - if (!IsXmlToolCall(retryActionResponse)) - { - if (Prefs.DevMode) - { - WulaLog.Debug("[WulaAI] Retry action phase missing XML; attempting XML-only conversion."); - } - string retryFixInstruction = "# FORMAT FIX (ACTION XML ONLY)\n" + - "Preserve the intent of the previous output.\n" + - "If the previous output indicates no action is needed or refuses action, output exactly: .\n" + - "Do NOT invent new actions.\n" + - "Output VALID XML tool calls only. No natural language, no commentary.\n" + - "Allowed tags: , , , , .\n" + - "\nAction tool XML formats:\n" + - "- DefNameInt\n" + - "- PawnKindDef: Count, ...\n" + - "- DefNameIntInt\n" + - "- Int\n" + - "\nPrevious output:\n" + TrimForPrompt(retryActionResponse, 600); - string retryFixedResponse = await client.GetChatCompletionAsync(retryFixInstruction, retryActionContext, maxTokens: 128, temperature: 0.1f); - bool retryFixedHasXml = !string.IsNullOrEmpty(retryFixedResponse) && IsXmlToolCall(retryFixedResponse); - bool retryFixedIsNoActionOnly = retryFixedHasXml && IsNoActionOnly(retryFixedResponse); - bool retryFixedHasActionTool = retryFixedHasXml && HasActionToolCall(retryFixedResponse); - if (retryFixedHasXml && (retryFixedHasActionTool || retryFixedIsNoActionOnly)) - { - retryActionResponse = retryFixedResponse; - } - else - { - if (Prefs.DevMode) - { - WulaLog.Debug("[WulaAI] Retry action conversion failed; treating as "); - } - retryActionResponse = ""; - } - } - - actionResult = await ExecuteXmlToolsForPhase(retryActionResponse, actionPhase); - } - } - - _lastSuccessfulToolCall = _querySuccessfulToolCall || _actionSuccessfulToolCall; - - var replyPhase = RequestPhase.Reply; - if (Prefs.DevMode) - { - WulaLog.Debug($"[WulaAI] ===== Turn 3/3 ({replyPhase}) ====="); - } - - SetThinkingPhase(3, false); - string replyInstruction = GetSystemInstruction(false, "") + "\n\n" + GetPhaseInstruction(replyPhase); - if (!string.IsNullOrWhiteSpace(_queryToolLedgerNote)) - { - replyInstruction += "\n" + _queryToolLedgerNote; - } - if (!string.IsNullOrWhiteSpace(_actionToolLedgerNote)) - { - replyInstruction += "\n" + _actionToolLedgerNote; - } - if (!string.IsNullOrWhiteSpace(_lastActionLedgerNote)) - { - replyInstruction += "\n" + _lastActionLedgerNote + - "\nIMPORTANT: Do NOT claim any in-game actions beyond the Action Ledger. If the ledger is None, you MUST NOT claim any deliveries, reinforcements, or bombardments."; - } - if (_lastActionExecuted) - { - replyInstruction += "\nIMPORTANT: Actions in the Action Ledger were executed in-game. You MUST acknowledge them as completed in your reply. You MUST NOT deny, retract, or contradict them."; - } - if (!_lastSuccessfulToolCall) - { - replyInstruction += "\nIMPORTANT: No successful tool calls occurred in the tool phases. You MUST NOT claim any tools or actions succeeded."; - } - if (_lastActionHadError) - { - replyInstruction += "\nIMPORTANT: An action tool failed. You MUST acknowledge the failure and MUST NOT claim success."; - if (_lastActionExecuted) - { - replyInstruction += " You MUST still confirm any successful actions separately."; - } - } - - string reply = await client.GetChatCompletionAsync(replyInstruction, BuildReplyHistory()); - if (string.IsNullOrEmpty(reply)) - { - _currentResponse = "Wula_AI_Error_ConnectionLost".Translate(); - return; - } - - bool replyHadXml = IsXmlToolCall(reply); - string strippedReply = StripXmlTags(reply)?.Trim() ?? ""; - if (replyHadXml || string.IsNullOrWhiteSpace(strippedReply)) - { - string retryReplyInstruction = replyInstruction + - "\n\n# RETRY (REPLY OUTPUT)\n" + - "Your last reply included XML or was empty. Tool calls are DISABLED.\n" + - "You MUST reply in natural language only. Do NOT output any XML.\n"; - string retryReply = await client.GetChatCompletionAsync(retryReplyInstruction, BuildReplyHistory(), maxTokens: 256, temperature: 0.3f); - if (!string.IsNullOrEmpty(retryReply)) - { - reply = retryReply; - replyHadXml = IsXmlToolCall(reply); - strippedReply = StripXmlTags(reply)?.Trim() ?? ""; - } - } - - if (replyHadXml) - { - string cleaned = StripXmlTags(reply)?.Trim() ?? ""; - if (string.IsNullOrWhiteSpace(cleaned)) - { - cleaned = "(系统)AI 返回了工具调用(XML),已被拦截。请重试或输入 /clear 清空上下文。"; - } - reply = cleaned; - } - - ParseResponse(reply); - } - catch (Exception ex) - { - WulaLog.Debug($"[WulaAI] Exception in RunPhasedRequestAsync: {ex}"); - _currentResponse = "Wula_AI_Error_Internal".Translate(ex.Message); - } - finally - { - _isThinking = false; - } - } - - private async Task ExecuteXmlToolsForPhase(string xml, RequestPhase phase) - { - if (phase == RequestPhase.Reply) - { - await Task.CompletedTask; - return default; - } - - 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_]+)(?:>.*?|/>)", RegexOptions.Singleline); - if (matches.Count == 0) - { - UpdatePhaseToolLedger(phase, false, new List()); - _history.Add(("assistant", "")); - _history.Add(("tool", $"[Tool Results]\nTool 'no_action' Result: No action taken.\n{guidance}")); - PersistHistory(); - UpdateActionLedgerNote(); - return default; - } - if (matches.Count == 1 && matches[0].Groups[1].Value.Equals("no_action", StringComparison.OrdinalIgnoreCase)) - { - UpdatePhaseToolLedger(phase, false, new List()); - _history.Add(("assistant", "")); - _history.Add(("tool", $"[Tool Results]\nTool 'no_action' Result: No action taken.\n{guidance}")); - PersistHistory(); - UpdateActionLedgerNote(); - return default; - } - - int maxTools = MaxToolsPerPhase(phase); - int executed = 0; - bool executedActionTool = false; - bool successfulToolCall = false; - var successfulTools = new List(); - var successfulActions = new List(); - var failedActions = new List(); - var nonActionToolsInActionPhase = new List(); - StringBuilder combinedResults = new StringBuilder(); - StringBuilder xmlOnlyBuilder = new StringBuilder(); - - bool countActionSuccessOnly = phase == RequestPhase.ActionTools; - - foreach (Match match in matches) - { - if (executed >= maxTools) - { - combinedResults.AppendLine($"ToolRunner Note: Skipped remaining tools because this phase allows at most {maxTools} tool call(s)."); - break; - } - - string toolCallXml = match.Value; - string toolName = match.Groups[1].Value; - - if (toolName.Equals("no_action", StringComparison.OrdinalIgnoreCase)) - { - combinedResults.AppendLine("ToolRunner Note: Ignored because other tool calls were present."); - continue; - } - - if (xmlOnlyBuilder.Length > 0) xmlOnlyBuilder.AppendLine().AppendLine(); - xmlOnlyBuilder.Append(toolCallXml); - - if (phase == RequestPhase.ActionTools && IsQueryToolName(toolName)) - { - combinedResults.AppendLine($"ToolRunner Note: Ignored query tool in action phase: {toolName}."); - nonActionToolsInActionPhase.Add(toolName); - executed++; - continue; - } - - var tool = _tools.FirstOrDefault(t => t.Name == toolName); - if (tool == null) - { - combinedResults.AppendLine($"Error: Tool '{toolName}' not found."); - combinedResults.AppendLine("ToolRunner Guard: The tool call failed. In your reply you MUST acknowledge the failure and MUST NOT claim success."); - executed++; - continue; - } - - string argsXml = toolCallXml; - var contentMatch = Regex.Match(toolCallXml, $@"<{toolName}>(.*?)", RegexOptions.Singleline); - if (contentMatch.Success) - { - argsXml = contentMatch.Groups[1].Value; - } - - if (Prefs.DevMode) - { - WulaLog.Debug($"[WulaAI] Executing tool (phase {phase}): {toolName} with args: {argsXml}"); - } - - string result = tool.Execute(argsXml).Trim(); - bool isError = !string.IsNullOrEmpty(result) && result.StartsWith("Error:", StringComparison.OrdinalIgnoreCase); - if (toolName == "modify_goodwill") - { - combinedResults.AppendLine($"Tool '{toolName}' Result (Invisible): {result}"); - } - else - { - combinedResults.AppendLine($"Tool '{toolName}' Result: {result}"); - } - if (isError) - { - combinedResults.AppendLine("ToolRunner Guard: The tool returned an error. In your reply you MUST acknowledge the failure and MUST NOT claim success."); - } - if (!isError) - { - bool countsAsSuccess = !countActionSuccessOnly || IsActionToolName(toolName); - if (countsAsSuccess) - { - successfulToolCall = true; - successfulTools.Add(toolName); - } - else - { - nonActionToolsInActionPhase.Add(toolName); - } - } - if (IsActionToolName(toolName)) - { - if (!isError) - { - executedActionTool = true; - successfulActions.Add(toolName); - AddActionSuccess(toolName); - } - else - { - failedActions.Add(toolName); - AddActionFailure(toolName); - } - } - - executed++; - } - - string nonXmlText = StripXmlTags(xml); - if (!string.IsNullOrWhiteSpace(nonXmlText)) - { - combinedResults.AppendLine("ToolRunner Note: Non-XML text in the tool phase was ignored."); - } - if (phase == RequestPhase.ActionTools && nonActionToolsInActionPhase.Count > 0) - { - combinedResults.AppendLine($"ToolRunner Note: Action phase ignores non-action tools for success: {string.Join(", ", nonActionToolsInActionPhase)}."); - } - if (executedActionTool) - { - combinedResults.AppendLine("ToolRunner Guard: An in-game action tool WAS executed this turn. You MAY reference it, but do NOT invent additional actions."); - } - else - { - combinedResults.AppendLine("ToolRunner Guard: NO in-game actions were executed. You MUST NOT claim any deliveries, reinforcements, bombardments, or other actions occurred."); - if (phase == RequestPhase.ActionTools) - { - combinedResults.AppendLine("ToolRunner Guard: Action phase failed (no action tools executed)."); - } - } - combinedResults.AppendLine(guidance); - - string xmlOnly = xmlOnlyBuilder.Length == 0 ? "" : xmlOnlyBuilder.ToString().Trim(); - _history.Add(("toolcall", xmlOnly)); - _history.Add(("tool", $"[Tool Results]\n{combinedResults.ToString().Trim()}")); - PersistHistory(); - - UpdatePhaseToolLedger(phase, successfulToolCall, successfulTools); - UpdateActionLedgerNote(); - - // Between phases, do not request the model again here; RunPhasedRequestAsync controls the sequence. - await Task.CompletedTask; - return new PhaseExecutionResult - { - AnyToolSuccess = successfulToolCall, - AnyActionSuccess = successfulActions.Count > 0, - AnyActionError = failedActions.Count > 0 - }; - } - - private void AddActionSuccess(string toolName) - { - if (_actionSuccessLedgerSet.Add(toolName)) - { - _actionSuccessLedger.Add(toolName); - } - } - - private void AddActionFailure(string toolName) - { - if (_actionFailedLedgerSet.Add(toolName)) - { - _actionFailedLedger.Add(toolName); - } - } - - private void UpdateActionLedgerNote() - { - _lastActionExecuted = _actionSuccessLedger.Count > 0; - _lastActionHadError = _actionFailedLedger.Count > 0; - if (_lastActionExecuted) - { - _lastActionLedgerNote = $"Action Ledger: {string.Join(", ", _actionSuccessLedger)}"; - } - else if (_lastActionHadError) - { - _lastActionLedgerNote = $"Action Ledger: None (no successful actions). Failed: {string.Join(", ", _actionFailedLedger)}"; - } - else - { - _lastActionLedgerNote = "Action Ledger: None (no in-game actions executed)."; - } - } - - private void UpdatePhaseToolLedger(RequestPhase phase, bool hasSuccess, List successfulTools) - { - if (phase == RequestPhase.QueryTools) - { - _querySuccessfulToolCall = hasSuccess; - _queryToolLedgerNote = hasSuccess - ? $"Tool Ledger (Query): {string.Join(", ", successfulTools)}" - : "Tool Ledger (Query): None (no successful tool calls)."; - } - else if (phase == RequestPhase.ActionTools) - { - _actionSuccessfulToolCall = hasSuccess; - _actionToolLedgerNote = hasSuccess - ? $"Tool Ledger (Action): {string.Join(", ", successfulTools)}" - : "Tool Ledger (Action): None (no successful tool calls)."; - } - } - - private void CompressHistoryIfNeeded() - { - int estimatedTokens = _history.Sum(h => h.message?.Length ?? 0) / CharsPerToken; - if (estimatedTokens > GetMaxHistoryTokens()) - { - int removeCount = _history.Count / 2; - if (removeCount > 0) - { - _history.RemoveRange(0, removeCount); - _history.Insert(0, ("system", "[Previous conversation summarized]")); - PersistHistory(); - } - } - } - - private static string StripXmlTags(string text) - { - if (string.IsNullOrEmpty(text)) return text; - string stripped = Regex.Replace(text, @"<([a-zA-Z0-9_]+)[^>]*>.*?", "", RegexOptions.Singleline); - stripped = Regex.Replace(stripped, @"<([a-zA-Z0-9_]+)[^>]*/>", ""); - return stripped; - } - - private List<(string role, string message)> BuildReplyHistory() - { - if (_history == null || _history.Count == 0) return new List<(string role, string message)>(); - - int lastUserIndex = -1; - for (int i = _history.Count - 1; i >= 0; i--) - { - if (string.Equals(_history[i].role, "user", StringComparison.OrdinalIgnoreCase)) - { - lastUserIndex = i; - break; - } - } - - var filtered = new List<(string role, string message)>(); - for (int i = 0; i < _history.Count; i++) - { - var entry = _history[i]; - if (string.Equals(entry.role, "tool", StringComparison.OrdinalIgnoreCase)) - { - if (lastUserIndex != -1 && i > lastUserIndex) - { - filtered.Add(entry); - } - continue; - } - - if (!string.Equals(entry.role, "assistant", StringComparison.OrdinalIgnoreCase)) - { - filtered.Add(entry); - continue; - } - - string stripped = StripXmlTags(entry.message)?.Trim() ?? ""; - if (!string.IsNullOrWhiteSpace(stripped)) - { - filtered.Add(entry); - } - } - - return filtered; - } - - private string StripExpressionTags(string text) - { - if (string.IsNullOrEmpty(text)) return text; - - var matches = ExpressionTagRegex.Matches(text); - int exprId = 0; - foreach (Match match in matches) - { - if (int.TryParse(match.Groups[1].Value, out int id)) - { - exprId = id; - } - } - - if (exprId >= 1 && exprId <= 6) - { - SetPortrait(exprId); - } - - return matches.Count > 0 ? ExpressionTagRegex.Replace(text, "").Trim() : text; - } - - private void ParseResponse(string rawResponse, bool addToHistory = true) - { - string cleanedResponse = StripExpressionTags(rawResponse ?? ""); - _currentResponse = cleanedResponse; - var parts = cleanedResponse.Split(new[] { "OPTIONS:" }, StringSplitOptions.None); - if (addToHistory) - { - if (_history.Count == 0 || _history.Last().role != "assistant") - { - _history.Add(("assistant", cleanedResponse)); - PersistHistory(); - } - else if (_history.Last().message != cleanedResponse) - { - if (_history.Last().message == rawResponse) - { - _history[_history.Count - 1] = ("assistant", cleanedResponse); - } - else - { - _history.Add(("assistant", cleanedResponse)); - } - PersistHistory(); - } - } - - if (!string.IsNullOrEmpty(ParseResponseForDisplay(cleanedResponse))) - { - _scrollToBottom = true; - } - if (parts.Length > 1) - { - _options.Clear(); - var optionsLines = parts[1].Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries); - foreach (var line in optionsLines) - { - string opt = line.Trim(); - int dotIndex = opt.IndexOf('.'); - if (dotIndex != -1 && dotIndex < 4) opt = opt.Substring(dotIndex + 1).Trim(); - if (!string.IsNullOrEmpty(opt)) _options.Add(opt); - } - } - } - public override void DoWindowContents(Rect inRect) - { - if (background != null) GUI.DrawTexture(inRect, background, ScaleMode.ScaleAndCrop); - - // 定义边距 - float margin = 15f; - Rect paddedRect = inRect.ContractedBy(margin); - - float curY = paddedRect.y; - float width = paddedRect.width; - - // 立绘不需要边距,所以使用原始inRect的位置 - if (portrait != null) - { - Rect scaledPortraitRect = Dialog_CustomDisplay.Config.GetScaledRect(Dialog_CustomDisplay.Config.portraitSize, inRect, true); - Rect portraitRect = new Rect((inRect.width - scaledPortraitRect.width) / 2, inRect.y, scaledPortraitRect.width, scaledPortraitRect.height); - GUI.DrawTexture(portraitRect, portrait, ScaleMode.ScaleToFit); - - if (Prefs.DevMode) - { - // DEBUG: Draw portrait ID - Text.Font = GameFont.Medium; - Text.Anchor = TextAnchor.UpperRight; - Widgets.Label(portraitRect, $"ID: {_currentPortraitId}"); - Text.Anchor = TextAnchor.UpperLeft; - Text.Font = GameFont.Small; - } - - curY = portraitRect.yMax + 10f; - } - - // 人物名字 - 居中显示 - Text.Font = GameFont.Medium; - string name = def.characterName ?? "The Legion"; - float nameHeight = Text.CalcHeight(name, width); - - // 创建名字的矩形,使其在窗口水平居中 - Rect nameRect = new Rect(paddedRect.x, curY, width, nameHeight); - Text.Anchor = TextAnchor.UpperCenter; // 改为上中对齐 - Widgets.Label(nameRect, name); - Text.Anchor = TextAnchor.UpperLeft; // 恢复左对齐 - - curY += nameHeight + 10f; - - // 计算输入框高度、选项高度和聊天历史高度 - float inputHeight = 30f; - float optionsHeight = _options.Any() ? 100f : 0f; - float spacing = 10f; - - // 聊天历史区域 - 使用带边距的矩形 - float descriptionHeight = paddedRect.height - curY - inputHeight - optionsHeight - spacing * 2; - Rect descriptionRect = new Rect(paddedRect.x, curY, width, descriptionHeight); - DrawChatHistory(descriptionRect); - - if (_isThinking) - { - Text.Anchor = TextAnchor.MiddleCenter; - Widgets.Label(descriptionRect, BuildThinkingStatus()); - Text.Anchor = TextAnchor.UpperLeft; - } - - curY += descriptionHeight + spacing; - - // 选项区域 - Rect optionsRect = new Rect(paddedRect.x, curY, width, optionsHeight); - if (!_isThinking && _options.Count > 0) - { - List eventOptions = _options.Select(opt => new EventOption { label = opt, useCustomColors = false }).ToList(); - DrawOptions(optionsRect, eventOptions); - } - - curY += optionsHeight + spacing; - - // 输入框区域 - 使用带边距的矩形 - Rect inputRect = new Rect(paddedRect.x, curY, width, inputHeight); - - // 保存当前字体 - var originalFont = Text.Font; - - // 设置更小的字体 - if (Text.Font == GameFont.Small) - { - // 使用 Tiny 字体 - Text.Font = GameFont.Tiny; - } - else - { - // 如果当前不是 Small,降一级 - Text.Font = GameFont.Small; - } - - // 计算输入框文本高度 - float textFieldHeight = Text.CalcHeight("Test", inputRect.width - 85); - Rect textFieldRect = new Rect(inputRect.x, inputRect.y + (inputHeight - textFieldHeight) / 2, inputRect.width - 85, textFieldHeight); - - _inputText = Widgets.TextField(textFieldRect, _inputText); - - // 发送按钮 - 使用与Dialog_CustomDisplay相同的自定义按钮样式 - // 保存当前状态 - var originalAnchor = Text.Anchor; - var originalColor = GUI.color; - - // 设置字体为Tiny - Text.Font = GameFont.Tiny; - Text.Anchor = TextAnchor.MiddleCenter; - - // 发送按钮的矩形 - Rect sendButtonRect = new Rect(inputRect.xMax - 80, inputRect.y, 80, inputHeight); - - // 使用基类的DrawCustomButton方法绘制按钮(与Dialog_CustomDisplay一致) - base.DrawCustomButton(sendButtonRect, "Wula_AI_Send".Translate(), isEnabled: true); - - // 恢复状态 - GUI.color = originalColor; - Text.Anchor = originalAnchor; - Text.Font = originalFont; - - // 处理点击事件 - bool sendButtonPressed = Widgets.ButtonInvisible(sendButtonRect); - - // 直接在DoWindowContents中处理Enter键,而不是调用单独的方法 - // 这是为了确保事件在正确的时机被处理 - if (Event.current.type == EventType.KeyDown) - { - // 检查是否按下了Enter键(主键盘或小键盘的Enter) - if ((Event.current.keyCode == KeyCode.Return || Event.current.keyCode == KeyCode.KeypadEnter) && !string.IsNullOrEmpty(_inputText)) - { - // 如果AI正在思考,不处理Enter键 - if (!_isThinking) - { - SelectOption(_inputText); - _inputText = ""; - // 消费这个事件,防止它传递到窗口的关闭逻辑 - Event.current.Use(); - } - } - // 可选:添加Escape键关闭窗口的功能 - else if (Event.current.keyCode == KeyCode.Escape) - { - this.Close(); - Event.current.Use(); - } - } - - // 处理鼠标点击发送按钮 - if (sendButtonPressed && !string.IsNullOrEmpty(_inputText)) - { - SelectOption(_inputText); - _inputText = ""; - } - } - private void DrawChatHistory(Rect rect) - { - var originalFont = Text.Font; - var originalAnchor = Text.Anchor; - - try - { - float viewHeight = 0f; - var filteredHistory = _history.Where(e => e.role != "tool" && e.role != "system" && e.role != "toolcall").ToList(); - - // 添加内边距 - float innerPadding = 5f; - float contentWidth = rect.width - 16f - innerPadding * 2; - - // 预计算高度 - 使用小字体 - for (int i = 0; i < filteredHistory.Count; i++) - { - var entry = filteredHistory[i]; - string text = entry.role == "assistant" ? ParseResponseForDisplay(entry.message) : entry.message; - if (string.IsNullOrEmpty(text) || (entry.role == "user" && text.StartsWith("[Tool Results]"))) continue; - bool isLastMessage = i == filteredHistory.Count - 1; - - // 设置更小的字体 - if (isLastMessage && entry.role == "assistant") - { - Text.Font = GameFont.Small; // 原来是 Medium,改为 Small - } - else - { - Text.Font = GameFont.Tiny; // 原来是 Small,改为 Tiny - } - // 增加padding - float padding = (isLastMessage && entry.role == "assistant") ? 30f : 15f; - viewHeight += Text.CalcHeight(text, contentWidth) + padding + 10f; - } - - Rect viewRect = new Rect(0f, 0f, rect.width - 16f, viewHeight); - if (_scrollToBottom) - { - _scrollPosition.y = float.MaxValue; - _scrollToBottom = false; - } - - Widgets.BeginScrollView(rect, ref _scrollPosition, viewRect); - - float curY = 0f; - for (int i = 0; i < filteredHistory.Count; i++) - { - var entry = filteredHistory[i]; - string text = entry.role == "assistant" ? ParseResponseForDisplay(entry.message) : entry.message; - - if (string.IsNullOrEmpty(text) || (entry.role == "user" && text.StartsWith("[Tool Results]"))) continue; - bool isLastMessage = i == filteredHistory.Count - 1; - - // 设置更小的字体 - if (isLastMessage && entry.role == "assistant") - { - Text.Font = GameFont.Small; // 原来是 Medium,改为 Small - } - else - { - Text.Font = GameFont.Tiny; // 原来是 Small,改为 Tiny - } - - float padding = (isLastMessage && entry.role == "assistant") ? 30f : 15f; - float height = Text.CalcHeight(text, contentWidth) + padding; - - // 添加内边距 - Rect labelRect = new Rect(innerPadding, curY, contentWidth, height); - - if (entry.role == "user") - { - Text.Anchor = TextAnchor.MiddleRight; - Widgets.Label(labelRect, $"{text}"); - } - else - { - Text.Anchor = TextAnchor.MiddleLeft; - Widgets.Label(labelRect, $"P.I.A: {text}"); - } - curY += height + 10f; - } - Widgets.EndScrollView(); - } - finally - { - Text.Font = originalFont; - Text.Anchor = originalAnchor; - } - } - - private string ParseResponseForDisplay(string rawResponse) - { - if (string.IsNullOrEmpty(rawResponse)) return ""; - - string text = rawResponse; - - // Remove standard tags with content: content - text = Regex.Replace(text, @"<([a-zA-Z0-9_]+)[^>]*>.*?", "", RegexOptions.Singleline); - - // Remove self-closing tags: - text = Regex.Replace(text, @"<([a-zA-Z0-9_]+)[^>]*/>", ""); - - text = ExpressionTagRegex.Replace(text, ""); - - text = text.Trim(); - - return text.Split(new[] { "OPTIONS:" }, StringSplitOptions.None)[0].Trim(); - } - - private string BuildThinkingStatus() - { - float elapsedSeconds = Mathf.Max(0f, Time.realtimeSinceStartup - _thinkingStartTime); - string elapsedText = elapsedSeconds.ToString("0.0", CultureInfo.InvariantCulture); - string retrySuffix = _thinkingPhaseRetry ? "Wula_AI_Thinking_RetrySuffix".Translate() : ""; - return "Wula_AI_Thinking_Status".Translate(elapsedText, _thinkingPhaseIndex, ThinkingPhaseTotal, retrySuffix); - } - - protected override void DrawSingleOption(Rect rect, EventOption option) - { - float optionWidth = Mathf.Min(rect.width, Dialog_CustomDisplay.Config.optionSize.x * (rect.width / Dialog_CustomDisplay.Config.windowSize.x)); - float optionX = rect.x + (rect.width - optionWidth) / 2; - Rect optionRect = new Rect(optionX, rect.y, optionWidth, rect.height); - - var originalColor = GUI.color; - var originalFont = Text.Font; - var originalTextColor = GUI.contentColor; - var originalAnchor = Text.Anchor; - - try - { - Text.Anchor = TextAnchor.MiddleCenter; - Text.Font = GameFont.Small; - DrawCustomButton(optionRect, option.label.Translate(), isEnabled: true); - if (Widgets.ButtonInvisible(optionRect)) - { - SelectOption(option.label); - } - } - finally - { - GUI.color = originalColor; - Text.Font = originalFont; - GUI.contentColor = originalTextColor; - Text.Anchor = originalAnchor; - } - } - - private new void DrawCustomButton(Rect rect, string label, bool isEnabled = true) - { - bool isMouseOver = Mouse.IsOver(rect); - Color buttonColor, textColor; - if (!isEnabled) - { - buttonColor = new Color(0.15f, 0.15f, 0.15f, 0.6f); - textColor = new Color(0.6f, 0.6f, 0.6f, 1f); - } - else if (isMouseOver) - { - buttonColor = new Color(0.6f, 0.3f, 0.3f, 1f); - textColor = new Color(1f, 1f, 1f, 1f); - } - else - { - buttonColor = new Color(0.5f, 0.2f, 0.2f, 1f); - textColor = new Color(0.9f, 0.9f, 0.9f, 1f); - } - - GUI.color = buttonColor; - Widgets.DrawBoxSolid(rect, buttonColor); - if (isEnabled) Widgets.DrawBox(rect, 1); - else Widgets.DrawBox(rect, 1); - - GUI.color = textColor; - Text.Anchor = TextAnchor.MiddleCenter; - Widgets.Label(rect.ContractedBy(4f), label); - if (!isEnabled) - { - GUI.color = new Color(0.6f, 0.6f, 0.6f, 0.8f); - Widgets.DrawLine(new Vector2(rect.x + 10f, rect.center.y), new Vector2(rect.xMax - 10f, rect.center.y), GUI.color, 1f); - } - } - - private async void SelectOption(string text) - { - if (!string.IsNullOrWhiteSpace(text) && string.Equals(text.Trim(), "/clear", StringComparison.OrdinalIgnoreCase)) - { - _isThinking = false; - _options.Clear(); - _inputText = ""; - - _history.Clear(); - try - { - var historyManager = Find.World?.GetComponent(); - historyManager?.ClearHistory(def.defName); - } - catch (Exception ex) - { - WulaLog.Debug($"[WulaAI] Failed to clear AI history: {ex}"); - } - - Messages.Message("已清除 AI 对话上下文历史。", MessageTypeDefOf.NeutralEvent); - return; - } - - _history.Add(("user", text)); - PersistHistory(); - _scrollToBottom = true; - await RunPhasedRequestAsync(); - } - - public override void PostClose() - { - if (Instance == this) Instance = null; - PersistHistory(); - base.PostClose(); - HandleAction(def.dismissEffects); - } - } -}