| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790 |
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- using System.Threading.Tasks;
- using System.Windows;
- using LocalizationTool.Models;
- using LocalizationTool.Services;
- using Newtonsoft.Json.Linq;
- namespace LocalizationTool.ViewModels
- {
- public partial class MainViewModel
- {
- private const Newtonsoft.Json.Formatting FormattingNone = Newtonsoft.Json.Formatting.None;
- /// <summary>AI 助手可用工具集(OpenAI Function Calling schema)。</summary>
- public JArray BuildAiTools()
- {
- JObject Fn(string name, string desc, JObject props, params string[] required) => new JObject
- {
- ["type"] = "function",
- ["function"] = new JObject
- {
- ["name"] = name,
- ["description"] = desc,
- ["parameters"] = new JObject
- {
- ["type"] = "object",
- ["properties"] = props,
- ["required"] = new JArray(required),
- },
- },
- };
- return new JArray
- {
- Fn("list_languages", "列出当前加载的所有目标语言及各语言词条数/缺译数", new JObject()),
- Fn("get_stats", "获取词条统计:总数、各语言已译/缺译数量", new JObject()),
- Fn("search_entries", "按关键词搜索词条(匹配中文原文,返回最多 20 条含各语言译文)",
- new JObject { ["keyword"] = new JObject { ["type"] = "string", ["description"] = "搜索关键词" } }, "keyword"),
- Fn("get_entry", "读取某个词条的全部语言译文",
- new JObject { ["key"] = new JObject { ["type"] = "string", ["description"] = "中文原文(词条 key)" } }, "key"),
- Fn("add_entry", "新增一条词条:中文 key + 各语言译文(已存在则报错)。写入后自动保存并刷新表格",
- new JObject
- {
- ["key"] = new JObject { ["type"] = "string", ["description"] = "中文原文(key)" },
- ["translations"] = new JObject { ["type"] = "object", ["description"] = "各语言译文,键为语言代码(如 en-US/ja-JP),可只给部分语言,留空的为缺译" },
- }, "key"),
- Fn("add_entries", "批量新增多个词条(key 已存在则跳过该条)。添加后自动保存并刷新表格。用户提供待拆分内容时,先按分隔符拆分、逐条翻译后用本工具一次性添加",
- new JObject
- {
- ["entries"] = new JObject
- {
- ["type"] = "array",
- ["description"] = "词条数组,每项 { \"key\": \"中文原文\", \"translations\": { \"en-US\": \"...\", \"ja-JP\": \"...\" } }",
- ["items"] = new JObject
- {
- ["type"] = "object",
- ["properties"] = new JObject
- {
- ["key"] = new JObject { ["type"] = "string", ["description"] = "中文原文(key)" },
- ["translations"] = new JObject { ["type"] = "object", ["description"] = "各语言译文" },
- },
- ["required"] = new JArray("key", "translations"),
- },
- },
- }, "entries"),
- Fn("update_translation", "更新某词条某语言的译文(不存在则创建该译文)。写入后自动保存并刷新表格",
- new JObject
- {
- ["key"] = new JObject { ["type"] = "string", ["description"] = "中文原文(词条 key)" },
- ["lang"] = new JObject { ["type"] = "string", ["description"] = "语言代码(如 en-US)" },
- ["value"] = new JObject { ["type"] = "string", ["description"] = "新译文" },
- }, "key", "lang", "value"),
- Fn("delete_entry", "删除一个词条(所有语言包中移除,需谨慎)",
- new JObject { ["key"] = new JObject { ["type"] = "string", ["description"] = "中文原文(词条 key)" } }, "key"),
- Fn("delete_entries", "批量删除多个词条(所有语言包中移除,只弹一次确认框)。keys 与 indices 至少提供一个;删除多个词条时务必用本工具一次性完成,不要逐条调用 delete_entry",
- new JObject
- {
- ["keys"] = new JObject { ["type"] = "array", ["items"] = new JObject { ["type"] = "string" }, ["description"] = "要删除的词条 key(中文原文)数组" },
- ["indices"] = new JObject { ["type"] = "array", ["items"] = new JObject { ["type"] = "integer" }, ["description"] = "要删除的表格序号(# 列)数组,如 [3,5,8]" },
- }),
- Fn("get_validation_issues", "获取最近一次校验发现的所有问题词条(含原文、语言、当前错误译文、问题原因)。用于批量重译:拿到后一次性调用 batch_update_translations 写回全部修正。注意:日语等豁免语言中译文与中文原文相同的汉字词属正常写法,已自动排除,无需重译",
- new JObject()),
- Fn("batch_update_translations", "批量写回多个词条的多个语言译文(推荐用于批量重译——一次调用完成全部修正,不要逐条调用 update_translation)",
- new JObject
- {
- ["entries"] = new JObject
- {
- ["type"] = "array",
- ["description"] = "词条数组,每项 { \"key\": \"中文原文\", \"translations\": { \"en-US\": \"...\", \"ja-JP\": \"...\" } }",
- ["items"] = new JObject
- {
- ["type"] = "object",
- ["properties"] = new JObject
- {
- ["key"] = new JObject { ["type"] = "string", ["description"] = "中文原文(词条 key)" },
- ["translations"] = new JObject { ["type"] = "object", ["description"] = "该词条要更新的各语言译文" },
- },
- ["required"] = new JArray("key", "translations"),
- },
- },
- }, "entries"),
- Fn("save_all", "把所有语言包保存到磁盘(.dat)", new JObject()),
- Fn("get_missing_translations", "列出缺译词条(含表格序号 index、中文 key)。lang 可选:只看某语言;不传返回所有语言的缺译",
- new JObject
- {
- ["lang"] = new JObject { ["type"] = "string", ["description"] = "语言代码(如 en-US),可选" },
- }),
- Fn("baidu_translate_missing", "用百度翻译批量补译缺译词条并保存(一次调用完成,内部自动分批)。lang 必填;indices 可选——按表格序号只补译指定行,缺省翻译该语言全部缺译",
- new JObject
- {
- ["lang"] = new JObject { ["type"] = "string", ["description"] = "目标语言代码(如 en-US)" },
- ["indices"] = new JObject { ["type"] = "array", ["description"] = "表格序号数组(可选),如 [3,5,8]" },
- }, "lang"),
- Fn("delete_entry_by_index", "按表格序号删除单个词条(会弹出确认框,用户确认后才执行)",
- new JObject { ["index"] = new JObject { ["type"] = "integer", ["description"] = "表格序号(# 列)" } }, "index"),
- Fn("read_file", "读取任意路径的文本文件内容(自动检测 UTF-8 / GBK)。适合查看代码、配置、日志",
- new JObject { ["path"] = new JObject { ["type"] = "string", ["description"] = "文件完整路径,如 F:\\Team\\TeamAAS\\Some\\File.cs" } }, "path"),
- Fn("write_file", "写入(覆盖)任意路径的文本文件,父目录不存在会自动创建。适合生成新文件、整文件重写",
- new JObject
- {
- ["path"] = new JObject { ["type"] = "string", ["description"] = "文件完整路径" },
- ["content"] = new JObject { ["type"] = "string", ["description"] = "要写入的完整文件内容" },
- }, "path", "content"),
- Fn("edit_file", "对任意路径的文本文件做局部替换(old_text 在文件中必须唯一出现一次,多/零次都会失败)。适合改代码:先 read_file 拿到内容,再精确替换",
- new JObject
- {
- ["path"] = new JObject { ["type"] = "string", ["description"] = "文件完整路径" },
- ["old_text"] = new JObject { ["type"] = "string", ["description"] = "被替换的原文(必须唯一出现)" },
- ["new_text"] = new JObject { ["type"] = "string", ["description"] = "替换后的新文本" },
- }, "path", "old_text", "new_text"),
- Fn("list_directory", "列出目录下的子目录与文件(自动跳过 bin / obj / .git / node_modules 等无关目录)。适合先摸清项目结构",
- new JObject { ["path"] = new JObject { ["type"] = "string", ["description"] = "目录完整路径" } }, "path"),
- Fn("run_command", "执行 cmd 或 dotnet 命令并获取输出。适合查询系统信息、目录内容、git 操作、编译等",
- new JObject
- {
- ["command"] = new JObject { ["type"] = "string", ["description"] = "要执行的命令,如 \"dir\" 或 \"--version\"" },
- ["dotnet"] = new JObject { ["type"] = "boolean", ["description"] = "true=执行 dotnet 命令;false(默认)=执行 cmd 命令" },
- }, "command"),
- Fn("web_search", "联网搜索:用搜索引擎查询互联网上的最新资料(行业术语、产品名称、文档、新闻等),返回标题/链接/摘要。需要了解详情时,再对结果里的 url 调用 fetch_url 读取网页正文",
- new JObject
- {
- ["query"] = new JObject { ["type"] = "string", ["description"] = "搜索关键词" },
- ["count"] = new JObject { ["type"] = "integer", ["description"] = "结果条数(可选,默认 8,最多 10)" },
- }, "query"),
- Fn("fetch_url", "读取指定网址网页的正文文本(自动去除 HTML 标签)。配合 web_search 使用:先搜索拿到链接,再读取正文了解详情",
- new JObject
- {
- ["url"] = new JObject { ["type"] = "string", ["description"] = "网页地址(http/https)" },
- }, "url"),
- };
- }
- /// <summary>
- /// AI 工具执行器:在 UI 线程操作语言包数据,返回 JSON 结果字符串回传给模型。
- /// 修改类工具执行后自动保存 + 刷新表格。
- /// </summary>
- public async Task<string> ExecuteAiToolAsync(string name, string argsJson)
- {
- try
- {
- var args = string.IsNullOrWhiteSpace(argsJson) ? new JObject() : JObject.Parse(argsJson);
- var source = _packs.FirstOrDefault(p => p.IsSource);
- switch (name)
- {
- case "list_languages":
- {
- var arr = new JArray();
- foreach (var t in _targets)
- {
- var total = _rows.Count;
- var missing = _rows.Sum(r => r.Cells.TryGetValue(t.LanguageCode, out var c) ? (c.IsMissing ? 1 : 0) : 0);
- arr.Add(new JObject { ["code"] = t.LanguageCode, ["name"] = t.LanguageName, ["translated"] = total - missing, ["missing"] = missing });
- }
- return new JObject { ["ok"] = true, ["languages"] = arr }.ToString(Newtonsoft.Json.Formatting.None);
- }
- case "get_stats":
- {
- var arr = new JArray();
- foreach (var t in _targets)
- {
- var missing = _rows.Sum(r => r.Cells.TryGetValue(t.LanguageCode, out var c) ? (c.IsMissing ? 1 : 0) : 0);
- arr.Add(new JObject { ["code"] = t.LanguageCode, ["total"] = _rows.Count, ["translated"] = _rows.Count - missing, ["missing"] = missing });
- }
- return new JObject { ["ok"] = true, ["totalEntries"] = _rows.Count, ["byLanguage"] = arr }.ToString(Newtonsoft.Json.Formatting.None);
- }
- case "search_entries":
- {
- var kw = (string)args["keyword"] ?? "";
- var arr = new JArray();
- foreach (var r in _rows.Where(r => r.Key != null && r.Key.Contains(kw)).Take(20))
- {
- var tr = new JObject();
- foreach (var c in r.Cells.Values)
- tr[c.LangCode] = c.Value;
- arr.Add(new JObject { ["index"] = r.Index, ["key"] = r.Key, ["translations"] = tr });
- }
- return new JObject { ["ok"] = true, ["count"] = arr.Count, ["entries"] = arr }.ToString(Newtonsoft.Json.Formatting.None);
- }
- case "get_entry":
- {
- var key = (string)args["key"];
- var row = _rows.FirstOrDefault(r => r.Key == key);
- if (row == null)
- return new JObject { ["ok"] = false, ["error"] = "词条不存在: " + key }.ToString(Newtonsoft.Json.Formatting.None);
- var tr = new JObject();
- foreach (var c in row.Cells.Values) tr[c.LangCode] = c.Value;
- return new JObject { ["ok"] = true, ["key"] = key, ["translations"] = tr }.ToString(Newtonsoft.Json.Formatting.None);
- }
- case "add_entry":
- {
- var key = (string)args["key"];
- if (string.IsNullOrWhiteSpace(key)) return new JObject { ["ok"] = false, ["error"] = "key 不能为空" }.ToString(Newtonsoft.Json.Formatting.None);
- if (source == null) return new JObject { ["ok"] = false, ["error"] = "未找到 zh-CN 源语言包" }.ToString(Newtonsoft.Json.Formatting.None);
- if (source.Translations.ContainsKey(key))
- return new JObject { ["ok"] = false, ["error"] = "词条已存在: " + key }.ToString(Newtonsoft.Json.Formatting.None);
- source.Translations[key] = key;
- var tr = args["translations"] as JObject;
- if (tr != null)
- {
- foreach (var lang in _targets)
- {
- var val = (string)tr[lang.LanguageCode];
- if (!string.IsNullOrWhiteSpace(val))
- lang.Translations[key] = val;
- }
- }
- foreach (var lang in _packs) PackRepository.Save(lang);
- LoadPacks();
- return new JObject { ["ok"] = true, ["message"] = "词条已添加并保存" }.ToString(Newtonsoft.Json.Formatting.None);
- }
- case "add_entries":
- {
- if (!(args["entries"] is JArray newEntries) || newEntries.Count == 0)
- return new JObject { ["ok"] = false, ["error"] = "entries 不能为空" }.ToString(Newtonsoft.Json.Formatting.None);
- if (source == null)
- return new JObject { ["ok"] = false, ["error"] = "未找到 zh-CN 源语言包" }.ToString(Newtonsoft.Json.Formatting.None);
- int added = 0;
- var skipped = new JArray();
- foreach (var e in newEntries)
- {
- var key = (string)e["key"];
- if (string.IsNullOrWhiteSpace(key)) continue;
- key = key.Trim();
- if (source.Translations.ContainsKey(key))
- {
- skipped.Add(key);
- continue;
- }
- source.Translations[key] = key;
- if (e["translations"] is JObject tr)
- {
- foreach (var lang in _targets)
- {
- var val = (string)tr[lang.LanguageCode];
- if (!string.IsNullOrWhiteSpace(val))
- lang.Translations[key] = val;
- }
- }
- added++;
- }
- foreach (var p in _packs) PackRepository.Save(p);
- LoadPacks();
- return new JObject
- {
- ["ok"] = true,
- ["added"] = added,
- ["skipped"] = skipped,
- ["message"] = $"批量新增完成:新增 {added} 条,跳过(已存在) {skipped.Count} 条。已保存并刷新表格。",
- }.ToString(Newtonsoft.Json.Formatting.None);
- }
- case "update_translation":
- {
- var key = (string)args["key"];
- var langCode = (string)args["lang"];
- var value = (string)args["value"] ?? "";
- var lang = _targets.FirstOrDefault(t => t.LanguageCode == langCode);
- if (lang == null) return new JObject { ["ok"] = false, ["error"] = "语言未加载: " + langCode }.ToString(Newtonsoft.Json.Formatting.None);
- lang.Translations[key] = value;
- PackRepository.Save(lang);
- LoadPacks();
- return new JObject { ["ok"] = true, ["message"] = $"已更新 {langCode} 译文" }.ToString(Newtonsoft.Json.Formatting.None);
- }
- case "delete_entry":
- {
- var key = (string)args["key"];
- if (string.IsNullOrWhiteSpace(key))
- return new JObject { ["ok"] = false, ["error"] = "key 不能为空" }.ToString(FormattingNone);
- return ConfirmAndDeleteKeys(new List<string> { key });
- }
- case "delete_entries":
- {
- var keys = new List<string>();
- if (args["keys"] is JArray keyArr)
- foreach (var k in keyArr) keys.Add((string)k);
- if (args["indices"] is JArray idxArr)
- {
- foreach (var i in idxArr)
- {
- var row = _rows.FirstOrDefault(r => r.Index == (int)i);
- if (row != null) keys.Add(row.Key);
- }
- }
- if (keys.Count == 0)
- return new JObject { ["ok"] = false, ["error"] = "keys 与 indices 至少提供一个" }.ToString(FormattingNone);
- return ConfirmAndDeleteKeys(keys);
- }
- case "get_validation_issues":
- {
- var arr = new JArray();
- foreach (var i in _lastIssues)
- {
- var langMain = i.LangCode.Split('-')[0].ToLowerInvariant();
- if (!i.IsError &&
- i.Message.Contains("疑似未翻译") &&
- TranslationValidator.NotTranslatedIgnoredLangs.Contains(langMain))
- continue;
- arr.Add(new JObject
- {
- ["key"] = i.Row.Key,
- ["lang"] = i.LangCode,
- ["current"] = i.Cell.Value,
- ["problem"] = i.Message,
- ["is_error"] = i.IsError,
- });
- }
- if (arr.Count == 0)
- return new JObject { ["ok"] = true, ["count"] = 0, ["message"] = "暂无需要重译的问题(日语等豁免语言的同文词条已自动排除;请先点“校验”生成问题清单)" }.ToString(Newtonsoft.Json.Formatting.None);
- return new JObject { ["ok"] = true, ["count"] = arr.Count, ["issues"] = arr }.ToString(Newtonsoft.Json.Formatting.None);
- }
- case "batch_update_translations":
- {
- if (!(args["entries"] is JArray entries) || entries.Count == 0)
- return new JObject { ["ok"] = false, ["error"] = "entries 不能为空" }.ToString(Newtonsoft.Json.Formatting.None);
- int updated = 0, missingKey = 0;
- var failed = new JArray();
- foreach (var e in entries)
- {
- var key = (string)e["key"];
- if (string.IsNullOrWhiteSpace(key)) continue;
- var exists = _packs.Any(p => p.Translations.ContainsKey(key));
- if (!exists) { missingKey++; failed.Add(new JObject { ["key"] = key, ["error"] = "词条不存在" }); continue; }
- if (e["translations"] is JObject tr)
- {
- foreach (var lang in _targets)
- {
- var val = (string)tr[lang.LanguageCode];
- if (!string.IsNullOrWhiteSpace(val))
- lang.Translations[key] = val;
- }
- updated++;
- }
- }
- foreach (var p in _packs) PackRepository.Save(p);
- LoadPacks();
- return new JObject
- {
- ["ok"] = true,
- ["updated"] = updated,
- ["missingKey"] = missingKey,
- ["failed"] = failed,
- ["message"] = $"批量更新完成:成功 {updated} 条,key 不存在 {missingKey} 条。已保存并刷新表格。",
- }.ToString(Newtonsoft.Json.Formatting.None);
- }
- case "save_all":
- {
- foreach (var p in _packs) PackRepository.Save(p);
- return new JObject { ["ok"] = true, ["message"] = "全部语言包已保存" }.ToString(Newtonsoft.Json.Formatting.None);
- }
- case "get_missing_translations":
- {
- var langCode = (string)args["lang"];
- var langs = string.IsNullOrWhiteSpace(langCode)
- ? _targets
- : _targets.Where(t => t.LanguageCode == langCode).ToList();
- if (!string.IsNullOrWhiteSpace(langCode) && langs.Count == 0)
- return new JObject { ["ok"] = false, ["error"] = "语言未加载: " + langCode }.ToString(Newtonsoft.Json.Formatting.None);
- var arr = new JArray();
- foreach (var lang in langs)
- {
- foreach (var r in _rows)
- {
- if (r.Cells.TryGetValue(lang.LanguageCode, out var c) && c.IsMissing)
- arr.Add(new JObject { ["index"] = r.Index, ["key"] = r.Key, ["lang"] = lang.LanguageCode });
- }
- }
- return new JObject { ["ok"] = true, ["count"] = arr.Count, ["missing"] = arr }.ToString(Newtonsoft.Json.Formatting.None);
- }
- case "baidu_translate_missing":
- {
- var langCode = (string)args["lang"];
- var lang = _targets.FirstOrDefault(t => t.LanguageCode == langCode);
- if (lang == null)
- return new JObject { ["ok"] = false, ["error"] = "语言未加载: " + langCode }.ToString(Newtonsoft.Json.Formatting.None);
- IEnumerable<RowVm> scopeRows = _rows;
- if (args["indices"] is JArray idxArr && idxArr.Count > 0)
- {
- var idxSet = new HashSet<int>(idxArr.Select(x => (int)x));
- scopeRows = _rows.Where(r => idxSet.Contains(r.Index));
- }
- var missing = scopeRows
- .Select(r => new { r, cell = r.Cells.TryGetValue(lang.LanguageCode, out var c) ? c : null })
- .Where(x => x.cell != null && x.cell.IsMissing)
- .ToList();
- if (missing.Count == 0)
- return new JObject { ["ok"] = true, ["translated"] = 0, ["message"] = $"{langCode} 没有缺译项" }.ToString(Newtonsoft.Json.Formatting.None);
- var baidu = new BaiduTranslateProvider(_cfg.Baidu);
- int done = 0;
- var fails = new JArray();
- for (int i = 0; i < missing.Count; i += BaiduTranslateProvider.BatchSize)
- {
- var batch = missing.Skip(i).Take(BaiduTranslateProvider.BatchSize).ToList();
- try
- {
- var results = await baidu.TranslateBatchAsync(
- batch.Select(x => x.r.Key).ToList(),
- _cfg.Translation.FromLang,
- lang.LanguageCode);
- for (int k = 0; k < batch.Count && k < results.Count; k++)
- {
- if (!string.IsNullOrWhiteSpace(results[k]))
- {
- batch[k].cell.Value = results[k];
- lang.Translations[batch[k].r.Key] = results[k];
- done++;
- }
- else
- fails.Add(new JObject { ["index"] = batch[k].r.Index, ["key"] = batch[k].r.Key });
- }
- }
- catch (Exception ex)
- {
- foreach (var x in batch)
- fails.Add(new JObject { ["index"] = x.r.Index, ["key"] = x.r.Key, ["error"] = ex.Message });
- }
- }
- PackRepository.Save(lang);
- LoadPacks();
- return new JObject
- {
- ["ok"] = true,
- ["lang"] = langCode,
- ["translated"] = done,
- ["failed"] = fails,
- ["message"] = $"百度翻译补译完成:成功 {done},失败 {fails.Count}。已保存并刷新表格。",
- }.ToString(Newtonsoft.Json.Formatting.None);
- }
- case "delete_entry_by_index":
- {
- var idx = (int)args["index"];
- var row = _rows.FirstOrDefault(r => r.Index == idx);
- if (row == null)
- return new JObject { ["ok"] = false, ["error"] = $"序号 {idx} 不存在" }.ToString(FormattingNone);
- return ConfirmAndDeleteKeys(new List<string> { row.Key });
- }
- case "run_command":
- {
- var command = (string)args["command"];
- if (string.IsNullOrWhiteSpace(command))
- return new JObject { ["ok"] = false, ["error"] = "command 不能为空" }.ToString(Newtonsoft.Json.Formatting.None);
- var isDotnet = args["dotnet"] != null && (bool)args["dotnet"];
- var psi = new System.Diagnostics.ProcessStartInfo
- {
- FileName = isDotnet ? "dotnet" : "cmd.exe",
- Arguments = isDotnet ? command : $"/c {command}",
- RedirectStandardOutput = true,
- RedirectStandardError = true,
- UseShellExecute = false,
- CreateNoWindow = true,
- WorkingDirectory = string.IsNullOrWhiteSpace(_dir) ? AppDomain.CurrentDomain.BaseDirectory : _dir,
- StandardOutputEncoding = Encoding.GetEncoding(936),
- StandardErrorEncoding = Encoding.GetEncoding(936),
- };
- using (var p = System.Diagnostics.Process.Start(psi))
- {
- if (p == null)
- return new JObject { ["ok"] = false, ["error"] = "进程启动失败" }.ToString(Newtonsoft.Json.Formatting.None);
- var outputTask = p.StandardOutput.ReadToEndAsync();
- var errorTask = p.StandardError.ReadToEndAsync();
- var exited = await Task.Run(() => p.WaitForExit(30000));
- var output = (await outputTask) ?? "";
- var errOut = (await errorTask) ?? "";
- if (!exited) { try { p.Kill(); } catch { } }
- var resultObj = new JObject
- {
- ["ok"] = exited,
- ["exitCode"] = exited ? p.ExitCode : -1,
- ["stdout"] = output,
- ["stderr"] = errOut,
- };
- if ((output + errOut).Length > 3000)
- resultObj["stdout"] = output.Length > 2500 ? output.Substring(0, 2500) + "…(截断)" : output;
- return resultObj.ToString(Newtonsoft.Json.Formatting.None);
- }
- }
- case "web_search":
- {
- var query = (string)args["query"];
- if (string.IsNullOrWhiteSpace(query))
- return new JObject { ["ok"] = false, ["error"] = "query 不能为空" }.ToString(Newtonsoft.Json.Formatting.None);
- int count = args["count"] != null && args["count"].Type == JTokenType.Integer
- ? (int)args["count"] : _cfg.WebSearch.MaxResults;
- count = Math.Min(Math.Max(count, 1), 10);
- var web = new WebSearchProvider(_cfg.WebSearch);
- var results = await web.SearchAsync(query, count);
- return new JObject
- {
- ["ok"] = true,
- ["query"] = query,
- ["count"] = results.Count,
- ["results"] = results,
- }.ToString(Newtonsoft.Json.Formatting.None);
- }
- case "fetch_url":
- {
- var url = (string)args["url"];
- if (string.IsNullOrWhiteSpace(url))
- return new JObject { ["ok"] = false, ["error"] = "url 不能为空" }.ToString(Newtonsoft.Json.Formatting.None);
- var web = new WebSearchProvider(_cfg.WebSearch);
- var text = await web.FetchUrlAsync(url);
- return new JObject { ["ok"] = true, ["url"] = url, ["text"] = text }
- .ToString(Newtonsoft.Json.Formatting.None);
- }
- // ---------- 文件工具(任意路径读写,权限由 ask/trusted 统一管控) ----------
- case "read_file":
- {
- var path = (string)args["path"];
- if (string.IsNullOrWhiteSpace(path) || !System.IO.File.Exists(path))
- return new JObject { ["ok"] = false, ["error"] = "文件不存在: " + path }.ToString(Newtonsoft.Json.Formatting.None);
- try
- {
- var text = ReadTextFile(path);
- const int limit = 20000;
- var truncated = text.Length > limit;
- if (truncated) text = text.Substring(0, limit) + "\n…(内容较长已截断)";
- return new JObject
- {
- ["ok"] = true,
- ["path"] = path,
- ["length"] = new System.IO.FileInfo(path).Length,
- ["truncated"] = truncated,
- ["content"] = text,
- }.ToString(Newtonsoft.Json.Formatting.None);
- }
- catch (Exception ex)
- {
- return new JObject { ["ok"] = false, ["error"] = $"读取失败:{ex.Message}" }.ToString(Newtonsoft.Json.Formatting.None);
- }
- }
- case "write_file":
- {
- var path = (string)args["path"];
- var content = (string)args["content"] ?? "";
- if (string.IsNullOrWhiteSpace(path))
- return new JObject { ["ok"] = false, ["error"] = "path 不能为空" }.ToString(Newtonsoft.Json.Formatting.None);
- try
- {
- var dir = System.IO.Path.GetDirectoryName(path);
- if (!string.IsNullOrEmpty(dir)) System.IO.Directory.CreateDirectory(dir);
- System.IO.File.WriteAllText(path, content, new UTF8Encoding(false));
- return new JObject { ["ok"] = true, ["path"] = path, ["bytes"] = content.Length, ["message"] = "已写入文件" }.ToString(Newtonsoft.Json.Formatting.None);
- }
- catch (Exception ex)
- {
- return new JObject { ["ok"] = false, ["error"] = $"写入失败:{ex.Message}" }.ToString(Newtonsoft.Json.Formatting.None);
- }
- }
- case "edit_file":
- {
- var path = (string)args["path"];
- var oldText = (string)args["old_text"] ?? "";
- var newText = (string)args["new_text"] ?? "";
- if (string.IsNullOrWhiteSpace(path) || !System.IO.File.Exists(path))
- return new JObject { ["ok"] = false, ["error"] = "文件不存在: " + path }.ToString(Newtonsoft.Json.Formatting.None);
- if (string.IsNullOrEmpty(oldText))
- return new JObject { ["ok"] = false, ["error"] = "old_text 不能为空" }.ToString(Newtonsoft.Json.Formatting.None);
- try
- {
- var text = ReadTextFile(path);
- var idx = text.IndexOf(oldText, StringComparison.Ordinal);
- if (idx < 0)
- return new JObject { ["ok"] = false, ["error"] = "old_text 在文件中不存在(请先 read_file 确认原文再替换)" }.ToString(Newtonsoft.Json.Formatting.None);
- var last = text.LastIndexOf(oldText, StringComparison.Ordinal);
- if (last != idx)
- return new JObject { ["ok"] = false, ["error"] = "old_text 在文件中出现多次,请给出更长的唯一上下文" }.ToString(Newtonsoft.Json.Formatting.None);
- text = text.Substring(0, idx) + newText + text.Substring(idx + oldText.Length);
- System.IO.File.WriteAllText(path, text, new UTF8Encoding(false));
- return new JObject { ["ok"] = true, ["path"] = path, ["message"] = "替换成功" }.ToString(Newtonsoft.Json.Formatting.None);
- }
- catch (Exception ex)
- {
- return new JObject { ["ok"] = false, ["error"] = $"替换失败:{ex.Message}" }.ToString(Newtonsoft.Json.Formatting.None);
- }
- }
- case "list_directory":
- {
- var path = (string)args["path"];
- if (string.IsNullOrWhiteSpace(path) || !System.IO.Directory.Exists(path))
- return new JObject { ["ok"] = false, ["error"] = "目录不存在: " + path }.ToString(Newtonsoft.Json.Formatting.None);
- try
- {
- var ignored = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
- { "bin", "obj", ".git", ".vs", "node_modules", "packages", ".idea", "dist", "build", "debug", "release" };
- var dirs = System.IO.Directory.GetDirectories(path)
- .Where(d => !ignored.Contains(System.IO.Path.GetFileName(d)))
- .OrderBy(d => d, StringComparer.OrdinalIgnoreCase)
- .Select(d => $"[目录] {System.IO.Path.GetFileName(d)}")
- .ToList();
- var files = System.IO.Directory.GetFiles(path)
- .OrderBy(f => f, StringComparer.OrdinalIgnoreCase)
- .Select(f => $"[文件] {System.IO.Path.GetFileName(f)} ({new System.IO.FileInfo(f).Length} B)")
- .ToList();
- var all = dirs.Concat(files).ToList();
- return new JObject
- {
- ["ok"] = true,
- ["path"] = path,
- ["count"] = all.Count,
- ["entries"] = new JArray(all),
- }.ToString(Newtonsoft.Json.Formatting.None);
- }
- catch (Exception ex)
- {
- return new JObject { ["ok"] = false, ["error"] = $"读取目录失败:{ex.Message}" }.ToString(Newtonsoft.Json.Formatting.None);
- }
- }
- default:
- return new JObject { ["ok"] = false, ["error"] = $"未知工具: {name}" }.ToString(Newtonsoft.Json.Formatting.None);
- }
- }
- catch (Exception ex)
- {
- return new JObject { ["ok"] = false, ["error"] = ex.Message }.ToString(Newtonsoft.Json.Formatting.None);
- }
- }
- /// <summary>
- /// 删除工具统一入口:无论删几条,只弹一次确认框(列出数量与前几条 key),
- /// 确认后从所有语言包移除、统一保存一次、刷新一次。
- /// </summary>
- private string ConfirmAndDeleteKeys(List<string> keys)
- {
- keys = keys.Where(k => !string.IsNullOrWhiteSpace(k)).Distinct().ToList();
- if (keys.Count == 0)
- return new JObject { ["ok"] = false, ["error"] = "没有可删除的词条" }.ToString(FormattingNone);
- var existing = keys.Where(k => _packs.Any(p => p.Translations.ContainsKey(k))).ToList();
- var notFound = keys.Except(existing).ToList();
- if (existing.Count == 0)
- return new JObject { ["ok"] = false, ["error"] = "词条均不存在,未删除任何内容", ["notFound"] = JArray.FromObject(keys) }.ToString(FormattingNone);
- var preview = string.Join("、", existing.Take(8).Select(k => $"「{k}」"));
- var more = existing.Count > 8 ? $"\n… 等共 {existing.Count} 条" : "";
- var confirm = MessageBox.Show(
- $"确认删除 {existing.Count} 个词条?\n{preview}{more}\n\n该操作将从所有语言包中移除(不可撤销)。",
- "删除确认", MessageBoxButton.YesNo, MessageBoxImage.Warning);
- if (confirm != MessageBoxResult.Yes)
- return new JObject { ["ok"] = false, ["error"] = "用户取消了删除" }.ToString(FormattingNone);
- foreach (var p in _packs)
- foreach (var k in existing)
- p.Translations.Remove(k);
- foreach (var p in _packs) PackRepository.Save(p);
- LoadPacks();
- var result = new JObject
- {
- ["ok"] = true,
- ["deleted"] = existing.Count,
- ["message"] = $"已删除 {existing.Count} 个词条并保存全部语言包。",
- };
- if (notFound.Count > 0) result["notFound"] = JArray.FromObject(notFound);
- return result.ToString(FormattingNone);
- }
- /// <summary>读取文本文件,自动检测编码:UTF-8 无 BOM 或带 BOM,否则回退 GBK(Windows 中文系统常见)。</summary>
- private static string ReadTextFile(string path)
- {
- var bytes = System.IO.File.ReadAllBytes(path);
- if (bytes.Length >= 3 && bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF)
- return new UTF8Encoding(true).GetString(bytes, 3, bytes.Length - 3);
- try
- {
- var utf8 = new UTF8Encoding(false, true);
- return utf8.GetString(bytes);
- }
- catch (DecoderFallbackException)
- {
- return Encoding.GetEncoding(936).GetString(bytes); // GBK
- }
- }
- /// <summary>
- /// 执行 AI 生成的"添加行"指令:中文源新增 key(key 即中文原文),
- /// AI 提供的各语言译文直接写入对应语言包。已存在的 key 不覆盖。写入后保存全部并刷新表格。
- /// </summary>
- public void ApplyRowsFromAi(List<AddRowPayload> rows)
- {
- if (rows == null || rows.Count == 0) return;
- var source = _packs.FirstOrDefault(p => p.IsSource);
- int addedKeys = 0;
- foreach (var row in rows)
- {
- if (string.IsNullOrWhiteSpace(row.Key)) continue;
- if (source != null && !source.Translations.ContainsKey(row.Key))
- {
- source.Translations[row.Key] = row.Key;
- addedKeys++;
- }
- foreach (var lang in _targets)
- {
- if (row.Translations != null &&
- row.Translations.TryGetValue(lang.LanguageCode, out var val) &&
- !string.IsNullOrWhiteSpace(val))
- {
- lang.Translations[row.Key] = val;
- }
- }
- }
- foreach (var lang in _packs) PackRepository.Save(lang);
- LoadPacks();
- Stats = $"AI 指令已执行:新增词条 {addedKeys} 个(含译文同步)。";
- }
- }
- }
|