diff --git a/.agent/workflows/refactor-ai-conversation.md b/.agent/workflows/refactor-ai-conversation.md new file mode 100644 index 00000000..9abdd87a --- /dev/null +++ b/.agent/workflows/refactor-ai-conversation.md @@ -0,0 +1,114 @@ +# AI 对话系统重构任务 + +## 目标 +合并 `Dialog_AIConversation` 和 `AIIntelligenceCore`,简化 AI 对话系统架构。 + +## 当前架构问题 + +### 1. 两个类存在重复逻辑 +- `Dialog_AIConversation.cs` - 大对话框 UI,包含自己的: + - `_history` 历史记录 + - `RunPhasedRequestAsync()` 三阶段对话逻辑 + - `ExecuteXmlToolsForPhase()` 工具执行 + - `BuildSystemInstruction()` 系统指令构建 + - `BuildUserMessageWithContext()` 上下文附加 + +- `AIIntelligenceCore.cs` - WorldComponent,包含: + - `_history` 历史记录 (重复!) + - `RunPhasedRequestAsync()` 三阶段对话逻辑 (重复!) + - `ExecuteXmlToolsForPhase()` 工具执行 (重复!) + - `SendUserMessage()` 用户消息发送 + - 但 **没有** `BuildUserMessageWithContext()` 导致小窗口看不到选中对象上下文 + +### 2. 小窗口 `Overlay_WulaLink` 使用 `AIIntelligenceCore.SendUserMessage()` +- 这个方法没有附加选中对象的上下文 +- 导致通过小窗口对话时,AI 看不到玩家选中了什么 + +### 3. 历史记录不同步 +- `Dialog_AIConversation` 和 `AIIntelligenceCore` 各自维护历史记录 +- 可能导致状态不一致 + +## 重构方案 + +### 方案 A: 让 AIIntelligenceCore 成为唯一的对话逻辑中心 + +1. **在 `AIIntelligenceCore.SendUserMessage()` 中添加上下文附加逻辑**: +```csharp +public void SendUserMessage(string text) +{ + string messageWithContext = BuildUserMessageWithContext(text); + _history.Add(("user", messageWithContext)); + PersistHistory(); + _ = RunPhasedRequestAsync(); +} + +private string BuildUserMessageWithContext(string userText) +{ + StringBuilder sb = new StringBuilder(); + sb.Append(userText); + + if (Find.Selector != null) + { + if (Find.Selector.SingleSelectedThing != null) + { + var selected = Find.Selector.SingleSelectedThing; + sb.AppendLine(); + sb.AppendLine(); + sb.Append($"[Context: Player has selected '{selected.LabelCap}'"); + if (selected is Pawn pawn) + { + sb.Append($" ({pawn.def.label}) at ({pawn.Position.x}, {pawn.Position.z})"); + } + else + { + sb.Append($" at ({selected.Position.x}, {selected.Position.z})"); + } + sb.Append("]"); + } + else if (Find.Selector.SelectedObjects.Count > 1) + { + sb.AppendLine(); + sb.AppendLine(); + sb.Append($"[Context: Player has selected {Find.Selector.SelectedObjects.Count} objects: "); + var selectedThings = Find.Selector.SelectedObjects.OfType().Take(5).ToList(); + sb.Append(string.Join(", ", selectedThings.Select(t => t.LabelCap))); + if (Find.Selector.SelectedObjects.Count > 5) sb.Append("..."); + sb.Append("]"); + } + } + + return sb.ToString(); +} +``` + +2. **让 `Dialog_AIConversation` 使用 `AIIntelligenceCore` 而不是自己的逻辑**: + - 移除 `Dialog_AIConversation` 中的 `RunPhasedRequestAsync()` + - 移除 `Dialog_AIConversation` 中的 `ExecuteXmlToolsForPhase()` + - 让 `SelectOption()` 调用 `_core.SendUserMessage()` 而不是自己处理 + - 订阅 `_core.OnMessageReceived` 事件来更新 UI + +3. **统一历史记录**: + - 只使用 `AIIntelligenceCore._history` + - `Dialog_AIConversation` 通过 `_core.GetHistorySnapshot()` 获取历史 + +### 方案 B: 完全删除 AIIntelligenceCore,合并到 Dialog_AIConversation + +不推荐,因为 `AIIntelligenceCore` 作为 WorldComponent 可以在游戏暂停/存档时保持状态。 + +## 文件位置 + +- `c:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\EventSystem\AI\AIIntelligenceCore.cs` +- `c:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\EventSystem\AI\UI\Dialog_AIConversation.cs` +- `c:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\EventSystem\AI\UI\Overlay_WulaLink.cs` + +## 验证标准 + +1. 编译通过 (`dotnet build Source\WulaFallenEmpire\WulaFallenEmpire.csproj`) +2. 通过大窗口和小窗口对话时,AI 都能看到选中对象的上下文 +3. 历史记录在两个窗口之间保持同步 +4. 没有空行/空消息问题 +5. 工具调用正常工作 + +## 最小修复 (如果不想大重构) + +只在 `AIIntelligenceCore.SendUserMessage()` 中添加 `BuildUserMessageWithContext()` 逻辑即可解决小窗口看不到上下文的问题。 diff --git a/1.6/1.6/Assemblies/WulaFallenEmpire.dll b/1.6/1.6/Assemblies/WulaFallenEmpire.dll index 79906df8..4ca9a7cb 100644 Binary files a/1.6/1.6/Assemblies/WulaFallenEmpire.dll and b/1.6/1.6/Assemblies/WulaFallenEmpire.dll differ diff --git a/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs b/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs index 07afb3e4..b3f445c3 100644 --- a/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs +++ b/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs @@ -186,11 +186,67 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori return; } - _history.Add(("user", text)); + // 附加选中对象的上下文信息 + string messageWithContext = BuildUserMessageWithContext(text); + _history.Add(("user", messageWithContext)); PersistHistory(); _ = RunPhasedRequestAsync(); } + private string BuildUserMessageWithContext(string userText) + { + var sb = new System.Text.StringBuilder(); + sb.Append(userText); + + try + { + if (Find.Selector != null) + { + if (Find.Selector.SingleSelectedThing != null) + { + var selected = Find.Selector.SingleSelectedThing; + sb.AppendLine(); + sb.AppendLine(); + sb.Append($"[Context: Player has selected '{selected.LabelCap}'"); + + if (selected is Pawn pawn) + { + sb.Append($" ({pawn.def.label}) at ({pawn.Position.x}, {pawn.Position.z})"); + } + else + { + sb.Append($" at ({selected.Position.x}, {selected.Position.z})"); + } + sb.Append("]"); + } + else if (Find.Selector.SelectedObjects.Count > 1) + { + sb.AppendLine(); + sb.AppendLine(); + sb.Append($"[Context: Player has selected {Find.Selector.SelectedObjects.Count} objects"); + + var selectedThings = Find.Selector.SelectedObjects.OfType().Take(5).ToList(); + if (selectedThings.Count > 0) + { + sb.Append(": "); + sb.Append(string.Join(", ", selectedThings.Select(t => t.LabelCap))); + if (Find.Selector.SelectedObjects.Count > 5) + { + sb.Append("..."); + } + } + sb.Append("]"); + } + } + } + catch (Exception ex) + { + WulaLog.Debug($"[WulaAI] Error building context: {ex.Message}"); + } + + return sb.ToString(); + } + private void InitializeTools() { _tools.Clear(); diff --git a/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs b/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs index 9730958d..526d840f 100644 --- a/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs +++ b/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using RimWorld; using UnityEngine; using Verse; +using WulaFallenEmpire.EventSystem.AI; using WulaFallenEmpire.EventSystem.AI.Tools; using System.Text.RegularExpressions; @@ -22,6 +23,7 @@ namespace WulaFallenEmpire.EventSystem.AI.UI private Vector2 _scrollPosition = Vector2.zero; private bool _scrollToBottom = false; private List _tools = new List(); + private AIIntelligenceCore _core; private Dictionary _portraits = new Dictionary(); private static readonly Regex ExpressionTagRegex = new Regex(@"\[EXPR\s*:\s*([1-6])\s*\]", RegexOptions.IgnoreCase); private bool _lastActionExecuted = false; @@ -111,12 +113,12 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori this.absorbInputAroundWindow = false; this.doCloseX = true; this.doWindowBackground = Dialog_CustomDisplay.Config.showMainWindow; - this.drawShadow = false; // 禁用阴影 + this.drawShadow = Dialog_CustomDisplay.Config.showMainWindow; this.closeOnClickedOutside = false; this.draggable = true; this.resizeable = true; - // 关键修改:禁止Enter键自动关闭窗口 + // 关键修改:禁止Enter键自动关闭窗? this.closeOnAccept = false; _tools.Add(new Tool_SpawnResources()); @@ -138,11 +140,63 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori Instance = this; base.PostOpen(); LoadPortraits(); - StartConversation(); + + _core = Find.World?.GetComponent(); + if (_core != null) + { + _core.InitializeConversation(def.defName); + _core.OnMessageReceived += OnCoreMessageReceived; + _core.OnThinkingStateChanged += OnCoreThinkingStateChanged; + _core.OnExpressionChanged += OnCoreExpressionChanged; + + _history = _core.GetHistorySnapshot(); + _isThinking = _core.IsThinking; + SyncPortraitFromCore(); + } + else + { + StartConversation(); + } + } + + private void OnCoreMessageReceived(string message) + { + if (_core == null) + { + return; + } + + _history = _core.GetHistorySnapshot(); + _scrollToBottom = true; + } + + private void OnCoreThinkingStateChanged(bool isThinking) + { + _isThinking = isThinking; + } + + private void OnCoreExpressionChanged(int id) + { + SetPortrait(id); + } + + private void SyncPortraitFromCore() + { + if (_core == null) + { + return; + } + + SetPortrait(_core.ExpressionId); } public List<(string role, string message)> GetHistorySnapshot() { + if (_core != null) + { + return _core.GetHistorySnapshot(); + } + return _history?.ToList() ?? new List<(string role, string message)>(); } @@ -795,7 +849,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori string cleaned = StripXmlTags(reply)?.Trim() ?? ""; if (string.IsNullOrWhiteSpace(cleaned)) { - cleaned = "(系统)AI 返回了工具调用(XML),已被拦截。请重试或输入 /clear 清空上下文。"; + cleaned = "AI returned a tool-only response (XML), which was blocked. Retry or use /clear to reset context."; } reply = cleaned; } @@ -827,7 +881,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori if (matches.Count == 0) { UpdatePhaseToolLedger(phase, false, new List()); - _history.Add(("toolcall", "")); + _history.Add(("assistant", "")); _history.Add(("tool", $"[Tool Results]\nTool 'no_action' Result: No action taken.\n{guidance}")); PersistHistory(); UpdateActionLedgerNote(); @@ -836,7 +890,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori if (matches.Count == 1 && matches[0].Groups[1].Value.Equals("no_action", StringComparison.OrdinalIgnoreCase)) { UpdatePhaseToolLedger(phase, false, new List()); - _history.Add(("toolcall", "")); + _history.Add(("assistant", "")); _history.Add(("tool", $"[Tool Results]\nTool 'no_action' Result: No action taken.\n{guidance}")); PersistHistory(); UpdateActionLedgerNote(); @@ -1178,6 +1232,12 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori { if (background != null) GUI.DrawTexture(inRect, background, ScaleMode.ScaleAndCrop); + if (_core != null) + { + _history = _core.GetHistorySnapshot(); + _isThinking = _core.IsThinking; + } + // 定义边距 float margin = 15f; Rect paddedRect = inRect.ContractedBy(margin); @@ -1185,7 +1245,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori float curY = paddedRect.y; float width = paddedRect.width; - // 立绘不需要边距,所以使用原始inRect的位置 + // 立绘不需要边距,所以使用原始inRect的位? if (portrait != null) { Rect scaledPortraitRect = Dialog_CustomDisplay.Config.GetScaledRect(Dialog_CustomDisplay.Config.portraitSize, inRect, true); @@ -1210,15 +1270,15 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori 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; // 恢复左对齐 + Text.Anchor = TextAnchor.UpperLeft; // 恢复左对? curY += nameHeight + 10f; - // 计算输入框高度、选项高度和聊天历史高度 + // 计算输入框高度、选项高度和聊天历史高? float inputHeight = 30f; float optionsHeight = _options.Any() ? 100f : 0f; float spacing = 10f; @@ -1247,13 +1307,13 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori curY += optionsHeight + spacing; - // 输入框区域 - 使用带边距的矩形 + // 输入框区?- 使用带边距的矩形 Rect inputRect = new Rect(paddedRect.x, curY, width, inputHeight); // 保存当前字体 var originalFont = Text.Font; - // 设置更小的字体 + // 设置更小的字? if (Text.Font == GameFont.Small) { // 使用 Tiny 字体 @@ -1261,18 +1321,18 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori } else { - // 如果当前不是 Small,降一级 + // 如果当前不是 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相同的自定义按钮样式 - // 保存当前状态 + // 发送按?- 使用与Dialog_CustomDisplay相同的自定义按钮样式 + // 保存当前状? var originalAnchor = Text.Anchor; var originalColor = GUI.color; @@ -1286,7 +1346,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori // 使用基类的DrawCustomButton方法绘制按钮(与Dialog_CustomDisplay一致) base.DrawCustomButton(sendButtonRect, "Wula_AI_Send".Translate(), isEnabled: true); - // 恢复状态 + // 恢复状? GUI.color = originalColor; Text.Anchor = originalAnchor; Text.Font = originalFont; @@ -1295,13 +1355,13 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori bool sendButtonPressed = Widgets.ButtonInvisible(sendButtonRect); // 直接在DoWindowContents中处理Enter键,而不是调用单独的方法 - // 这是为了确保事件在正确的时机被处理 + // 这是为了确保事件在正确的时机被处? if (Event.current.type == EventType.KeyDown) { - // 检查是否按下了Enter键(主键盘或小键盘的Enter) + // 检查是否按下了Enter键(主键盘或小键盘的Enter? if ((Event.current.keyCode == KeyCode.Return || Event.current.keyCode == KeyCode.KeypadEnter) && !string.IsNullOrEmpty(_inputText)) { - // 如果AI正在思考,不处理Enter键 + // 如果AI正在思考,不处理Enter? if (!_isThinking) { SelectOption(_inputText); @@ -1318,52 +1378,50 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori } } - // 处理鼠标点击发送按钮 + // 处理鼠标点击发送按? if (sendButtonPressed && !string.IsNullOrEmpty(_inputText)) { SelectOption(_inputText); _inputText = ""; } } - // 气泡样式常量 - private const float BubblePadding = 8f; - private const float AvatarSize = 32f; - private const float MessageSpacing = 8f; - private const float MaxBubbleWidthRatio = 0.75f; - private const float BubbleCornerRadius = 8f; - private void DrawChatHistory(Rect rect) { var originalFont = Text.Font; var originalAnchor = Text.Anchor; - var originalColor = GUI.color; try { float viewHeight = 0f; var filteredHistory = _history.Where(e => e.role != "tool" && e.role != "system" && e.role != "toolcall").ToList(); - float containerWidth = rect.width - 16f; - float maxBubbleWidth = containerWidth * MaxBubbleWidthRatio; + // 添加内边? + float innerPadding = 5f; + float contentWidth = rect.width - 16f - innerPadding * 2; - // 预计算高度 - Text.Font = GameFont.Small; + // 预计算高?- 使用小字? for (int i = 0; i < filteredHistory.Count; i++) { var entry = filteredHistory[i]; string text = entry.role == "assistant" ? ParseResponseForDisplay(entry.message) : entry.message; - if (string.IsNullOrWhiteSpace(text) || (entry.role == "user" && text.StartsWith("[Tool Results]"))) continue; + if (string.IsNullOrEmpty(text) || (entry.role == "user" && text.StartsWith("[Tool Results]"))) continue; + bool isLastMessage = i == filteredHistory.Count - 1; - float textWidth = maxBubbleWidth - (BubblePadding * 2); - if (entry.role == "assistant") textWidth -= (AvatarSize + BubblePadding); - - float textHeight = Text.CalcHeight(text, textWidth); - float bubbleHeight = textHeight + (BubblePadding * 2); - float rowHeight = Mathf.Max(bubbleHeight, entry.role == "assistant" ? AvatarSize : 0f); - viewHeight += rowHeight + MessageSpacing; + // 设置更小的字? + 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, containerWidth, viewHeight); + Rect viewRect = new Rect(0f, 0f, rect.width - 16f, viewHeight); if (_scrollToBottom) { _scrollPosition.y = float.MaxValue; @@ -1378,22 +1436,36 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori var entry = filteredHistory[i]; string text = entry.role == "assistant" ? ParseResponseForDisplay(entry.message) : entry.message; - if (string.IsNullOrWhiteSpace(text) || (entry.role == "user" && text.StartsWith("[Tool Results]"))) continue; + if (string.IsNullOrEmpty(text) || (entry.role == "user" && text.StartsWith("[Tool Results]"))) continue; + bool isLastMessage = i == filteredHistory.Count - 1; - Text.Font = GameFont.Small; - - if (entry.role == "user") + // 设置更小的字? + if (isLastMessage && entry.role == "assistant") { - // 用户消息 - 右对齐,深蓝色气泡 - DrawUserMessage(new Rect(0, curY, containerWidth, 0), text, out float userMsgHeight); - curY += userMsgHeight + MessageSpacing; + Text.Font = GameFont.Small; // 原来?Medium,改?Small } else { - // AI消息 - 左对齐,带头像,深红色气泡 - DrawAIMessage(new Rect(0, curY, containerWidth, 0), text, out float aiMsgHeight); - curY += aiMsgHeight + MessageSpacing; + 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(); } @@ -1401,141 +1473,9 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori { Text.Font = originalFont; Text.Anchor = originalAnchor; - GUI.color = originalColor; } } - private void DrawUserMessage(Rect containerRect, string text, out float totalHeight) - { - float maxBubbleWidth = containerRect.width * MaxBubbleWidthRatio; - float textWidth = maxBubbleWidth - (BubblePadding * 2); - - Text.Font = GameFont.Small; - float textHeight = Text.CalcHeight(text, textWidth); - float bubbleHeight = textHeight + (BubblePadding * 2); - float bubbleWidth = Mathf.Min(Text.CalcSize(text).x + (BubblePadding * 2), maxBubbleWidth); - - // 右对齐 - float bubbleX = containerRect.xMax - bubbleWidth; - Rect bubbleRect = new Rect(bubbleX, containerRect.y, bubbleWidth, bubbleHeight); - - // 绘制圆角气泡背景 - 用户用深蓝色 (StudentBubble) - DrawRoundedRect(bubbleRect, WulaLinkStyles.StudentBubbleColor, BubbleCornerRadius); - - // 绘制文字 - GUI.color = WulaLinkStyles.StudentTextColor; - Text.Anchor = TextAnchor.MiddleLeft; - Widgets.Label(bubbleRect.ContractedBy(BubblePadding), text); - GUI.color = Color.white; - - totalHeight = bubbleHeight; - } - - private void DrawAIMessage(Rect containerRect, string text, out float totalHeight) - { - float maxBubbleWidth = containerRect.width * MaxBubbleWidthRatio; - float textWidth = maxBubbleWidth - (BubblePadding * 2) - AvatarSize - BubblePadding; - - Text.Font = GameFont.Small; - float textHeight = Text.CalcHeight(text, textWidth); - float bubbleHeight = textHeight + (BubblePadding * 2); - float bubbleWidth = Mathf.Min(Text.CalcSize(text).x + (BubblePadding * 2), maxBubbleWidth - AvatarSize - BubblePadding); - - totalHeight = Mathf.Max(bubbleHeight, AvatarSize); - - // 头像区域 - 左侧 - Rect avatarRect = new Rect(containerRect.x, containerRect.y + (totalHeight - AvatarSize) / 2f, AvatarSize, AvatarSize); - - // 绘制圆形头像 - DrawCircularAvatar(avatarRect); - - // 气泡区域 - 头像右侧 - float bubbleX = avatarRect.xMax + BubblePadding; - Rect bubbleRect = new Rect(bubbleX, containerRect.y + (totalHeight - bubbleHeight) / 2f, bubbleWidth, bubbleHeight); - - // 绘制圆角气泡背景 - AI用深红色 (SenseiBubble) - DrawRoundedRect(bubbleRect, WulaLinkStyles.SenseiBubbleColor, BubbleCornerRadius); - - // 绘制文字 - GUI.color = WulaLinkStyles.SenseiTextColor; - Text.Anchor = TextAnchor.MiddleLeft; - Widgets.Label(bubbleRect.ContractedBy(BubblePadding), text); - GUI.color = Color.white; - } - - private void DrawCircularAvatar(Rect rect) - { - // 获取当前头像 - Texture2D avatarTex = portrait; - if (avatarTex == null && _portraits.Count > 0) - { - avatarTex = _portraits.Values.FirstOrDefault(); - } - - // 绘制圆形背景 - DrawRoundedRect(rect, new Color(0.3f, 0.15f, 0.15f, 1f), rect.width / 2f); - - if (avatarTex != null) - { - // 使用圆形遮罩绘制头像 - if (WulaLinkStyles.TexCircleMask != null) - { - // 先绘制头像 - GUI.DrawTexture(rect, avatarTex, ScaleMode.ScaleToFit); - // 再用遮罩(如果可用) - GUI.color = new Color(1f, 1f, 1f, 1f); - } - else - { - // 无遮罩时直接绘制 - GUI.DrawTexture(rect, avatarTex, ScaleMode.ScaleToFit); - } - } - else - { - // 无头像时显示占位符 - GUI.color = WulaLinkStyles.SenseiTextColor; - Text.Font = GameFont.Tiny; - Text.Anchor = TextAnchor.MiddleCenter; - Widgets.Label(rect, "P.I.A"); - } - GUI.color = Color.white; - } - - private void DrawRoundedRect(Rect rect, Color color, float radius) - { - var originalColor = GUI.color; - GUI.color = color; - - // RimWorld没有内置圆角矩形,使用实心矩形模拟 - // 主体矩形 - Widgets.DrawBoxSolid(new Rect(rect.x + radius, rect.y, rect.width - radius * 2, rect.height), color); - Widgets.DrawBoxSolid(new Rect(rect.x, rect.y + radius, rect.width, rect.height - radius * 2), color); - - // 四个角的圆(用小方块近似) - float step = radius / 3f; - for (float dx = 0; dx < radius; dx += step) - { - for (float dy = 0; dy < radius; dy += step) - { - float dist = Mathf.Sqrt(dx * dx + dy * dy); - if (dist <= radius) - { - // 左上角 - Widgets.DrawBoxSolid(new Rect(rect.x + radius - dx - step, rect.y + radius - dy - step, step, step), color); - // 右上角 - Widgets.DrawBoxSolid(new Rect(rect.xMax - radius + dx, rect.y + radius - dy - step, step, step), color); - // 左下角 - Widgets.DrawBoxSolid(new Rect(rect.x + radius - dx - step, rect.yMax - radius + dy, step, step), color); - // 右下角 - Widgets.DrawBoxSolid(new Rect(rect.xMax - radius + dx, rect.yMax - radius + dy, step, step), color); - } - } - } - - GUI.color = originalColor; - } - private string ParseResponseForDisplay(string rawResponse) { if (string.IsNullOrEmpty(rawResponse)) return ""; @@ -1630,7 +1570,27 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori private async void SelectOption(string text) { - if (!string.IsNullOrWhiteSpace(text) && string.Equals(text.Trim(), "/clear", StringComparison.OrdinalIgnoreCase)) + if (string.IsNullOrWhiteSpace(text)) + { + return; + } + + if (_core != null) + { + if (string.Equals(text.Trim(), "/clear", StringComparison.OrdinalIgnoreCase)) + { + _isThinking = false; + _options.Clear(); + _inputText = ""; + } + + _scrollToBottom = true; + _core.SendUserMessage(text); + _history = _core.GetHistorySnapshot(); + return; + } + + if (string.Equals(text.Trim(), "/clear", StringComparison.OrdinalIgnoreCase)) { _isThinking = false; _options.Clear(); @@ -1647,75 +1607,35 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori WulaLog.Debug($"[WulaAI] Failed to clear AI history: {ex}"); } - Messages.Message("已清除 AI 对话上下文历史。", MessageTypeDefOf.NeutralEvent); + Messages.Message("AI conversation history cleared.", MessageTypeDefOf.NeutralEvent); return; } - // 构建带选中对象上下文的用户消息 - string messageWithContext = BuildUserMessageWithContext(text); - _history.Add(("user", messageWithContext)); + _history.Add(("user", text)); PersistHistory(); _scrollToBottom = true; await RunPhasedRequestAsync(); } - private string BuildUserMessageWithContext(string userText) - { - StringBuilder sb = new StringBuilder(); - sb.Append(userText); - - // 获取当前选中的对象信息 - if (Find.Selector != null) - { - if (Find.Selector.SingleSelectedThing != null) - { - var selected = Find.Selector.SingleSelectedThing; - sb.AppendLine(); - sb.AppendLine(); - sb.Append($"[Context: Player has selected '{selected.LabelCap}'"); - - // 如果是Pawn,提供更多信息 - if (selected is Pawn pawn) - { - sb.Append($" ({pawn.def.label})"); - sb.Append($" at ({pawn.Position.x}, {pawn.Position.z})"); - } - else - { - sb.Append($" at ({selected.Position.x}, {selected.Position.z})"); - } - sb.Append("]"); - } - else if (Find.Selector.SelectedObjects.Count > 1) - { - sb.AppendLine(); - sb.AppendLine(); - sb.Append($"[Context: Player has selected {Find.Selector.SelectedObjects.Count} objects"); - - // 列出前几个选中的对象 - var selectedThings = Find.Selector.SelectedObjects.OfType().Take(5).ToList(); - if (selectedThings.Count > 0) - { - sb.Append(": "); - sb.Append(string.Join(", ", selectedThings.Select(t => t.LabelCap))); - if (Find.Selector.SelectedObjects.Count > 5) - { - sb.Append("..."); - } - } - sb.Append("]"); - } - } - - return sb.ToString(); - } - public override void PostClose() { + if (_core != null) + { + _core.OnMessageReceived -= OnCoreMessageReceived; + _core.OnThinkingStateChanged -= OnCoreThinkingStateChanged; + _core.OnExpressionChanged -= OnCoreExpressionChanged; + } + if (Instance == this) Instance = null; - PersistHistory(); + if (_core == null) + { + PersistHistory(); + } base.PostClose(); HandleAction(def.dismissEffects); } } } + + + diff --git a/Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink.cs b/Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink.cs index a4885365..920b9322 100644 --- a/Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink.cs +++ b/Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using UnityEngine; @@ -45,9 +45,9 @@ namespace WulaFallenEmpire.EventSystem.AI.UI this.forcePause = false; this.absorbInputAroundWindow = false; this.closeOnClickedOutside = false; - this.closeOnAccept = false; // 防止 Enter 键误关闭 + this.closeOnAccept = false; // ֹ Enter ر this.doWindowBackground = false; // We draw our own - this.drawShadow = false; // 禁用阴影 + this.drawShadow = false; // Ӱ this.draggable = true; this.resizeable = true; this.preventCameraMotion = false; @@ -65,16 +65,16 @@ namespace WulaFallenEmpire.EventSystem.AI.UI if (_isMinimized) { - // 最小化时保持当前位置,只调整大小 + // СʱֵǰλãֻС windowRect.width = _minimizedSize.x; windowRect.height = _minimizedSize.y; - // 确保不超出屏幕边界 + // ȷĻ߽ windowRect.x = Mathf.Clamp(windowRect.x, 0, Verse.UI.screenWidth - _minimizedSize.x); windowRect.y = Mathf.Clamp(windowRect.y, 0, Verse.UI.screenHeight - _minimizedSize.y); } else { - // 展开时居中到屏幕中心 + // չʱеĻ windowRect.width = _expandedSize.x; windowRect.height = _expandedSize.y; windowRect.x = (Verse.UI.screenWidth - _expandedSize.x) / 2f; @@ -183,7 +183,7 @@ namespace WulaFallenEmpire.EventSystem.AI.UI Color statusColor = _core.IsThinking ? Color.yellow : Color.green; GUI.color = statusColor; - Widgets.Label(statusRect, $"鈼?{status}"); + Widgets.Label(statusRect, $"?{status}"); GUI.color = Color.white; // Unread Badge @@ -245,17 +245,17 @@ namespace WulaFallenEmpire.EventSystem.AI.UI titleRect.x += 10f; Widgets.Label(titleRect, _def.characterName ?? "MomoTalk"); - // Header Icons (Minimize/Close) - 自定义样式 + // Header Icons (Minimize/Close) - Զʽ Rect closeRect = new Rect(rect.width - 35f, 10f, 25f, 25f); Rect minRect = new Rect(rect.width - 65f, 10f, 25f, 25f); - // 最小化按钮 + // Сť if (DrawHeaderButton(minRect, "-")) { ToggleMinimize(); } - // 关闭按钮 + // رհť if (DrawHeaderButton(closeRect, "X")) { Close(); @@ -269,8 +269,8 @@ namespace WulaFallenEmpire.EventSystem.AI.UI { bool isMouseOver = Mouse.IsOver(rect); Color buttonColor = isMouseOver - ? new Color(0.6f, 0.3f, 0.3f, 1f) // Hover: 深红色 - : new Color(0.4f, 0.2f, 0.2f, 0.8f); // Normal: 暗红色 + ? new Color(0.6f, 0.3f, 0.3f, 1f) // Hover: ɫ + : new Color(0.4f, 0.2f, 0.2f, 0.8f); // Normal: ɫ Color textColor = isMouseOver ? Color.white : new Color(0.9f, 0.9f, 0.9f); var originalColor = GUI.color; @@ -301,8 +301,9 @@ namespace WulaFallenEmpire.EventSystem.AI.UI var displayHistory = new List<(string role, string message, string displayText)>(); foreach (var msg in history) { - // Skip tool/toolcall/system messages in normal mode - if ((msg.role == "tool" || msg.role == "toolcall" || msg.role == "system") && !Prefs.DevMode) continue; + // Skip tool/toolcall messages to avoid empty spacing + if (msg.role == "tool" || msg.role == "toolcall") continue; + if (msg.role == "system" && !Prefs.DevMode) continue; // For assistant messages, strip XML and check if empty string displayText = msg.message; @@ -456,18 +457,18 @@ namespace WulaFallenEmpire.EventSystem.AI.UI float bubbleHeight = textHeight + (BubblePadding * 2); float bubbleWidth = Mathf.Min(Text.CalcSize(text).x + (BubblePadding * 2) + 10f, maxBubbleWidth); - // 气泡位置 - 右对齐,留出箭头空间 + // λ - Ҷ룬ͷռ float arrowSize = 8f; Rect bubbleRect = new Rect(rect.xMax - bubbleWidth - arrowSize - 5f, rect.y, bubbleWidth, bubbleHeight); - // 绘制圆角气泡背景 - 蓝色 (Sensei color) + // Բݱ - ɫ (Sensei color) Color bubbleColor = new Color(0.29f, 0.54f, 0.78f, 1f); // #4a8ac6 MomoTalk Sensei blue DrawRoundedBubble(bubbleRect, bubbleColor, 8f); - // 绘制右侧箭头 + // Ҳͷ DrawBubbleArrow(bubbleRect.xMax, bubbleRect.y + 10f, arrowSize, bubbleColor, false); - // 绘制文字 + // GUI.color = Color.white; Text.Anchor = TextAnchor.MiddleLeft; Widgets.Label(bubbleRect.ContractedBy(BubblePadding), text); @@ -480,11 +481,11 @@ namespace WulaFallenEmpire.EventSystem.AI.UI float avatarX = 10f; Rect avatarRect = new Rect(avatarX, rect.y, AvatarSize, AvatarSize); - // 绘制圆形头像背景 + // Բͷ񱳾 Color avatarBgColor = new Color(0.2f, 0.2f, 0.25f, 1f); DrawRoundedBubble(avatarRect, avatarBgColor, AvatarSize / 2f); - // 绘制头像 + // ͷ int expId = _core?.ExpressionId ?? 1; string portraitPath = _def.portraitPath ?? $"Wula/Events/Portraits/WULA_Legion_{expId}"; if (expId > 1 && _def.portraitPath == null) @@ -499,7 +500,7 @@ namespace WulaFallenEmpire.EventSystem.AI.UI } else { - // 显示占位符 + // ʾռλ GUI.color = Color.white; Text.Font = GameFont.Tiny; Text.Anchor = TextAnchor.MiddleCenter; @@ -507,7 +508,7 @@ namespace WulaFallenEmpire.EventSystem.AI.UI } GUI.color = Color.white; - // 气泡 + // float maxBubbleWidth = rect.width * MaxBubbleWidthRatio; float arrowSize = 8f; float bubbleX = avatarRect.xMax + arrowSize + 5f; @@ -520,14 +521,14 @@ namespace WulaFallenEmpire.EventSystem.AI.UI Rect bubbleRect = new Rect(bubbleX, rect.y, bubbleWidth, bubbleHeight); - // 绘制圆角气泡背景 - 灰色 (Student color) + // Բݱ - ɫ (Student color) Color bubbleColor = new Color(0.85f, 0.85f, 0.87f, 1f); // Light gray like MomoTalk DrawRoundedBubble(bubbleRect, bubbleColor, 8f); - // 绘制左侧箭头 + // ͷ DrawBubbleArrow(bubbleRect.x, bubbleRect.y + 10f, arrowSize, bubbleColor, true); - // 绘制文字 + // GUI.color = new Color(0.1f, 0.1f, 0.1f, 1f); // Dark text Text.Anchor = TextAnchor.MiddleLeft; Widgets.Label(bubbleRect.ContractedBy(BubblePadding), text); @@ -609,17 +610,17 @@ namespace WulaFallenEmpire.EventSystem.AI.UI return Widgets.ButtonInvisible(rect); } - // MomoTalk 风格的圆角气泡 + // MomoTalk Բ private void DrawRoundedBubble(Rect rect, Color color, float radius) { var originalColor = GUI.color; GUI.color = color; - // 主体矩形 + // Widgets.DrawBoxSolid(new Rect(rect.x + radius, rect.y, rect.width - radius * 2, rect.height), color); Widgets.DrawBoxSolid(new Rect(rect.x, rect.y + radius, rect.width, rect.height - radius * 2), color); - // 四个角的近似圆角 + // ĸǵĽԲ float step = radius / 4f; for (float dx = 0; dx < radius; dx += step) { @@ -639,13 +640,13 @@ namespace WulaFallenEmpire.EventSystem.AI.UI GUI.color = originalColor; } - // MomoTalk 风格的气泡箭头 + // MomoTalk ݼͷ private void DrawBubbleArrow(float x, float y, float size, Color color, bool pointLeft) { var originalColor = GUI.color; GUI.color = color; - // 用小方块模拟三角形箭头 + // Сģμͷ float step = size / 4f; for (int i = 0; i < 4; i++) { @@ -660,3 +661,7 @@ namespace WulaFallenEmpire.EventSystem.AI.UI } } + + + +