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 = "";
}
}
}