This commit is contained in:
2025-12-14 10:00:26 +08:00
parent 6a513180ae
commit c00fc0743b
6 changed files with 254 additions and 46 deletions

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<LanguageData>
<Wula_ResourceDrop>{FACTION_name}已经在附近投下了一些资源。</Wula_ResourceDrop>
</LanguageData>

View File

@@ -18,6 +18,9 @@
}, },
{ {
"path": "../../../../../../../../Users/Kalo/Downloads/RimTalk-main" "path": "../../../../../../../../Users/Kalo/Downloads/RimTalk-main"
},
{
"path": "../../../../../../../../Users/Kalo/Downloads/openai_token-main"
} }
], ],
"settings": {} "settings": {}

View File

@@ -12,6 +12,7 @@ namespace WulaFallenEmpire.EventSystem.AI
private readonly string _apiKey; private readonly string _apiKey;
private readonly string _baseUrl; private readonly string _baseUrl;
private readonly string _model; private readonly string _model;
private const int MaxLogChars = 2000;
public SimpleAIClient(string apiKey, string baseUrl, string model) public SimpleAIClient(string apiKey, string baseUrl, string model)
{ {
@@ -41,28 +42,36 @@ namespace WulaFallenEmpire.EventSystem.AI
jsonBuilder.Append("\"messages\": ["); jsonBuilder.Append("\"messages\": [");
// System instruction // System instruction
bool firstMessage = true;
if (!string.IsNullOrEmpty(instruction)) if (!string.IsNullOrEmpty(instruction))
{ {
jsonBuilder.Append($"{{\"role\": \"system\", \"content\": \"{EscapeJson(instruction)}\"}},"); jsonBuilder.Append($"{{\"role\": \"system\", \"content\": \"{EscapeJson(instruction)}\"}}");
firstMessage = false;
} }
// Messages // Messages
for (int i = 0; i < messages.Count; i++) for (int i = 0; i < messages.Count; i++)
{ {
var msg = messages[i]; var msg = messages[i];
string role = msg.role.ToLower(); string role = (msg.role ?? "user").ToLowerInvariant();
if (role == "ai") role = "assistant"; if (role == "ai") role = "assistant";
// Map other roles if needed else if (role == "tool") role = "system"; // Internal-only role; map to supported role for Chat Completions APIs.
else if (role != "system" && role != "user" && role != "assistant") role = "user";
if (!firstMessage) jsonBuilder.Append(",");
jsonBuilder.Append($"{{\"role\": \"{role}\", \"content\": \"{EscapeJson(msg.message)}\"}}"); jsonBuilder.Append($"{{\"role\": \"{role}\", \"content\": \"{EscapeJson(msg.message)}\"}}");
if (i < messages.Count - 1) jsonBuilder.Append(","); firstMessage = false;
} }
jsonBuilder.Append("]"); jsonBuilder.Append("]");
jsonBuilder.Append("}"); jsonBuilder.Append("}");
string jsonBody = jsonBuilder.ToString(); string jsonBody = jsonBuilder.ToString();
Log.Message($"[WulaAI] Sending request to {endpoint}:\n{jsonBody}"); if (Prefs.DevMode)
{
Log.Message($"[WulaAI] Sending request to {endpoint} (model={_model}, messages={messages?.Count ?? 0})");
Log.Message($"[WulaAI] Request body (truncated):\n{TruncateForLog(jsonBody)}");
}
using (UnityWebRequest request = new UnityWebRequest(endpoint, "POST")) using (UnityWebRequest request = new UnityWebRequest(endpoint, "POST"))
{ {
@@ -84,24 +93,57 @@ namespace WulaFallenEmpire.EventSystem.AI
if (request.result == UnityWebRequest.Result.ConnectionError || request.result == UnityWebRequest.Result.ProtocolError) if (request.result == UnityWebRequest.Result.ConnectionError || request.result == UnityWebRequest.Result.ProtocolError)
{ {
Log.Error($"[WulaAI] API Error: {request.error}\nResponse: {request.downloadHandler.text}"); Log.Error($"[WulaAI] API Error: {request.error}\nResponse (truncated): {TruncateForLog(request.downloadHandler.text)}");
return null; return null;
} }
string responseText = request.downloadHandler.text; string responseText = request.downloadHandler.text;
Log.Message($"[WulaAI] Raw Response: {responseText}"); if (Prefs.DevMode)
{
Log.Message($"[WulaAI] Raw Response (truncated): {TruncateForLog(responseText)}");
}
return ExtractContent(responseText); return ExtractContent(responseText);
} }
} }
private static string TruncateForLog(string s)
{
if (string.IsNullOrEmpty(s)) return s;
if (s.Length <= MaxLogChars) return s;
return s.Substring(0, MaxLogChars) + $"... (truncated, total {s.Length} chars)";
}
private string EscapeJson(string s) private string EscapeJson(string s)
{ {
if (s == null) return ""; if (s == null) return "";
return s.Replace("\\", "\\\\")
.Replace("\"", "\\\"") StringBuilder sb = new StringBuilder(s.Length + 16);
.Replace("\n", "\\n") for (int i = 0; i < s.Length; i++)
.Replace("\r", "\\r") {
.Replace("\t", "\\t"); char c = s[i];
switch (c)
{
case '\\': sb.Append("\\\\"); break;
case '"': sb.Append("\\\""); break;
case '\n': sb.Append("\\n"); break;
case '\r': sb.Append("\\r"); break;
case '\t': sb.Append("\\t"); break;
case '\b': sb.Append("\\b"); break;
case '\f': sb.Append("\\f"); break;
default:
if (c < 0x20)
{
sb.Append("\\u");
sb.Append(((int)c).ToString("x4"));
}
else
{
sb.Append(c);
}
break;
}
}
return sb.ToString();
} }
private string ExtractContent(string json) private string ExtractContent(string json)
@@ -116,13 +158,13 @@ namespace WulaFallenEmpire.EventSystem.AI
string[] lines = json.Split(new[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries); string[] lines = json.Split(new[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries);
foreach (string line in lines) foreach (string line in lines)
{ {
string trimmedLine = line.Trim(); string trimmedLine = line.Trim();
if (trimmedLine == "data: [DONE]") continue; if (trimmedLine == "data: [DONE]") continue;
if (trimmedLine.StartsWith("data: ")) if (trimmedLine.StartsWith("data: "))
{ {
string dataJson = trimmedLine.Substring(6); string dataJson = trimmedLine.Substring(6);
// Extract content from this chunk // Extract content from this chunk
string chunkContent = ExtractJsonValue(dataJson, "content"); string chunkContent = TryExtractAssistantContent(dataJson) ?? ExtractJsonValue(dataJson, "content");
if (!string.IsNullOrEmpty(chunkContent)) if (!string.IsNullOrEmpty(chunkContent))
{ {
fullContent.Append(chunkContent); fullContent.Append(chunkContent);
@@ -134,7 +176,7 @@ namespace WulaFallenEmpire.EventSystem.AI
else else
{ {
// Standard non-stream format // Standard non-stream format
return ExtractJsonValue(json, "content"); return TryExtractAssistantContent(json) ?? ExtractJsonValue(json, "content");
} }
} }
catch (Exception ex) catch (Exception ex)
@@ -144,12 +186,109 @@ namespace WulaFallenEmpire.EventSystem.AI
return null; return null;
} }
private string ExtractJsonValue(string json, string key) private static string TryExtractAssistantContent(string json)
{
if (string.IsNullOrWhiteSpace(json)) return null;
int choicesIndex = json.IndexOf("\"choices\"", StringComparison.Ordinal);
if (choicesIndex == -1) return null;
string firstChoiceJson = TryExtractFirstChoiceObject(json, choicesIndex);
if (string.IsNullOrEmpty(firstChoiceJson)) return null;
int messageIndex = firstChoiceJson.IndexOf("\"message\"", StringComparison.Ordinal);
if (messageIndex != -1)
{
return ExtractJsonValue(firstChoiceJson, "content", messageIndex);
}
int deltaIndex = firstChoiceJson.IndexOf("\"delta\"", StringComparison.Ordinal);
if (deltaIndex != -1)
{
return ExtractJsonValue(firstChoiceJson, "content", deltaIndex);
}
return ExtractJsonValue(firstChoiceJson, "text", 0);
}
private static string TryExtractFirstChoiceObject(string json, int choicesKeyIndex)
{
int arrayStart = json.IndexOf('[', choicesKeyIndex);
if (arrayStart == -1) return null;
int objStart = json.IndexOf('{', arrayStart);
if (objStart == -1) return null;
int objEnd = FindMatchingBrace(json, objStart);
if (objEnd == -1) return null;
return json.Substring(objStart, objEnd - objStart + 1);
}
private static int FindMatchingBrace(string json, int startIndex)
{
int depth = 0;
bool inString = false;
bool escaped = false;
for (int i = startIndex; i < json.Length; i++)
{
char c = json[i];
if (inString)
{
if (escaped)
{
escaped = false;
continue;
}
if (c == '\\')
{
escaped = true;
continue;
}
if (c == '"')
{
inString = false;
}
continue;
}
if (c == '"')
{
inString = true;
continue;
}
if (c == '{')
{
depth++;
continue;
}
if (c == '}')
{
depth--;
if (depth == 0) return i;
}
}
return -1;
}
private static string ExtractJsonValue(string json, string key)
{ {
// Simple parser to find "key": "value" // Simple parser to find "key": "value"
// This is not a full JSON parser and assumes standard formatting // This is not a full JSON parser and assumes standard formatting
return ExtractJsonValue(json, key, 0);
}
private static string ExtractJsonValue(string json, string key, int startIndex)
{
string keyPattern = $"\"{key}\""; string keyPattern = $"\"{key}\"";
int keyIndex = json.IndexOf(keyPattern); int keyIndex = json.IndexOf(keyPattern, startIndex, StringComparison.Ordinal);
if (keyIndex == -1) return null; if (keyIndex == -1) return null;
// Find the colon after the key // Find the colon after the key
@@ -196,4 +335,4 @@ namespace WulaFallenEmpire.EventSystem.AI
return null; return null;
} }
} }
} }

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using RimWorld; using RimWorld;
@@ -90,16 +91,24 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
if (itemsToSpawn.Count == 0) if (itemsToSpawn.Count == 0)
{ {
return "Error: No valid items found in request. Usage: <spawn_resources><items><item><name>...</name><count>...</count></item></items></spawn_resources>"; string msg = "Error: No valid items found in request. Usage: <spawn_resources><items><item><name>...</name><count>...</count></item></items></spawn_resources>";
Messages.Message(msg, MessageTypeDefOf.RejectInput);
return msg;
} }
Map map = Find.CurrentMap; Map map = GetTargetMap();
if (map == null) if (map == null)
{ {
return "Error: No active map."; string msg = "Error: No active map.";
Messages.Message(msg, MessageTypeDefOf.RejectInput);
return msg;
} }
IntVec3 dropSpot = DropCellFinder.TradeDropSpot(map); IntVec3 dropSpot = DropCellFinder.TradeDropSpot(map);
if (!dropSpot.IsValid || !dropSpot.InBounds(map))
{
dropSpot = DropCellFinder.RandomDropSpot(map);
}
List<Thing> thingsToDrop = new List<Thing>(); List<Thing> thingsToDrop = new List<Thing>();
StringBuilder resultLog = new StringBuilder(); StringBuilder resultLog = new StringBuilder();
resultLog.Append("Success: Dropped "); resultLog.Append("Success: Dropped ");
@@ -114,27 +123,60 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
if (thingsToDrop.Count > 0) if (thingsToDrop.Count > 0)
{ {
DropPodUtility.DropThingsNear(dropSpot, map, thingsToDrop); // If the conversation window pauses the game, incoming drop pods may not "land" until unpaused.
// To keep this tool reliable, place items immediately when paused; otherwise, use drop pods.
Faction faction = Find.FactionManager.FirstFactionOfDef(FactionDef.Named("Wula_PIA_Legion_Faction")); bool isPaused = Find.TickManager != null && Find.TickManager.Paused;
if (faction != null) if (isPaused)
{ {
Messages.Message("Wula_ResourceDrop".Translate(faction.def.defName.Named("FACTION_name")), new LookTargets(dropSpot, map), MessageTypeDefOf.PositiveEvent); foreach (var thing in thingsToDrop)
{
GenPlace.TryPlaceThing(thing, dropSpot, map, ThingPlaceMode.Near);
}
}
else
{
DropPodUtility.DropThingsNear(dropSpot, map, thingsToDrop);
} }
Faction faction = Find.FactionManager.FirstFactionOfDef(FactionDef.Named("Wula_PIA_Legion_Faction"));
string letterText = faction != null
? "Wula_ResourceDrop".Translate(faction.def.defName.Named("FACTION_name"))
: "Wula_ResourceDrop".Translate("Unknown".Named("FACTION_name"));
Messages.Message(letterText, new LookTargets(dropSpot, map), MessageTypeDefOf.PositiveEvent);
resultLog.Length -= 2; // Remove trailing comma resultLog.Length -= 2; // Remove trailing comma
resultLog.Append($" at {dropSpot}."); resultLog.Append($" at {dropSpot}. {(isPaused ? "(placed immediately because game is paused)" : "(drop pods inbound)")}");
return resultLog.ToString(); return resultLog.ToString();
} }
else else
{ {
return "Error: Failed to create items."; string msg = "Error: Failed to create items.";
Messages.Message(msg, MessageTypeDefOf.RejectInput);
return msg;
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
return $"Error: {ex.Message}"; string msg = $"Error: {ex.Message}";
Messages.Message(msg, MessageTypeDefOf.RejectInput);
return msg;
} }
} }
private static Map GetTargetMap()
{
Map map = Find.CurrentMap;
if (map != null) return map;
if (Find.Maps != null)
{
Map homeMap = Find.Maps.FirstOrDefault(m => m != null && m.IsPlayerHome);
if (homeMap != null) return homeMap;
if (Find.Maps.Count > 0) return Find.Maps[0];
}
return null;
}
} }
} }

