Files
YMhut-box-C-/src/YMhut.Box.Core/Tools/ToolResultBuilder.cs
T
2026-07-06 23:08:53 +08:00

1233 lines
53 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Globalization;
using YMhut.Box.Core.Api;
namespace YMhut.Box.Core.Tools;
public static class ToolResultBuilder
{
public static ToolResultDocument FromOutput(IToolModule module, string output, string language = "zh-CN")
{
var spec = ToolPageSpecCatalog.For(module);
var lines = Lines(output);
var blocks = BuildBlocks(module, spec, output, lines, language);
if (blocks.Count == 0)
{
blocks = ToolResultDocument.FromOutput(module, output).Blocks;
}
return Document(module, spec, output, blocks, language);
}
public static ToolResultDocument FromRemote(ApiEndpoint endpoint, ApiResponse response, string output, string language = "zh-CN")
{
var lines = Lines(output);
var displayLines = RemoveSourceHeader(lines);
var blocks = new List<ToolResultBlock>();
blocks.AddRange(BuildRemoteBlocks(endpoint.Id, displayLines, language));
if (blocks.Count == 0)
{
blocks.AddRange(GenericTextBlocks(displayLines.Count > 0 ? displayLines : lines, language));
}
blocks.Add(ToolResultBlock.KeyValue(
T(language, "来源与隐私", "Source and privacy"),
[
Pair(T(language, "数据源", "Data source"), endpoint.SourceName),
Pair(T(language, "来源类型", "Source type"), SourceTypeLabel(endpoint, language)),
Pair(T(language, "获取时间", "Fetched at"), response.FetchedAt.ToString("yyyy-MM-dd HH:mm:ss zzz"))
],
SourceMetadata(endpoint)));
return new ToolResultDocument(
endpoint.Id,
GuessRemoteResultKind(endpoint.Id),
output,
blocks,
RemoteMetadata(endpoint, GuessRemoteResultKind(endpoint.Id), language),
output,
endpoint.SourceName,
response.Success ? "ok" : "error");
}
private static IReadOnlyList<ToolResultBlock> BuildBlocks(IToolModule module, ToolPageSpec spec, string output, IReadOnlyList<string> lines, string language)
{
if (string.IsNullOrWhiteSpace(output))
{
return [ToolResultBlock.List(ToolResultBlockKind.Status, T(language, "空结果", "Empty result"), [new("info", T(language, "暂无可展示的结果。", "There is no result to display."), string.Empty, "info", string.Empty)])];
}
if (module.Id is "qr_generator" && LooksLikeSvg(output))
{
return
[
ToolResultBlock.Media(
ToolResultBlockKind.Image,
T(language, "二维码预览", "QR code preview"),
output,
string.Empty,
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["contentType"] = "image/svg+xml",
["displayMode"] = "preview",
["rawSvg"] = output
})
];
}
if (module.Id is "weather")
{
return [ToolResultBlock.Metric(T(language, "天气摘要", "Weather summary"), KeyValues(lines).ToArray())];
}
if (module.Id is "train_query")
{
return BuildTrainBlocks(lines, language);
}
if (module.Id is "dns_query")
{
return BuildDnsBlocks(lines, language);
}
if (module.Id is "ip_lookup" or "ip_info" or "rdap_ip_lookup" or "rdap_domain_lookup" or "domain_price")
{
return BuildRdapBlocks(lines, language);
}
if (module.Id is "sanguosha_skin")
{
return BuildSanguoshaSkinBlocks(lines, language);
}
if (IsReferenceTool(module.Id))
{
return BuildReferenceBlocksV2(lines, language);
}
if (module.Id is "smart_search")
{
return BuildSearchBlocks(lines, language);
}
if (module.Id is "system_tool" or "system_info" or "pc_benchmark")
{
return BuildSystemBlocks(lines, language);
}
if (spec.Result == ToolResultKind.JsonTree || LooksLikeJson(output))
{
return BuildJsonBlocks(output, language);
}
if (module.Id is "jwt_decoder")
{
return BuildJwtBlocks(output, language);
}
if (spec.Result == ToolResultKind.Diff || module.Id is "text_diff")
{
return [ToolResultBlock.List(ToolResultBlockKind.Diff, T(language, "差异结果", "Diff result"), lines.Select(line => new ToolResultListItem(DiffLeading(line), line, string.Empty, DiffStatus(line), string.Empty)).ToArray())];
}
if (module.Id is "base64_codec" or "url_codec" or "html_entity" or "unicode_codec" or "punycode_codec")
{
return BuildCodecBlocks(lines, language);
}
if (module.Id is "hash_generator" or "hmac_generator")
{
return [ToolResultBlock.Table(T(language, "摘要值", "Digest values"), DigestRows(lines, language))];
}
if (spec.Result == ToolResultKind.CodePreview || module.Id.Contains("formatter", StringComparison.OrdinalIgnoreCase) || module.Id.Contains("minifier", StringComparison.OrdinalIgnoreCase))
{
return [ToolResultBlock.Code(T(language, "代码预览", "Code preview"), output, CodeLanguage(module.Id))];
}
if (spec.Result == ToolResultKind.CalculatorTable || module.Metadata.Category == ToolCategory.Calculator || spec.Layout is ToolLayoutKind.CalculatorForm or ToolLayoutKind.UnitConverter)
{
return [ToolResultBlock.Metric(T(language, "计算结果", "Calculation result"), MetricPairs(lines).ToArray())];
}
if (spec.Result is ToolResultKind.FileCards or ToolResultKind.ImagePreview or ToolResultKind.Media or ToolResultKind.ColorSwatch)
{
return ToolResultDocument.FromOutput(module, output).Blocks;
}
if (spec.Result == ToolResultKind.Table)
{
var rows = ParseTableRows(lines);
return rows.Count == 0 ? GenericTextBlocks(lines, language) : [ToolResultBlock.Table(T(language, "表格结果", "Table result"), rows)];
}
if (spec.Result is ToolResultKind.RankedList or ToolResultKind.NewsCards)
{
return spec.Result == ToolResultKind.NewsCards
? BuildNewsCards(lines, T(language, "新闻列表", "News list"))
: BuildRankedCards(lines, T(language, "榜单", "Ranked list"));
}
if (spec.Result == ToolResultKind.StatusList)
{
return [ToolResultBlock.List(ToolResultBlockKind.Status, T(language, "状态", "Status"), lines.Select(line => new ToolResultListItem(StatusLeading(line), line, string.Empty, StatusOf(line), string.Empty)).ToArray())];
}
var keyValues = KeyValues(lines).ToArray();
if (keyValues.Length >= 2)
{
return [ToolResultBlock.KeyValue(T(language, "详情", "Details"), keyValues)];
}
return GenericTextBlocks(lines, language);
}
private static IReadOnlyList<ToolResultBlock> BuildRemoteBlocks(string id, IReadOnlyList<string> lines, string language)
{
if (id.Equals("weather", StringComparison.OrdinalIgnoreCase))
{
return BuildWeatherBlocks(lines, language);
}
return id switch
{
"dns_query" => BuildDnsBlocks(lines, language),
"weather" => [ToolResultBlock.Metric(T(language, "天气摘要", "Weather summary"), KeyValues(lines).ToArray())],
"ip_info" or "ip_lookup" or "rdap_ip_lookup" or "rdap_domain_lookup" or "domain_price" => BuildRdapBlocks(lines, language),
"baidu_hot" or "hotboard" or "bili_hot" or "zhihu_hot" or "earthquake_info" or "movie_box_office" => BuildRankedCards(lines, T(language, "数据列表", "Data list")),
"history_today" => BuildHistoryBlocks(lines, language),
"ai_latest_news" or "tech_news" or "football_news" or "cctv_news" => BuildNewsCards(lines, T(language, "新闻列表", "News list")),
"gold_price" => BuildGoldPriceBlocks(lines, language),
"oil_price" or "wx_domain_check" or "http_diagnostic" => BuildHtmlSummaryBlocks(lines, language),
"city_route_query" => BuildMetricTextBlocks(lines, T(language, "城际路线", "City route"), language),
"train_query" => BuildTrainBlocks(lines, language),
_ => []
};
}
private static IReadOnlyList<ToolResultBlock> BuildHistoryBlocks(IReadOnlyList<string> lines, string language)
{
var blocks = new List<ToolResultBlock>();
var currentSection = string.Empty;
var items = new List<ToolResultListItem>();
void Flush()
{
if (items.Count > 0)
{
blocks.Add(ToolResultBlock.Timeline(
string.IsNullOrWhiteSpace(currentSection) ? T(language, "历史事件", "History") : currentSection,
items.ToArray(),
BlockMeta("text/plain", "timeline", "normal")));
items.Clear();
}
}
foreach (var line in lines)
{
var trimmed = line.Trim();
if (IsRemoteHeaderOrPrivacyLine(trimmed))
{
continue;
}
if (string.IsNullOrWhiteSpace(trimmed))
{
continue;
}
if (!trimmed.StartsWith("-", StringComparison.Ordinal) && trimmed == trimmed.ToUpperInvariant() && trimmed.Length <= 24)
{
Flush();
currentSection = trimmed;
continue;
}
var match = Regex.Match(trimmed, @"^-?\s*(?<year>-?\d{1,4})\s*:\s*(?<text>.+)$");
if (match.Success)
{
items.Add(new ToolResultListItem(match.Groups["year"].Value, match.Groups["text"].Value, currentSection, "info", ExtractLinks([line]).FirstOrDefault() ?? string.Empty));
}
else
{
items.Add(new ToolResultListItem("•", trimmed.TrimStart('-', ' '), currentSection, "info", ExtractLinks([line]).FirstOrDefault() ?? string.Empty));
}
}
Flush();
return blocks.Count == 0 ? GenericTextBlocks(lines, language) : blocks;
}
private static IReadOnlyList<ToolResultBlock> BuildMetricTextBlocks(IReadOnlyList<string> lines, string title, string language)
{
var pairs = KeyValues(lines).ToArray();
if (pairs.Length > 0)
{
return [ToolResultBlock.Metric(title, pairs, BlockMeta("metric", "summary", "high"))];
}
return BuildHtmlSummaryBlocks(lines, language);
}
private static IReadOnlyList<ToolResultBlock> BuildGoldPriceBlocks(IReadOnlyList<string> lines, string language)
{
var blocks = new List<ToolResultBlock>();
var pairs = KeyValues(lines).ToArray();
if (pairs.Length > 0)
{
blocks.Add(ToolResultBlock.Metric(T(language, "黄金价格", "Gold price"), pairs, BlockMeta("metric", "summary", "high")));
}
var rows = ParseTableRows(lines);
if (rows.Count > 1)
{
blocks.Add(ToolResultBlock.Table(T(language, "近期定盘价", "Recent fixes"), rows, BlockMeta("table", "details", "normal")));
var chartRows = rows
.Skip(1)
.Where(row => row.Length >= 2 && TryParseDecimal(row[1], out _))
.Select(row => new[] { row[0], row[1] })
.Take(60)
.ToArray();
if (chartRows.Length >= 2)
{
blocks.Add(ToolResultBlock.LineChart(
T(language, "USD/oz 趋势", "USD/oz trend"),
new[] { new[] { T(language, "日期", "Date"), "USD/oz" } }.Concat(chartRows).ToArray(),
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["contentType"] = "chart/line",
["displayMode"] = "chart",
["priority"] = "high",
["chartUnit"] = "USD/oz"
}));
}
}
return blocks.Count == 0
? BuildMetricTextBlocks(lines, T(language, "黄金价格", "Gold price"), language)
: blocks;
}
private static IReadOnlyList<ToolResultBlock> BuildHtmlSummaryBlocks(IReadOnlyList<string> lines, string language)
{
var blocks = new List<ToolResultBlock>();
var title = lines.FirstOrDefault(line => !string.IsNullOrWhiteSpace(line) && !Regex.IsMatch(line, @"^\d+[\.\)]")) ?? string.Empty;
if (!string.IsNullOrWhiteSpace(title))
{
blocks.Add(ToolResultBlock.Metric(
T(language, "查询摘要", "Query summary"),
[Pair(T(language, "标题", "Title"), title)],
BlockMeta("text/plain", "summary", "high")));
}
var cards = BuildRankedCards(lines.Where(line => Regex.IsMatch(line.Trim(), @"^\d+[\.\)]")).ToArray(), T(language, "详情", "Details"));
blocks.AddRange(cards);
if (blocks.Count == 0)
{
blocks.AddRange(GenericTextBlocks(lines, language));
}
return blocks;
}
private static IReadOnlyList<ToolResultBlock> BuildJsonBlocks(string output, string language)
{
try
{
using var json = JsonDocument.Parse(output);
var root = json.RootElement;
var blocks = new List<ToolResultBlock>
{
ToolResultBlock.JsonTree(T(language, "JSON 树", "JSON tree"), JsonSerializer.Serialize(root, new JsonSerializerOptions { WriteIndented = true }))
};
if (root.ValueKind == JsonValueKind.Object)
{
blocks.Insert(0, ToolResultBlock.KeyValue(
T(language, "字段摘要", "Field summary"),
root.EnumerateObject().Take(40).Select(property => Pair(property.Name, JsonPreview(property.Value))).ToArray()));
}
else if (root.ValueKind == JsonValueKind.Array)
{
blocks.Insert(0, ToolResultBlock.Metric(T(language, "数组摘要", "Array summary"), [Pair(T(language, "项目数", "Items"), root.GetArrayLength().ToString())]));
var rows = JsonArrayRows(root).ToArray();
if (rows.Length > 0)
{
blocks.Insert(1, ToolResultBlock.Table(T(language, "数组预览", "Array preview"), rows));
}
}
return blocks;
}
catch (JsonException exception)
{
return
[
ToolResultBlock.List(ToolResultBlockKind.Status, T(language, "JSON 解析失败", "JSON parse failed"), [new("error", exception.Message, string.Empty, "error", string.Empty)]),
ToolResultBlock.Raw(T(language, "原始内容", "Raw content"), output, "json")
];
}
}
private static IReadOnlyList<ToolResultBlock> BuildJwtBlocks(string output, string language)
{
var blocks = new List<ToolResultBlock>();
var sections = output.Replace("\r\n", "\n")
.Split("\n\n", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Where(section => section.TrimStart().StartsWith('{') || section.TrimStart().StartsWith('['))
.Take(3)
.ToArray();
foreach (var section in sections)
{
blocks.AddRange(BuildJsonBlocks(section, language));
}
return blocks.Count == 0 ? [ToolResultBlock.Code("JWT", output, "json")] : blocks;
}
private static IReadOnlyList<ToolResultBlock> BuildDnsBlocks(IReadOnlyList<string> lines, string language)
{
var records = new List<string[]>
{
new[] { T(language, "名称", "Name"), "TYPE", "TTL", T(language, "数据", "Data") }
};
var details = new List<KeyValuePair<string, string>>();
foreach (var line in lines)
{
var match = Regex.Match(line, @"^(?<name>.*?)\s+TYPE\s+(?<type>\S+)\s+TTL\s+(?<ttl>\S+)\s+(?<data>.*)$", RegexOptions.IgnoreCase);
if (match.Success)
{
records.Add([match.Groups["name"].Value, match.Groups["type"].Value, match.Groups["ttl"].Value, match.Groups["data"].Value]);
continue;
}
details.AddRange(KeyValues([line]));
}
var blocks = new List<ToolResultBlock>();
if (details.Count > 0)
{
blocks.Add(ToolResultBlock.KeyValue(T(language, "查询摘要", "Query summary"), details));
}
if (records.Count > 1)
{
blocks.Add(ToolResultBlock.Table("DNS", records));
}
return blocks;
}
private static IReadOnlyList<ToolResultBlock> BuildWeatherBlocks(IReadOnlyList<string> lines, string language)
{
var pairs = KeyValues(lines).ToList();
var blocks = new List<ToolResultBlock>();
if (pairs.Count > 0)
{
blocks.Add(ToolResultBlock.Metric(
T(language, "天气摘要", "Weather summary"),
pairs.Take(8).ToArray(),
BlockMeta("metric", "summary", "high")));
}
var forecastRows = new List<string[]> { new[] { T(language, "日期", "Date"), T(language, "最低", "Low"), T(language, "最高", "High"), T(language, "说明", "Note") } };
foreach (var line in lines)
{
var match = Regex.Match(line, @"^(?<date>\d{4}-\d{2}-\d{2})\s*:\s*(?<low>-?\d+(?:\.\d+)?)\s*-\s*(?<high>-?\d+(?:\.\d+)?)(?<unit>.*)$");
if (match.Success)
{
forecastRows.Add([
match.Groups["date"].Value,
match.Groups["low"].Value,
match.Groups["high"].Value,
match.Groups["unit"].Value.Trim()
]);
}
}
if (forecastRows.Count > 1)
{
blocks.Add(ToolResultBlock.Table(
T(language, "未来预报", "Forecast"),
forecastRows,
BlockMeta("table", "details", "normal")));
}
return blocks.Count > 0 ? blocks : GenericTextBlocks(lines, language);
}
private static IReadOnlyList<ToolResultBlock> BuildRdapBlocks(IReadOnlyList<string> lines, string language)
{
var events = new List<ToolResultListItem>();
var pairs = new List<KeyValuePair<string, string>>();
foreach (var pair in KeyValues(lines))
{
if (pair.Key.Contains("event", StringComparison.OrdinalIgnoreCase) || pair.Key.Contains("Notice", StringComparison.OrdinalIgnoreCase))
{
events.Add(new ToolResultListItem("•", pair.Key, pair.Value, "info", string.Empty));
}
else
{
pairs.Add(pair);
}
}
var blocks = new List<ToolResultBlock>();
if (pairs.Count > 0)
{
blocks.Add(ToolResultBlock.KeyValue("RDAP", pairs));
}
if (events.Count > 0)
{
blocks.Add(ToolResultBlock.Timeline(T(language, "事件/公告", "Events / notices"), events));
}
return blocks;
}
private static IReadOnlyList<ToolResultBlock> BuildReferenceBlocks(IReadOnlyList<string> lines, string language)
{
var rows = new List<string[]> { new[] { T(language, "代码", "Code"), T(language, "名称", "Name"), T(language, "说明", "Description"), T(language, "备注", "Note") } };
var sourcePairs = new List<KeyValuePair<string, string>>();
foreach (var line in lines)
{
if (line.StartsWith("数据源:", StringComparison.Ordinal) || line.StartsWith("Data source:", StringComparison.OrdinalIgnoreCase))
{
sourcePairs.Add(Pair(T(language, "数据源", "Data source"), SplitValue(line)));
continue;
}
var match = Regex.Match(line, @"^(?<code>.*?)\s+-\s+(?<name>.*?)(?:[:]\s*(?<desc>.*?))?(?:\s+/\s+(?<note>.*))?$");
if (match.Success)
{
rows.Add([match.Groups["code"].Value, match.Groups["name"].Value, match.Groups["desc"].Value, match.Groups["note"].Value]);
}
}
var blocks = new List<ToolResultBlock>();
if (sourcePairs.Count > 0)
{
blocks.Add(ToolResultBlock.KeyValue(T(language, "来源", "Source"), sourcePairs));
}
if (rows.Count > 1)
{
blocks.Add(ToolResultBlock.Table(T(language, "参考数据", "Reference data"), rows));
}
return blocks.Count == 0 ? GenericTextBlocks(lines, language) : blocks;
}
private static IReadOnlyList<ToolResultBlock> BuildSearchBlocks(IReadOnlyList<string> lines, string language)
{
var pairs = KeyValues(lines).ToList();
var links = ExtractLinks(lines).Distinct(StringComparer.OrdinalIgnoreCase).Select(link => ToolResultBlock.Link(T(language, "打开入口", "Open entry"), link, link)).ToList();
var blocks = new List<ToolResultBlock>();
if (pairs.Count > 0)
{
blocks.Add(ToolResultBlock.KeyValue(T(language, "搜索信息", "Search info"), pairs));
}
blocks.AddRange(links);
return blocks.Count == 0 ? GenericTextBlocks(lines, language) : blocks;
}
private static IReadOnlyList<ToolResultBlock> BuildReferenceBlocksV2(IReadOnlyList<string> lines, string language)
{
var rows = new List<string[]> { new[] { T(language, "代码", "Code"), T(language, "名称", "Name"), T(language, "说明", "Description"), T(language, "备注", "Note") } };
var sourcePairs = new List<KeyValuePair<string, string>>();
foreach (var line in lines)
{
if (IsSourceLine(line))
{
sourcePairs.Add(Pair(T(language, "数据源", "Data source"), SplitValue(line)));
continue;
}
var match = Regex.Match(line, @"^(?<code>.*?)\s+-\s+(?<name>.*?)(?:[:]\s*(?<desc>.*?))?(?:\s+/\s+(?<note>.*))?$");
if (match.Success)
{
rows.Add([match.Groups["code"].Value, match.Groups["name"].Value, match.Groups["desc"].Value, match.Groups["note"].Value]);
}
}
var blocks = new List<ToolResultBlock>();
if (rows.Count > 1)
{
blocks.Add(ToolResultBlock.Metric(
T(language, "参考摘要", "Reference summary"),
[
Pair(T(language, "命中数", "Matches"), (rows.Count - 1).ToString()),
Pair(T(language, "展示上限", "Display limit"), "80")
],
BlockMeta("metric", "summary", "high")));
blocks.Add(ToolResultBlock.Table(T(language, "参考数据", "Reference data"), rows, BlockMeta("table", "details", "normal")));
}
if (sourcePairs.Count > 0)
{
blocks.Add(ToolResultBlock.KeyValue(T(language, "来源", "Source"), sourcePairs, BlockMeta("text/plain", "source", "normal")));
}
return blocks.Count == 0 ? GenericTextBlocks(lines, language) : blocks;
}
private static IReadOnlyList<ToolResultBlock> BuildSanguoshaSkinBlocks(IReadOnlyList<string> lines, string language)
{
var rows = new List<string[]>
{
new[]
{
T(language, "皮肤 ID", "Skin ID"),
T(language, "条目", "Item"),
T(language, "武将", "General"),
T(language, "皮肤", "Skin"),
T(language, "性别/品质", "Gender / quality")
}
};
var sourcePairs = new List<KeyValuePair<string, string>>();
var statuses = new List<ToolResultListItem>();
foreach (var line in lines)
{
if (IsSourceLine(line) || line.Contains("data/sanguosha", StringComparison.OrdinalIgnoreCase) || line.Contains("Assets/data/sanguosha", StringComparison.OrdinalIgnoreCase))
{
sourcePairs.Add(Pair(T(language, "配置来源", "Config source"), SplitValue(line)));
continue;
}
if (line.Contains("未找到", StringComparison.OrdinalIgnoreCase) ||
line.Contains("缺", StringComparison.OrdinalIgnoreCase) ||
line.Contains("修复", StringComparison.OrdinalIgnoreCase) ||
line.Contains("not found", StringComparison.OrdinalIgnoreCase) ||
line.Contains("missing", StringComparison.OrdinalIgnoreCase))
{
statuses.Add(new ToolResultListItem("!", line, string.Empty, "error", string.Empty));
continue;
}
var parts = line.Split(" / ", StringSplitOptions.TrimEntries);
if (parts.Length >= 4)
{
var id = parts.ElementAtOrDefault(0) ?? string.Empty;
var item = parts.ElementAtOrDefault(1) ?? string.Empty;
var general = StripLabel(parts.ElementAtOrDefault(2) ?? string.Empty);
var skin = StripLabel(parts.ElementAtOrDefault(3) ?? string.Empty);
var gender = parts.Length > 4 ? parts[4] : string.Empty;
rows.Add([id, item, general, skin, gender]);
continue;
}
if (!string.IsNullOrWhiteSpace(line))
{
statuses.Add(new ToolResultListItem("i", line, string.Empty, StatusOf(line), string.Empty));
}
}
var blocks = new List<ToolResultBlock>();
if (rows.Count > 1)
{
blocks.Add(ToolResultBlock.Metric(
T(language, "三国杀皮肤摘要", "Sanguosha skin summary"),
[
Pair(T(language, "命中皮肤", "Matches"), (rows.Count - 1).ToString()),
Pair(T(language, "资源目录", "Resource folder"), "Assets/data/sanguosha"),
Pair(T(language, "配置文件", "Config file"), "skin_config.json")
],
BlockMeta("metric", "summary", "high")));
blocks.Add(ToolResultBlock.Table(
T(language, "皮肤配置", "Skin configuration"),
rows,
BlockMeta("table", "sanguosha-skins", "high")));
}
if (sourcePairs.Count > 0)
{
blocks.Add(ToolResultBlock.KeyValue(
T(language, "资源与路径", "Resources and paths"),
sourcePairs,
BlockMeta("text/plain", "source", "normal")));
}
if (statuses.Count > 0)
{
blocks.Add(ToolResultBlock.List(
ToolResultBlockKind.Status,
T(language, "检查结果", "Check result"),
statuses,
BlockMeta("text/plain", "status", rows.Count > 1 ? "normal" : "high")));
}
return blocks.Count == 0 ? GenericTextBlocks(lines, language) : blocks;
}
private static IReadOnlyList<ToolResultBlock> BuildCodecBlocks(IReadOnlyList<string> lines, string language)
{
var blocks = new List<ToolResultBlock>();
var sections = new List<KeyValuePair<string, string>>();
var currentTitle = string.Empty;
var currentText = new List<string>();
void Flush()
{
if (!string.IsNullOrWhiteSpace(currentTitle))
{
sections.Add(Pair(currentTitle.TrimEnd(':'), string.Join(Environment.NewLine, currentText).Trim()));
}
currentText.Clear();
}
foreach (var line in lines)
{
if (line.EndsWith(":", StringComparison.Ordinal) && line.Length < 32)
{
Flush();
currentTitle = line;
}
else
{
currentText.Add(line);
}
}
Flush();
if (sections.Count > 0)
{
blocks.Add(ToolResultBlock.KeyValue(T(language, "转换结果", "Conversion result"), sections));
}
else
{
blocks.Add(ToolResultBlock.Code(T(language, "转换文本", "Converted text"), string.Join(Environment.NewLine, lines)));
}
return blocks;
}
private static IReadOnlyList<string[]> DigestRows(IReadOnlyList<string> lines, string language)
{
var rows = new List<string[]> { new[] { T(language, "算法", "Algorithm"), T(language, "摘要", "Digest") } };
foreach (var pair in KeyValues(lines))
{
rows.Add([pair.Key, pair.Value]);
}
if (rows.Count == 1)
{
rows.AddRange(lines.Select((line, index) => new[] { (index + 1).ToString(), line }));
}
return rows;
}
private static IReadOnlyList<ToolResultBlock> BuildSystemBlocks(IReadOnlyList<string> lines, string language)
{
var metrics = KeyValues(lines).ToArray();
return metrics.Length > 0
? [ToolResultBlock.Metric(T(language, "系统摘要", "System summary"), metrics)]
: GenericTextBlocks(lines, language);
}
private static IReadOnlyList<ToolResultBlock> BuildTrainBlocks(IReadOnlyList<string> lines, string language)
{
var rows = new List<string[]> { new[] { T(language, "车次/车站", "Train / station"), T(language, "信息", "Info"), T(language, "备注", "Note") } };
foreach (var line in lines.Where(line => !line.Contains("数据源", StringComparison.Ordinal) && !line.StartsWith("Fetched", StringComparison.OrdinalIgnoreCase)))
{
var parts = line.Split(" ", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (parts.Length >= 2)
{
rows.Add([parts[0], string.Join(" / ", parts.Skip(1)), string.Empty]);
}
else if (line.Contains(" / ", StringComparison.Ordinal))
{
var station = line.Split(" / ", StringSplitOptions.TrimEntries);
rows.Add([station.ElementAtOrDefault(0) ?? string.Empty, station.ElementAtOrDefault(1) ?? string.Empty, station.ElementAtOrDefault(2) ?? string.Empty]);
}
}
return rows.Count > 1 ? [ToolResultBlock.Table(T(language, "列车/车站", "Train / station"), rows)] : GenericTextBlocks(lines, language);
}
private static IReadOnlyList<ToolResultBlock> BuildRankedCards(IReadOnlyList<string> lines, string title)
{
var items = new List<ToolResultListItem>();
foreach (var line in lines)
{
var trimmed = line.Trim();
if (IsRemoteHeaderOrPrivacyLine(trimmed))
{
continue;
}
var match = Regex.Match(trimmed, @"^(?<rank>\d+)[\.\)、]\s*(?<title>.*)$");
if (match.Success)
{
var visibleTitle = CleanVisibleText(match.Groups["title"].Value);
var uri = ExtractLinks([line]).FirstOrDefault() ?? string.Empty;
items.Add(new ToolResultListItem(match.Groups["rank"].Value, visibleTitle, string.Empty, "info", uri));
}
else if (items.Count > 0 && trimmed.Length > 0 && !trimmed.Contains("数据源", StringComparison.Ordinal))
{
var previous = items[^1];
var visible = CleanVisibleText(trimmed);
items[^1] = previous with { Subtitle = string.IsNullOrWhiteSpace(previous.Subtitle) ? visible : $"{previous.Subtitle}\n{visible}" };
}
}
return items.Count == 0 ? [] : [ToolResultBlock.List(ToolResultBlockKind.RankedList, title, items)];
}
private static IReadOnlyList<ToolResultBlock> BuildNewsCards(IReadOnlyList<string> lines, string title)
{
var items = new List<ToolResultListItem>();
foreach (var line in lines)
{
var trimmed = line.Trim();
if (IsRemoteHeaderOrPrivacyLine(trimmed) || string.IsNullOrWhiteSpace(trimmed))
{
continue;
}
var match = Regex.Match(trimmed, @"^(?<rank>\d+)[\.\)\u3001\uff09\s]+(?<title>.*)$");
if (match.Success)
{
var visibleTitle = CleanVisibleText(match.Groups["title"].Value);
var uri = ExtractLinks([line]).FirstOrDefault() ?? string.Empty;
items.Add(new ToolResultListItem(match.Groups["rank"].Value, visibleTitle, string.Empty, "info", uri));
continue;
}
if (items.Count > 0)
{
var previous = items[^1];
var visible = CleanVisibleText(trimmed);
items[^1] = previous with { Subtitle = string.IsNullOrWhiteSpace(previous.Subtitle) ? visible : $"{previous.Subtitle}\n{visible}" };
}
}
return items.Count == 0 ? [] : [ToolResultBlock.List(ToolResultBlockKind.NewsList, title, items)];
}
private static IReadOnlyList<ToolResultBlock> GenericTextBlocks(IReadOnlyList<string> lines, string language)
{
return [ToolResultBlock.List(ToolResultBlockKind.Text, T(language, "结果", "Result"), lines.Take(160).Select((line, index) => new ToolResultListItem((index + 1).ToString(), CleanVisibleText(line), string.Empty, "info", ExtractLinks([line]).FirstOrDefault() ?? string.Empty)).ToArray())];
}
private static IReadOnlyList<KeyValuePair<string, string>> KeyValues(IReadOnlyList<string> lines)
{
var pairs = new List<KeyValuePair<string, string>>();
foreach (var line in lines)
{
var normalized = line.Trim();
var separators = new[] { "", ":", "=", "\t" };
foreach (var separator in separators)
{
var index = normalized.IndexOf(separator, StringComparison.Ordinal);
if (index > 0 && index < normalized.Length - separator.Length)
{
var key = normalized[..index].Trim().Trim('-', '*', ' ');
var value = normalized[(index + separator.Length)..].Trim();
if (key.Length is > 0 and < 64 && value.Length > 0)
{
pairs.Add(Pair(key, value));
}
break;
}
}
}
return pairs;
}
private static IEnumerable<KeyValuePair<string, string>> MetricPairs(IReadOnlyList<string> lines)
{
var pairs = KeyValues(lines).ToArray();
if (pairs.Length > 0)
{
return pairs;
}
return lines.Take(12).Select((line, index) => Pair((index + 1).ToString(), line));
}
private static IReadOnlyList<string[]> ParseTableRows(IReadOnlyList<string> lines)
{
return lines
.Where(line => line.Contains('|') || line.Contains('\t'))
.Select(line => line.Contains('|') ? line.Trim().Trim('|').Split('|', StringSplitOptions.TrimEntries) : line.Split('\t', StringSplitOptions.TrimEntries))
.Where(row => row.Length >= 2)
.Take(80)
.ToArray();
}
private static bool TryParseDecimal(string value, out decimal number)
{
var cleaned = Regex.Replace(value ?? string.Empty, @"[^\d\.\,\-]", string.Empty).Replace(",", string.Empty, StringComparison.Ordinal);
return decimal.TryParse(cleaned, NumberStyles.Float, CultureInfo.InvariantCulture, out number) ||
decimal.TryParse(cleaned, NumberStyles.Float, CultureInfo.CurrentCulture, out number);
}
private static IEnumerable<string[]> JsonArrayRows(JsonElement root)
{
if (root.ValueKind != JsonValueKind.Array)
{
yield break;
}
var items = root.EnumerateArray().Take(20).ToArray();
if (items.Length == 0)
{
yield break;
}
if (items.Any(item => item.ValueKind != JsonValueKind.Object))
{
yield return ["#", "Value"];
for (var index = 0; index < items.Length; index++)
{
yield return [(index + 1).ToString(), JsonPreview(items[index])];
}
yield break;
}
var headers = items.SelectMany(item => item.EnumerateObject().Select(property => property.Name)).Distinct(StringComparer.Ordinal).Take(8).ToArray();
yield return headers;
foreach (var item in items)
{
yield return headers.Select(header => item.TryGetProperty(header, out var value) ? JsonPreview(value) : string.Empty).ToArray();
}
}
private static string JsonPreview(JsonElement value)
{
return value.ValueKind switch
{
JsonValueKind.String => value.GetString() ?? string.Empty,
JsonValueKind.Number or JsonValueKind.True or JsonValueKind.False => value.GetRawText(),
JsonValueKind.Null => "null",
JsonValueKind.Array => $"Array[{value.GetArrayLength()}]",
JsonValueKind.Object => JsonSerializer.Serialize(value),
_ => value.GetRawText()
};
}
private static bool IsReferenceTool(string id)
{
return id is "mime_lookup" or "http_status_lookup" or "port_lookup" or "dns_record_lookup" or "unicode_block_lookup" or "charset_lookup" or "timezone_abbr_lookup" or "regex_preset_lookup" or "magic_number_lookup" or "car_info" or "sanguosha_skin";
}
private static ToolResultKind GuessRemoteResultKind(string id)
{
return id is "baidu_hot" or "hotboard" or "bili_hot" or "zhihu_hot" or "movie_box_office" or "history_today"
? ToolResultKind.RankedList
: id is "tech_news" or "football_news" or "cctv_news" or "ai_latest_news"
? ToolResultKind.NewsCards
: id is "city_route_query"
? ToolResultKind.KeyValueCards
: ToolResultKind.KeyValueCards;
}
private static ToolResultDocument Document(IToolModule module, ToolPageSpec spec, string output, IReadOnlyList<ToolResultBlock> blocks, string language)
{
var experience = ToolResultExperienceCatalog.CreateExperience(module);
return new ToolResultDocument(module.Id, spec.Result, output, blocks, Metadata(module.Id, spec.Result, language, DisplayProfile(spec.Result), experience), output, string.Empty, "ok");
}
private static Dictionary<string, string> Metadata(string toolId, ToolResultKind kind, string language, string displayProfile, ToolResultExperience? experience = null)
{
var metadata = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["toolId"] = toolId,
["resultKind"] = kind.ToString(),
["language"] = language,
["generatedAt"] = DateTimeOffset.Now.ToString("O"),
["loadedAt"] = DateTimeOffset.Now.ToString("O"),
["dataFreshness"] = "live",
["displayProfile"] = displayProfile
};
if (experience is not null)
{
metadata["profile"] = experience.Profile;
metadata["domain"] = experience.Domain;
metadata["preferredBlocks"] = string.Join(",", experience.PreferredBlocks ?? []);
metadata["copyPolicy"] = experience.CopyPolicy;
metadata["privacyLevel"] = experience.PrivacyLevel;
metadata["defaultExpandedBlocks"] = string.Join(",", experience.DefaultExpandedBlocks ?? []);
}
var localJsProfile = LocalJsProfileFor(toolId);
if (!string.IsNullOrWhiteSpace(localJsProfile))
{
metadata["localJsProfile"] = localJsProfile;
}
var relatedActions = RelatedActionsFor(toolId, language);
if (!string.IsNullOrWhiteSpace(relatedActions))
{
metadata["relatedActions"] = relatedActions;
}
return metadata;
}
private static string LocalJsProfileFor(string toolId)
=> toolId switch
{
"json_formatter" => "json",
"yaml_json_converter" => "yaml",
"xml_formatter" => "xml",
"html_minifier" or "js_obfuscator" => "markup",
"markdown_preview" or "markdown_table_normalizer" => "markdown",
"html_entity" => "html-entity",
"punycode_codec" => "idn",
"timestamp_converter" or "date_time_calculator" => "time",
"base64_codec" or "url_codec" or "unicode_codec" => "codec",
"hash_generator" or "hmac_generator" => "hash",
"qr_generator" => "qrcode",
_ => string.Empty
};
private static string RelatedActionsFor(string toolId, string language)
{
var english = language.StartsWith("en", StringComparison.OrdinalIgnoreCase);
object Action(string kind, string labelZh, string labelEn, string target, string toolId = "", string routeTag = "")
=> new
{
kind,
label = english ? labelEn : labelZh,
target,
toolId,
routeTag
};
var actions = toolId switch
{
"json_formatter" => new[]
{
Action("navigateTool", "转到 JSONPath", "Open JSONPath", "json_path_helper", "json_path_helper"),
Action("navigateTool", "转 YAML/JSON", "Open YAML/JSON", "yaml_json_converter", "yaml_json_converter")
},
"yaml_json_converter" => new[]
{
Action("navigateTool", "转到 JSON 格式化", "Open JSON formatter", "json_formatter", "json_formatter")
},
"markdown_preview" => new[]
{
Action("navigateTool", "整理 Markdown 表格", "Normalize markdown table", "markdown_table_normalizer", "markdown_table_normalizer")
},
"markdown_table_normalizer" => new[]
{
Action("navigateTool", "预览 Markdown", "Preview markdown", "markdown_preview", "markdown_preview")
},
"base64_codec" or "url_codec" or "html_entity" or "unicode_codec" or "punycode_codec" => new[]
{
Action("navigateTool", "转到 URL 编解码", "Open URL codec", "url_codec", "url_codec"),
Action("navigateTool", "转到 Base64", "Open Base64", "base64_codec", "base64_codec")
},
"url_inspector" or "url_redirect_trace" => new[]
{
Action("navigateTool", "转到 URL 编解码", "Open URL codec", "url_codec", "url_codec"),
Action("navigateTool", "转到域名 RDAP", "Open domain RDAP", "rdap_domain_lookup", "rdap_domain_lookup")
},
"dns_query" or "ip_lookup" or "ip_info" => new[]
{
Action("navigateTool", "转到 IP 信息", "Open IP info", "ip_info", "ip_info"),
Action("navigateTool", "转到 RDAP 查询", "Open RDAP lookup", "rdap_ip_lookup", "rdap_ip_lookup")
},
"hash_generator" or "hmac_generator" => new[]
{
Action("navigateTool", "生成哈希清单", "Build hash manifest", "hash_manifest_builder", "hash_manifest_builder"),
Action("navigateTool", "校验哈希清单", "Verify hash manifest", "hash_manifest_verify", "hash_manifest_verify")
},
"hash_manifest_builder" => new[]
{
Action("navigateTool", "校验哈希清单", "Verify hash manifest", "hash_manifest_verify", "hash_manifest_verify")
},
"hash_manifest_verify" => new[]
{
Action("navigateTool", "生成哈希清单", "Build hash manifest", "hash_manifest_builder", "hash_manifest_builder")
},
"qr_generator" => new[]
{
Action("navigateTool", "扫描二维码", "Scan QR code", "qrcode_scanner", "qrcode_scanner")
},
"qrcode_scanner" => new[]
{
Action("navigateTool", "生成二维码", "Generate QR code", "qr_generator", "qr_generator")
},
"sanguosha_skin" => new[]
{
Action("navigateRoute", "查看服务状态", "Open service status", "serviceStatus", routeTag: "serviceStatus")
},
_ => []
};
return actions.Length == 0 ? string.Empty : JsonSerializer.Serialize(actions);
}
private static Dictionary<string, string> RemoteMetadata(ApiEndpoint endpoint, ToolResultKind kind, string language)
{
var metadata = Metadata(endpoint.Id, kind, language, "remote-dashboard");
metadata["sourceName"] = endpoint.SourceName;
metadata["sourceVisibility"] = endpoint.SourceVisibility.ToString();
metadata["sourceSensitivity"] = endpoint.SourceVisibility == ApiSourceVisibility.SensitivePrivate ? "sensitive" : "public";
metadata["dataFreshness"] = "remote-live";
return metadata;
}
private static string DisplayProfile(ToolResultKind kind)
{
return kind switch
{
ToolResultKind.RankedList or ToolResultKind.NewsCards => "cards",
ToolResultKind.Table or ToolResultKind.CalculatorTable or ToolResultKind.ReferenceRows => "table",
ToolResultKind.ImagePreview or ToolResultKind.Media => "preview",
ToolResultKind.JsonTree or ToolResultKind.CodePreview => "document",
_ => "summary-details"
};
}
private static IReadOnlyDictionary<string, string> BlockMeta(string contentType, string displayMode, string priority)
{
return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["contentType"] = contentType,
["displayMode"] = displayMode,
["priority"] = priority
};
}
private static KeyValuePair<string, string> Pair(string key, string value) => new(key, value);
private static IReadOnlyList<string> Lines(string output)
{
return output.Replace("\r\n", "\n").Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
}
private static IReadOnlyList<string> RemoveSourceHeader(IReadOnlyList<string> lines)
{
return lines
.Where(line => !IsRemoteHeaderOrPrivacyLine(line))
.ToArray();
}
private static bool IsRemoteHeaderOrPrivacyLine(string line)
{
var normalized = line.Trim();
return IsSourceLine(normalized) ||
normalized.StartsWith("\u6570\u636e\u6e90", StringComparison.OrdinalIgnoreCase) ||
normalized.StartsWith("\u6765\u6e90", StringComparison.OrdinalIgnoreCase) ||
normalized.StartsWith("\u83b7\u53d6\u65f6\u95f4", StringComparison.OrdinalIgnoreCase) ||
normalized.Contains("Source note:", StringComparison.OrdinalIgnoreCase) ||
normalized.Contains("remote address is hidden", StringComparison.OrdinalIgnoreCase) ||
normalized.Contains("sanitized source name", StringComparison.OrdinalIgnoreCase) ||
normalized.Contains("\u6765\u6e90\u8bf4\u660e", StringComparison.OrdinalIgnoreCase) ||
normalized.Contains("\u5df2\u9690\u85cf\u8fdc\u7a0b\u5730\u5740", StringComparison.OrdinalIgnoreCase) ||
normalized.Contains("\u8131\u654f\u6765\u6e90", StringComparison.OrdinalIgnoreCase) ||
normalized.Contains("璇存槑", StringComparison.OrdinalIgnoreCase) && normalized.Contains("闅愯棌", StringComparison.OrdinalIgnoreCase) ||
normalized.Contains("璇存槑", StringComparison.OrdinalIgnoreCase) && normalized.Contains("宸查殣", StringComparison.OrdinalIgnoreCase) ||
normalized.Contains("鑴辨晱", StringComparison.OrdinalIgnoreCase) ||
normalized.Contains("杩滅▼鍦板潃", StringComparison.OrdinalIgnoreCase) ||
normalized.Contains("繙绋嬪湴鍧", StringComparison.OrdinalIgnoreCase);
}
private static bool IsSourceLine(string line)
{
var normalized = line.Trim();
return normalized.StartsWith("Data source:", StringComparison.OrdinalIgnoreCase) ||
normalized.StartsWith("Fetched at:", StringComparison.OrdinalIgnoreCase) ||
normalized.StartsWith("Source:", StringComparison.OrdinalIgnoreCase) ||
normalized.StartsWith("来源", StringComparison.OrdinalIgnoreCase) ||
normalized.StartsWith("数据源", StringComparison.OrdinalIgnoreCase) ||
normalized.StartsWith("获取时间", StringComparison.OrdinalIgnoreCase) ||
normalized.StartsWith("鏉ユ簮", StringComparison.OrdinalIgnoreCase) ||
normalized.StartsWith("鏁版嵁", StringComparison.OrdinalIgnoreCase) ||
normalized.StartsWith("鑾峰彇", StringComparison.OrdinalIgnoreCase);
}
private static string SplitValue(string line)
{
var index = line.IndexOfAny(['', ':']);
return index >= 0 && index < line.Length - 1 ? line[(index + 1)..].Trim() : line;
}
private static string StripLabel(string value)
{
var index = value.IndexOfAny(['', ':']);
return index >= 0 && index < value.Length - 1 ? value[(index + 1)..].Trim() : value.Trim();
}
private static IEnumerable<string> ExtractLinks(IEnumerable<string> lines)
{
foreach (var line in lines)
{
foreach (Match match in Regex.Matches(line, @"https?://[^\s\]\)>'""]+", RegexOptions.IgnoreCase))
{
yield return match.Value.TrimEnd('.', ',', ';');
}
}
}
private static string CleanVisibleText(string value)
{
var text = value ?? string.Empty;
text = Regex.Replace(text, @"\s*https?://[^\s\]\)>'""]+", string.Empty, RegexOptions.IgnoreCase);
text = Regex.Replace(text, @"\s*/\s*$", string.Empty);
text = Regex.Replace(text, @"\s{2,}", " ");
return text.Trim();
}
private static bool LooksLikeJson(string output)
{
var trimmed = output.TrimStart();
return trimmed.StartsWith('{') || trimmed.StartsWith('[');
}
private static bool LooksLikeSvg(string output)
{
var trimmed = output.TrimStart();
return trimmed.StartsWith("<svg", StringComparison.OrdinalIgnoreCase) ||
(trimmed.StartsWith("<?xml", StringComparison.OrdinalIgnoreCase) &&
trimmed.Contains("<svg", StringComparison.OrdinalIgnoreCase));
}
private static string SourceTypeLabel(ApiEndpoint endpoint, string language)
{
return endpoint.SourceVisibility switch
{
ApiSourceVisibility.PublicOfficial => T(language, "官方/权威源", "Official / authoritative"),
ApiSourceVisibility.PublicTrusted => T(language, "公开可信源", "Public trusted"),
ApiSourceVisibility.SensitivePrivate => T(language, "私有/敏感源", "Private / sensitive"),
_ => endpoint.IsOfficial ? T(language, "官方/权威源", "Official / authoritative") : T(language, "公开可信源", "Public trusted")
};
}
private static IReadOnlyDictionary<string, string> SourceMetadata(ApiEndpoint endpoint)
{
var metadata = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["contentType"] = "text/plain",
["displayMode"] = "source",
["priority"] = "low",
["sourceVisibility"] = endpoint.SourceVisibility.ToString(),
["sourceSensitivity"] = endpoint.SourceVisibility == ApiSourceVisibility.SensitivePrivate ? "sensitive" : "public",
["sourceName"] = endpoint.SourceName
};
return metadata;
}
private static string CodeLanguage(string id)
{
return id switch
{
var value when value.Contains("json", StringComparison.OrdinalIgnoreCase) => "json",
var value when value.Contains("xml", StringComparison.OrdinalIgnoreCase) => "xml",
var value when value.Contains("sql", StringComparison.OrdinalIgnoreCase) => "sql",
var value when value.Contains("html", StringComparison.OrdinalIgnoreCase) => "html",
_ => string.Empty
};
}
private static string DiffLeading(string line) => line.StartsWith('+') ? "+" : line.StartsWith('-') ? "-" : " ";
private static string DiffStatus(string line) => line.StartsWith('+') ? "added" : line.StartsWith('-') ? "removed" : "context";
private static string StatusLeading(string line) => StatusOf(line) switch { "ok" => "OK", "error" => "!", _ => "i" };
private static string StatusOf(string line)
{
return line.Contains("OK", StringComparison.OrdinalIgnoreCase) || line.Contains("SUCCESS", StringComparison.OrdinalIgnoreCase)
? "ok"
: line.Contains("FAIL", StringComparison.OrdinalIgnoreCase) || line.Contains("MISMATCH", StringComparison.OrdinalIgnoreCase) || line.Contains("ERROR", StringComparison.OrdinalIgnoreCase)
? "error"
: "info";
}
private static string T(string language, string zh, string en) => string.Equals(language, "en-US", StringComparison.OrdinalIgnoreCase) ? en : zh;
}