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(); 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 BuildBlocks(IToolModule module, ToolPageSpec spec, string output, IReadOnlyList 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(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 BuildRemoteBlocks(string id, IReadOnlyList 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 BuildHistoryBlocks(IReadOnlyList lines, string language) { var blocks = new List(); var currentSection = string.Empty; var items = new List(); 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*(?-?\d{1,4})\s*:\s*(?.+)$"); 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 BuildMetricTextBlocks(IReadOnlyList 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 BuildGoldPriceBlocks(IReadOnlyList lines, string language) { var blocks = new List(); 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(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 BuildHtmlSummaryBlocks(IReadOnlyList lines, string language) { var blocks = new List(); 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 BuildJsonBlocks(string output, string language) { try { using var json = JsonDocument.Parse(output); var root = json.RootElement; var blocks = new List { 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 BuildJwtBlocks(string output, string language) { var blocks = new List(); 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 BuildDnsBlocks(IReadOnlyList lines, string language) { var records = new List { new[] { T(language, "名称", "Name"), "TYPE", "TTL", T(language, "数据", "Data") } }; var details = new List>(); foreach (var line in lines) { var match = Regex.Match(line, @"^(?.*?)\s+TYPE\s+(?\S+)\s+TTL\s+(?\S+)\s+(?.*)$", 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(); 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 BuildWeatherBlocks(IReadOnlyList lines, string language) { var pairs = KeyValues(lines).ToList(); var blocks = new List(); if (pairs.Count > 0) { blocks.Add(ToolResultBlock.Metric( T(language, "天气摘要", "Weather summary"), pairs.Take(8).ToArray(), BlockMeta("metric", "summary", "high"))); } var forecastRows = new List { new[] { T(language, "日期", "Date"), T(language, "最低", "Low"), T(language, "最高", "High"), T(language, "说明", "Note") } }; foreach (var line in lines) { var match = Regex.Match(line, @"^(?\d{4}-\d{2}-\d{2})\s*:\s*(?-?\d+(?:\.\d+)?)\s*-\s*(?-?\d+(?:\.\d+)?)(?.*)$"); 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 BuildRdapBlocks(IReadOnlyList lines, string language) { var events = new List(); var pairs = new List>(); 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(); 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 BuildReferenceBlocks(IReadOnlyList lines, string language) { var rows = new List { new[] { T(language, "代码", "Code"), T(language, "名称", "Name"), T(language, "说明", "Description"), T(language, "备注", "Note") } }; var sourcePairs = new List>(); 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, @"^(?.*?)\s+-\s+(?.*?)(?:[::]\s*(?.*?))?(?:\s+/\s+(?.*))?$"); 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(); 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 BuildSearchBlocks(IReadOnlyList 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(); 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 BuildReferenceBlocksV2(IReadOnlyList lines, string language) { var rows = new List { new[] { T(language, "代码", "Code"), T(language, "名称", "Name"), T(language, "说明", "Description"), T(language, "备注", "Note") } }; var sourcePairs = new List>(); foreach (var line in lines) { if (IsSourceLine(line)) { sourcePairs.Add(Pair(T(language, "数据源", "Data source"), SplitValue(line))); continue; } var match = Regex.Match(line, @"^(?.*?)\s+-\s+(?.*?)(?:[::]\s*(?.*?))?(?:\s+/\s+(?.*))?$"); 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(); 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 BuildSanguoshaSkinBlocks(IReadOnlyList lines, string language) { var rows = new List { new[] { T(language, "皮肤 ID", "Skin ID"), T(language, "条目", "Item"), T(language, "武将", "General"), T(language, "皮肤", "Skin"), T(language, "性别/品质", "Gender / quality") } }; var sourcePairs = new List>(); var statuses = new List(); 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(); 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 BuildCodecBlocks(IReadOnlyList lines, string language) { var blocks = new List(); var sections = new List>(); var currentTitle = string.Empty; var currentText = new List(); 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 DigestRows(IReadOnlyList lines, string language) { var rows = new List { 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 BuildSystemBlocks(IReadOnlyList 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 BuildTrainBlocks(IReadOnlyList lines, string language) { var rows = new List { 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 BuildRankedCards(IReadOnlyList lines, string title) { var items = new List(); foreach (var line in lines) { var trimmed = line.Trim(); if (IsRemoteHeaderOrPrivacyLine(trimmed)) { continue; } var match = Regex.Match(trimmed, @"^(?\d+)[\.\)、]\s*(?.*)$"); 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; }