TranslationSettingsViewModel.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Threading.Tasks;
  5. using System.Windows;
  6. using LocalizationTool.Services;
  7. namespace LocalizationTool.ViewModels
  8. {
  9. /// <summary>
  10. /// 翻译设置窗口 ViewModel。
  11. /// 语义:BaseUrl / Model / Keys / AppId / Secret 留空 = 内置默认;填了 = 自定义优先;
  12. /// "恢复默认"清空自定义字段。
  13. /// </summary>
  14. public class TranslationSettingsViewModel : ViewModelBase
  15. {
  16. private readonly Config.ConfigRoot _cfg;
  17. private bool _isBaidu;
  18. private string _baseUrl;
  19. private string _model;
  20. private bool _isLlm;
  21. private string _reference;
  22. private string _testText = "保存配置";
  23. private string _testResult = "翻译结果将显示在这里...";
  24. private LanguageEntry _testLang;
  25. private bool _isBusy;
  26. private List<string> _allModels = new List<string>();
  27. public TranslationSettingsViewModel(Config.ConfigRoot cfg)
  28. {
  29. _cfg = cfg ?? new Config.ConfigRoot();
  30. // 掩码回显
  31. var firstKey = Config.GetOpenAiKeys(_cfg.Translation).FirstOrDefault() ?? "";
  32. OpenAiKeyMask = Config.MaskKey(firstKey);
  33. var (appId, secret) = Config.GetBaiduCredentials(_cfg.Baidu);
  34. BaiduAppIdMask = Config.MaskKey(appId);
  35. BaiduSecretMask = Config.MaskKey(secret);
  36. BaseUrl = Config.ResolveBaseUrl(_cfg.Translation);
  37. Model = Config.ResolveModel(_cfg.Translation);
  38. IsBaidu = string.Equals(_cfg.Translation.Provider, "baidu", StringComparison.OrdinalIgnoreCase);
  39. IsLlm = string.Equals(_cfg.Baidu.ModelType, "llm", StringComparison.OrdinalIgnoreCase);
  40. Reference = _cfg.Baidu.Reference ?? "";
  41. // 输入框初始显示掩码(内容 == 掩码 = 未修改,保存时保持原值)
  42. OpenAiKeyInput = OpenAiKeyMask;
  43. BaiduAppIdInput = BaiduAppIdMask;
  44. BaiduSecretInput = BaiduSecretMask;
  45. TestCommand = new RelayCommand(() => { var _ = TestTranslateAsync(); });
  46. FetchModelsCommand = new RelayCommand(() => { var _ = FetchModelsAsync(); });
  47. ResetOpenAICommand = new RelayCommand(ResetOpenAiToDefault);
  48. ResetBaiduCommand = new RelayCommand(ResetBaiduToDefault);
  49. SaveCommand = new RelayCommand(Save);
  50. CancelCommand = new RelayCommand(Cancel);
  51. SwitchToOpenAiCommand = new RelayCommand(() => IsOpenAi = true);
  52. SwitchToBaiduCommand = new RelayCommand(() => IsBaidu = true);
  53. }
  54. // ---- 引擎选择 ----
  55. public bool IsOpenAi
  56. {
  57. get => !_isBaidu;
  58. set { if (value) IsBaidu = false; }
  59. }
  60. public bool IsBaidu
  61. {
  62. get => _isBaidu;
  63. set { if (Set(ref _isBaidu, value)) { OnPropertyChanged(nameof(IsOpenAi)); UpdateStatus(); } }
  64. }
  65. // ---- OpenAI ----
  66. public string BaseUrl
  67. {
  68. get => _baseUrl;
  69. set => Set(ref _baseUrl, value);
  70. }
  71. public string Model
  72. {
  73. get => _model;
  74. set
  75. {
  76. if (Set(ref _model, value)) OnPropertyChanged(nameof(FilteredModels));
  77. }
  78. }
  79. /// <summary>模型最大上下文(token)输入;空/非法 = 用内置默认(agnes-2.5-flash 512K)。</summary>
  80. public string MaxContextTokensText
  81. {
  82. get => _cfg.Translation.MaxContextTokens > 0 ? _cfg.Translation.MaxContextTokens.ToString() : "";
  83. set
  84. {
  85. _cfg.Translation.MaxContextTokens =
  86. int.TryParse(value?.Trim(), out var n) && n > 0 ? n : 0;
  87. }
  88. }
  89. /// <summary>当前生效的最大上下文(含内置默认值说明)。</summary>
  90. public string MaxContextHint =>
  91. $"留空 = 内置默认 {Config.BuiltInMaxContextTokens:N0} token(agnes-2.5-flash 规格)";
  92. /// <summary>已存 Key 的掩码(输入框内容 == 掩码 = 未修改)。</summary>
  93. public string OpenAiKeyMask
  94. {
  95. get => _openAiKeyMask;
  96. set => Set(ref _openAiKeyMask, value);
  97. }
  98. private string _openAiKeyMask = "";
  99. /// <summary>输入框当前内容(由 View 在 PasswordBox 变化时更新)。</summary>
  100. public string OpenAiKeyInput
  101. {
  102. get => _openAiKeyInput;
  103. set => Set(ref _openAiKeyInput, value);
  104. }
  105. private string _openAiKeyInput = "";
  106. // ---- 百度 ----
  107. public string BaiduAppIdMask
  108. {
  109. get => _baiduAppIdMask;
  110. set => Set(ref _baiduAppIdMask, value);
  111. }
  112. private string _baiduAppIdMask = "";
  113. public string BaiduSecretMask
  114. {
  115. get => _baiduSecretMask;
  116. set => Set(ref _baiduSecretMask, value);
  117. }
  118. private string _baiduSecretMask = "";
  119. public string BaiduAppIdInput
  120. {
  121. get => _baiduAppIdInput;
  122. set => Set(ref _baiduAppIdInput, value);
  123. }
  124. private string _baiduAppIdInput = "";
  125. public string BaiduSecretInput
  126. {
  127. get => _baiduSecretInput;
  128. set => Set(ref _baiduSecretInput, value);
  129. }
  130. private string _baiduSecretInput = "";
  131. public bool IsLlm
  132. {
  133. get => _isLlm;
  134. set
  135. {
  136. if (Set(ref _isLlm, value)) OnPropertyChanged(nameof(IsNmt));
  137. }
  138. }
  139. /// <summary>NMT 模式(与 IsLlm 互斥)。</summary>
  140. public bool IsNmt
  141. {
  142. get => !_isLlm;
  143. set
  144. {
  145. var newLlm = !value;
  146. if (_isLlm == newLlm) return;
  147. _isLlm = newLlm;
  148. OnPropertyChanged(nameof(IsLlm));
  149. OnPropertyChanged(nameof(IsNmt));
  150. }
  151. }
  152. public string Reference
  153. {
  154. get => _reference;
  155. set => Set(ref _reference, value);
  156. }
  157. // ---- 测试 ----
  158. /// <summary>测试目标语言候选(全量语言目录)。</summary>
  159. public List<LanguageEntry> TestLanguages => LanguageCatalog.All;
  160. public LanguageEntry TestLang
  161. {
  162. get => _testLang;
  163. set => Set(ref _testLang, value);
  164. }
  165. public string TestText
  166. {
  167. get => _testText;
  168. set => Set(ref _testText, value);
  169. }
  170. public string TestResult
  171. {
  172. get => _testResult;
  173. set => Set(ref _testResult, value);
  174. }
  175. public bool IsBusy
  176. {
  177. get => _isBusy;
  178. set => Set(ref _isBusy, value);
  179. }
  180. // ---- 状态 ----
  181. private string _openAiStatus = "";
  182. private string _baiduStatus = "";
  183. public string OpenAiStatus
  184. {
  185. get => _openAiStatus;
  186. private set => Set(ref _openAiStatus, value);
  187. }
  188. public string BaiduStatus
  189. {
  190. get => _baiduStatus;
  191. private set => Set(ref _baiduStatus, value);
  192. }
  193. /// <summary>请求关闭窗口(true = 保存成功)。</summary>
  194. public event Action<bool> RequestClose;
  195. // ---- 命令 ----
  196. public RelayCommand TestCommand { get; }
  197. public RelayCommand FetchModelsCommand { get; }
  198. public RelayCommand ResetOpenAICommand { get; }
  199. public RelayCommand ResetBaiduCommand { get; }
  200. public RelayCommand SaveCommand { get; }
  201. public RelayCommand CancelCommand { get; }
  202. public RelayCommand SwitchToOpenAiCommand { get; }
  203. public RelayCommand SwitchToBaiduCommand { get; }
  204. // ---- 初始化 ----
  205. public void Initialize(LanguageEntry defaultLang)
  206. {
  207. TestLang = defaultLang;
  208. UpdateStatus();
  209. }
  210. public void UpdateStatus()
  211. {
  212. OpenAiStatus = Config.IsDefaultOpenAi(_cfg.Translation)
  213. ? "✅ 当前使用内置默认 AI(留空即内置默认)"
  214. : "✏️ 当前使用自定义 AI 配置";
  215. BaiduStatus = Config.IsDefaultBaidu(_cfg.Baidu)
  216. ? "✅ 当前使用内置默认凭证(留空即内置默认)"
  217. : "✏️ 当前使用自定义凭证";
  218. }
  219. // ---- 恢复默认 ----
  220. private void ResetOpenAiToDefault()
  221. {
  222. Config.ResetOpenAiToDefault(_cfg.Translation);
  223. BaseUrl = Config.BuiltInBaseUrl;
  224. Model = Config.BuiltInModel;
  225. OpenAiKeyMask = Config.MaskKey(Config.GetOpenAiKeys(_cfg.Translation).FirstOrDefault() ?? "");
  226. OpenAiKeyInput = OpenAiKeyMask;
  227. UpdateStatus();
  228. }
  229. private void ResetBaiduToDefault()
  230. {
  231. Config.ResetBaiduToDefault(_cfg.Baidu);
  232. BaiduAppIdMask = Config.MaskKey(Config.GetBaiduCredentials(_cfg.Baidu).appId);
  233. BaiduSecretMask = Config.MaskKey(Config.GetBaiduCredentials(_cfg.Baidu).secret);
  234. BaiduAppIdInput = BaiduAppIdMask;
  235. BaiduSecretInput = BaiduSecretMask;
  236. UpdateStatus();
  237. }
  238. // ---- 保存 ----
  239. private void Save()
  240. {
  241. if (IsOpenAi)
  242. {
  243. _cfg.Translation.Provider = "openai";
  244. _cfg.Translation.ApiKey = null;
  245. var url = BaseUrl?.Trim();
  246. _cfg.Translation.BaseUrl =
  247. (string.IsNullOrEmpty(url) || url == Config.BuiltInBaseUrl) ? "" : url;
  248. var model = Model?.Trim();
  249. _cfg.Translation.Model =
  250. (string.IsNullOrEmpty(model) || model == Config.BuiltInModel) ? "" : model;
  251. if (OpenAiKeyInput.Length == 0)
  252. _cfg.Translation.Keys = new List<string>();
  253. else if (OpenAiKeyInput != OpenAiKeyMask)
  254. _cfg.Translation.Keys = new List<string> { Config.EncryptKey(OpenAiKeyInput) };
  255. }
  256. else
  257. {
  258. _cfg.Translation.Provider = "baidu";
  259. _cfg.Baidu.ModelType = IsLlm ? "llm" : "nmt";
  260. _cfg.Baidu.Reference = Reference?.Trim();
  261. if (BaiduAppIdInput.Length == 0)
  262. _cfg.Baidu.AppId = "";
  263. else if (BaiduAppIdInput != BaiduAppIdMask)
  264. _cfg.Baidu.AppId = Config.EncryptKey(BaiduAppIdInput);
  265. if (BaiduSecretInput.Length == 0)
  266. _cfg.Baidu.Secret = "";
  267. else if (BaiduSecretInput != BaiduSecretMask)
  268. _cfg.Baidu.Secret = Config.EncryptKey(BaiduSecretInput);
  269. }
  270. Config.Save(_cfg);
  271. Translation.Reset();
  272. RequestClose?.Invoke(true);
  273. }
  274. private void Cancel() => RequestClose?.Invoke(false);
  275. // ---- 测试翻译 ----
  276. private async Task TestTranslateAsync()
  277. {
  278. var text = TestText?.Trim();
  279. if (string.IsNullOrEmpty(text))
  280. {
  281. MessageBox.Show("请输入测试文本", "提示");
  282. return;
  283. }
  284. var langEntry = TestLang;
  285. var targetLang = langEntry?.Code ?? "en-US";
  286. var targetDisplay = langEntry?.Display ?? targetLang;
  287. try
  288. {
  289. IsBusy = true;
  290. TestResult = $"翻译中...(目标:{targetDisplay})";
  291. var tempConfig = new Config.ConfigRoot
  292. {
  293. Translation = new Config.TranslationConfig
  294. {
  295. Provider = IsOpenAi ? "openai" : "baidu",
  296. BaseUrl = string.IsNullOrWhiteSpace(BaseUrl) ? Config.BuiltInBaseUrl : BaseUrl.Trim(),
  297. Model = string.IsNullOrWhiteSpace(Model) ? Config.BuiltInModel : Model.Trim(),
  298. Keys = OpenAiKeyInput.Length > 0 && OpenAiKeyInput != OpenAiKeyMask
  299. ? new List<string> { Config.EncryptKey(OpenAiKeyInput) }
  300. : (_cfg.Translation.Keys ?? new List<string>()),
  301. },
  302. Baidu = new Config.BaiduConfig
  303. {
  304. AppId = BaiduAppIdInput.Length > 0 && BaiduAppIdInput != BaiduAppIdMask
  305. ? Config.EncryptKey(BaiduAppIdInput) : _cfg.Baidu.AppId,
  306. Secret = BaiduSecretInput.Length > 0 && BaiduSecretInput != BaiduSecretMask
  307. ? Config.EncryptKey(BaiduSecretInput) : _cfg.Baidu.Secret,
  308. ModelType = IsLlm ? "llm" : "nmt",
  309. Reference = Reference?.Trim(),
  310. }
  311. };
  312. Translation.Reset();
  313. var provider = Translation.Create(tempConfig);
  314. var result = await provider.TranslateAsync(text, "zh", targetLang);
  315. TestResult = $"✅ 翻译成功(引擎:{provider.Name},目标:{targetDisplay}):\n{result}";
  316. }
  317. catch (Exception ex)
  318. {
  319. TestResult = $"❌ 翻译失败:\n{ex.Message}";
  320. }
  321. finally
  322. {
  323. IsBusy = false;
  324. }
  325. }
  326. // ---- 获取模型列表 ----
  327. private async Task FetchModelsAsync()
  328. {
  329. var baseUrl = string.IsNullOrWhiteSpace(BaseUrl)
  330. ? Config.BuiltInBaseUrl
  331. : BaseUrl.Trim().TrimEnd('/');
  332. var key = ResolveOpenAiKey();
  333. if (string.IsNullOrWhiteSpace(baseUrl) || string.IsNullOrWhiteSpace(key))
  334. {
  335. MessageBox.Show("请先填写 Base URL 和 API Key", "提示");
  336. return;
  337. }
  338. try
  339. {
  340. IsBusy = true;
  341. using (var client = new System.Net.Http.HttpClient { Timeout = TimeSpan.FromSeconds(20) })
  342. {
  343. client.DefaultRequestHeaders.Authorization =
  344. new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", key);
  345. var resp = await client.GetAsync(baseUrl + "/models");
  346. var body = await resp.Content.ReadAsStringAsync();
  347. var j = Newtonsoft.Json.Linq.JObject.Parse(body);
  348. var arr = j["data"] as Newtonsoft.Json.Linq.JArray;
  349. var models = arr?.Select(t => (string)t["id"])
  350. .Where(s => !string.IsNullOrEmpty(s))
  351. .Distinct()
  352. .OrderBy(s => s)
  353. .ToList() ?? new List<string>();
  354. if (models.Count == 0)
  355. {
  356. MessageBox.Show("接口返回为空,未获取到模型", "提示");
  357. return;
  358. }
  359. _allModels = models;
  360. Model = models.First();
  361. OnPropertyChanged(nameof(FilteredModels));
  362. }
  363. }
  364. catch (Exception ex)
  365. {
  366. MessageBox.Show($"获取模型失败:{ex.Message}", "错误");
  367. }
  368. finally
  369. {
  370. IsBusy = false;
  371. }
  372. }
  373. private string ResolveOpenAiKey()
  374. {
  375. var input = OpenAiKeyInput?.Trim() ?? "";
  376. if (!string.IsNullOrEmpty(input) && input != OpenAiKeyMask) return input;
  377. return Config.GetOpenAiKeys(_cfg.Translation).FirstOrDefault() ?? "";
  378. }
  379. /// <summary>模型下拉候选(按 Model 输入过滤)。</summary>
  380. public List<string> FilteredModels
  381. {
  382. get
  383. {
  384. if (_allModels.Count == 0) return null;
  385. var kw = Model?.Trim() ?? "";
  386. return string.IsNullOrEmpty(kw)
  387. ? _allModels
  388. : _allModels.Where(m => m.IndexOf(kw, StringComparison.OrdinalIgnoreCase) >= 0).ToList();
  389. }
  390. }
  391. public void RefreshModelFilter() => OnPropertyChanged(nameof(FilteredModels));
  392. }
  393. }