diff --git a/1.6/1.6/Assemblies/WulaFallenEmpire.dll b/1.6/1.6/Assemblies/WulaFallenEmpire.dll index 642dccc7..da3f51a3 100644 Binary files a/1.6/1.6/Assemblies/WulaFallenEmpire.dll and b/1.6/1.6/Assemblies/WulaFallenEmpire.dll differ diff --git a/1.6/1.6/Defs/PrefabDefs/WULA_Prefabs.xml b/1.6/1.6/Defs/PrefabDefs/WULA_Prefabs.xml index c1ebdd6c..c52b2923 100644 --- a/1.6/1.6/Defs/PrefabDefs/WULA_Prefabs.xml +++ b/1.6/1.6/Defs/PrefabDefs/WULA_Prefabs.xml @@ -723,59 +723,199 @@ - - WULA_RavenShuttleBase - (13,13) - - - -
  • (1,6,1,6)
  • -
  • (11,6,11,6)
  • -
    - Clockwise -
    - - -
  • (1,2,1,2)
  • -
  • (1,10,1,10)
  • -
    - Counterclockwise -
    - - -
  • (11,2,11,2)
  • -
  • (11,10,11,10)
  • -
    - Clockwise -
    - - -
  • (0,0,1,0)
  • -
  • (11,0,12,0)
  • -
  • (0,1,0,5)
  • -
  • (12,1,12,5)
  • -
  • (1,5,1,5)
  • -
  • (11,5,11,5)
  • -
  • (0,7,1,7)
  • -
  • (11,7,12,7)
  • -
  • (0,8,0,12)
  • -
  • (12,8,12,12)
  • -
  • (1,12,1,12)
  • -
  • (11,12,11,12)
  • -
    -
    - - (6, 0, 6) - -
    - + + WULA_PrisonBase + (13,13) + + + +
  • (1,4,1,4)
  • +
  • (4,4,4,4)
  • +
  • (8,4,8,4)
  • +
  • (11,4,11,4)
  • +
  • (1,8,1,8)
  • +
  • (4,8,4,8)
  • +
  • (8,8,8,8)
  • +
  • (11,8,11,8)
  • +
    +
    + + +
  • (0,6,0,6)
  • +
  • (12,6,12,6)
  • +
    + Clockwise +
    + + +
  • (4,5,4,5)
  • +
  • (8,5,8,5)
  • +
  • (4,7,4,7)
  • +
  • (8,7,8,7)
  • +
    +
    + + +
  • (2, 0, 1)
  • +
  • (5, 0, 1)
  • +
  • (7, 0, 1)
  • +
  • (10, 0, 1)
  • +
    + WULA_Alloy + Normal +
    + + +
  • (2, 0, 11)
  • +
  • (5, 0, 11)
  • +
  • (7, 0, 11)
  • +
  • (10, 0, 11)
  • +
    + Opposite + WULA_Alloy + Normal +
    + + +
  • (1,1,1,1)
  • +
  • (4,1,4,1)
  • +
  • (8,1,8,1)
  • +
  • (11,1,11,1)
  • +
    + Opposite +
    + + (3, 0, 6) + Clockwise + + + (9, 0, 6) + Counterclockwise + + + +
  • (1,11,1,11)
  • +
  • (4,11,4,11)
  • +
  • (8,11,8,11)
  • +
  • (11,11,11,11)
  • +
    +
    + + +
  • (1,1,1,1)
  • +
  • (4,1,4,1)
  • +
  • (8,1,8,1)
  • +
  • (11,1,11,1)
  • +
    + WULA_Alloy + Normal +
    + + +
  • (1,11,1,11)
  • +
  • (4,11,4,11)
  • +
  • (8,11,8,11)
  • +
  • (11,11,11,11)
  • +
    + Opposite + WULA_Alloy + Normal +
    + + +
  • (0,0,12,0)
  • +
  • (0,1,0,5)
  • +
  • (3,1,3,4)
  • +
  • (6,1,6,4)
  • +
  • (9,1,9,4)
  • +
  • (12,1,12,5)
  • +
  • (2,4,2,4)
  • +
  • (5,4,5,4)
  • +
  • (7,4,7,4)
  • +
  • (10,4,10,4)
  • +
  • (4,6,4,6)
  • +
  • (8,6,8,6)
  • +
  • (0,7,0,12)
  • +
  • (12,7,12,12)
  • +
  • (2,8,3,8)
  • +
  • (5,8,7,8)
  • +
  • (9,8,10,8)
  • +
  • (3,9,3,12)
  • +
  • (6,9,6,12)
  • +
  • (9,9,9,12)
  • +
  • (1,12,2,12)
  • +
  • (4,12,5,12)
  • +
  • (7,12,8,12)
  • +
  • (10,12,11,12)
  • +
    +
    + + (6, 0, 6) + +
    +
  • (0,1,12,12)
  • -
    +
    + + + + WULA_RavenShuttleBase + (13,13) + + + +
  • (1,6,1,6)
  • +
  • (11,6,11,6)
  • +
    + Clockwise +
    + + +
  • (1,2,1,2)
  • +
  • (1,10,1,10)
  • +
    + Counterclockwise +
    + + +
  • (11,2,11,2)
  • +
  • (11,10,11,10)
  • +
    + Clockwise +
    + + +
  • (0,0,1,0)
  • +
  • (11,0,12,0)
  • +
  • (0,1,0,5)
  • +
  • (12,1,12,5)
  • +
  • (1,5,1,5)
  • +
  • (11,5,11,5)
  • +
  • (0,7,1,7)
  • +
  • (11,7,12,7)
  • +
  • (0,8,0,12)
  • +
  • (12,8,12,12)
  • +
  • (1,12,1,12)
  • +
  • (11,12,11,12)
  • +
    +
    + + (6, 0, 6) + +
    + + + +
  • (0,1,12,12)
  • +
    +
    +
    +
    diff --git a/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs b/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs index 535f6b59..9a8cfce1 100644 --- a/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs +++ b/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs @@ -941,6 +941,14 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori return cleaned.Trim(); } + private static bool LooksLikeNaturalReply(string response) + { + if (string.IsNullOrWhiteSpace(response)) return false; + string trimmed = response.Trim(); + if (JsonToolCallParser.LooksLikeJson(trimmed)) return false; + return trimmed.Length >= 4; + } + private bool IsToolAvailable(string toolName) { if (string.IsNullOrWhiteSpace(toolName)) return false; @@ -1758,6 +1766,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori maxSeconds = Mathf.Clamp(settings.reactMaxSeconds, 2f, 60f); } _thinkingPhaseTotal = maxSteps; + string toolPhaseReplyCandidate = null; for (int step = 1; step <= maxSteps; step++) { if (Time.realtimeSinceStartup - startTime > maxSeconds) @@ -1783,6 +1792,12 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori string normalizedResponse = NormalizeReactResponse(response); if (!JsonToolCallParser.TryParseToolCallsFromText(normalizedResponse, out var toolCalls, out string jsonFragment)) { + if (LooksLikeNaturalReply(normalizedResponse)) + { + toolPhaseReplyCandidate = normalizedResponse; + break; + } + if (Prefs.DevMode) { WulaLog.Debug("[WulaAI] ReAct step missing JSON envelope; attempting format fix."); @@ -1793,6 +1808,10 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori if (string.IsNullOrEmpty(normalizedFixed) || !JsonToolCallParser.TryParseToolCallsFromText(normalizedFixed, out toolCalls, out jsonFragment)) { + if (LooksLikeNaturalReply(normalizedFixed)) + { + toolPhaseReplyCandidate = normalizedFixed; + } if (Prefs.DevMode) { WulaLog.Debug("[WulaAI] ReAct format fix failed."); @@ -1893,10 +1912,21 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori return; } + string fallbackReply = string.IsNullOrWhiteSpace(toolPhaseReplyCandidate) + ? "" + : StripToolCallJson(toolPhaseReplyCandidate)?.Trim() ?? ""; bool replyHadToolCalls = IsToolCallJson(reply); string strippedReply = StripToolCallJson(reply)?.Trim() ?? ""; if (replyHadToolCalls || string.IsNullOrWhiteSpace(strippedReply)) { + if (!string.IsNullOrWhiteSpace(fallbackReply)) + { + reply = fallbackReply; + replyHadToolCalls = false; + strippedReply = fallbackReply; + } + else + { string retryReplyInstruction = replyInstruction + "\n\n# RETRY (REPLY OUTPUT)\n" + "Your last reply included tool call JSON or was empty. Tool calls are DISABLED.\n" + @@ -1908,16 +1938,24 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori replyHadToolCalls = IsToolCallJson(reply); strippedReply = StripToolCallJson(reply)?.Trim() ?? ""; } + } } if (replyHadToolCalls) { + if (!string.IsNullOrWhiteSpace(fallbackReply)) + { + reply = fallbackReply; + } + else + { string cleaned = StripToolCallJson(reply)?.Trim() ?? ""; if (string.IsNullOrWhiteSpace(cleaned)) { cleaned = "(system) AI reply returned tool call JSON only and was discarded. Please retry or send /clear to reset context."; } reply = cleaned; + } } AddAssistantMessage(reply); diff --git a/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs b/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs index 40d9b2d4..86f8fe80 100644 --- a/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs +++ b/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs @@ -30,13 +30,7 @@ namespace WulaFallenEmpire.EventSystem.AI.UI private Dictionary _portraits = new Dictionary(); private int _currentPortraitId = 0; private static readonly Regex ExpressionTagRegex = new Regex(@"\[EXPR\s*:\s*([1-6])\s*\]", RegexOptions.IgnoreCase); - private bool _reactTraceExpanded = false; - private bool _hasReactTrace = false; - private float _reactTraceHeight = 0f; - private float _reactTraceYOffset = 0f; - private float _reactTraceHeaderHeight = 0f; - private string _reactTraceHeader = ""; - private List _reactTraceLines = new List(); + private readonly Dictionary _traceExpandedByAssistantIndex = new Dictionary(); private class CachedMessage { @@ -46,13 +40,12 @@ namespace WulaFallenEmpire.EventSystem.AI.UI public float height; public float yOffset; public GameFont font; - } - - private class ReactTraceStep - { - public int Step; - public List Calls = new List(); - public List Results = new List(); + public bool isTrace; + public int traceKey; + public string traceHeader; + public List traceLines; + public bool traceExpanded; + public float traceHeaderHeight; } public static Dialog_AIConversation Instance { get; private set; } @@ -338,15 +331,91 @@ namespace WulaFallenEmpire.EventSystem.AI.UI float curY = 0f; float innerPadding = 5f; float contentWidth = width - innerPadding * 2; + var toolcallBuffer = new List(); + var toolResultBuffer = new List(); + bool traceEnabled = WulaFallenEmpireMod.settings?.showReactTraceInUI == true; for (int i = 0; i < history.Count; i++) { var entry = history[i]; - string messageText = entry.role == "assistant" ? ParseResponseForDisplay(entry.message) : AIIntelligenceCore.StripContextInfo(entry.message); - - if (entry.role == "tool" || entry.role == "system" || entry.role == "toolcall") continue; + if (entry.role == "user") + { + toolcallBuffer.Clear(); + toolResultBuffer.Clear(); + } + + if (entry.role == "toolcall") + { + if (traceEnabled) + { + toolcallBuffer.Add(entry.message ?? ""); + } + continue; + } + + if (entry.role == "tool") + { + if (traceEnabled) + { + toolResultBuffer.Add(entry.message ?? ""); + } + continue; + } + + string messageText = entry.role == "assistant" + ? ParseResponseForDisplay(entry.message) + : AIIntelligenceCore.StripContextInfo(entry.message); + + if (entry.role == "system") continue; // Hide auto-commentary system messages (user-side) from display if (entry.role == "user" && entry.message.Contains("[AUTO_COMMENTARY]")) continue; + if (entry.role == "assistant" && traceEnabled && toolcallBuffer.Count > 0) + { + var traceLines = BuildTraceLines(toolcallBuffer, toolResultBuffer); + if (traceLines.Count > 0) + { + int traceKey = i; + bool expanded = _traceExpandedByAssistantIndex.TryGetValue(traceKey, out bool saved) && saved; + string header = BuildReactTraceHeader(); + + Text.Font = GameFont.Tiny; + float tracePadding = 8f; + float headerWidth = Mathf.Max(10f, contentWidth - tracePadding * 2f); + string headerLine = $"{(expanded ? "v" : ">")} {header}"; + float headerHeight = Text.CalcHeight(headerLine, headerWidth) + 10f; + float linesHeight = 0f; + if (expanded) + { + float lineWidth = Mathf.Max(10f, contentWidth - tracePadding * 2f); + foreach (string line in traceLines) + { + linesHeight += Text.CalcHeight(line, lineWidth) + 2f; + } + linesHeight += 8f; + } + float traceHeight = headerHeight + linesHeight; + + _cachedMessages.Add(new CachedMessage + { + role = "trace", + message = "", + displayText = "", + height = traceHeight, + yOffset = curY, + font = GameFont.Tiny, + isTrace = true, + traceKey = traceKey, + traceHeader = header, + traceLines = traceLines, + traceExpanded = expanded, + traceHeaderHeight = headerHeight + }); + curY += traceHeight + 10f; + } + + toolcallBuffer.Clear(); + toolResultBuffer.Clear(); + } if (string.IsNullOrEmpty(messageText) || (entry.role == "user" && messageText.StartsWith("[Tool Results]"))) continue; bool isLastMessage = i == history.Count - 1; @@ -368,8 +437,6 @@ namespace WulaFallenEmpire.EventSystem.AI.UI curY += height + 10f; } - - UpdateReactTraceCache(history, contentWidth, ref curY); _cachedTotalHeight = curY; } @@ -407,7 +474,11 @@ namespace WulaFallenEmpire.EventSystem.AI.UI Text.Font = entry.font; Rect labelRect = new Rect(innerPadding, entry.yOffset, contentWidth, entry.height); - if (entry.role == "user") + if (entry.isTrace) + { + DrawReactTracePanel(labelRect, entry); + } + else if (entry.role == "user") { Text.Anchor = TextAnchor.MiddleRight; Widgets.Label(labelRect, $"{entry.displayText}"); @@ -426,15 +497,6 @@ namespace WulaFallenEmpire.EventSystem.AI.UI } } - if (_hasReactTrace) - { - Rect traceRect = new Rect(innerPadding, _reactTraceYOffset, contentWidth, _reactTraceHeight); - if (traceRect.yMax >= viewTop && traceRect.y <= viewBottom) - { - DrawReactTracePanel(traceRect); - } - } - if (_isThinking) { float thinkingY = _cachedTotalHeight > 0 ? _cachedTotalHeight : 0f; @@ -471,83 +533,20 @@ namespace WulaFallenEmpire.EventSystem.AI.UI return text.Split(new[] { "OPTIONS:" }, StringSplitOptions.None)[0].Trim(); } - private void UpdateReactTraceCache(List<(string role, string message)> history, float contentWidth, ref float curY) + private List BuildTraceLines(List toolcallBuffer, List toolResultBuffer) { - _hasReactTrace = false; - _reactTraceLines.Clear(); - _reactTraceHeader = ""; - _reactTraceHeight = 0f; - _reactTraceYOffset = 0f; - _reactTraceHeaderHeight = 0f; - - if (WulaFallenEmpireMod.settings?.showReactTraceInUI != true) - { - return; - } - - List steps = BuildReactTraceSteps(history); - if (steps.Count == 0) - { - return; - } - - _hasReactTrace = true; - _reactTraceHeader = BuildReactTraceHeader(); - foreach (var step in steps) - { - foreach (string call in step.Calls) - { - _reactTraceLines.Add($"Step {step.Step}: call {call}"); - } - foreach (string result in step.Results) - { - _reactTraceLines.Add($"Step {step.Step}: result {result}"); - } - } - - var originalFont = Text.Font; - Text.Font = GameFont.Tiny; - float panelPadding = 6f; - float headerWidth = Mathf.Max(10f, contentWidth - panelPadding * 2f); - string headerLine = $"{(_reactTraceExpanded ? "v" : ">")} {_reactTraceHeader}"; - _reactTraceHeaderHeight = Text.CalcHeight(headerLine, headerWidth) + 6f; - - float linesHeight = 0f; - if (_reactTraceExpanded && _reactTraceLines.Count > 0) - { - float lineWidth = Mathf.Max(10f, contentWidth - panelPadding * 2f); - foreach (string line in _reactTraceLines) - { - linesHeight += Text.CalcHeight(line, lineWidth) + 2f; - } - linesHeight += 4f; - } - _reactTraceHeight = _reactTraceHeaderHeight + linesHeight; - Text.Font = originalFont; - - curY += 6f; - _reactTraceYOffset = curY; - curY += _reactTraceHeight + 8f; - } - - private List BuildReactTraceSteps(List<(string role, string message)> history) - { - var steps = new List(); - if (history == null || history.Count == 0) return steps; - - int lastUserIndex = history.FindLastIndex(entry => entry.role == "user"); - if (lastUserIndex < 0) return steps; + var lines = new List(); + if (toolcallBuffer == null || toolcallBuffer.Count == 0) return lines; int stepIndex = 0; - for (int i = lastUserIndex + 1; i < history.Count; i++) + int maxSteps = Math.Max(toolcallBuffer.Count, toolResultBuffer.Count); + for (int i = 0; i < maxSteps; i++) { - var entry = history[i]; - if (entry.role != "toolcall") continue; - + bool anyStepContent = false; stepIndex++; - var step = new ReactTraceStep { Step = stepIndex }; - if (JsonToolCallParser.TryParseToolCallsFromText(entry.message, out var calls, out _)) + if (i < toolcallBuffer.Count && + JsonToolCallParser.TryParseToolCallsFromText(toolcallBuffer[i], out var calls, out _)) { foreach (var call in calls) { @@ -556,23 +555,27 @@ namespace WulaFallenEmpire.EventSystem.AI.UI string callText = string.IsNullOrWhiteSpace(args) || args == "{}" ? call.Name : $"{call.Name} {TrimForDisplay(args, 160)}"; - step.Calls.Add(callText); + lines.Add($"步骤 {stepIndex} · 调用 {callText}"); + anyStepContent = true; } } - if (i + 1 < history.Count && history[i + 1].role == "tool") + if (i < toolResultBuffer.Count) { - step.Results.AddRange(ExtractToolResultLines(history[i + 1].message, 4)); - i++; + foreach (string resultLine in ExtractToolResultLines(toolResultBuffer[i], 4)) + { + lines.Add($"步骤 {stepIndex} · 结果 {resultLine}"); + anyStepContent = true; + } } - if (step.Calls.Count > 0 || step.Results.Count > 0) + if (!anyStepContent) { - steps.Add(step); + stepIndex--; } } - return steps; + return lines; } private static List ExtractToolResultLines(string toolMessage, int maxLines) @@ -586,6 +589,13 @@ namespace WulaFallenEmpire.EventSystem.AI.UI string line = raw.Trim(); if (string.IsNullOrEmpty(line)) continue; if (line.StartsWith("[Tool Results]", StringComparison.OrdinalIgnoreCase)) continue; + if (!line.StartsWith("Tool '", StringComparison.OrdinalIgnoreCase) && + !line.StartsWith("ToolRunner", StringComparison.OrdinalIgnoreCase) && + !line.StartsWith("Query Result:", StringComparison.OrdinalIgnoreCase) && + !line.StartsWith("Error:", StringComparison.OrdinalIgnoreCase)) + { + continue; + } lines.Add(TrimForDisplay(line, 200)); if (lines.Count >= maxLines) break; } @@ -606,37 +616,43 @@ namespace WulaFallenEmpire.EventSystem.AI.UI ? Mathf.Max(0f, Time.realtimeSinceStartup - (_core?.ThinkingStartTime ?? 0f)) : _core?.LastThinkingDuration ?? 0f; string elapsedText = elapsed > 0f ? elapsed.ToString("0.0", CultureInfo.InvariantCulture) : "0.0"; - return $"{state} ({elapsedText}s · Loop {_core?.ThinkingPhaseIndex ?? 0}/{_core?.ThinkingPhaseTotal ?? 0})"; + return $"{state} (用时 {elapsedText}s · Loop {_core?.ThinkingPhaseIndex ?? 0}/{_core?.ThinkingPhaseTotal ?? 0})"; } - private void DrawReactTracePanel(Rect rect) + private void DrawReactTracePanel(Rect rect, CachedMessage traceEntry) { var originalColor = GUI.color; var originalFont = Text.Font; var originalAnchor = Text.Anchor; - float padding = 6f; - Rect headerRect = new Rect(rect.x, rect.y, rect.width, _reactTraceHeaderHeight); + float padding = 8f; + Rect headerRect = new Rect(rect.x, rect.y, rect.width, traceEntry.traceHeaderHeight); GUI.color = new Color(0.15f, 0.15f, 0.15f, 0.8f); Widgets.DrawBoxSolid(headerRect, GUI.color); GUI.color = Color.white; Text.Font = GameFont.Tiny; Text.Anchor = TextAnchor.MiddleLeft; - string headerLine = $"{(_reactTraceExpanded ? "v" : ">")} {_reactTraceHeader}"; - Widgets.Label(headerRect.ContractedBy(padding), headerLine); + string headerLine = $"{(traceEntry.traceExpanded ? "v" : ">")} {traceEntry.traceHeader}"; + Widgets.Label(headerRect.ContractedBy(padding, 4f), headerLine); if (Widgets.ButtonInvisible(headerRect)) { - _reactTraceExpanded = !_reactTraceExpanded; + traceEntry.traceExpanded = !traceEntry.traceExpanded; + _traceExpandedByAssistantIndex[traceEntry.traceKey] = traceEntry.traceExpanded; _lastHistoryCount = -1; _lastUsedWidth = -1f; } - if (_reactTraceExpanded && _reactTraceLines.Count > 0) + if (traceEntry.traceExpanded && traceEntry.traceLines != null && traceEntry.traceLines.Count > 0) { - float y = headerRect.yMax + 2f; - foreach (string line in _reactTraceLines) + Rect bodyRect = new Rect(rect.x, headerRect.yMax, rect.width, rect.height - traceEntry.traceHeaderHeight); + GUI.color = new Color(0.12f, 0.12f, 0.12f, 0.45f); + Widgets.DrawBoxSolid(bodyRect, GUI.color); + GUI.color = Color.white; + + float y = headerRect.yMax + 6f; + foreach (string line in traceEntry.traceLines) { float lineHeight = Text.CalcHeight(line, rect.width - padding * 2f) + 2f; Rect lineRect = new Rect(rect.x + padding, y, rect.width - padding * 2f, lineHeight); diff --git a/Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink.cs b/Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink.cs index 44d98a32..75f74016 100644 --- a/Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink.cs +++ b/Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink.cs @@ -25,13 +25,7 @@ namespace WulaFallenEmpire.EventSystem.AI.UI private float _lastUsedWidth = -1f; private List _cachedMessages = new List(); private float _cachedTotalHeight = 0f; - private bool _reactTraceExpanded = false; - private bool _hasReactTrace = false; - private float _reactTraceHeight = 0f; - private float _reactTraceYOffset = 0f; - private float _reactTraceHeaderHeight = 0f; - private string _reactTraceHeader = ""; - private List _reactTraceLines = new List(); + private readonly Dictionary _traceExpandedByAssistantIndex = new Dictionary(); private class CachedMessage { @@ -40,13 +34,12 @@ namespace WulaFallenEmpire.EventSystem.AI.UI public string displayText; public float height; public float yOffset; - } - - private class ReactTraceStep - { - public int Step; - public List Calls = new List(); - public List Results = new List(); + public bool isTrace; + public int traceKey; + public string traceHeader; + public List traceLines; + public bool traceExpanded; + public float traceHeaderHeight; } @@ -389,11 +382,38 @@ namespace WulaFallenEmpire.EventSystem.AI.UI _cachedTotalHeight = 0f; float reducedSpacing = 8f; float curY = 10f; + var toolcallBuffer = new List(); + var toolResultBuffer = new List(); + bool traceEnabled = WulaFallenEmpireMod.settings?.showReactTraceInUI == true; - foreach (var msg in history) + for (int i = 0; i < history.Count; i++) { + var msg = history[i]; // Filter logic - if (msg.role == "tool" || msg.role == "toolcall") continue; + if (msg.role == "user") + { + toolcallBuffer.Clear(); + toolResultBuffer.Clear(); + } + + if (msg.role == "toolcall") + { + if (traceEnabled) + { + toolcallBuffer.Add(msg.message ?? ""); + } + continue; + } + + if (msg.role == "tool") + { + if (traceEnabled) + { + toolResultBuffer.Add(msg.message ?? ""); + } + continue; + } + if (msg.role == "system" && !Prefs.DevMode) continue; // Hide auto-commentary system messages (user-side) from display @@ -409,6 +429,53 @@ namespace WulaFallenEmpire.EventSystem.AI.UI displayText = AIIntelligenceCore.StripContextInfo(msg.message); } + if (msg.role == "assistant" && traceEnabled && toolcallBuffer.Count > 0) + { + var traceLines = BuildTraceLines(toolcallBuffer, toolResultBuffer); + if (traceLines.Count > 0) + { + int traceKey = i; + bool expanded = _traceExpandedByAssistantIndex.TryGetValue(traceKey, out bool saved) && saved; + string header = BuildReactTraceHeader(); + + Text.Font = GameFont.Tiny; + float padding = 8f; + float headerWidth = Mathf.Max(10f, width - padding * 2f); + string headerLine = $"{(expanded ? "v" : ">")} {header}"; + float headerHeight = Text.CalcHeight(headerLine, headerWidth) + 10f; + float linesHeight = 0f; + if (expanded) + { + float lineWidth = Mathf.Max(10f, width - padding * 2f); + foreach (string line in traceLines) + { + linesHeight += Text.CalcHeight(line, lineWidth) + 2f; + } + linesHeight += 8f; + } + float traceHeight = headerHeight + linesHeight; + + _cachedMessages.Add(new CachedMessage + { + role = "trace", + message = "", + displayText = "", + height = traceHeight, + yOffset = curY, + isTrace = true, + traceKey = traceKey, + traceHeader = header, + traceLines = traceLines, + traceExpanded = expanded, + traceHeaderHeight = headerHeight + }); + curY += traceHeight + reducedSpacing; + } + + toolcallBuffer.Clear(); + toolResultBuffer.Clear(); + } + if (string.IsNullOrWhiteSpace(displayText)) continue; float h = CalcMessageHeight(displayText, width); @@ -425,7 +492,6 @@ namespace WulaFallenEmpire.EventSystem.AI.UI curY += h + reducedSpacing; } - UpdateReactTraceCache(history, width, ref curY); _cachedTotalHeight = curY; } @@ -461,8 +527,12 @@ namespace WulaFallenEmpire.EventSystem.AI.UI if (entry.yOffset > viewBottom + 100f) break; Rect msgRect = new Rect(0, entry.yOffset, width, entry.height); - - if (entry.role == "user") + + if (entry.isTrace) + { + DrawReactTracePanel(msgRect, entry); + } + else if (entry.role == "user") { DrawSenseiMessage(msgRect, entry.displayText); } @@ -480,15 +550,6 @@ namespace WulaFallenEmpire.EventSystem.AI.UI } } - if (_hasReactTrace) - { - Rect traceRect = new Rect(0, _reactTraceYOffset, width, _reactTraceHeight); - if (traceRect.yMax >= viewTop && traceRect.y <= viewBottom) - { - DrawReactTracePanel(traceRect); - } - } - if (_core != null && _core.IsThinking) { float thinkingY = _cachedTotalHeight > 0 ? _cachedTotalHeight : 10f; @@ -515,83 +576,20 @@ namespace WulaFallenEmpire.EventSystem.AI.UI return text.Remove(index, fragment.Length).Trim(); } - private void UpdateReactTraceCache(List<(string role, string message)> history, float contentWidth, ref float curY) + private List BuildTraceLines(List toolcallBuffer, List toolResultBuffer) { - _hasReactTrace = false; - _reactTraceLines.Clear(); - _reactTraceHeader = ""; - _reactTraceHeight = 0f; - _reactTraceYOffset = 0f; - _reactTraceHeaderHeight = 0f; - - if (WulaFallenEmpireMod.settings?.showReactTraceInUI != true) - { - return; - } - - List steps = BuildReactTraceSteps(history); - if (steps.Count == 0) - { - return; - } - - _hasReactTrace = true; - _reactTraceHeader = BuildReactTraceHeader(); - foreach (var step in steps) - { - foreach (string call in step.Calls) - { - _reactTraceLines.Add($"Step {step.Step}: call {call}"); - } - foreach (string result in step.Results) - { - _reactTraceLines.Add($"Step {step.Step}: result {result}"); - } - } - - var originalFont = Text.Font; - Text.Font = GameFont.Tiny; - float panelPadding = 8f; - float headerWidth = Mathf.Max(10f, contentWidth - panelPadding * 2f); - string headerLine = $"{(_reactTraceExpanded ? "v" : ">")} {_reactTraceHeader}"; - _reactTraceHeaderHeight = Text.CalcHeight(headerLine, headerWidth) + 6f; - - float linesHeight = 0f; - if (_reactTraceExpanded && _reactTraceLines.Count > 0) - { - float lineWidth = Mathf.Max(10f, contentWidth - panelPadding * 2f); - foreach (string line in _reactTraceLines) - { - linesHeight += Text.CalcHeight(line, lineWidth) + 2f; - } - linesHeight += 6f; - } - _reactTraceHeight = _reactTraceHeaderHeight + linesHeight; - Text.Font = originalFont; - - curY += 6f; - _reactTraceYOffset = curY; - curY += _reactTraceHeight + 6f; - } - - private List BuildReactTraceSteps(List<(string role, string message)> history) - { - var steps = new List(); - if (history == null || history.Count == 0) return steps; - - int lastUserIndex = history.FindLastIndex(entry => entry.role == "user"); - if (lastUserIndex < 0) return steps; + var lines = new List(); + if (toolcallBuffer == null || toolcallBuffer.Count == 0) return lines; int stepIndex = 0; - for (int i = lastUserIndex + 1; i < history.Count; i++) + int maxSteps = Math.Max(toolcallBuffer.Count, toolResultBuffer.Count); + for (int i = 0; i < maxSteps; i++) { - var entry = history[i]; - if (entry.role != "toolcall") continue; - + bool anyStepContent = false; stepIndex++; - var step = new ReactTraceStep { Step = stepIndex }; - if (JsonToolCallParser.TryParseToolCallsFromText(entry.message, out var calls, out _)) + if (i < toolcallBuffer.Count && + JsonToolCallParser.TryParseToolCallsFromText(toolcallBuffer[i], out var calls, out _)) { foreach (var call in calls) { @@ -600,23 +598,27 @@ namespace WulaFallenEmpire.EventSystem.AI.UI string callText = string.IsNullOrWhiteSpace(args) || args == "{}" ? call.Name : $"{call.Name} {TrimForDisplay(args, 160)}"; - step.Calls.Add(callText); + lines.Add($"步骤 {stepIndex} · 调用 {callText}"); + anyStepContent = true; } } - if (i + 1 < history.Count && history[i + 1].role == "tool") + if (i < toolResultBuffer.Count) { - step.Results.AddRange(ExtractToolResultLines(history[i + 1].message, 4)); - i++; + foreach (string resultLine in ExtractToolResultLines(toolResultBuffer[i], 4)) + { + lines.Add($"步骤 {stepIndex} · 结果 {resultLine}"); + anyStepContent = true; + } } - if (step.Calls.Count > 0 || step.Results.Count > 0) + if (!anyStepContent) { - steps.Add(step); + stepIndex--; } } - return steps; + return lines; } private static List ExtractToolResultLines(string toolMessage, int maxLines) @@ -630,6 +632,13 @@ namespace WulaFallenEmpire.EventSystem.AI.UI string line = raw.Trim(); if (string.IsNullOrEmpty(line)) continue; if (line.StartsWith("[Tool Results]", StringComparison.OrdinalIgnoreCase)) continue; + if (!line.StartsWith("Tool '", StringComparison.OrdinalIgnoreCase) && + !line.StartsWith("ToolRunner", StringComparison.OrdinalIgnoreCase) && + !line.StartsWith("Query Result:", StringComparison.OrdinalIgnoreCase) && + !line.StartsWith("Error:", StringComparison.OrdinalIgnoreCase)) + { + continue; + } lines.Add(TrimForDisplay(line, 200)); if (lines.Count >= maxLines) break; } @@ -653,37 +662,43 @@ namespace WulaFallenEmpire.EventSystem.AI.UI string elapsedText = elapsed > 0f ? elapsed.ToString("0.0", System.Globalization.CultureInfo.InvariantCulture) : "0.0"; int phaseIndex = _core?.ThinkingPhaseIndex ?? 0; int phaseTotal = _core?.ThinkingPhaseTotal ?? 0; - return $"{state} ({elapsedText}s · Loop {phaseIndex}/{phaseTotal})"; + return $"{state} (用时 {elapsedText}s · Loop {phaseIndex}/{phaseTotal})"; } - private void DrawReactTracePanel(Rect rect) + private void DrawReactTracePanel(Rect rect, CachedMessage traceEntry) { var originalColor = GUI.color; var originalFont = Text.Font; var originalAnchor = Text.Anchor; float padding = 8f; - Rect headerRect = new Rect(rect.x, rect.y, rect.width, _reactTraceHeaderHeight); + Rect headerRect = new Rect(rect.x, rect.y, rect.width, traceEntry.traceHeaderHeight); GUI.color = new Color(0.12f, 0.12f, 0.12f, 0.8f); Widgets.DrawBoxSolid(headerRect, GUI.color); GUI.color = Color.white; Text.Font = GameFont.Tiny; Text.Anchor = TextAnchor.MiddleLeft; - string headerLine = $"{(_reactTraceExpanded ? "v" : ">")} {_reactTraceHeader}"; - Widgets.Label(headerRect.ContractedBy(padding), headerLine); + string headerLine = $"{(traceEntry.traceExpanded ? "v" : ">")} {traceEntry.traceHeader}"; + Widgets.Label(headerRect.ContractedBy(padding, 4f), headerLine); if (Widgets.ButtonInvisible(headerRect)) { - _reactTraceExpanded = !_reactTraceExpanded; + traceEntry.traceExpanded = !traceEntry.traceExpanded; + _traceExpandedByAssistantIndex[traceEntry.traceKey] = traceEntry.traceExpanded; _lastHistoryCount = -1; _lastUsedWidth = -1f; } - if (_reactTraceExpanded && _reactTraceLines.Count > 0) + if (traceEntry.traceExpanded && traceEntry.traceLines != null && traceEntry.traceLines.Count > 0) { - float y = headerRect.yMax + 2f; - foreach (string line in _reactTraceLines) + Rect bodyRect = new Rect(rect.x, headerRect.yMax, rect.width, rect.height - traceEntry.traceHeaderHeight); + GUI.color = new Color(0.1f, 0.1f, 0.1f, 0.45f); + Widgets.DrawBoxSolid(bodyRect, GUI.color); + GUI.color = Color.white; + + float y = headerRect.yMax + 6f; + foreach (string line in traceEntry.traceLines) { float lineHeight = Text.CalcHeight(line, rect.width - padding * 2f) + 2f; Rect lineRect = new Rect(rect.x + padding, y, rect.width - padding * 2f, lineHeight);