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; /// AI 助手可用工具集(OpenAI Function Calling schema)。 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"), }; } /// /// AI 工具执行器:在 UI 线程操作语言包数据,返回 JSON 结果字符串回传给模型。 /// 修改类工具执行后自动保存 + 刷新表格。 /// public async Task 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 { key }); } case "delete_entries": { var keys = new List(); 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 scopeRows = _rows; if (args["indices"] is JArray idxArr && idxArr.Count > 0) { var idxSet = new HashSet(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 { 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(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); } } /// /// 删除工具统一入口:无论删几条,只弹一次确认框(列出数量与前几条 key), /// 确认后从所有语言包移除、统一保存一次、刷新一次。 /// private string ConfirmAndDeleteKeys(List 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); } /// 读取文本文件,自动检测编码:UTF-8 无 BOM 或带 BOM,否则回退 GBK(Windows 中文系统常见)。 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 } } /// /// 执行 AI 生成的"添加行"指令:中文源新增 key(key 即中文原文), /// AI 提供的各语言译文直接写入对应语言包。已存在的 key 不覆盖。写入后保存全部并刷新表格。 /// public void ApplyRowsFromAi(List 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} 个(含译文同步)。"; } } }