using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using Newtonsoft.Json; namespace LocalizationTool.Services { /// /// 工具配置(exe 旁 LocalizationTool.config.json)。 /// 语义:BaseUrl / Model / Keys / AppId / Secret 字段为空 = 使用内置默认(开箱即用); /// 非空 = 自定义优先(互斥,不混入内置 Key)。配置文件里不写入内置凭证。 /// public static class Config { /// 内置默认 OpenAI 兼容 Base URL。 public const string BuiltInBaseUrl = "https://api.agnes-ai.cn/v1"; /// 内置默认模型。 public const string BuiltInModel = "agnes-2.5-flash"; /// /// 内置默认模型的最大上下文(token)。agnes-2.5-flash 官方规格:512K context / 65.5K max output。 /// 上下文用量指示器以此估算占比;0 = 未知,不显示占比。 /// public const int BuiltInMaxContextTokens = 512000; /// 内置默认单次回复的最大输出 token(防止思考模式把 arguments 截断)。 public const int BuiltInMaxOutputTokens = 65536; public class TranslationConfig { /// 提供方:openai(OpenAI 兼容 LLM)或 baidu(百度通用翻译) public string Provider { get; set; } = "openai"; /// Base URL;空 = 内置默认(BuiltInBaseUrl),非空 = 自定义优先。 public string BaseUrl { get; set; } = ""; /// 模型名;空 = 内置默认(BuiltInModel),非空 = 自定义优先。 public string Model { get; set; } = ""; /// /// 加密后的 API Key 列表(多 Key 轮询:一个超时/失败自动切下一个)。 /// 空列表 = 使用内置默认 Key;非空 = 只用自定义(互斥)。 /// public List Keys { get; set; } = new List(); /// /// 模型最大上下文(token),用于上下文用量指示器;0 或空 = 使用内置默认(BuiltInMaxContextTokens)。 /// public int MaxContextTokens { get; set; } = 0; /// /// 单次回复最大输出 token;0 或空 = 使用内置默认(BuiltInMaxOutputTokens)。 /// 思考模型若太小会把 tool call arguments 截断成非法 JSON(400)。 /// public int MaxOutputTokens { get; set; } = 0; /// 旧版单字符串 ApiKey(仅兼容读取,保存时不再写入)。 [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public string ApiKey { get; set; } public string FromLang { get; set; } = "zh"; public string ToLang { get; set; } = "en"; } public class BaiduConfig { /// 加密后的 APP ID public string AppId { get; set; } = ""; /// 加密后的密钥 public string Secret { get; set; } = ""; public string FromLang { get; set; } = "zh"; public string ToLang { get; set; } = "en"; /// 翻译模式:nmt = 机器翻译(默认,快)/ llm = 大模型翻译(质量好,需开通服务) public string ModelType { get; set; } = "nmt"; /// /// 自定义翻译指令(reference 字段,llm 大模型模式生效): /// 对翻译结果的要求,如保留数字/符号只翻文本。 /// public string Reference { get; set; } = "数字、英文单词、单位、特殊符号和表情图标保持原样不要翻译,只翻译其中的中文文本部分,符号的位置和数量保持不变。"; } /// AI 助手(对话)配置 public class AiChatConfig { /// /// 工具权限模式: /// ask(默认)= 修改类工具执行前弹确认框; /// trusted = 完全信任,所有工具自动执行不弹框(全部操作仍记录日志)。 /// public string PermissionMode { get; set; } = "ask"; /// /// 用户自定义提示词。为空 = 使用按当前语种动态生成的默认提示词 /// (默认提示词会随加载的语言包动态变化,见 AiChatWindow.BuildDefaultPrompt)。 /// public string CustomPrompt { get; set; } = ""; } /// AI 助手联网查资料(web_search / fetch_url 工具)配置 public class WebSearchConfig { /// 搜索引擎:bing(默认,免 Key)/ bocha(博查,国内,需 Key)/ tavily(海外,需 Key) public string Provider { get; set; } = "bing"; public string ApiKey { get; set; } = ""; /// web_search 默认返回的结果条数(1~10) public int MaxResults { get; set; } = 8; } public class ConfigRoot { public string LastDirectory { get; set; } = ""; public TranslationConfig Translation { get; set; } = new TranslationConfig(); public BaiduConfig Baidu { get; set; } = new BaiduConfig(); public AiChatConfig AiChat { get; set; } = new AiChatConfig(); public WebSearchConfig WebSearch { get; set; } = new WebSearchConfig(); } private static string ConfigPath => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "LocalizationTool.config.json"); public static ConfigRoot Load() { ConfigRoot root = null; try { if (File.Exists(ConfigPath)) { root = JsonConvert.DeserializeObject( File.ReadAllText(ConfigPath, Encoding.UTF8)); } } catch { root = null; } if (root == null) root = new ConfigRoot(); // 迁移:老版本曾把内置凭证密文写入配置文件 → 清空,统一"空 = 默认"语义 if (MigrateLegacyDefaults(root)) Save(root); return root; } public static void Save(ConfigRoot cfg) { File.WriteAllText(ConfigPath, JsonConvert.SerializeObject(cfg, Formatting.Indented), new UTF8Encoding(false)); } /// /// 内置默认凭证(已按存储方案加密的密文常量,源码/二进制中不出现明文)。 /// 配置文件里这些字段为空时,运行时兜底使用本内置默认。 /// private static readonly string[] BuiltInKeys = { "UqvOuuwdHj1BrKHS7QAcTH3ml9SEFUd5DJ+B/tP9IQs3d6K45O0OCGR+v4DPxR5GJmmy", "UqvOyvoCIkUemLjJwB0tU16IpMvuLhJ0X+6IqMHWIhJIbqSly+o8X11qut/ShjQPYmOq", "UqvO7sQADm8Zu6L65g1Eewa9q+TREjsme4Ky8rLWE1B7GPGa1903WkFeg7jxxQ0hQEGX", "UqvO98IJFlQYu5nT/x4BcwGCwqT6DjotcK6p/7HmFAtRTbKm65gXJ0x++drw9jgDXQmy", }; private const string BuiltInBaiduAppId = "E/DRsZ19XzsZ+Nq/h2RDJgQ="; private const string BuiltInBaiduSecret = "Spiq9tQPKH5Kq4by/DUeeFaltd8="; /// 内置默认 Key 的明文(解密一次缓存)。 private static readonly List BuiltInKeyPlain = BuiltInKeys.Select(DecryptKey).Where(k => !string.IsNullOrEmpty(k)).ToList(); private static readonly string BuiltInBaiduAppIdPlain = DecryptKey(BuiltInBaiduAppId); private static readonly string BuiltInBaiduSecretPlain = DecryptKey(BuiltInBaiduSecret); /// /// 迁移旧版配置:老版本 EnsureDefaults 会把内置凭证密文写入配置文件, /// 这里把它们清空,统一为"字段为空 = 使用内置默认"的新语义。返回是否有改动。 /// private static bool MigrateLegacyDefaults(ConfigRoot root) { var changed = false; var t = root.Translation ?? (root.Translation = new TranslationConfig()); if (t.Keys == null) { t.Keys = new List(); changed = true; } else if (t.Keys.Count > 0) { // 老版本保存时会把内置 Key 附加到自定义 Key 之后做轮询兜底(混合列表)。 // 迁移:过滤掉解密后 == 内置 Key 的密文;剩下的若为空 = 未自定义。 var custom = t.Keys.Where(k => !BuiltInKeyPlain.Contains(DecryptKey(k))).ToList(); if (custom.Count != t.Keys.Count) { t.Keys = custom; changed = true; } } // 等于内置默认值的 BaseUrl / Model 也归一为空(保持"空 = 默认"语义) if (string.Equals(t.BaseUrl?.Trim(), BuiltInBaseUrl, StringComparison.OrdinalIgnoreCase)) { t.BaseUrl = ""; changed = true; } if (string.Equals(t.Model?.Trim(), BuiltInModel, StringComparison.OrdinalIgnoreCase)) { t.Model = ""; changed = true; } var b = root.Baidu ?? (root.Baidu = new BaiduConfig()); if (!string.IsNullOrEmpty(b.AppId) && DecryptKey(b.AppId) == BuiltInBaiduAppIdPlain) { b.AppId = ""; changed = true; } if (!string.IsNullOrEmpty(b.Secret) && DecryptKey(b.Secret) == BuiltInBaiduSecretPlain) { b.Secret = ""; changed = true; } return changed; } // Key 混淆:位置相关 XOR + Base64。 // 明文不出现在源码、二进制常量与配置文件中;解码逻辑随字节位置变化, // 直接对二进制做字符串扫描或反编译搜 "sk-" 均取不到 Key。 private static byte KeyByte(int i) => (byte)(0x5A ^ ((i * 31 + 0x7B) & 0xFF)); /// 加密:明文 → Base64 密文(保存到配置文件用)。 public static string EncryptKey(string plain) { if (string.IsNullOrEmpty(plain)) return ""; var bytes = Encoding.UTF8.GetBytes(plain); for (int i = 0; i < bytes.Length; i++) bytes[i] ^= KeyByte(i); return Convert.ToBase64String(bytes); } /// /// 解密:Base64 密文 → 明文。 /// 依次尝试当前方案、旧版方案;都解不开时按明文原样返回(兼容手动填写的 Key)。 /// public static string DecryptKey(string encrypted) { if (string.IsNullOrEmpty(encrypted)) return ""; var s = TryDecrypt(encrypted, i => KeyByte(i)); // 当前方案:位置相关 XOR if (s == null) s = TryDecrypt(encrypted, _ => 0x5A); // 旧版:固定 0x5A return s ?? encrypted; } /// 按给定密钥函数尝试解码;结果含控制字符/乱码则视为失败返回 null。 private static string TryDecrypt(string encrypted, Func keyFn) { try { var bytes = Convert.FromBase64String(encrypted); if (bytes.Length == 0) return null; for (int i = 0; i < bytes.Length; i++) bytes[i] ^= keyFn(i); var s = Encoding.UTF8.GetString(bytes); if (s.Contains('\uFFFD')) return null; foreach (var c in s) if (char.IsControl(c)) return null; return s; } catch { return null; } } /// /// 掩码显示(设置界面回显用):保留前 5 后 4,中间打码,不显示完整 Key。 /// public static string MaskKey(string plain) { if (string.IsNullOrEmpty(plain)) return ""; if (plain.Length <= 10) return "****"; return plain.Substring(0, 5) + "…" + plain.Substring(plain.Length - 4); } /// /// 获取解密后的 OpenAI 兼容 Key 列表(轮询用),已去重。 /// 空配置 = 内置默认 Key;非空 = 只用自定义(互斥,不混入内置)。 /// 兼容旧版单字符串 ApiKey 字段(该字段历史为明文,直接追加)。 /// public static List GetOpenAiKeys(TranslationConfig cfg) { var keys = new List(); if (cfg != null) { if (cfg.Keys != null) { foreach (var k in cfg.Keys) { var decoded = DecryptKey(k); if (!string.IsNullOrEmpty(decoded) && !keys.Contains(decoded)) keys.Add(decoded); } } if (!string.IsNullOrEmpty(cfg.ApiKey) && !keys.Contains(cfg.ApiKey)) keys.Add(cfg.ApiKey); } if (keys.Count == 0) return BuiltInKeyPlain.ToList(); // 空 = 内置默认 return keys; } /// /// 解析实际使用的 Base URL:配置为空 → 内置默认;非空 → 自定义(去掉末尾斜杠)。 /// public static string ResolveBaseUrl(TranslationConfig cfg) => string.IsNullOrWhiteSpace(cfg?.BaseUrl) ? BuiltInBaseUrl : cfg.BaseUrl.TrimEnd('/'); /// 解析实际使用的模型名:配置为空 → 内置默认;非空 → 自定义。 public static string ResolveModel(TranslationConfig cfg) => string.IsNullOrWhiteSpace(cfg?.Model) ? BuiltInModel : cfg.Model.Trim(); /// 解析最大上下文 token:0/空 → 内置默认(agnes-2.5-flash 512K)。 public static int ResolveMaxContextTokens(TranslationConfig cfg) => cfg != null && cfg.MaxContextTokens > 0 ? cfg.MaxContextTokens : BuiltInMaxContextTokens; /// 解析单次回复最大输出 token:0/空 → 内置默认(65536)。 public static int ResolveMaxOutputTokens(TranslationConfig cfg) => cfg != null && cfg.MaxOutputTokens > 0 ? cfg.MaxOutputTokens : BuiltInMaxOutputTokens; /// /// 解密百度凭证。AppId/Secret 任一为空或两者皆未自定义 → 整体回退内置默认 /// (避免只填一半导致请求失败)。非空且完整 = 只用自定义。 /// public static (string appId, string secret) GetBaiduCredentials(BaiduConfig cfg) { if (cfg == null) return (BuiltInBaiduAppIdPlain, BuiltInBaiduSecretPlain); var appId = DecryptKey(cfg.AppId); var secret = DecryptKey(cfg.Secret); if (string.IsNullOrWhiteSpace(appId) || string.IsNullOrWhiteSpace(secret)) return (BuiltInBaiduAppIdPlain, BuiltInBaiduSecretPlain); return (appId, secret); } /// OpenAI 兼容 AI 是否使用内置默认(BaseUrl / Model / Keys 均未自定义)。 public static bool IsDefaultOpenAi(TranslationConfig cfg) => cfg != null && string.IsNullOrWhiteSpace(cfg.BaseUrl) && string.IsNullOrWhiteSpace(cfg.Model) && (cfg.Keys == null || cfg.Keys.Count == 0) && string.IsNullOrWhiteSpace(cfg.ApiKey); /// 百度翻译是否使用内置默认凭证(AppId / Secret 均未自定义)。 public static bool IsDefaultBaidu(BaiduConfig cfg) => cfg != null && string.IsNullOrWhiteSpace(cfg.AppId) && string.IsNullOrWhiteSpace(cfg.Secret); /// 清空 OpenAI 兼容自定义配置,恢复为内置默认。 public static void ResetOpenAiToDefault(TranslationConfig cfg) { if (cfg == null) return; cfg.BaseUrl = ""; cfg.Model = ""; cfg.Keys = new List(); cfg.ApiKey = null; } /// 清空百度自定义凭证,恢复为内置默认。 public static void ResetBaiduToDefault(BaiduConfig cfg) { if (cfg == null) return; cfg.AppId = ""; cfg.Secret = ""; } } }