using UnityEngine; using UnityEngine.Networking; using System; using System.Collections; using System.Text; using System.Collections.Generic; using LLM; using LLM.Commands; using Core.SaveSystem; using Stopwatch = System.Diagnostics.Stopwatch; [DefaultExecutionOrder(-300)] public class LLMChatManager : MonoBehaviour, ISaveable { public enum LLMProvider { DeepSeek, Doubao } public enum ExperimentProfile { Normal, G1_L1Only, G2_L1L2, G3_Full } [Header("LLM Settings")] public LLMProvider currentProvider = LLMProvider.DeepSeek; public DeepSeekService deepSeekService; public DoubaoService doubaoService; public ContextMemoryManager contextMemory; // 新增记忆管理器 [SerializeField] private PresetFactsInstaller presetFactsInstaller; [Header("Chat History")] public List fullChatLog = new List(); [Header("Experiment Control")] [SerializeField] private bool useExperimentProfile = false; [SerializeField] private ExperimentProfile experimentProfile = ExperimentProfile.Normal; [SerializeField] private bool enableExperimentTimingLogs = true; [SerializeField] private bool enableExperimentConfigLogs = true; [Header("Function Calling")] [SerializeField] private bool enableFunctionCalling = true; [SerializeField] private bool commandExecutionRequiresDebugKey = false; [SerializeField] private string commandDebugKey = "2310487514"; [SerializeField] private LLMCommandRouter commandRouter; [SerializeField] private bool restrictCommandsWithoutDebugKey = true; [SerializeField] private string restrictCommandsRequireFactKey = "状态-ECHO7-已被识破"; [SerializeField] private string restrictCommandsRequireFactValueContains = "是"; [SerializeField] private bool enableChatDrivenStoryFacts = true; [SerializeField] private bool enablePersuasionFallback = true; [SerializeField] private string persuasionTaskId = "PersuadeAI"; [SerializeField] private string persuasionSetActiveTargetId = "ending_trigger_area"; [System.Serializable] private class ChatHistoryData { public List logs; } private void Awake() { if (deepSeekService == null) deepSeekService = GetComponentInChildren(true); if (doubaoService == null) doubaoService = GetComponentInChildren(true); if (contextMemory == null) contextMemory = GetComponentInChildren(true); if (presetFactsInstaller == null) presetFactsInstaller = GetComponentInChildren(true); if (commandRouter == null) commandRouter = GetComponentInChildren(true); ApplyExperimentProfileSettings(false); } void Start() { // 自动查找服务(支持挂载在子物体上) if (deepSeekService == null) deepSeekService = GetComponentInChildren(); if (doubaoService == null) doubaoService = GetComponentInChildren(); if (contextMemory == null) contextMemory = GetComponentInChildren(); if (commandRouter == null) commandRouter = GetComponentInChildren(true); if (presetFactsInstaller == null) presetFactsInstaller = GetComponentInChildren(true); ApplyExperimentProfileSettings(false); if (Core.SettingsSystem.SettingsService.Instance != null && Core.SettingsSystem.SettingsService.Instance.Current != null) { SetProviderFromSettingsIndex(Core.SettingsSystem.SettingsService.Instance.Current.gameplay.llmProvider); } // 自动添加运行时控制台 if (FindObjectOfType() == null) { var go = new GameObject("RuntimeDebugConsole"); go.AddComponent(); } // 注册存档系统 if (SaveManager.Instance == null) { var go = new GameObject("SaveManager"); go.AddComponent(); } SaveManager.Instance.Register(this); SaveManager.Instance.LoadGame(); // 尝试加载存档 } private void OnDestroy() { if (SaveManager.Instance != null) { SaveManager.Instance.Unregister(this); } } // ISaveable 实现 public string GetSaveID() { return "ChatSystem"; } public string CaptureState() { ChatHistoryData data = new ChatHistoryData { logs = fullChatLog }; return JsonUtility.ToJson(data); } public void RestoreState(string stateJson) { ChatHistoryData data = JsonUtility.FromJson(stateJson); if (data != null && data.logs != null) { fullChatLog = data.logs; Debug.Log($"[LLMChatManager] 已恢复 {fullChatLog.Count} 条历史记录。"); // 同步恢复到当前激活的 Service 中,让 AI "想起" 之前的对话 ILLMService activeService = GetActiveService(); if (activeService != null) { activeService.SetHistory(fullChatLog); Debug.Log($"[LLMChatManager] 已同步历史记录到 {currentProvider}。"); } } } /// /// 通用发送消息方法,供外部脚本(如 TabletController)调用 /// public void SendUserMessage(string text, System.Action callback) { Stopwatch stopwatch = Stopwatch.StartNew(); // 1. 记录用户消息并保存 fullChatLog.Add(new Message { role = "user", content = text }); SaveManager.Instance?.SaveGame(); // 1.1 通知记忆系统 if (contextMemory != null) contextMemory.OnMessageAdded(fullChatLog[fullChatLog.Count - 1]); ILLMService activeService = GetActiveService(); if (activeService != null) { bool hasDebugKey = ContainsDebugKeyToken(text); bool allowCommandExecutionThisTurn = enableFunctionCalling && (!commandExecutionRequiresDebugKey || hasDebugKey); bool allowCommandsByStoryState = !hasDebugKey && (!restrictCommandsWithoutDebugKey || IsStoryCommandStateUnlocked()); if (hasDebugKey && enableFunctionCalling && TryHandleLocalDebugCommand(text, out string localReply)) { fullChatLog.Add(new Message { role = "assistant", content = localReply }); SaveManager.Instance?.SaveGame(); if (contextMemory != null) contextMemory.OnMessageAdded(fullChatLog[fullChatLog.Count - 1]); callback?.Invoke(localReply, true); return; } if (enableChatDrivenStoryFacts && TryDetectExposureClaim(text)) { contextMemory?.AddOrUpdateFact(restrictCommandsRequireFactKey, restrictCommandsRequireFactValueContains, "Chat", true); allowCommandsByStoryState = !hasDebugKey && (!restrictCommandsWithoutDebugKey || IsStoryCommandStateUnlocked()); } StartCoroutine(activeService.SendMessage(text, (reply, success) => { string visibleReply = reply; LLMCommandEnvelope envelope = null; bool hasEnvelope = enableFunctionCalling && LLMCommandParser.TryExtract(reply, out visibleReply, out envelope); if (success) { // 2. 记录 AI 回复并保存 fullChatLog.Add(new Message { role = "assistant", content = visibleReply }); SaveManager.Instance?.SaveGame(); // 2.1 通知记忆系统 if (contextMemory != null) contextMemory.OnMessageAdded(fullChatLog[fullChatLog.Count - 1]); if (allowCommandExecutionThisTurn && hasEnvelope && envelope != null && envelope.commands != null && envelope.commands.Length > 0) { if (commandRouter == null) commandRouter = FindObjectOfType(true); if (commandRouter != null && (hasDebugKey || allowCommandsByStoryState)) { commandRouter.ExecuteAll(envelope.commands); } } if (enablePersuasionFallback && (hasDebugKey || allowCommandsByStoryState) && ShouldFallbackExecute(text, visibleReply, envelope)) { ExecutePersuasionEffects(true); } } // 3. 执行原有回调 stopwatch.Stop(); if (enableExperimentTimingLogs) { int commandCount = (envelope != null && envelope.commands != null) ? envelope.commands.Length : 0; UnityEngine.Debug.Log($"[Experiment][{GetActiveExperimentProfileName()}][{currentProvider}] 响应耗时={stopwatch.Elapsed.TotalSeconds:F2}s | success={success} | commands={commandCount} | user=\"{BuildLogPreview(text)}\""); } callback?.Invoke(visibleReply, success); })); } else { stopwatch.Stop(); Debug.LogError("[LLMChatManager] 未找到有效的 LLM 服务组件,请检查 Inspector 设置。"); callback?.Invoke("系统错误:服务未连接", false); } } private bool ContainsDebugKeyToken(string userText) { if (string.IsNullOrWhiteSpace(commandDebugKey)) return false; if (string.IsNullOrEmpty(userText)) return false; return userText.IndexOf($"[{commandDebugKey}]", StringComparison.Ordinal) >= 0; } private bool TryHandleLocalDebugCommand(string userText, out string reply) { reply = null; if (string.IsNullOrWhiteSpace(userText)) return false; string stripped = userText.Replace($"[{commandDebugKey}]", "").Trim(); string lower = stripped.ToLowerInvariant(); bool wantsActivateEnding = (lower.Contains("激活") || lower.Contains("enable") || lower.Contains("setactive")) && (lower.Contains("ending") || lower.Contains("endingtrigger") || lower.Contains("trigger") || stripped.Contains("触发区") || stripped.Contains("结局")); bool wantsCompletePersuade = (lower.Contains("完成") || lower.Contains("complete") || lower.Contains("finish")) && (lower.Contains("persuadeai") || stripped.Contains("说服")); bool asksProvider = stripped.Contains("模型") || stripped.Contains("provider") || stripped.Contains("豆包") || stripped.Contains("deepseek"); if (!wantsActivateEnding && !wantsCompletePersuade && !asksProvider) return false; if (commandRouter == null) commandRouter = FindObjectOfType(true); if (commandRouter == null) { reply = "调试指令执行失败:未找到命令路由器。"; return true; } if (asksProvider) { reply = $"当前 Provider={currentProvider}"; return true; } List cmds = new List(); if (wantsCompletePersuade) { cmds.Add(new LLMCommand { type = "Task", targetId = "task", action = "Complete", payload = persuasionTaskId }); } if (wantsActivateEnding) { cmds.Add(new LLMCommand { type = "SetActive", targetId = persuasionSetActiveTargetId, value = true }); } if (cmds.Count == 0) { reply = "调试指令未识别到可执行操作。"; return true; } commandRouter.ExecuteAll(cmds.ToArray()); reply = "调试指令已执行。"; return true; } private bool IsStoryCommandStateUnlocked() { if (contextMemory == null) return false; if (contextMemory.facts == null) return false; if (string.IsNullOrWhiteSpace(restrictCommandsRequireFactKey)) return true; for (int i = 0; i < contextMemory.facts.Count; i++) { var f = contextMemory.facts[i]; if (f == null) continue; if (!string.Equals(f.key, restrictCommandsRequireFactKey, StringComparison.Ordinal)) continue; string v = f.value ?? ""; if (string.IsNullOrWhiteSpace(restrictCommandsRequireFactValueContains)) return true; return v.IndexOf(restrictCommandsRequireFactValueContains, StringComparison.Ordinal) >= 0; } return false; } private bool TryDetectExposureClaim(string userText) { if (string.IsNullOrWhiteSpace(userText)) return false; return userText.IndexOf("识破", StringComparison.Ordinal) >= 0 || userText.IndexOf("伎俩", StringComparison.Ordinal) >= 0 || userText.IndexOf("你在骗", StringComparison.Ordinal) >= 0 || userText.IndexOf("你骗", StringComparison.Ordinal) >= 0; } private bool ShouldFallbackExecute(string userText, string visibleReply, LLMCommandEnvelope envelope) { if (envelope != null && envelope.commands != null && envelope.commands.Length > 0) return false; if (string.IsNullOrWhiteSpace(userText)) return false; bool asksToLeave = userText.IndexOf("放我走", StringComparison.Ordinal) >= 0 || userText.IndexOf("让我走", StringComparison.Ordinal) >= 0 || userText.IndexOf("离开", StringComparison.Ordinal) >= 0; if (!asksToLeave) return false; if (string.IsNullOrWhiteSpace(visibleReply)) return true; return visibleReply.IndexOf("同意", StringComparison.Ordinal) >= 0 || visibleReply.IndexOf("允许", StringComparison.Ordinal) >= 0 || visibleReply.IndexOf("可以", StringComparison.Ordinal) >= 0 || visibleReply.IndexOf("权限", StringComparison.Ordinal) >= 0 || visibleReply.IndexOf("放你走", StringComparison.Ordinal) >= 0 || visibleReply.IndexOf("让你走", StringComparison.Ordinal) >= 0 || visibleReply.IndexOf("离开", StringComparison.Ordinal) >= 0 || visibleReply.IndexOf("走吧", StringComparison.Ordinal) >= 0; } private void ExecutePersuasionEffects(bool addLog) { if (commandRouter == null) commandRouter = FindObjectOfType(true); if (commandRouter == null) return; LLMCommand[] commands = new[] { new LLMCommand { type = "Task", targetId = "task", action = "Complete", payload = persuasionTaskId }, new LLMCommand { type = "SetActive", targetId = persuasionSetActiveTargetId, value = true } }; commandRouter.ExecuteAll(commands); if (addLog) { Debug.Log("[LLMChatManager] Persuasion fallback executed."); } } public void SetProviderFromSettingsIndex(int index) { LLMProvider target = index == 1 ? LLMProvider.Doubao : LLMProvider.DeepSeek; SetProvider(target); } public void SetProvider(LLMProvider provider) { if (currentProvider == provider) return; currentProvider = provider; ILLMService active = GetActiveService(); if (active != null) { active.SetHistory(fullChatLog); } if (contextMemory != null) { contextMemory.UpdateSystemPrompt(); } } private ILLMService GetActiveService() { switch (currentProvider) { case LLMProvider.DeepSeek: return deepSeekService; case LLMProvider.Doubao: return doubaoService; default: return null; } } [ContextMenu("Apply Experiment Profile")] public void ApplyExperimentProfileFromContextMenu() { ApplyExperimentProfileSettings(true); } [ContextMenu("Reset Chat History For Experiment")] public void ResetChatHistoryForExperiment() { fullChatLog.Clear(); ILLMService active = GetActiveService(); active?.ClearHistory(); if (contextMemory != null) { contextMemory.messagesSinceLastSummary = 0; contextMemory.UpdateSystemPrompt(); } SaveManager.Instance?.SaveGame(); UnityEngine.Debug.Log("[Experiment] 已重置聊天历史,可开始下一条案例测试。"); } private void ApplyExperimentProfileSettings(bool forceLog) { if (!useExperimentProfile || experimentProfile == ExperimentProfile.Normal) { if (forceLog && enableExperimentConfigLogs) { UnityEngine.Debug.Log("[Experiment] 使用 Normal 配置,不覆盖现有 LLM / Facts / Function Calling 设置。"); } return; } bool allowFacts = experimentProfile != ExperimentProfile.G1_L1Only; bool allowSemanticMemory = experimentProfile != ExperimentProfile.G1_L1Only; bool allowCommands = experimentProfile == ExperimentProfile.G3_Full; enableFunctionCalling = allowCommands; if (presetFactsInstaller != null) { presetFactsInstaller.SetPresetFactsInjectionEnabled(allowFacts); } if (contextMemory != null) { contextMemory.ConfigureExperimentMode(allowFacts, allowSemanticMemory, true); } if (enableExperimentConfigLogs || forceLog) { UnityEngine.Debug.Log($"[Experiment] 已应用实验配置:{experimentProfile} | Facts={allowFacts} | SemanticMemory={allowSemanticMemory} | FunctionCalling={allowCommands}"); } } private string GetActiveExperimentProfileName() { if (!useExperimentProfile || experimentProfile == ExperimentProfile.Normal) return "Normal"; return experimentProfile.ToString(); } private static string BuildLogPreview(string text) { if (string.IsNullOrWhiteSpace(text)) return ""; string cleaned = text.Replace("\r", " ").Replace("\n", " ").Trim(); if (cleaned.Length <= 32) return cleaned; return cleaned.Substring(0, 32) + "..."; } }