zc'
This commit is contained in:
Binary file not shown.
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<LanguageData>
|
||||
|
||||
<Wula_ResourceDrop>{FACTION_name}已经在附近投下了一些资源。</Wula_ResourceDrop>
|
||||
|
||||
</LanguageData>
|
||||
@@ -18,6 +18,9 @@
|
||||
},
|
||||
{
|
||||
"path": "../../../../../../../../Users/Kalo/Downloads/RimTalk-main"
|
||||
},
|
||||
{
|
||||
"path": "../../../../../../../../Users/Kalo/Downloads/openai_token-main"
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace WulaFallenEmpire.EventSystem.AI
|
||||
private readonly string _apiKey;
|
||||
private readonly string _baseUrl;
|
||||
private readonly string _model;
|
||||
private const int MaxLogChars = 2000;
|
||||
|
||||
public SimpleAIClient(string apiKey, string baseUrl, string model)
|
||||
{
|
||||
@@ -41,28 +42,36 @@ namespace WulaFallenEmpire.EventSystem.AI
|
||||
jsonBuilder.Append("\"messages\": [");
|
||||
|
||||
// System instruction
|
||||
bool firstMessage = true;
|
||||
if (!string.IsNullOrEmpty(instruction))
|
||||
{
|
||||
jsonBuilder.Append($"{{\"role\": \"system\", \"content\": \"{EscapeJson(instruction)}\"}},");
|
||||
jsonBuilder.Append($"{{\"role\": \"system\", \"content\": \"{EscapeJson(instruction)}\"}}");
|
||||
firstMessage = false;
|
||||
}
|
||||
|
||||
// Messages
|
||||
for (int i = 0; i < messages.Count; i++)
|
||||
{
|
||||
var msg = messages[i];
|
||||
string role = msg.role.ToLower();
|
||||
string role = (msg.role ?? "user").ToLowerInvariant();
|
||||
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)}\"}}");
|
||||
if (i < messages.Count - 1) jsonBuilder.Append(",");
|
||||
firstMessage = false;
|
||||
}
|
||||
|
||||
jsonBuilder.Append("]");
|
||||
jsonBuilder.Append("}");
|
||||
|
||||
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"))
|
||||
{
|
||||
@@ -84,24 +93,57 @@ namespace WulaFallenEmpire.EventSystem.AI
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (s == null) return "";
|
||||
return s.Replace("\\", "\\\\")
|
||||
.Replace("\"", "\\\"")
|
||||
.Replace("\n", "\\n")
|
||||
.Replace("\r", "\\r")
|
||||
.Replace("\t", "\\t");
|
||||
|
||||
StringBuilder sb = new StringBuilder(s.Length + 16);
|
||||
for (int i = 0; i < s.Length; i++)
|
||||
{
|
||||
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)
|
||||
@@ -116,13 +158,13 @@ namespace WulaFallenEmpire.EventSystem.AI
|
||||
string[] lines = json.Split(new[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (string line in lines)
|
||||
{
|
||||
string trimmedLine = line.Trim();
|
||||
if (trimmedLine == "data: [DONE]") continue;
|
||||
if (trimmedLine.StartsWith("data: "))
|
||||
{
|
||||
string dataJson = trimmedLine.Substring(6);
|
||||
string trimmedLine = line.Trim();
|
||||
if (trimmedLine == "data: [DONE]") continue;
|
||||
if (trimmedLine.StartsWith("data: "))
|
||||
{
|
||||
string dataJson = trimmedLine.Substring(6);
|
||||
// Extract content from this chunk
|
||||
string chunkContent = ExtractJsonValue(dataJson, "content");
|
||||
string chunkContent = TryExtractAssistantContent(dataJson) ?? ExtractJsonValue(dataJson, "content");
|
||||
if (!string.IsNullOrEmpty(chunkContent))
|
||||
{
|
||||
fullContent.Append(chunkContent);
|
||||
@@ -134,7 +176,7 @@ namespace WulaFallenEmpire.EventSystem.AI
|
||||
else
|
||||
{
|
||||
// Standard non-stream format
|
||||
return ExtractJsonValue(json, "content");
|
||||
return TryExtractAssistantContent(json) ?? ExtractJsonValue(json, "content");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -144,12 +186,109 @@ namespace WulaFallenEmpire.EventSystem.AI
|
||||
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"
|
||||
// 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}\"";
|
||||
int keyIndex = json.IndexOf(keyPattern);
|
||||
int keyIndex = json.IndexOf(keyPattern, startIndex, StringComparison.Ordinal);
|
||||
if (keyIndex == -1) return null;
|
||||
|
||||
// Find the colon after the key
|
||||
@@ -196,4 +335,4 @@ namespace WulaFallenEmpire.EventSystem.AI
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using RimWorld;
|
||||
@@ -90,16 +91,24 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
|
||||
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)
|
||||
{
|
||||
return "Error: No active map.";
|
||||
string msg = "Error: No active map.";
|
||||
Messages.Message(msg, MessageTypeDefOf.RejectInput);
|
||||
return msg;
|
||||
}
|
||||
|
||||
IntVec3 dropSpot = DropCellFinder.TradeDropSpot(map);
|
||||
if (!dropSpot.IsValid || !dropSpot.InBounds(map))
|
||||
{
|
||||
dropSpot = DropCellFinder.RandomDropSpot(map);
|
||||
}
|
||||
List<Thing> thingsToDrop = new List<Thing>();
|
||||
StringBuilder resultLog = new StringBuilder();
|
||||
resultLog.Append("Success: Dropped ");
|
||||
@@ -114,27 +123,60 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
|
||||
if (thingsToDrop.Count > 0)
|
||||
{
|
||||
DropPodUtility.DropThingsNear(dropSpot, map, thingsToDrop);
|
||||
|
||||
Faction faction = Find.FactionManager.FirstFactionOfDef(FactionDef.Named("Wula_PIA_Legion_Faction"));
|
||||
if (faction != null)
|
||||
// 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.
|
||||
bool isPaused = Find.TickManager != null && Find.TickManager.Paused;
|
||||
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.Append($" at {dropSpot}.");
|
||||
resultLog.Append($" at {dropSpot}. {(isPaused ? "(placed immediately because game is paused)" : "(drop pods inbound)")}");
|
||||
return resultLog.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
return "Error: Failed to create items.";
|
||||
string msg = "Error: Failed to create items.";
|
||||
Messages.Message(msg, MessageTypeDefOf.RejectInput);
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,6 +228,19 @@ When the player requests any form of resources, you MUST follow this multi-turn
|
||||
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()
|
||||
{
|
||||
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)
|
||||
{
|
||||
_history.Add(("user", "Hello"));
|
||||
PersistHistory();
|
||||
await GenerateResponse();
|
||||
}
|
||||
else
|
||||
@@ -379,6 +393,7 @@ When the player requests any form of resources, you MUST follow this multi-turn
|
||||
{
|
||||
_history.RemoveRange(0, removeCount);
|
||||
_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;
|
||||
}
|
||||
|
||||
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();
|
||||
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")
|
||||
{
|
||||
@@ -436,9 +460,9 @@ When the player requests any form of resources, you MUST follow this multi-turn
|
||||
}
|
||||
|
||||
_history.Add(("assistant", xml));
|
||||
// Use role "user" for tool results to avoid API errors about tool_calls format.
|
||||
// This is safe and won't break AI logic, as the AI still sees the result clearly.
|
||||
_history.Add(("user", $"[Tool Results]\n{combinedResults.ToString().Trim()}"));
|
||||
// Persist tool results with a dedicated role; the API request maps this role to a supported one.
|
||||
_history.Add(("tool", $"[Tool Results]\n{combinedResults.ToString().Trim()}"));
|
||||
PersistHistory();
|
||||
|
||||
// Check if there is any text content in the response
|
||||
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}");
|
||||
_history.Add(("tool", $"Error processing tool call: {ex.Message}"));
|
||||
PersistHistory();
|
||||
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)
|
||||
{
|
||||
_history.Add(("assistant", rawResponse));
|
||||
PersistHistory();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -563,7 +589,9 @@ When the player requests any form of resources, you MUST follow this multi-turn
|
||||
try
|
||||
{
|
||||
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
|
||||
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")
|
||||
{
|
||||
Text.Anchor = TextAnchor.MiddleRight;
|
||||
Widgets.Label(labelRect, $"<color=#add8e6>{text} :你</color>");
|
||||
Widgets.Label(labelRect, $"<color=#add8e6>{text}</color>");
|
||||
}
|
||||
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)
|
||||
{
|
||||
_history.Add(("user", text));
|
||||
PersistHistory();
|
||||
_scrollToBottom = true;
|
||||
await GenerateResponse();
|
||||
}
|
||||
@@ -714,6 +743,7 @@ When the player requests any form of resources, you MUST follow this multi-turn
|
||||
public override void PostClose()
|
||||
{
|
||||
if (Instance == this) Instance = null;
|
||||
PersistHistory();
|
||||
base.PostClose();
|
||||
HandleAction(def.dismissEffects);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user