Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of AI Localization v0.1.3
AI Localization.dll
Decompiled 3 days agousing 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<string, Dictionary<string, Dictionary<string, string>>> 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<ExemptionRule> _rules = new List<ExemptionRule>(); 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<string, Dictionary<string, Dictionary<string, string>>> 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<string, Dictionary<string, Dictionary<string, string>>>? 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<string, Dictionary<string, Dictionary<string, string>>> dictionary = new DeserializerBuilder().Build().Deserialize<Dictionary<string, Dictionary<string, Dictionary<string, string>>>>(File.ReadAllText(exportPath)); ModLogger.LogDebugAsInfo($"[文件] 已读取导出:{dictionary.Count} 个模组 ← {exportPath}"); return dictionary; } catch (Exception ex) { ModLogger.LogError("读取导出文件失败:" + ex.Message); return null; } } public static Dictionary<string, Dictionary<string, Dictionary<string, string>>>? 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<string, Dictionary<string, Dictionary<string, string>>> dictionary = new DeserializerBuilder().Build().Deserialize<Dictionary<string, Dictionary<string, Dictionary<string, string>>>>(File.ReadAllText(translationPath)); ModLogger.LogDebugAsInfo($"[文件] 已读取译文:{dictionary.Count} 个模组 ← {translationPath}"); return dictionary; } catch (Exception ex) { ModLogger.LogError("读取翻译文件失败:" + ex.Message); return null; } } public static void SaveTranslation(Dictionary<string, Dictionary<string, Dictionary<string, string>>> 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<string, Dictionary<string, Dictionary<string, string>>> translationData, Dictionary<string, Dictionary<string, Dictionary<string, string>>> exportData) { if (translationData == null || translationData.Count == 0) { return 0; } if (exportData == null || exportData.Count == 0) { return 0; } int num = 0; foreach (KeyValuePair<string, Dictionary<string, Dictionary<string, string>>> item in exportData.ToList()) { string key = item.Key; Dictionary<string, Dictionary<string, string>> value = item.Value; if (!translationData.TryGetValue(key, out Dictionary<string, Dictionary<string, string>> value2)) { continue; } foreach (KeyValuePair<string, Dictionary<string, string>> item2 in value.ToList()) { string key2 = item2.Key; Dictionary<string, string> value3 = item2.Value; if (!value2.TryGetValue(key2, out var value4)) { continue; } foreach (KeyValuePair<string, string> 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<CollectionState>? 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<string, Dictionary<string, Dictionary<string, string>>> 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<string, Dictionary<string, Dictionary<string, string>>> 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<string, Dictionary<string, Dictionary<string, string>>> 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<string, Dictionary<string, Dictionary<string, string>>> 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<string, Dictionary<string, Dictionary<string, string>>> LocaleData { get; private set; } = new Dictionary<string, Dictionary<string, Dictionary<string, string>>>(); 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<string, Dictionary<string, string>>(); } if (!LocaleData[guid].ContainsKey(section)) { LocaleData[guid][section] = new Dictionary<string, string>(); 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<Dictionary<string, string>?> 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<string, string> 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<Dictionary<string, string>?> 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<string>((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<string?> ReadLineWithIdleTimeoutAsync(StreamReader reader, CancellationToken ct) { Task<string> 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<int>((IEnumerable<JToken>)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<string, string>? 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<Dictionary<string, string>>(text); } catch (Exception ex) { ModLogger.LogDebugAsInfo("[AI] 解析响应失败:" + ex.Message); return null; } } internal static string BuildPrompt(List<TranslationEntry> 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("<Section>k__BackingField", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo? KeyField = typeof(ConfigDefinition).GetField("<Key>k__BackingField", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo? DescriptionField = typeof(ConfigDescription).GetField("<Description>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<ConfigDefinition, string> orphanedEntries = GetOrphanedEntries(_orphanFile); if (orphanedEntries == null) { return; } foreach (KeyValuePair<ConfigDefinition, string> 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<ConfigDefinition, ConfigEntryBase> entries = GetEntries(config); if (entries == null) { continue; } string pluginGuid = ReflectionHelper.GetPluginGuid(config); List<ConfigEntryBase> 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<ConfigDefinition, ConfigEntryBase>? GetEntries(ConfigFile file) { return EntriesProp?.GetValue(file) as Dictionary<ConfigDefinition, ConfigEntryBase>; } private static Dictionary<ConfigDefinition, string>? GetOrphanedEntries(ConfigFile file) { return OrphanedProp?.GetValue(file) as Dictionary<ConfigDefinition, string>; } 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<TranslationEntry> entries)> BuildBatches(Dictionary<string, Dictionary<string, Dictionary<string, string>>> data, int maxEntriesPerBatch, int maxCharsPerBatch) { List<(string, List<TranslationEntry>)> list = new List<(string, List<TranslationEntry>)>(); int idCounter = 0; foreach (KeyValuePair<string, Dictionary<string, Dictionary<string, string>>> datum in data) { string key = datum.Key; Dictionary<string, Dictionary<string, string>> dictionary = FilterUntranslated(key, datum.Value); if (dictionary.Count == 0) { continue; } foreach (List<TranslationEntry> item in SplitByLimits(dictionary, key, ref idCounter, maxEntriesPerBatch, maxCharsPerBatch)) { list.Add((key, item)); } } return list; } internal static int GetTotalUntranslatedCount(Dictionary<string, Dictionary<string, Dictionary<string, string>>> data) { int num = 0; foreach (KeyValuePair<string, Dictionary<string, Dictionary<string, string>>> datum in data) { Dictionary<string, Dictionary<string, string>> source = FilterUntranslated(datum.Key, datum.Value); num += source.Sum((KeyValuePair<string, Dictionary<string, string>> s) => s.Value.Count); } return num; } private static Dictionary<string, Dictionary<string, string>> FilterUntranslated(string guid, Dictionary<string, Dictionary<string, string>> sections) { Dictionary<string, Dictionary<string, string>> dictionary = new Dictionary<string, Dictionary<string, string>>(); foreach (KeyValuePair<string, Dictionary<string, string>> section in sections) { Dictionary<string, string> dictionary2 = new Dictionary<string, string>(); foreach (KeyValuePair<string, string> 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<List<TranslationEntry>> SplitByLimits(Dictionary<string, Dictionary<string, string>> untranslated, string guid, ref int idCounter, int maxEntriesPerBatch, int maxCharsPerBatch) { List<List<TranslationEntry>> list = new List<List<TranslationEntry>>(); List<TranslationEntry> list2 = new List<TranslationEntry>(); int num = 0; foreach (KeyValuePair<string, Dictionary<string, string>> item in untranslated) { foreach (KeyValuePair<string, string> 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<TranslationEntry>(); 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<string, Dictionary<string, Dictionary<string, string>>>? TranslationCache { get; private set; } internal static bool HasTranslation { get { if (TranslationCache != null) { return TranslationCache.Count > 0; } return false; } } internal static void SetCache(Dictionary<string, Dictionary<string, Dictionary<string, string>>>? 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<string, Dictionary<string, string>> 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<string, Dictionary<string, Dictionary<string, string>>> 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<TranslationEntry> 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<Dictionary<string, string>?>[] tasks = new Task<Dictionary<string, string>>[batches.Count]; for (int i = 0; i < batches.Count; i++) { List<TranslationEntry> item = batches[i].entries; string prompt = DeepSeekApiClient.BuildPrompt(item); tasks[i] = DeepSeekApiClient.TranslateBatchAsync(prompt, item.Count); } Task<Dictionary<string, string>[]> 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<string, Dictionary<string, Dictionary<string, string>>> dictionary = TranslationManager.TranslationCache; if (dictionary == null) { dictionary = new Dictionary<string, Dictionary<string, Dictionary<string, string>>>(); TranslationManager.SetCache(dictionary); } for (int k = 0; k < batches.Count; k++) { (string, List<TranslationEntry>) tuple = batches[k]; Dictionary<string, string> 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<string, Dictionary<string, string>>(); } if (!dictionary[item2.Guid].ContainsKey(item2.Section)) { dictionary[item2.Guid][item2.Section] = new Dictionary<string, string>(); } 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<string, string>? SafeGetResult(Task<Dictionary<string, string>?> 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<string, Dictionary<string, Dictionary<string, string>>> translationCache = TranslationManager.TranslationCache; if (translationCache == null) { return; } int count = translationCache.Count; int num = 0; int num2 = 0; foreach (KeyValuePair<string, Dictionary<string, Dictionary<string, string>>> item in translationCache) { foreach (KeyValuePair<string, Dictionary<string, string>> item2 in item.Value) { foreach (KeyValuePair<string, string> item3 in item2.Value) { if (item3.Key == item2.Key) { num++; } else { num2++; } } } } ModLogger.LogDebugAsInfo($"[AI] 缓存合计:{count} 模组,{num} 分类,{num2} 条目"); } private static void DeduplicateCollectionFile(Dictionary<string, Dictionary<string, Dictionary<string, string>>> translationData) { try { Dictionary<string, Dictionary<string, Dictionary<string, string>>> 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<bool> EnableCollection { get; private set; } internal static ConfigEntry<string> Exemptions { get; private set; } internal static ConfigEntry<bool> LogTranslations { get; private set; } internal static ConfigEntry<string> ApiKey { get; private set; } internal static ConfigEntry<string> AiModel { get; private set; } internal static ConfigEntry<bool> EnableAiTranslation { get; private set; } internal static ConfigEntry<string> ThroughputParams { get; private set; } internal static ConfigEntry<string> TargetLanguage { get; private set; } public static void Init(BaseUnityPlugin plugin) { EnableCollection = plugin.Config.Bind<bool>("1. 收集", "启用收集", true, "是否启用配置项收集功能。关闭后,Harmony 补丁将不会收集任何新配置项。"); Exemptions = plugin.Config.Bind<string>("2. 翻译豁免", "豁免名单", "", "哪些条目不翻译。格式: GUID|Section|Key,; 表示字段内多选,, 分下条规则,* 是通配\n例: Dreanegade.Hunter_Legacy|Draw Lever|* , *|*|Lock Configuration"); LogTranslations = plugin.Config.Bind<bool>("3. 调试", "记录翻译日志", false, "是否输出调试详情(逐条界面应用、API 重试、内部阶段、文件读写路径等)。\n默认关闭。日常只需看正常进度与 Warning;排查问题时再打开。"); ApiKey = plugin.Config.Bind<string>("4. AI 翻译", "API密钥", "", "DeepSeek API 密钥。从 https://platform.deepseek.com/api_keys 获取"); AiModel = plugin.Config.Bind<string>("4. AI 翻译", "AI模型", "deepseek-v4-flash", "DeepSeek 模型。推荐 deepseek-v4-flash / deepseek-v4-pro(旧名 deepseek-chat、deepseek-reasoner 即将弃用)"); EnableAiTranslation = plugin.Config.Bind<bool>("4. AI 翻译", "启用AI翻译", true, "是否启用 AI 翻译。开启后:收集完成会自动翻译;若关闭收集则用已有导出文件翻译。关闭则只加载已有译文、不调用 API。"); ThroughputParams = plugin.Config.Bind<string>("4. AI 翻译", "吞吐参数", "12|200", "格式:并发请求数|每批条数,例 12|200。\n左=同时发几批(越大越快也越容易失败);右=每批多少条。\n官方参考:Flash≈2500,Pro≈500。推荐起步 12|200。"); TargetLanguage = plugin.Config.Bind<string>("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; } } }