| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356 |
- using System;
- using System.Collections.Generic;
- using System.IO;
- using System.Linq;
- using System.Text;
- using Newtonsoft.Json;
- namespace LocalizationTool.Services
- {
- /// <summary>
- /// 工具配置(exe 旁 LocalizationTool.config.json)。
- /// 语义:BaseUrl / Model / Keys / AppId / Secret 字段为空 = 使用内置默认(开箱即用);
- /// 非空 = 自定义优先(互斥,不混入内置 Key)。配置文件里不写入内置凭证。
- /// </summary>
- public static class Config
- {
- /// <summary>内置默认 OpenAI 兼容 Base URL。</summary>
- public const string BuiltInBaseUrl = "https://api.agnes-ai.cn/v1";
- /// <summary>内置默认模型。</summary>
- public const string BuiltInModel = "agnes-2.5-flash";
- /// <summary>
- /// 内置默认模型的最大上下文(token)。agnes-2.5-flash 官方规格:512K context / 65.5K max output。
- /// 上下文用量指示器以此估算占比;0 = 未知,不显示占比。
- /// </summary>
- public const int BuiltInMaxContextTokens = 512000;
- /// <summary>内置默认单次回复的最大输出 token(防止思考模式把 arguments 截断)。</summary>
- public const int BuiltInMaxOutputTokens = 65536;
- public class TranslationConfig
- {
- /// <summary>提供方:openai(OpenAI 兼容 LLM)或 baidu(百度通用翻译)</summary>
- public string Provider { get; set; } = "openai";
- /// <summary>Base URL;空 = 内置默认(BuiltInBaseUrl),非空 = 自定义优先。</summary>
- public string BaseUrl { get; set; } = "";
- /// <summary>模型名;空 = 内置默认(BuiltInModel),非空 = 自定义优先。</summary>
- public string Model { get; set; } = "";
- /// <summary>
- /// 加密后的 API Key 列表(多 Key 轮询:一个超时/失败自动切下一个)。
- /// 空列表 = 使用内置默认 Key;非空 = 只用自定义(互斥)。
- /// </summary>
- public List<string> Keys { get; set; } = new List<string>();
- /// <summary>
- /// 模型最大上下文(token),用于上下文用量指示器;0 或空 = 使用内置默认(BuiltInMaxContextTokens)。
- /// </summary>
- public int MaxContextTokens { get; set; } = 0;
- /// <summary>
- /// 单次回复最大输出 token;0 或空 = 使用内置默认(BuiltInMaxOutputTokens)。
- /// 思考模型若太小会把 tool call arguments 截断成非法 JSON(400)。
- /// </summary>
- public int MaxOutputTokens { get; set; } = 0;
- /// <summary>旧版单字符串 ApiKey(仅兼容读取,保存时不再写入)。</summary>
- [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
- public string ApiKey { get; set; }
- public string FromLang { get; set; } = "zh";
- public string ToLang { get; set; } = "en";
- }
- public class BaiduConfig
- {
- /// <summary>加密后的 APP ID</summary>
- public string AppId { get; set; } = "";
- /// <summary>加密后的密钥</summary>
- public string Secret { get; set; } = "";
- public string FromLang { get; set; } = "zh";
- public string ToLang { get; set; } = "en";
- /// <summary>翻译模式:nmt = 机器翻译(默认,快)/ llm = 大模型翻译(质量好,需开通服务)</summary>
- public string ModelType { get; set; } = "nmt";
- /// <summary>
- /// 自定义翻译指令(reference 字段,llm 大模型模式生效):
- /// 对翻译结果的要求,如保留数字/符号只翻文本。
- /// </summary>
- public string Reference { get; set; } = "数字、英文单词、单位、特殊符号和表情图标保持原样不要翻译,只翻译其中的中文文本部分,符号的位置和数量保持不变。";
- }
- /// <summary>AI 助手(对话)配置</summary>
- public class AiChatConfig
- {
- /// <summary>
- /// 工具权限模式:
- /// ask(默认)= 修改类工具执行前弹确认框;
- /// trusted = 完全信任,所有工具自动执行不弹框(全部操作仍记录日志)。
- /// </summary>
- public string PermissionMode { get; set; } = "ask";
- /// <summary>
- /// 用户自定义提示词。为空 = 使用按当前语种动态生成的默认提示词
- /// (默认提示词会随加载的语言包动态变化,见 AiChatWindow.BuildDefaultPrompt)。
- /// </summary>
- public string CustomPrompt { get; set; } = "";
- }
- /// <summary>AI 助手联网查资料(web_search / fetch_url 工具)配置</summary>
- public class WebSearchConfig
- {
- /// <summary>搜索引擎:bing(默认,免 Key)/ bocha(博查,国内,需 Key)/ tavily(海外,需 Key)</summary>
- public string Provider { get; set; } = "bing";
- public string ApiKey { get; set; } = "";
- /// <summary>web_search 默认返回的结果条数(1~10)</summary>
- 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<ConfigRoot>(
- 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));
- }
- /// <summary>
- /// 内置默认凭证(已按存储方案加密的密文常量,源码/二进制中不出现明文)。
- /// 配置文件里这些字段为空时,运行时兜底使用本内置默认。
- /// </summary>
- 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=";
- /// <summary>内置默认 Key 的明文(解密一次缓存)。</summary>
- private static readonly List<string> BuiltInKeyPlain =
- BuiltInKeys.Select(DecryptKey).Where(k => !string.IsNullOrEmpty(k)).ToList();
- private static readonly string BuiltInBaiduAppIdPlain = DecryptKey(BuiltInBaiduAppId);
- private static readonly string BuiltInBaiduSecretPlain = DecryptKey(BuiltInBaiduSecret);
- /// <summary>
- /// 迁移旧版配置:老版本 EnsureDefaults 会把内置凭证密文写入配置文件,
- /// 这里把它们清空,统一为"字段为空 = 使用内置默认"的新语义。返回是否有改动。
- /// </summary>
- 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<string>(); 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));
- /// <summary>加密:明文 → Base64 密文(保存到配置文件用)。</summary>
- 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);
- }
- /// <summary>
- /// 解密:Base64 密文 → 明文。
- /// 依次尝试当前方案、旧版方案;都解不开时按明文原样返回(兼容手动填写的 Key)。
- /// </summary>
- 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;
- }
- /// <summary>按给定密钥函数尝试解码;结果含控制字符/乱码则视为失败返回 null。</summary>
- private static string TryDecrypt(string encrypted, Func<int, byte> 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;
- }
- }
- /// <summary>
- /// 掩码显示(设置界面回显用):保留前 5 后 4,中间打码,不显示完整 Key。
- /// </summary>
- 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);
- }
- /// <summary>
- /// 获取解密后的 OpenAI 兼容 Key 列表(轮询用),已去重。
- /// 空配置 = 内置默认 Key;非空 = 只用自定义(互斥,不混入内置)。
- /// 兼容旧版单字符串 ApiKey 字段(该字段历史为明文,直接追加)。
- /// </summary>
- public static List<string> GetOpenAiKeys(TranslationConfig cfg)
- {
- var keys = new List<string>();
- 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;
- }
- /// <summary>
- /// 解析实际使用的 Base URL:配置为空 → 内置默认;非空 → 自定义(去掉末尾斜杠)。
- /// </summary>
- public static string ResolveBaseUrl(TranslationConfig cfg) =>
- string.IsNullOrWhiteSpace(cfg?.BaseUrl) ? BuiltInBaseUrl : cfg.BaseUrl.TrimEnd('/');
- /// <summary>解析实际使用的模型名:配置为空 → 内置默认;非空 → 自定义。</summary>
- public static string ResolveModel(TranslationConfig cfg) =>
- string.IsNullOrWhiteSpace(cfg?.Model) ? BuiltInModel : cfg.Model.Trim();
- /// <summary>解析最大上下文 token:0/空 → 内置默认(agnes-2.5-flash 512K)。</summary>
- public static int ResolveMaxContextTokens(TranslationConfig cfg) =>
- cfg != null && cfg.MaxContextTokens > 0 ? cfg.MaxContextTokens : BuiltInMaxContextTokens;
- /// <summary>解析单次回复最大输出 token:0/空 → 内置默认(65536)。</summary>
- public static int ResolveMaxOutputTokens(TranslationConfig cfg) =>
- cfg != null && cfg.MaxOutputTokens > 0 ? cfg.MaxOutputTokens : BuiltInMaxOutputTokens;
- /// <summary>
- /// 解密百度凭证。AppId/Secret 任一为空或两者皆未自定义 → 整体回退内置默认
- /// (避免只填一半导致请求失败)。非空且完整 = 只用自定义。
- /// </summary>
- 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);
- }
- /// <summary>OpenAI 兼容 AI 是否使用内置默认(BaseUrl / Model / Keys 均未自定义)。</summary>
- 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);
- /// <summary>百度翻译是否使用内置默认凭证(AppId / Secret 均未自定义)。</summary>
- public static bool IsDefaultBaidu(BaiduConfig cfg) =>
- cfg != null &&
- string.IsNullOrWhiteSpace(cfg.AppId) &&
- string.IsNullOrWhiteSpace(cfg.Secret);
- /// <summary>清空 OpenAI 兼容自定义配置,恢复为内置默认。</summary>
- public static void ResetOpenAiToDefault(TranslationConfig cfg)
- {
- if (cfg == null) return;
- cfg.BaseUrl = "";
- cfg.Model = "";
- cfg.Keys = new List<string>();
- cfg.ApiKey = null;
- }
- /// <summary>清空百度自定义凭证,恢复为内置默认。</summary>
- public static void ResetBaiduToDefault(BaiduConfig cfg)
- {
- if (cfg == null) return;
- cfg.AppId = "";
- cfg.Secret = "";
- }
- }
- }
|