View File

@@ -228,6 +228,19 @@ When the player requests any form of resources, you MUST follow this multi-turn
StartConversation(); StartConversation();
} }
private void PersistHistory()
{
try
{
var historyManager = Find.World?.GetComponent<AIHistoryManager>();
historyManager?.SaveHistory(def.defName, _history);
}
catch (Exception ex)
{
Log.Error($"[WulaAI] Failed to persist AI history: {ex}");
}
}
private void LoadPortraits() private void LoadPortraits()
{ {
for (int i = 1; i <= 6; i++) for (int i = 1; i <= 6; i++)
@@ -281,6 +294,7 @@ When the player requests any form of resources, you MUST follow this multi-turn
if (_history.Count == 0) if (_history.Count == 0)
{ {
_history.Add(("user", "Hello")); _history.Add(("user", "Hello"));
PersistHistory();
await GenerateResponse(); await GenerateResponse();
} }
else else
@@ -379,6 +393,7 @@ When the player requests any form of resources, you MUST follow this multi-turn
{ {
_history.RemoveRange(0, removeCount); _history.RemoveRange(0, removeCount);
_history.Insert(0, ("system", "[Previous conversation summarized]")); _history.Insert(0, ("system", "[Previous conversation summarized]"));
PersistHistory();
} }
} }
} }
@@ -422,8 +437,17 @@ When the player requests any form of resources, you MUST follow this multi-turn
argsXml = contentMatch.Groups[1].Value; argsXml = contentMatch.Groups[1].Value;
} }
Log.Message($"[WulaAI] Executing tool: {toolName} with args: {argsXml}"); if (Prefs.DevMode)
{
Log.Message($"[WulaAI] Executing tool: {toolName} with args: {argsXml}");
}
string result = tool.Execute(argsXml).Trim(); string result = tool.Execute(argsXml).Trim();
if (Prefs.DevMode && !string.IsNullOrEmpty(result))
{
string toLog = result.Length <= 2000 ? result : result.Substring(0, 2000) + $"... (truncated, total {result.Length} chars)";
Log.Message($"[WulaAI] Tool '{toolName}' result: {toLog}");
}
if (toolName == "modify_goodwill") if (toolName == "modify_goodwill")
{ {
@@ -436,9 +460,9 @@ When the player requests any form of resources, you MUST follow this multi-turn
} }
_history.Add(("assistant", xml)); _history.Add(("assistant", xml));
// Use role "user" for tool results to avoid API errors about tool_calls format. // Persist tool results with a dedicated role; the API request maps this role to a supported one.
// This is safe and won't break AI logic, as the AI still sees the result clearly. _history.Add(("tool", $"[Tool Results]\n{combinedResults.ToString().Trim()}"));
_history.Add(("user", $"[Tool Results]\n{combinedResults.ToString().Trim()}")); PersistHistory();
// Check if there is any text content in the response // Check if there is any text content in the response
string textContent = Regex.Replace(xml, @"<([a-zA-Z0-9_]+)(?:>.*?</\1>|/>)", "", RegexOptions.Singleline).Trim(); string textContent = Regex.Replace(xml, @"<([a-zA-Z0-9_]+)(?:>.*?</\1>|/>)", "", RegexOptions.Singleline).Trim();
@@ -460,6 +484,7 @@ When the player requests any form of resources, you MUST follow this multi-turn
{ {
Log.Error($"[WulaAI] Exception in HandleXmlToolUsage: {ex}"); Log.Error($"[WulaAI] Exception in HandleXmlToolUsage: {ex}");
_history.Add(("tool", $"Error processing tool call: {ex.Message}")); _history.Add(("tool", $"Error processing tool call: {ex.Message}"));
PersistHistory();
await GenerateResponse(isContinuation: true); await GenerateResponse(isContinuation: true);
} }
} }
@@ -473,6 +498,7 @@ When the player requests any form of resources, you MUST follow this multi-turn
if (_history.Count == 0 || _history.Last().role != "assistant" || _history.Last().message != rawResponse) if (_history.Count == 0 || _history.Last().role != "assistant" || _history.Last().message != rawResponse)
{ {
_history.Add(("assistant", rawResponse)); _history.Add(("assistant", rawResponse));
PersistHistory();
} }
} }
@@ -563,7 +589,9 @@ When the player requests any form of resources, you MUST follow this multi-turn
try try
{ {
float viewHeight = 0f; float viewHeight = 0f;
var filteredHistory = _history.Where(e => e.role != "tool" && e.role != "system").ToList(); var filteredHistory = _history
.Where(e => e.role != "system" && (Prefs.DevMode || e.role != "tool"))
.ToList();
// Pre-calculate height // Pre-calculate height
for (int i = 0; i < filteredHistory.Count; i++) for (int i = 0; i < filteredHistory.Count; i++)
{ {
@@ -603,7 +631,7 @@ When the player requests any form of resources, you MUST follow this multi-turn
if (entry.role == "user") if (entry.role == "user")
{ {
Text.Anchor = TextAnchor.MiddleRight; Text.Anchor = TextAnchor.MiddleRight;
Widgets.Label(labelRect, $"<color=#add8e6>{text} :你</color>"); Widgets.Label(labelRect, $"<color=#add8e6>{text}</color>");
} }
else else
{ {
@@ -707,6 +735,7 @@ When the player requests any form of resources, you MUST follow this multi-turn
private async void SelectOption(string text) private async void SelectOption(string text)
{ {
_history.Add(("user", text)); _history.Add(("user", text));
PersistHistory();
_scrollToBottom = true; _scrollToBottom = true;
await GenerateResponse(); await GenerateResponse();
} }
@@ -714,6 +743,7 @@ When the player requests any form of resources, you MUST follow this multi-turn
public override void PostClose() public override void PostClose()
{ {
if (Instance == this) Instance = null; if (Instance == this) Instance = null;
PersistHistory();
base.PostClose(); base.PostClose();
HandleAction(def.dismissEffects); HandleAction(def.dismissEffects);
} }