using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using System.Threading; using System.Threading.Tasks; using AI_Localization.Config; using AI_Localization.Core; using AI_Localization.Core.AITranslation; using AI_Localization.Core.DataCollection; using AI_Localization.Core.DataExporter; using AI_Localization.HarmonyPatches; using AI_Localization.Utils; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using UnityEngine; using YamlDotNet.Serialization; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.8.1", FrameworkDisplayName = ".NET Framework 4.8.1")] [assembly: AssemblyCompany("HsgtLgt")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyCopyright("Copyright © Valheim_Localization 2026")] [assembly: AssemblyDescription("AI Localization")] [assembly: AssemblyFileVersion("0.1.3.0")] [assembly: AssemblyInformationalVersion("0.1.3+a83599d4d29408d8f8aa93fa69d07d7cc0c48fbf")] [assembly: AssemblyProduct("AI Localization")] [assembly: AssemblyTitle("AI Localization")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.1.3.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace AI_Localization { [BepInPlugin(".com.HsgtLgt.AI_Localization", "AI_Localization", "0.1.3")] public class AI_Localization : BaseUnityPlugin { public const string modGUID = ".com.HsgtLgt.AI_Localization"; public const string modName = "AI_Localization"; public const string modVersion = "0.1.3"; private DataExporter? _exporter; private void Awake() { ModLogger.Initialize(((BaseUnityPlugin)this).Logger); ModConfig.Init((BaseUnityPlugin)(object)this); ModLogger.SetDebugEnabled(ModConfig.LogTranslations.Value); ExemptionManager.Load(ModConfig.Exemptions.Value); Dictionary>> dictionary = Yaml.LoadTranslation(); if (dictionary != null && dictionary.Count > 0) { TranslationManager.SetCache(dictionary); ModLogger.LogInfo($"已加载译文:{dictionary.Count} 个模组"); } else { ModLogger.LogDebug("启动时未找到译文文件"); } Patches.Instance.Apply(); TranslationApplicator.ApplyToAlreadyLoadedPlugins(); if (ModConfig.EnableCollection.Value) { _exporter = new DataExporter((MonoBehaviour)(object)this); } CollectionStatus.Initialize((MonoBehaviour)(object)this); } private void OnDestroy() { _exporter?.Stop(); CollectionStatus.Shutdown(); } } } namespace AI_Localization.Utils { public static class ExemptionManager { private struct ExemptionRule { public readonly string[] Guids; public readonly string[] Sections; public readonly string[] Keys; public ExemptionRule(string[] guids, string[] sections, string[] keys) { Guids = guids; Sections = sections; Keys = keys; } } private static readonly List _rules = new List(); public static int RuleCount => _rules.Count; public static void Load(string configValue) { _rules.Clear(); if (string.IsNullOrWhiteSpace(configValue)) { return; } string[] array = configValue.Split(new char[1] { ',' }); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (text.Length != 0) { string[] array2 = text.Split(new char[1] { '|' }); if (array2.Length == 3) { _rules.Add(new ExemptionRule(array2[0].Trim().Split(new char[1] { ';' }), array2[1].Trim().Split(new char[1] { ';' }), array2[2].Trim().Split(new char[1] { ';' }))); } } } } public static bool IsExempt(string guid, string section, string key) { foreach (ExemptionRule rule in _rules) { if (Match(rule.Guids, guid) && Match(rule.Sections, section) && Match(rule.Keys, key)) { return true; } } return false; } private static bool Match(string[] patterns, string value) { for (int i = 0; i < patterns.Length; i++) { string text = patterns[i].Trim(); if (text == "*" || text == value) { return true; } } return false; } } internal static class ModLogger { private static ManualLogSource? _logSource; private static bool _debugEnabled; public static bool IsDebugEnabled { get { try { if (ModConfig.LogTranslations != null) { return ModConfig.LogTranslations.Value; } } catch { } return _debugEnabled; } } public static void Initialize(ManualLogSource logSource) { _logSource = logSource; } public static void SetDebugEnabled(bool debugEnabled) { _debugEnabled = debugEnabled; } public static void LogInfo(object data) { ManualLogSource? logSource = _logSource; if (logSource != null) { logSource.LogInfo(data); } } public static void LogWarning(object data) { ManualLogSource? logSource = _logSource; if (logSource != null) { logSource.LogWarning(data); } } public static void LogError(object data) { ManualLogSource? logSource = _logSource; if (logSource != null) { logSource.LogError(data); } } public static void LogDebug(object data) { if (IsDebugEnabled) { ManualLogSource? logSource = _logSource; if (logSource != null) { logSource.LogDebug(data); } } } public static void LogDebugAsInfo(object data) { if (IsDebugEnabled) { ManualLogSource? logSource = _logSource; if (logSource != null) { logSource.LogInfo(data); } } } } public class ReflectionHelper { private static readonly FieldInfo? _ownerMetadataField = typeof(ConfigFile).GetField("_ownerMetadata", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); internal static string GetPluginGuid(ConfigFile configFile) { object? obj = _ownerMetadataField?.GetValue(configFile); object? obj2 = ((obj is BepInPlugin) ? obj : null); return ((obj2 != null) ? ((BepInPlugin)obj2).GUID : null) ?? Path.GetFileNameWithoutExtension(configFile.ConfigFilePath); } } internal static class TextHelper { internal static bool IsMostlyChinese(string? text, double threshold = 0.3) { if (string.IsNullOrEmpty(text)) { return false; } int num = 0; int num2 = 0; foreach (char c in text) { bool flag = c >= '一' && c <= '鿿'; if (flag || char.IsLetter(c)) { num++; if (flag) { num2++; } } } if (num > 0) { return (double)num2 / (double)num >= threshold; } return false; } } internal class Yaml { private static readonly string DataDir = Path.Combine(Paths.ConfigPath, "AI_Localization"); private static string ExportPath => Path.Combine(DataDir, "AI_Locale_Export.yaml"); private static string TranslationPath { get { ModConfig.GetTargetLanguage(out string code, out string _); if (code == "zh") { return Path.Combine(DataDir, "AI_Locale_Translation.yaml"); } return Path.Combine(DataDir, "AI_Locale_Translation." + code + ".yaml"); } } private static void EnsureDataDir() { if (!Directory.Exists(DataDir)) { Directory.CreateDirectory(DataDir); } } public static void ExportYaml(Dictionary>> data) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) EnsureDataDir(); string exportPath = ExportPath; string contents = new SerializerBuilder().Build().Serialize((object)data); File.WriteAllText(exportPath, contents); ModLogger.LogDebugAsInfo($"[文件] 已写入导出:{data.Count} 个模组 → {exportPath}"); } public static Dictionary>>? LoadExport() { //IL_0020: Unknown result type (might be due to invalid IL or missing references) EnsureDataDir(); string exportPath = ExportPath; if (!File.Exists(exportPath)) { ModLogger.LogDebug("未找到导出文件"); return null; } try { Dictionary>> dictionary = new DeserializerBuilder().Build().Deserialize>>>(File.ReadAllText(exportPath)); ModLogger.LogDebugAsInfo($"[文件] 已读取导出:{dictionary.Count} 个模组 ← {exportPath}"); return dictionary; } catch (Exception ex) { ModLogger.LogError("读取导出文件失败:" + ex.Message); return null; } } public static Dictionary>>? LoadTranslation() { //IL_0020: Unknown result type (might be due to invalid IL or missing references) EnsureDataDir(); string translationPath = TranslationPath; if (!File.Exists(translationPath)) { ModLogger.LogDebug("未找到翻译文件"); return null; } try { Dictionary>> dictionary = new DeserializerBuilder().Build().Deserialize>>>(File.ReadAllText(translationPath)); ModLogger.LogDebugAsInfo($"[文件] 已读取译文:{dictionary.Count} 个模组 ← {translationPath}"); return dictionary; } catch (Exception ex) { ModLogger.LogError("读取翻译文件失败:" + ex.Message); return null; } } public static void SaveTranslation(Dictionary>> data) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) try { EnsureDataDir(); string translationPath = TranslationPath; string contents = new SerializerBuilder().Build().Serialize((object)data); File.WriteAllText(translationPath, contents); ModLogger.LogDebugAsInfo("[文件] 已保存译文 → " + translationPath); } catch (Exception ex) { ModLogger.LogError("保存翻译文件失败:" + ex.Message); } } public static int DeduplicateExport(Dictionary>> translationData, Dictionary>> exportData) { if (translationData == null || translationData.Count == 0) { return 0; } if (exportData == null || exportData.Count == 0) { return 0; } int num = 0; foreach (KeyValuePair>> item in exportData.ToList()) { string key = item.Key; Dictionary> value = item.Value; if (!translationData.TryGetValue(key, out Dictionary> value2)) { continue; } foreach (KeyValuePair> item2 in value.ToList()) { string key2 = item2.Key; Dictionary value3 = item2.Value; if (!value2.TryGetValue(key2, out var value4)) { continue; } foreach (KeyValuePair item3 in value3.ToList()) { string key3 = item3.Key; if (!ExemptionManager.IsExempt(key, key2, key3) && value4.ContainsKey(key3)) { value3.Remove(key3); num++; } } if (value3.Count == 0) { value.Remove(key2); } } if (value.Count == 0) { exportData.Remove(key); } } return num; } } } namespace AI_Localization.HarmonyPatches { public class Patches { private static Patches? _instance; private readonly Harmony _harmony; public static Patches Instance => _instance ?? (_instance = new Patches()); private Patches() { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Expected O, but got Unknown _harmony = new Harmony(".com.HsgtLgt.AI_Localization"); } public void Apply() { _harmony.PatchAll(typeof(HarmonyPatchTarget)); } public void UnpatchSelf() { _harmony.UnpatchSelf(); } } public class HarmonyPatchTarget { [ThreadStatic] private static Queue<(string section, string key)>? _pendingEntries; [ThreadStatic] private static string? _pendingDesc; [HarmonyPatch(/*Could not decode attribute arguments.*/)] [HarmonyPrefix] [HarmonyPriority(0)] private static void ConfigDefinition_Prefix(string section, string key) { if (_pendingEntries == null) { _pendingEntries = new Queue<(string, string)>(); } _pendingEntries.Enqueue((section, key)); } [HarmonyPatch(/*Could not decode attribute arguments.*/)] [HarmonyPrefix] [HarmonyPriority(0)] private static void ConfigDescription_Prefix(string description) { _pendingDesc = description; } [HarmonyPatch(/*Could not decode attribute arguments.*/)] [HarmonyPrefix] [HarmonyPriority(0)] private static void ConfigEntryBase_Constructor_Prefix(ConfigFile configFile, ConfigDefinition definition, ConfigDescription configDescription) { ZConfigPatches.CollectAndApply(configFile, definition, configDescription, _pendingEntries, _pendingDesc, ModConfig.EnableCollection.Value); _pendingDesc = null; _pendingEntries?.Clear(); } [HarmonyPatch(/*Could not decode attribute arguments.*/)] [HarmonyPostfix] [HarmonyPriority(0)] private static void ConfigEntryBase_Constructor_Postfix(ConfigEntryBase __instance) { ZConfigPatches.AfterEntryConstructed(__instance); } } public static class ZConfigPatches { public static void CollectAndApply(ConfigFile configFile, ConfigDefinition definition, ConfigDescription configDescription, Queue<(string section, string key)>? pendingEntries, string? pendingDesc, bool enableCollection) { string text = null; string text2 = null; if (pendingEntries != null && pendingEntries.Count > 0) { (text, text2) = pendingEntries.Dequeue(); } string section = text ?? definition.Section; string key = text2 ?? definition.Key; string description = pendingDesc ?? ((configDescription != null) ? configDescription.Description : null) ?? ""; string pluginGuid = ReflectionHelper.GetPluginGuid(configFile); if (enableCollection) { DataCollector.Collect(pluginGuid, section, key, description); } TranslationApplicator.RememberOrphanSource(configFile, section, key); TranslationApplicator.ApplyInPlace(pluginGuid, definition, configDescription); } public static void AfterEntryConstructed(ConfigEntryBase entry) { TranslationApplicator.TryMigrateOrphanValue(entry); } } } namespace AI_Localization.Core { internal enum CollectionState { NotStarted, Collecting, Completed, Stopped } internal static class CollectionStatus { private static CollectionState _state; private static MonoBehaviour? _coroutineOwner; private static bool _translationTriggered; internal static CollectionState State => _state; internal static bool IsCompleted => _state == CollectionState.Completed; internal static bool IsCollecting => _state == CollectionState.Collecting; internal static bool CanStartTranslation { get { if (_state != CollectionState.Completed) { return !ModConfig.EnableCollection.Value; } return true; } } internal static event Action? OnStateChanged; internal static void Initialize(MonoBehaviour owner) { _coroutineOwner = owner; _translationTriggered = false; LoadCacheAndMaybeAutoTranslate(); OnStateChanged += OnStateChangedHandler; } internal static void Shutdown() { OnStateChanged -= OnStateChangedHandler; _coroutineOwner = null; } private static void LoadCacheAndMaybeAutoTranslate() { if (!TranslationManager.HasTranslation) { Dictionary>> dictionary = Yaml.LoadTranslation(); if (dictionary != null && dictionary.Count > 0) { TranslationManager.SetCache(dictionary); ModLogger.LogInfo($"已加载译文:{dictionary.Count} 个模组"); } } if (!ModConfig.EnableAiTranslation.Value) { ModLogger.LogInfo("AI 翻译已关闭(仍会使用已有译文)"); } else if (ModConfig.EnableCollection.Value) { ModLogger.LogDebug("[AI] 已启用收集,等待收集完成后自动翻译"); } else { TryStartAutoTranslate("未启用收集"); } } private static void OnStateChangedHandler(CollectionState state) { if (state == CollectionState.Completed && ModConfig.EnableCollection.Value) { TryStartAutoTranslate("收集已完成"); } } private static void TryStartAutoTranslate(string reason) { if (_translationTriggered) { return; } if (!ModConfig.EnableAiTranslation.Value) { ModLogger.LogDebug("[AI] " + reason + ",但 AI 翻译已禁用,跳过"); return; } if (ModConfig.EnableCollection.Value && _state != CollectionState.Completed) { ModLogger.LogDebug("[AI] " + reason + ",收集未完成,暂不开翻"); return; } if (string.IsNullOrWhiteSpace(ModConfig.ApiKey.Value)) { ModLogger.LogWarning("请在模组配置「4. AI 翻译 > API密钥」中填写 DeepSeek API 密钥后再翻译"); return; } Dictionary>> localeData = DataCollector.LocaleData; if (localeData != null && localeData.Count > 0) { _translationTriggered = true; ModLogger.LogInfo("开始自动翻译…"); ModLogger.LogDebugAsInfo("[AI] 触发原因:" + reason + ",数据来源:内存"); MonoBehaviour? coroutineOwner = _coroutineOwner; if (coroutineOwner != null) { coroutineOwner.StartCoroutine(AITranslationService.RunTranslationFromMemory()); } return; } Dictionary>> dictionary = Yaml.LoadExport(); if (dictionary != null && dictionary.Count > 0) { _translationTriggered = true; ModLogger.LogInfo("开始自动翻译(使用本地导出文件)…"); ModLogger.LogDebugAsInfo($"[AI] 触发原因:{reason},数据来源:文件,模组数={dictionary.Count}"); MonoBehaviour? coroutineOwner2 = _coroutineOwner; if (coroutineOwner2 != null) { coroutineOwner2.StartCoroutine(AITranslationService.RunTranslationFromFile()); } } else { ModLogger.LogInfo("没有可翻译的配置,已跳过"); ModLogger.LogDebugAsInfo("[AI] 触发原因:" + reason + ",内存与导出均空"); } } internal static void MarkCollecting() { if (_state == CollectionState.NotStarted || _state == CollectionState.Stopped) { _state = CollectionState.Collecting; CollectionStatus.OnStateChanged?.Invoke(_state); ModLogger.LogDebug($"[状态] 收集开始 → {_state}"); } } internal static void MarkCompleted() { if (_state == CollectionState.Collecting) { _state = CollectionState.Completed; CollectionStatus.OnStateChanged?.Invoke(_state); ModLogger.LogDebug($"[状态] 收集完成 → {_state}"); } } internal static void MarkStopped() { if (_state != CollectionState.Stopped) { _state = CollectionState.Stopped; CollectionStatus.OnStateChanged?.Invoke(_state); ModLogger.LogDebug($"[状态] 收集停止 → {_state}"); } } internal static void Reset() { _state = CollectionState.NotStarted; CollectionStatus.OnStateChanged?.Invoke(_state); ModLogger.LogDebug($"[状态] 状态已重置 → {_state}"); } } } namespace AI_Localization.Core.DataExporter { public class DataExporter { private readonly MonoBehaviour _owner; private readonly float _checkInterval = 5f; private readonly float _quietThreshold = 10f; private DateTime _lastCollectTime = DateTime.MinValue; private bool _collectionEnded; private bool _exportDone; private bool _isRunning; private bool _shouldStop; private Coroutine? _checkCoroutine; public DataExporter(MonoBehaviour owner, float checkInterval = 5f, float quietThreshold = 10f) { _owner = owner ?? throw new ArgumentNullException("owner"); _checkInterval = checkInterval; _quietThreshold = quietThreshold; Start(); } public void Start() { if (!_isRunning) { _shouldStop = false; _lastCollectTime = DateTime.MinValue; _collectionEnded = false; _exportDone = false; CollectionStatus.MarkCollecting(); DataCollector.OnCollect += OnCollectHandler; _checkCoroutine = _owner.StartCoroutine(CheckCollectionEndCoroutine()); _isRunning = true; if (ModConfig.EnableAiTranslation.Value) { ModLogger.LogInfo("开始收集模组配置,完成后将自动翻译"); } else { ModLogger.LogInfo("开始收集模组配置"); } } } public void Stop() { if (_isRunning) { DataCollector.OnCollect -= OnCollectHandler; _shouldStop = true; if (_checkCoroutine != null) { _owner.StopCoroutine(_checkCoroutine); _checkCoroutine = null; } _isRunning = false; ModLogger.LogDebug("收集已停止"); } } private void OnCollectHandler() { _lastCollectTime = DateTime.Now; if (_collectionEnded) { _collectionEnded = false; _exportDone = false; ModLogger.LogDebug("检测到新的收集活动,重置结束状态"); } } private IEnumerator CheckCollectionEndCoroutine() { while (!_collectionEnded && !_shouldStop) { yield return (object)new WaitForSeconds(_checkInterval); if (_shouldStop) { yield break; } if (_lastCollectTime == DateTime.MinValue) { continue; } double totalSeconds = (DateTime.Now - _lastCollectTime).TotalSeconds; if (!(totalSeconds >= (double)_quietThreshold)) { continue; } _collectionEnded = true; if (!_exportDone) { _exportDone = true; Dictionary>> localeData = DataCollector.LocaleData; if (localeData.Count > 0) { Yaml.ExportYaml(localeData); ModLogger.LogInfo($"配置收集完成:{localeData.Count} 个模组"); ModLogger.LogDebugAsInfo($"[收集] 安静 {totalSeconds:F1}s 后导出"); } else { ModLogger.LogWarning("配置收集结束,但未收集到任何条目"); } CollectionStatus.MarkCompleted(); } DataCollector.OnCollect -= OnCollectHandler; _isRunning = false; } if (_shouldStop) { DataCollector.OnCollect -= OnCollectHandler; _isRunning = false; } } } } namespace AI_Localization.Core.DataCollection { public static class DataCollector { internal static Dictionary>> LocaleData { get; private set; } = new Dictionary>>(); internal static event Action? OnCollect; internal static void Reset() { LocaleData.Clear(); } internal static void UnsubscribeAll() { if (DataCollector.OnCollect != null) { Delegate[] invocationList = DataCollector.OnCollect.GetInvocationList(); for (int i = 0; i < invocationList.Length; i++) { OnCollect -= (Action)invocationList[i]; } } } internal static void Collect(string guid, string section, string key, string description) { if (!LocaleData.ContainsKey(guid)) { LocaleData[guid] = new Dictionary>(); } if (!LocaleData[guid].ContainsKey(section)) { LocaleData[guid][section] = new Dictionary(); LocaleData[guid][section][section] = section; } LocaleData[guid][section][key] = key; if (!string.IsNullOrEmpty(description)) { LocaleData[guid][section][description] = description; } DataCollector.OnCollect?.Invoke(); } } } namespace AI_Localization.Core.AITranslation { internal static class AITranslationService { internal static IEnumerator RunTranslationFromMemory() { return TranslationPipeline.Run(DataCollector.LocaleData, "内存"); } internal static IEnumerator RunTranslationFromFile() { return TranslationPipeline.Run(Yaml.LoadExport(), "文件"); } internal static IEnumerator RunTranslationCoroutine() { return RunTranslationFromMemory(); } } internal static class DeepSeekApiClient { private class ChatRequest { [JsonProperty("model")] public string Model = "deepseek-v4-flash"; [JsonProperty("messages")] public Message[] Messages = new Message[0]; [JsonProperty("temperature")] public float? Temperature = 0f; [JsonProperty("max_tokens")] public int MaxTokens = 32768; [JsonProperty("stream")] public bool Stream = true; [JsonProperty("thinking")] public ThinkingConfig Thinking = new ThinkingConfig(); [JsonProperty("stream_options")] public StreamOptions StreamOptions = new StreamOptions(); } private class ThinkingConfig { [JsonProperty("type")] public string Type = "disabled"; } private class StreamOptions { [JsonProperty("include_usage")] public bool IncludeUsage = true; } private class Message { [JsonProperty("role")] public string Role = ""; [JsonProperty("content")] public string Content = ""; } private static readonly HttpClient _httpClient = CreateHttpClient(); private static readonly object _semaphoreLock = new object(); private static SemaphoreSlim _semaphore = new SemaphoreSlim(12, 12); private static int _configuredConcurrency = 12; private const int IdleTimeoutSeconds = 120; private const int ResponseHeaderTimeoutSeconds = 120; private const int TotalTimeoutMinutes = 25; private const int DefaultMaxTokens = 32768; internal static int ConfiguredConcurrency { get { lock (_semaphoreLock) { return _configuredConcurrency; } } } internal static void ConfigureConcurrency(int concurrency) { if (concurrency < 1) { concurrency = 1; } if (concurrency > 10000) { concurrency = 10000; } lock (_semaphoreLock) { if (concurrency == _configuredConcurrency && _semaphore != null) { return; } SemaphoreSlim semaphore = _semaphore; _semaphore = new SemaphoreSlim(concurrency, concurrency); _configuredConcurrency = concurrency; try { semaphore?.Dispose(); } catch { } } } private static string BuildSystemPrompt() { ModConfig.GetTargetLanguage(out string code, out string promptName); GetDemoLines(code, out string d, out string d2, out string d3, out string d4, out string d5); return "你是 Valheim Mod 配置项的专业翻译。\n用户消息是待译列表,每行格式为:ID: 原文\n请为每个原文提供" + promptName + "翻译。\n输出格式要求:\n1. 仅输出纯 YAML 键值对,每行格式为 ID: \"翻译\"\n2. 每个翻译值必须用双引号包裹,以避免特殊字符(如冒号、逗号)干扰\n3. 不要输出任何解释文字、代码块标记或其他内容\n4. 保持技术标识符不变,只翻译自然语言部分\n\n格式示例(仅示意结构,勿照抄译文):\nentry_demo_1: \"" + d + "\"\nentry_demo_2: \"" + d2 + "\"\nentry_demo_3: \"" + d3 + "\"\nentry_demo_4: \"" + d4 + "\"\nentry_demo_5: \"" + d5 + "\""; } private static void GetDemoLines(string code, out string d1, out string d2, out string d3, out string d4, out string d5) { switch (code) { case "en": d1 = "Maximum Health"; d2 = "Enable Debug Mode"; d3 = "Regeneration per Second"; d4 = "Damage Multiplier"; d5 = "Show Floating Damage"; break; case "ja": d1 = "最大体力"; d2 = "デバッグモードを有効化"; d3 = "毎秒回復"; d4 = "ダメージ倍率"; d5 = "フローティングダメージを表示"; break; case "de": d1 = "Maximale Lebenspunkte"; d2 = "Debug-Modus aktivieren"; d3 = "Regeneration pro Sekunde"; d4 = "Schadensmultiplikator"; d5 = "Schadenszahlen anzeigen"; break; case "zh": d1 = "最大生命值"; d2 = "启用调试模式"; d3 = "每秒恢复"; d4 = "伤害倍率"; d5 = "显示浮动伤害"; break; default: d1 = "Maximum Health"; d2 = "Enable Debug Mode"; d3 = "Regeneration per Second"; d4 = "Damage Multiplier"; d5 = "Show Floating Damage"; break; } } private static HttpClient CreateHttpClient() { return new HttpClient(new HttpClientHandler { UseProxy = false }) { Timeout = Timeout.InfiniteTimeSpan }; } internal static async Task?> TranslateBatchAsync(string prompt, int entryCount, int maxRetries = 3) { await _semaphore.WaitAsync().ConfigureAwait(continueOnCapturedContext: false); try { int maxTokens = ComputeMaxTokens(entryCount); ChatRequest chatRequest = new ChatRequest(); chatRequest.Model = ModConfig.AiModel.Value; chatRequest.MaxTokens = maxTokens; chatRequest.Messages = new Message[2] { new Message { Role = "system", Content = BuildSystemPrompt() }, new Message { Role = "user", Content = prompt } }; ChatRequest chatRequest2 = chatRequest; string json = JsonConvert.SerializeObject((object)chatRequest2); for (int retry = 0; retry < maxRetries; retry++) { try { Dictionary dictionary = await SendStreamOnceAsync(json).ConfigureAwait(continueOnCapturedContext: false); if (dictionary != null) { return dictionary; } if (retry < maxRetries - 1) { ModLogger.LogDebugAsInfo($"[AI] 空响应,重试 {retry + 1}/{maxRetries}"); await Task.Delay((1 << retry) * 1000).ConfigureAwait(continueOnCapturedContext: false); } } catch (Exception ex) when (IsRetryable(ex)) { if (retry < maxRetries - 1) { int num = (1 << retry) * 1000; if (IsRateLimited(ex)) { num = Math.Max(num, 5000 * (retry + 1)); } ModLogger.LogDebugAsInfo($"[AI] 请求可重试失败,重试 {retry + 1}/{maxRetries}:{Describe(ex)}"); await Task.Delay(num).ConfigureAwait(continueOnCapturedContext: false); continue; } ModLogger.LogWarning("翻译请求失败:" + Describe(ex)); return null; } catch (Exception ex2) { ModLogger.LogWarning("翻译请求不可重试错误:" + Describe(ex2)); return null; } } return null; } catch (Exception ex3) { ModLogger.LogWarning("翻译批处理异常:" + Describe(ex3)); return null; } finally { _semaphore.Release(); } } private static async Task?> SendStreamOnceAsync(string json) { using CancellationTokenSource totalCts = new CancellationTokenSource(TimeSpan.FromMinutes(25.0)); CancellationToken ct = totalCts.Token; using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "https://api.deepseek.com/chat/completions") { Content = new StringContent(json, Encoding.UTF8, "application/json") }; request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", ModConfig.ApiKey.Value); using CancellationTokenSource headerCts = CancellationTokenSource.CreateLinkedTokenSource(new CancellationToken[1] { ct }); headerCts.CancelAfter(TimeSpan.FromSeconds(120.0)); using HttpResponseMessage response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, headerCts.Token).ConfigureAwait(continueOnCapturedContext: false); if (!response.IsSuccessStatusCode) { string errBody = ""; try { errBody = await response.Content.ReadAsStringAsync().ConfigureAwait(continueOnCapturedContext: false); } catch { } int statusCode = (int)response.StatusCode; if (statusCode == 429) { throw new HttpRequestException("请求过于频繁(429):" + Trim(errBody, 200)); } if (statusCode >= 500 || statusCode == 408) { throw new HttpRequestException($"服务端错误({statusCode}):{Trim(errBody, 200)}"); } ModLogger.LogWarning($"翻译请求失败(状态码 {statusCode})"); return null; } StringBuilder contentBuilder = new StringBuilder(); int cacheHit = 0; int cacheMiss = 0; bool sawUsage = false; using (Stream stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(continueOnCapturedContext: false)) { using StreamReader reader = new StreamReader(stream, Encoding.UTF8); while (!ct.IsCancellationRequested) { string text = await ReadLineWithIdleTimeoutAsync(reader, ct).ConfigureAwait(continueOnCapturedContext: false); if (text == null) { break; } if (text.Length == 0 || !text.StartsWith("data:", StringComparison.Ordinal)) { continue; } string text2 = text.Substring(5).Trim(); if (text2 == "[DONE]") { break; } try { JObject obj2 = JObject.Parse(text2); JToken obj3 = obj2["choices"]; JArray val = (JArray)(object)((obj3 is JArray) ? obj3 : null); if (val != null && ((JContainer)val).Count > 0) { JToken obj4 = val[0]; JToken obj5 = ((obj4 != null) ? obj4[(object)"delta"] : null); JObject val2 = (JObject)(object)((obj5 is JObject) ? obj5 : null); string value = ((val2 != null) ? ((JToken)val2).Value((object)"content") : null); if (!string.IsNullOrEmpty(value)) { contentBuilder.Append(value); } } if (TryReadCacheUsage(obj2["usage"], out var hit, out var miss)) { sawUsage = true; cacheHit = hit; cacheMiss = miss; } } catch (Exception) { } } } if (sawUsage) { ModLogger.LogDebugAsInfo($"[AI] 前缀缓存 命中={cacheHit} 未命中={cacheMiss}"); } string text3 = contentBuilder.ToString().Trim(); if (string.IsNullOrWhiteSpace(text3)) { return null; } return ParseYamlResponse(text3); } private static async Task ReadLineWithIdleTimeoutAsync(StreamReader reader, CancellationToken ct) { Task readTask = reader.ReadLineAsync(); Task timeoutTask = Task.Delay(TimeSpan.FromSeconds(120.0), ct); if (await Task.WhenAny(new Task[2] { readTask, timeoutTask }).ConfigureAwait(continueOnCapturedContext: false) == timeoutTask) { if (ct.IsCancellationRequested) { throw new OperationCanceledException(ct); } throw new TimeoutException($"流式读取空闲超时({120} 秒无新数据)"); } return await readTask.ConfigureAwait(continueOnCapturedContext: false); } private static int ComputeMaxTokens(int entryCount) { if (entryCount <= 0) { return 32768; } int num = entryCount * 120; if (num < 8192) { num = 8192; } if (num > 32768) { num = 32768; } return num; } private static bool TryReadCacheUsage(JToken? usageToken, out int hit, out int miss) { hit = 0; miss = 0; JObject val = (JObject)(object)((usageToken is JObject) ? usageToken : null); if (val == null) { return false; } hit = ReadInt(val, "prompt_cache_hit_tokens"); miss = ReadInt(val, "prompt_cache_miss_tokens"); if (hit == 0) { JToken obj = val["prompt_tokens_details"]; JObject val2 = (JObject)(object)((obj is JObject) ? obj : null); if (val2 != null) { hit = ReadInt(val2, "cached_tokens"); } } if (miss == 0) { int num = ReadInt(val, "prompt_tokens"); if (num > 0 && hit > 0) { miss = Math.Max(0, num - hit); } } if (hit <= 0 && miss <= 0) { return val["prompt_tokens"] != null; } return true; } private static int ReadInt(JObject obj, string name) { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Invalid comparison between Unknown and I4 //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Invalid comparison between Unknown and I4 //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Invalid comparison between Unknown and I4 //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Invalid comparison between Unknown and I4 JToken val = obj[name]; if (val == null || (int)val.Type == 10) { return 0; } if ((int)val.Type == 6 || (int)val.Type == 7 || (int)val.Type == 8) { try { return Extensions.Value((IEnumerable)val); } catch { return 0; } } return 0; } private static bool IsRetryable(Exception ex) { if (ex is TimeoutException) { return true; } if (ex is TaskCanceledException) { return true; } if (ex is OperationCanceledException) { return true; } if (ex is HttpRequestException) { return true; } if (ex is IOException) { return true; } if (ex is WebException) { return true; } if (ex.InnerException != null && IsRetryable(ex.InnerException)) { return true; } return false; } private static bool IsRateLimited(Exception ex) { string text = Describe(ex); if (text.IndexOf("429", StringComparison.Ordinal) < 0 && text.IndexOf("请求过于频繁", StringComparison.Ordinal) < 0) { return text.IndexOf("Too Many Requests", StringComparison.OrdinalIgnoreCase) >= 0; } return true; } private static string Describe(Exception ex) { if (ex is AggregateException { InnerException: not null } ex2) { return Describe(ex2.InnerException); } if (ex.InnerException != null && !(ex is HttpRequestException)) { return ex.GetType().Name + ": " + ex.Message + " <- " + ex.InnerException.Message; } return ex.GetType().Name + ": " + ex.Message; } private static string Trim(string s, int max) { if (string.IsNullOrEmpty(s)) { return ""; } s = s.Replace("\r", " ").Replace("\n", " "); if (s.Length > max) { return s.Substring(0, max); } return s; } private static Dictionary? ParseYamlResponse(string yamlText) { //IL_0058: Unknown result type (might be due to invalid IL or missing references) if (string.IsNullOrWhiteSpace(yamlText)) { return null; } string text = yamlText.Trim(); if (text.StartsWith("```", StringComparison.Ordinal)) { int num = text.IndexOf('\n'); if (num > 0) { text = text.Substring(num + 1); } int num2 = text.LastIndexOf("```", StringComparison.Ordinal); if (num2 >= 0) { text = text.Substring(0, num2); } text = text.Trim(); } try { return new DeserializerBuilder().Build().Deserialize>(text); } catch (Exception ex) { ModLogger.LogDebugAsInfo("[AI] 解析响应失败:" + ex.Message); return null; } } internal static string BuildPrompt(List entries) { StringBuilder stringBuilder = new StringBuilder(); foreach (TranslationEntry entry in entries) { stringBuilder.Append(entry.Id).Append(": ").Append(entry.OriginalText) .Append('\n'); } return stringBuilder.ToString(); } } internal static class TranslationApplicator { private static readonly FieldInfo? SectionField = typeof(ConfigDefinition).GetField("
k__BackingField", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo? KeyField = typeof(ConfigDefinition).GetField("k__BackingField", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo? DescriptionField = typeof(ConfigDescription).GetField("k__BackingField", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly PropertyInfo? EntriesProp = typeof(ConfigFile).GetProperty("Entries", BindingFlags.Instance | BindingFlags.NonPublic); private static readonly PropertyInfo? OrphanedProp = typeof(ConfigFile).GetProperty("OrphanedEntries", BindingFlags.Instance | BindingFlags.NonPublic); [ThreadStatic] private static ConfigFile? _orphanFile; [ThreadStatic] private static string? _orphanSection; [ThreadStatic] private static string? _orphanKey; internal static void ApplyInPlace(string guid, ConfigDefinition definition, ConfigDescription? configDescription) { if (TranslationManager.HasTranslation && !string.IsNullOrEmpty(guid) && !(definition == (ConfigDefinition)null)) { string text = definition.Section ?? ""; string text2 = definition.Key ?? ""; string text3 = ((configDescription != null) ? configDescription.Description : null) ?? ""; if (TryResolve(guid, text, text, out string translation) && translation != null && translation != text) { SetField(SectionField, definition, translation); } if (TryResolve(guid, text, text2, out string translation2) && translation2 != null && translation2 != text2) { SetField(KeyField, definition, translation2); } if (configDescription != null && !string.IsNullOrEmpty(text3) && TryResolve(guid, text, text3, out string translation3) && translation3 != null && translation3 != text3) { SetField(DescriptionField, configDescription, translation3); } } } internal static void RememberOrphanSource(ConfigFile file, string section, string key) { _orphanFile = file; _orphanSection = section; _orphanKey = key; } internal static void TryMigrateOrphanValue(ConfigEntryBase entry) { if (_orphanFile == null || entry == null || string.IsNullOrEmpty(_orphanSection) || string.IsNullOrEmpty(_orphanKey)) { ClearOrphan(); return; } try { Dictionary orphanedEntries = GetOrphanedEntries(_orphanFile); if (orphanedEntries == null) { return; } foreach (KeyValuePair item in orphanedEntries.ToList()) { if (item.Key.Section == _orphanSection && item.Key.Key == _orphanKey) { entry.SetSerializedValue(item.Value); orphanedEntries.Remove(item.Key); ModLogger.LogDebug("[应用] 已迁移 cfg 原值: " + _orphanSection + "/" + _orphanKey); break; } } } catch (Exception ex) { ModLogger.LogDebug("[应用] cfg 孤儿迁移跳过: " + ex.Message); } finally { ClearOrphan(); } } internal static void ClearOrphan() { _orphanFile = null; _orphanSection = null; _orphanKey = null; } internal static void ApplyToAlreadyLoadedPlugins() { if (!TranslationManager.HasTranslation) { return; } int num = 0; int num2 = 0; foreach (PluginInfo value in Chainloader.PluginInfos.Values) { BaseUnityPlugin instance = value.Instance; if (instance == null || instance.Config == null) { continue; } ConfigFile config = instance.Config; Dictionary entries = GetEntries(config); if (entries == null) { continue; } string pluginGuid = ReflectionHelper.GetPluginGuid(config); List list = entries.Values.ToList(); if (list.Count == 0) { continue; } foreach (ConfigEntryBase item in list) { ConfigDefinition definition = item.Definition; if (!(definition == (ConfigDefinition)null)) { config.Remove(definition); ApplyInPlace(pluginGuid, definition, item.Description); entries[definition] = item; num2++; } } num++; } if (num > 0) { ModLogger.LogDebugAsInfo($"[应用] 已对 {num} 个先加载模组补应用({num2} 条)"); } } private static Dictionary? GetEntries(ConfigFile file) { return EntriesProp?.GetValue(file) as Dictionary; } private static Dictionary? GetOrphanedEntries(ConfigFile file) { return OrphanedProp?.GetValue(file) as Dictionary; } private static void SetField(FieldInfo? field, object target, string value) { if (!(field == null)) { field.SetValue(target, value); } } private static bool TryResolve(string guid, string section, string key, out string? translation) { translation = null; if (string.IsNullOrEmpty(key)) { return false; } if (ExemptionManager.IsExempt(guid, section, key)) { return false; } if (!TranslationManager.TryGetTranslation(guid, section, key, out translation) || string.IsNullOrEmpty(translation)) { return false; } ModLogger.LogDebugAsInfo("[应用] " + guid + " | " + section + " | " + key + " → " + translation); return true; } } internal static class TranslationBatchBuilder { internal static List<(string guid, List entries)> BuildBatches(Dictionary>> data, int maxEntriesPerBatch, int maxCharsPerBatch) { List<(string, List)> list = new List<(string, List)>(); int idCounter = 0; foreach (KeyValuePair>> datum in data) { string key = datum.Key; Dictionary> dictionary = FilterUntranslated(key, datum.Value); if (dictionary.Count == 0) { continue; } foreach (List item in SplitByLimits(dictionary, key, ref idCounter, maxEntriesPerBatch, maxCharsPerBatch)) { list.Add((key, item)); } } return list; } internal static int GetTotalUntranslatedCount(Dictionary>> data) { int num = 0; foreach (KeyValuePair>> datum in data) { Dictionary> source = FilterUntranslated(datum.Key, datum.Value); num += source.Sum((KeyValuePair> s) => s.Value.Count); } return num; } private static Dictionary> FilterUntranslated(string guid, Dictionary> sections) { Dictionary> dictionary = new Dictionary>(); foreach (KeyValuePair> section in sections) { Dictionary dictionary2 = new Dictionary(); foreach (KeyValuePair item in section.Value) { if (!ExemptionManager.IsExempt(guid, section.Key, item.Key) && (!TranslationManager.HasTranslation || !TranslationManager.TryGetTranslation(guid, section.Key, item.Key, out string translation) || translation == null) && (!ModConfig.IsChineseTarget() || !TextHelper.IsMostlyChinese(item.Value))) { dictionary2[item.Key] = item.Value; } } if (dictionary2.Count > 0) { dictionary[section.Key] = dictionary2; } } return dictionary; } private static List> SplitByLimits(Dictionary> untranslated, string guid, ref int idCounter, int maxEntriesPerBatch, int maxCharsPerBatch) { List> list = new List>(); List list2 = new List(); int num = 0; foreach (KeyValuePair> item in untranslated) { foreach (KeyValuePair item2 in item.Value) { TranslationEntry translationEntry = new TranslationEntry { Guid = guid, Section = item.Key, Key = item2.Key, OriginalText = (item2.Value ?? ""), Id = "entry_" + idCounter++ }; int num2 = EstimateCharCost(translationEntry); if (list2.Count > 0 && (list2.Count >= maxEntriesPerBatch || num + num2 > maxCharsPerBatch)) { list.Add(list2); list2 = new List(); num = 0; } list2.Add(translationEntry); num += num2; } } if (list2.Count > 0) { list.Add(list2); } return list; } private static int EstimateCharCost(TranslationEntry entry) { return (entry.Id?.Length ?? 0) + 2 + (entry.OriginalText?.Length ?? 0) + 1; } } internal struct TranslationEntry { public string Guid; public string Section; public string Key; public string OriginalText; public string Id; } internal static class TranslationManager { internal static Dictionary>>? TranslationCache { get; private set; } internal static bool HasTranslation { get { if (TranslationCache != null) { return TranslationCache.Count > 0; } return false; } } internal static void SetCache(Dictionary>>? cache) { TranslationCache = cache; } internal static bool TryGetTranslation(string guid, string section, string key, out string? translation) { translation = null; if (TranslationCache == null) { return false; } if (TranslationCache.TryGetValue(guid, out Dictionary> value) && value.TryGetValue(section, out var value2) && value2.TryGetValue(key, out translation)) { return true; } return false; } } internal static class TranslationPipeline { private const int MaxCharsPerBatch = 24000; internal static bool IsRunning { get; private set; } internal static IEnumerator Run(Dictionary>> data, string sourceName) { if (IsRunning) { ModLogger.LogDebugAsInfo("[AI] 翻译正在进行中,忽略重复触发"); yield break; } if (data == null || data.Count == 0) { ModLogger.LogInfo("没有可翻译的配置,已跳过"); ModLogger.LogDebugAsInfo("[AI] 数据为空(来源:" + sourceName + ")"); yield break; } if (string.IsNullOrWhiteSpace(ModConfig.ApiKey.Value)) { ModLogger.LogWarning("请在模组配置「4. AI 翻译 > API密钥」中填写 DeepSeek API 密钥后再翻译"); yield break; } if (!ModConfig.EnableAiTranslation.Value) { ModLogger.LogDebugAsInfo("[AI] 启用AI翻译=false,跳过管线"); yield break; } IsRunning = true; DateTime startTime = DateTime.Now; if (!ModConfig.TryGetThroughput(out var concurrency, out var batchSize)) { ModLogger.LogWarning("吞吐参数无效(当前「" + ModConfig.ThroughputParams.Value + "」),已回退默认 12|200"); concurrency = 12; batchSize = 200; } DeepSeekApiClient.ConfigureConcurrency(concurrency); ModConfig.GetTargetLanguage(out string code, out string promptName); ModLogger.LogInfo("目标语言:" + promptName + "(" + code + ")"); ModLogger.LogInfo($"吞吐参数:并发 {concurrency},每批 {batchSize} 条"); ModLogger.LogDebugAsInfo("[AI] 筛选未译条目(来源:" + sourceName + ")"); List<(string guid, List entries)> batches = TranslationBatchBuilder.BuildBatches(data, batchSize, 24000); int totalUntranslatedCount = TranslationBatchBuilder.GetTotalUntranslatedCount(data); if (batches.Count == 0) { ModLogger.LogInfo("无需翻译:条目均已有译文"); IsRunning = false; yield break; } ModLogger.LogInfo($"正在翻译:{totalUntranslatedCount} 条,共 {batches.Count} 批"); ModLogger.LogDebugAsInfo($"[AI] 并发 {concurrency},≤{batchSize} 条/批,≤{24000} 字符,开始请求翻译接口"); yield return null; Task?>[] tasks = new Task>[batches.Count]; for (int i = 0; i < batches.Count; i++) { List item = batches[i].entries; string prompt = DeepSeekApiClient.BuildPrompt(item); tasks[i] = DeepSeekApiClient.TranslateBatchAsync(prompt, item.Count); } Task[]> allTasks = Task.WhenAll(tasks); int lastLoggedDone = -1; DateTime lastHeartbeat = DateTime.Now; while (!allTasks.IsCompleted) { int num = 0; for (int j = 0; j < tasks.Length; j++) { if (tasks[j].IsCompleted) { num++; } } if (num != lastLoggedDone && (num == tasks.Length || num % 5 == 0 || num == 1)) { lastLoggedDone = num; lastHeartbeat = DateTime.Now; ModLogger.LogInfo($"翻译进度:{num}/{batches.Count} 批"); } else if ((DateTime.Now - lastHeartbeat).TotalSeconds >= 30.0) { lastHeartbeat = DateTime.Now; ModLogger.LogInfo($"翻译进行中:{num}/{batches.Count} 批(等待接口返回)"); } yield return null; } try { allTasks.Wait(0); } catch (AggregateException) { } ModLogger.LogDebugAsInfo("[AI] 请求结束,合并保存"); int num2 = 0; int num3 = 0; int num4 = 0; Dictionary>> dictionary = TranslationManager.TranslationCache; if (dictionary == null) { dictionary = new Dictionary>>(); TranslationManager.SetCache(dictionary); } for (int k = 0; k < batches.Count; k++) { (string, List) tuple = batches[k]; Dictionary dictionary2 = SafeGetResult(tasks[k]); if (dictionary2 == null) { num3++; continue; } int num5 = 0; foreach (TranslationEntry item2 in tuple.Item2) { if (dictionary2.TryGetValue(item2.Id, out var value)) { if (!dictionary.ContainsKey(item2.Guid)) { dictionary[item2.Guid] = new Dictionary>(); } if (!dictionary[item2.Guid].ContainsKey(item2.Section)) { dictionary[item2.Guid][item2.Section] = new Dictionary(); } dictionary[item2.Guid][item2.Section][item2.Key] = value; num5++; } else { ModLogger.LogDebugAsInfo("[AI] 响应缺少 ID " + item2.Id + ",跳过"); } } if (num5 > 0) { num2++; num4 += num5; } else { num3++; } } TranslationManager.SetCache(dictionary); if (num4 > 0) { Yaml.SaveTranslation(dictionary); DeduplicateCollectionFile(dictionary); double totalSeconds = (DateTime.Now - startTime).TotalSeconds; ModLogger.LogInfo($"翻译完成:{num4} 条,耗时 {totalSeconds:F0} 秒"); if (num3 > 0) { ModLogger.LogWarning($"有 {num3} 批未写入(软失败/缺响应),成功批次 {num2}/{batches.Count}"); } else { ModLogger.LogDebugAsInfo($"[AI] 批次成功 {num2}/{batches.Count},失败 {num3}"); } LogSummaryDebug(); } else { ModLogger.LogWarning("翻译未成功,已保留原有译文文件"); ModLogger.LogDebugAsInfo($"[AI] 批次成功 0/{batches.Count},失败 {num3}"); } IsRunning = false; } private static Dictionary? SafeGetResult(Task?> task) { if (task == null) { return null; } try { if (task.IsFaulted || task.IsCanceled) { Exception ex = task.Exception?.GetBaseException(); if (ex != null) { ModLogger.LogDebugAsInfo("[AI] 批任务异常:" + ex.GetType().Name + ": " + ex.Message); } return null; } return (task.Status == TaskStatus.RanToCompletion) ? task.Result : null; } catch (Exception ex2) { ModLogger.LogDebugAsInfo("[AI] 读取批结果失败:" + ex2.Message); return null; } } private static void LogSummaryDebug() { Dictionary>> translationCache = TranslationManager.TranslationCache; if (translationCache == null) { return; } int count = translationCache.Count; int num = 0; int num2 = 0; foreach (KeyValuePair>> item in translationCache) { foreach (KeyValuePair> item2 in item.Value) { foreach (KeyValuePair item3 in item2.Value) { if (item3.Key == item2.Key) { num++; } else { num2++; } } } } ModLogger.LogDebugAsInfo($"[AI] 缓存合计:{count} 模组,{num} 分类,{num2} 条目"); } private static void DeduplicateCollectionFile(Dictionary>> translationData) { try { Dictionary>> dictionary = Yaml.LoadExport(); if (dictionary == null || dictionary.Count == 0) { ModLogger.LogDebug("[去重] 收集文件为空,跳过"); return; } int num = Yaml.DeduplicateExport(translationData, dictionary); if (num > 0) { Yaml.ExportYaml(dictionary); ModLogger.LogDebugAsInfo($"[去重] 已从导出文件移除 {num} 条已译条目"); } else { ModLogger.LogDebug("[去重] 无需移除"); } } catch (Exception ex) { ModLogger.LogDebugAsInfo("[去重] 异常:" + ex.Message); } } } } namespace AI_Localization.Config { public static class ModConfig { internal const string DefaultThroughput = "12|200"; private const int DefaultConcurrency = 12; private const int DefaultBatchSize = 200; internal const string DefaultTargetLanguage = "中文(简体)"; internal static ConfigEntry EnableCollection { get; private set; } internal static ConfigEntry Exemptions { get; private set; } internal static ConfigEntry LogTranslations { get; private set; } internal static ConfigEntry ApiKey { get; private set; } internal static ConfigEntry AiModel { get; private set; } internal static ConfigEntry EnableAiTranslation { get; private set; } internal static ConfigEntry ThroughputParams { get; private set; } internal static ConfigEntry TargetLanguage { get; private set; } public static void Init(BaseUnityPlugin plugin) { EnableCollection = plugin.Config.Bind("1. 收集", "启用收集", true, "是否启用配置项收集功能。关闭后,Harmony 补丁将不会收集任何新配置项。"); Exemptions = plugin.Config.Bind("2. 翻译豁免", "豁免名单", "", "哪些条目不翻译。格式: GUID|Section|Key,; 表示字段内多选,, 分下条规则,* 是通配\n例: Dreanegade.Hunter_Legacy|Draw Lever|* , *|*|Lock Configuration"); LogTranslations = plugin.Config.Bind("3. 调试", "记录翻译日志", false, "是否输出调试详情(逐条界面应用、API 重试、内部阶段、文件读写路径等)。\n默认关闭。日常只需看正常进度与 Warning;排查问题时再打开。"); ApiKey = plugin.Config.Bind("4. AI 翻译", "API密钥", "", "DeepSeek API 密钥。从 https://platform.deepseek.com/api_keys 获取"); AiModel = plugin.Config.Bind("4. AI 翻译", "AI模型", "deepseek-v4-flash", "DeepSeek 模型。推荐 deepseek-v4-flash / deepseek-v4-pro(旧名 deepseek-chat、deepseek-reasoner 即将弃用)"); EnableAiTranslation = plugin.Config.Bind("4. AI 翻译", "启用AI翻译", true, "是否启用 AI 翻译。开启后:收集完成会自动翻译;若关闭收集则用已有导出文件翻译。关闭则只加载已有译文、不调用 API。"); ThroughputParams = plugin.Config.Bind("4. AI 翻译", "吞吐参数", "12|200", "格式:并发请求数|每批条数,例 12|200。\n左=同时发几批(越大越快也越容易失败);右=每批多少条。\n官方参考:Flash≈2500,Pro≈500。推荐起步 12|200。"); TargetLanguage = plugin.Config.Bind("4. AI 翻译", "目标语言", "中文(简体)", "翻译输出语言。常用:中文(简体) / English / 日本語 / Deutsch\n也可写语言代码:zh / en / ja / de。改语言后请重启;各语言译文分文件保存。"); } internal static bool TryGetThroughput(out int concurrency, out int batchSize) { concurrency = 12; batchSize = 200; string text = "12|200"; try { if (ThroughputParams != null && !string.IsNullOrWhiteSpace(ThroughputParams.Value)) { text = ThroughputParams.Value.Trim(); } } catch { } string[] array = text.Split(new char[1] { '|' }, StringSplitOptions.None); if (array.Length != 2) { return false; } if (!int.TryParse(array[0].Trim(), out var result) || !int.TryParse(array[1].Trim(), out var result2)) { return false; } if (result < 1 || result2 < 1) { return false; } if (result > 10000) { result = 10000; } if (result2 > 5000) { result2 = 5000; } concurrency = result; batchSize = result2; return true; } internal static void GetTargetLanguage(out string code, out string promptName) { string text = "中文(简体)"; try { if (TargetLanguage != null && !string.IsNullOrWhiteSpace(TargetLanguage.Value)) { text = TargetLanguage.Value.Trim(); } } catch { } string text2 = text.ToLowerInvariant(); switch (text2.Replace(" ", "").Replace("(", "(").Replace(")", ")")) { case "中文(简体)": case "简体中文": case "中文": case "zh": case "zh-cn": case "zh_cn": case "chinese": case "chinesesimplified": code = "zh"; promptName = "简体中文"; break; case "english": case "en": case "en-us": case "en_us": code = "en"; promptName = "English"; break; case "日本語": case "日语": case "japanese": case "ja": case "jp": code = "ja"; promptName = "日本語"; break; case "deutsch": case "german": case "de": case "de-de": case "德语": code = "de"; promptName = "Deutsch"; break; default: code = SanitizeLanguageCode(text); promptName = text; break; } } internal static bool IsChineseTarget() { GetTargetLanguage(out string code, out string _); return code == "zh"; } private static string SanitizeLanguageCode(string raw) { if (string.IsNullOrWhiteSpace(raw)) { return "custom"; } StringBuilder stringBuilder = new StringBuilder(); string text = raw.Trim().ToLowerInvariant(); foreach (char c in text) { if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) { stringBuilder.Append(c); } else if (c == '-' || c == '_') { stringBuilder.Append('_'); } } string text2 = stringBuilder.ToString(); if (text2.Length == 0) { return "custom"; } if (text2.Length > 16) { text2 = text2.Substring(0, 16); } return text2; } } }