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} 个(含译文同步)。";
}
}
}