import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.time.Duration; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * 龙虾引擎全行业验证 - 第一批基础验证 * * 通过 HTTP 调用 /api/lobster/admin/validation/** 完成基础验证: * 第1步:基础信息获取(health / industries / node-capabilities) * 第2步:语义意图识别(全行业 × 3条典型消息) * 第3步:敏感词全量验证(list + 逐词 check) * 第4步:Token 统计(daily / model) * 第5步:进化指标(evolution / learning) * * 技术要求: * - Java 11+ HttpClient(java.net.http.HttpClient) * - JSON 使用字符串拼接 + 正则解析,不依赖外部库 * - 默认请求超时 30 秒;语义/敏感词检测为 LLM/缓存预热场景,超时放宽至 150s/60s * * 编译运行: * javac -encoding UTF-8 FullIndustryValidation.java * java -cp . FullIndustryValidation * * 输出:reports/full-industry-validation-batch1.json */ public class FullIndustryValidation { // ════════ 配置 ════════ private static final String BASE = "http://localhost:8006"; private static final String API = BASE + "/api/lobster/admin/validation"; private static final String REPORT_DIR = "reports"; private static final String REPORT_FILE = "reports/full-industry-validation-batch1.json"; private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(15); private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(30); private static final Duration SEMANTIC_TIMEOUT = Duration.ofSeconds(150); private static final Duration SENSITIVE_TIMEOUT = Duration.ofSeconds(90); /** 语义分析并行度(LLM 调用,单次 40~60s;命中缓存/关键词快路径时毫秒级返回) */ private static final int SEMANTIC_CONCURRENCY = 8; /** 敏感词检测并行度(后端每个 check 会触发多次 LLM 语义确认;高并发会返回 500,故低并发) */ private static final int SENSITIVE_CONCURRENCY = 2; // 每个行业测试的 3 条典型消息 private static final String[] TEST_MESSAGES = { "想了解一下你们的服务", "这个多少钱,怎么购买", "不需要了,谢谢" }; private static final String[] MESSAGE_LABELS = { "咨询类", "购买意向", "拒绝类" }; private static final HttpClient HTTP = HttpClient.newBuilder() .connectTimeout(CONNECT_TIMEOUT) .build(); // 全部验证项(线程安全) private static final List ITEMS = Collections.synchronizedList(new ArrayList<>()); // 从第1步解析出的行业代码 private static volatile List INDUSTRY_CODES = new ArrayList<>(); // ════════ 入口 ════════ public static void main(String[] args) throws Exception { System.out.println("=== 龙虾引擎全行业验证 - 第一批基础验证 ==="); System.out.println("后端服务: " + BASE); System.out.println("API 基础路径: /api/lobster/admin/validation"); long startAll = System.currentTimeMillis(); // 第1步:基础信息获取 List step1 = step1BasicInfo(); ITEMS.addAll(step1); // 第2步:语义意图识别(依赖第1步解析的行业列表;若解析为空则使用内置 28 行业兜底) List codes = INDUSTRY_CODES; if (codes == null || codes.isEmpty()) { codes = builtinIndustries(); System.out.println(" [WARN] 未能从 /industries 解析出行业列表,使用内置 " + codes.size() + " 个行业兜底。"); } List step2 = step2Semantic(codes); ITEMS.addAll(step2); // 第3步:敏感词全量验证 List step3 = step3Sensitive(); ITEMS.addAll(step3); // 第4步:Token 统计 List step4 = step4TokenStats(); ITEMS.addAll(step4); // 第5步:进化指标 List step5 = step5Evolution(); ITEMS.addAll(step5); long totalMs = System.currentTimeMillis() - startAll; // 生成报告 Files.createDirectories(Paths.get(REPORT_DIR)); String json = buildReportJson(totalMs); Path out = Paths.get(REPORT_FILE).toAbsolutePath(); Files.write(out, json.getBytes(StandardCharsets.UTF_8)); System.out.println("\n=== 验证完成 ==="); System.out.println("总耗时: " + totalMs + " ms (" + (totalMs / 1000) + " s)"); System.out.println("报告文件: " + out); // 控制台汇总 int total = ITEMS.size(); int passed = 0; for (Item it : ITEMS) { if (it.passed) { passed++; } } System.out.println("验证项总数: " + total + ",通过: " + passed + ",失败: " + (total - passed) + ",通过率: " + String.format("%.1f%%", total == 0 ? 0 : (passed * 100.0 / total))); } // ════════ 第1步:基础信息获取 ════════ private static List step1BasicInfo() { System.out.println("\n--- 第1步:基础信息获取 ---"); List list = new ArrayList<>(); // 1.1 health { Resp r = httpGet("/health"); Item it = new Item(); it.step = 1; it.module = "health"; it.input = "GET /health"; it.output = truncate(r.body, 3000); it.elapsedMs = r.elapsedMs; if (r.error != null) { it.passed = false; it.error = r.error; } else { int code = extractCode(r.body); Boolean healthy = extractBool(r.body, "healthy"); it.passed = (r.statusCode == 200 && code == 200 && Boolean.TRUE.equals(healthy)); if (!it.passed) { it.error = "http=" + r.statusCode + " code=" + code + " healthy=" + healthy; } } list.add(it); System.out.println(" [health] passed=" + it.passed + " elapsed=" + it.elapsedMs + "ms" + (it.error != null ? " err=" + it.error : "")); } // 1.2 industries { Resp r = httpGet("/industries"); Item it = new Item(); it.step = 1; it.module = "industries"; it.input = "GET /industries"; it.output = truncate(r.body, 4000); it.elapsedMs = r.elapsedMs; if (r.error != null) { it.passed = false; it.error = r.error; } else { int code = extractCode(r.body); Integer total = extractInt(r.body, "total"); List codes = extractAllStr(r.body, "code"); INDUSTRY_CODES = codes; it.passed = (r.statusCode == 200 && code == 200 && total != null && total >= 27); if (!it.passed) { it.error = "http=" + r.statusCode + " code=" + code + " total=" + total; } } list.add(it); System.out.println(" [industries] passed=" + it.passed + " total=" + extractInt(r.body, "total") + " codes=" + INDUSTRY_CODES.size() + " elapsed=" + it.elapsedMs + "ms"); } // 1.3 node-capabilities { Resp r = httpGet("/node-capabilities"); Item it = new Item(); it.step = 1; it.module = "node-capabilities"; it.input = "GET /node-capabilities"; it.output = truncate(r.body, 4000); it.elapsedMs = r.elapsedMs; if (r.error != null) { it.passed = false; it.error = r.error; } else { int code = extractCode(r.body); // capabilities 数组项里含 "maturityStars" 字段,统计其出现次数作为能力数量 int capCount = countMatches(r.body, "\"maturityStars\"\\s*:"); it.passed = (r.statusCode == 200 && code == 200 && capCount > 0); if (!it.passed) { it.error = "http=" + r.statusCode + " code=" + code + " capabilities=" + capCount; } } list.add(it); System.out.println(" [node-capabilities] passed=" + it.passed + " capabilities=" + countMatches(r.body, "\"maturityStars\"\\s*:") + " elapsed=" + it.elapsedMs + "ms"); } return list; } // ════════ 第2步:语义意图识别(全行业,并行) ════════ private static List step2Semantic(List codes) throws Exception { int total = codes.size() * TEST_MESSAGES.length; System.out.println("\n--- 第2步:语义意图识别(全行业 " + codes.size() + " × " + TEST_MESSAGES.length + " 条 = " + total + " 次调用,并行度=" + SEMANTIC_CONCURRENCY + ") ---"); ExecutorService pool = Executors.newFixedThreadPool(SEMANTIC_CONCURRENCY); List> futures = new ArrayList<>(); AtomicInteger orderSeq = new AtomicInteger(0); long t0 = System.currentTimeMillis(); for (String code : codes) { for (int i = 0; i < TEST_MESSAGES.length; i++) { final String c = code; final int mi = i; final int order = orderSeq.getAndIncrement(); futures.add(pool.submit(() -> runSemantic(c, mi, order))); } } pool.shutdown(); List list = new ArrayList<>(); int done = 0; for (Future f : futures) { try { Item it = f.get(SEMANTIC_TIMEOUT.getSeconds() + 30, TimeUnit.SECONDS); list.add(it); } catch (Exception e) { Item it = new Item(); it.step = 2; it.module = "semantic"; it.passed = false; it.error = "future-get-error: " + e; list.add(it); } done++; if (done % 12 == 0 || done == futures.size()) { long elapsed = System.currentTimeMillis() - t0; System.out.println(" 语义进度: " + done + "/" + futures.size() + " 累计耗时=" + (elapsed / 1000) + "s"); } } pool.awaitTermination(5, TimeUnit.MINUTES); // 按 order 排序,保证报告顺序稳定 list.sort((a, b) -> Integer.compare(a.order, b.order)); int pass = 0; for (Item it : list) { if (it.passed) { pass++; } } System.out.println(" 语义完成: " + pass + "/" + list.size() + " 通过"); return list; } private static Item runSemantic(String industry, int msgIdx, int order) { String message = TEST_MESSAGES[msgIdx]; String label = MESSAGE_LABELS[msgIdx]; String body = "{\"message\":" + jsonStr(message) + ",\"industryType\":" + jsonStr(industry) + ",\"context\":{}}"; Resp r = httpPost("/semantic/analyze", body, SEMANTIC_TIMEOUT); Item it = new Item(); it.step = 2; it.module = "semantic"; it.industry = industry; it.messageLabel = label; it.order = order; it.input = body; it.output = truncate(r.body, 2500); it.elapsedMs = r.elapsedMs; if (r.error != null) { it.passed = false; it.error = r.error; } else { int code = extractCode(r.body); String intent = extractStr(r.body, "intent"); it.intent = intent; it.passed = (r.statusCode == 200 && code == 200 && intent != null && !intent.isEmpty()); if (!it.passed) { it.error = "http=" + r.statusCode + " code=" + code + " intent=" + intent; } } return it; } // ════════ 第3步:敏感词全量验证 ════════ private static List step3Sensitive() throws Exception { System.out.println("\n--- 第3步:敏感词全量验证 ---"); List list = new ArrayList<>(); // 3.1 获取所有敏感词 Resp r = httpGet("/sensitive/list"); Item listIt = new Item(); listIt.step = 3; listIt.module = "sensitive-list"; listIt.input = "GET /sensitive/list"; listIt.output = truncate(r.body, 4000); listIt.elapsedMs = r.elapsedMs; List words = new ArrayList<>(); if (r.error != null) { listIt.passed = false; listIt.error = r.error; } else { int code = extractCode(r.body); words = extractAllStr(r.body, "word"); listIt.passed = (r.statusCode == 200 && code == 200 && !words.isEmpty()); if (!listIt.passed) { listIt.error = "http=" + r.statusCode + " code=" + code + " words=" + words.size(); } } list.add(listIt); System.out.println(" [sensitive-list] passed=" + listIt.passed + " words=" + words.size() + " elapsed=" + listIt.elapsedMs + "ms"); // 3.2 逐词 check(并行) if (!words.isEmpty()) { System.out.println(" 开始逐词检测 " + words.size() + " 个敏感词,并行度=" + SENSITIVE_CONCURRENCY + " ..."); ExecutorService pool = Executors.newFixedThreadPool(SENSITIVE_CONCURRENCY); List> futures = new ArrayList<>(); int order = 0; for (String w : words) { final String word = w; futures.add(pool.submit(() -> runSensitiveCheck(word))); } pool.shutdown(); long t0 = System.currentTimeMillis(); int done = 0; for (Future f : futures) { try { list.add(f.get(SENSITIVE_TIMEOUT.getSeconds() + 30, TimeUnit.SECONDS)); } catch (Exception e) { Item it = new Item(); it.step = 3; it.module = "sensitive-check"; it.passed = false; it.error = "future-get-error: " + e; list.add(it); } done++; if (done % 30 == 0 || done == futures.size()) { long el = System.currentTimeMillis() - t0; System.out.println(" 敏感词进度: " + done + "/" + futures.size() + " 累计耗时=" + (el / 1000) + "s"); } } pool.awaitTermination(5, TimeUnit.MINUTES); } int pass = 0; for (Item it : list) { if (it.passed) { pass++; } } System.out.println(" 敏感词完成: " + pass + "/" + list.size() + " 通过"); return list; } private static Item runSensitiveCheck(String word) { // API 实际读取 content 字段(controller: body.get("content")); // 同时附带 message 字段以对齐任务示例。 String body = "{\"content\":" + jsonStr(word) + ",\"message\":" + jsonStr(word) + ",\"industryType\":\"general\",\"companyId\":1}"; // 首次失败(500/超时)则重试一次,缓解后端偶发错误 Resp r = httpPost("/sensitive/check", body, SENSITIVE_TIMEOUT); String retryNote = null; if (isBadResp(r)) { Resp r2 = httpPost("/sensitive/check", body, SENSITIVE_TIMEOUT); retryNote = "retried; first=" + briefResp(r); r = r2; } Item it = new Item(); it.step = 3; it.module = "sensitive-check"; it.sensitiveWord = word; it.input = body; it.output = truncate(r.body, 2500); it.elapsedMs = r.elapsedMs; if (r.error != null) { it.passed = false; it.error = (retryNote != null ? retryNote + "; " : "") + r.error; } else { int code = extractCode(r.body); Boolean hit = extractBool(r.body, "containsSensitive"); it.containsSensitive = hit; // 命中以 detectedWords 非空为准(关键词级命中,最可靠); // containsSensitive 为 LLM 语义确认结果,作为附加信号记录。 boolean detected = r.body != null && r.body.contains("\"detectedWords\":[{"); it.passed = (r.statusCode == 200 && code == 200 && detected); if (!it.passed) { it.error = (retryNote != null ? retryNote + "; " : "") + "http=" + r.statusCode + " code=" + code + " detected=" + detected + " containsSensitive=" + hit; } } return it; } private static boolean isBadResp(Resp r) { if (r.error != null) { return true; } int code = extractCode(r.body); return r.statusCode != 200 || code == 500; } private static String briefResp(Resp r) { if (r.error != null) { return "err:" + r.error; } return "http=" + r.statusCode + " code=" + extractCode(r.body); } // ════════ 第4步:Token 统计 ════════ private static List step4TokenStats() { System.out.println("\n--- 第4步:Token 统计 ---"); List list = new ArrayList<>(); // 4.1 daily { Resp r = httpGet("/token-stats/daily"); Item it = new Item(); it.step = 4; it.module = "token-stats-daily"; it.input = "GET /token-stats/daily"; it.output = truncate(r.body, 4000); it.elapsedMs = r.elapsedMs; if (r.error != null) { it.passed = false; it.error = r.error; } else { int code = extractCode(r.body); it.passed = (r.statusCode == 200 && code == 200); if (!it.passed) { it.error = "http=" + r.statusCode + " code=" + code; } } list.add(it); System.out.println(" [token-stats/daily] passed=" + it.passed + " elapsed=" + it.elapsedMs + "ms"); } // 4.2 model { Resp r = httpGet("/token-stats/model"); Item it = new Item(); it.step = 4; it.module = "token-stats-model"; it.input = "GET /token-stats/model"; it.output = truncate(r.body, 4000); it.elapsedMs = r.elapsedMs; if (r.error != null) { it.passed = false; it.error = r.error; } else { int code = extractCode(r.body); it.passed = (r.statusCode == 200 && code == 200); if (!it.passed) { it.error = "http=" + r.statusCode + " code=" + code; } } list.add(it); System.out.println(" [token-stats/model] passed=" + it.passed + " elapsed=" + it.elapsedMs + "ms"); } return list; } // ════════ 第5步:进化指标 ════════ private static List step5Evolution() { System.out.println("\n--- 第5步:进化指标 ---"); List list = new ArrayList<>(); // 5.1 evolution/metrics { Resp r = httpGet("/evolution/metrics"); Item it = new Item(); it.step = 5; it.module = "evolution-metrics"; it.input = "GET /evolution/metrics"; it.output = truncate(r.body, 4000); it.elapsedMs = r.elapsedMs; if (r.error != null) { it.passed = false; it.error = r.error; } else { int code = extractCode(r.body); it.passed = (r.statusCode == 200 && code == 200); if (!it.passed) { it.error = "http=" + r.statusCode + " code=" + code; } } list.add(it); System.out.println(" [evolution/metrics] passed=" + it.passed + " elapsed=" + it.elapsedMs + "ms"); } // 5.2 learning/metrics { Resp r = httpGet("/learning/metrics"); Item it = new Item(); it.step = 5; it.module = "learning-metrics"; it.input = "GET /learning/metrics"; it.output = truncate(r.body, 4000); it.elapsedMs = r.elapsedMs; if (r.error != null) { it.passed = false; it.error = r.error; } else { int code = extractCode(r.body); it.passed = (r.statusCode == 200 && code == 200); if (!it.passed) { it.error = "http=" + r.statusCode + " code=" + code; } } list.add(it); System.out.println(" [learning/metrics] passed=" + it.passed + " elapsed=" + it.elapsedMs + "ms"); } return list; } // ════════ HTTP 工具 ════════ private static Resp httpGet(String path) { return httpGet(path, DEFAULT_TIMEOUT); } private static Resp httpGet(String path, Duration timeout) { long t0 = System.currentTimeMillis(); try { HttpRequest req = HttpRequest.newBuilder() .uri(URI.create(API + path)) .timeout(timeout) .GET() .build(); HttpResponse resp = HTTP.send(req, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); long elapsed = System.currentTimeMillis() - t0; return new Resp(resp.statusCode(), resp.body(), elapsed, null); } catch (Exception e) { return new Resp(0, "", System.currentTimeMillis() - t0, unwrap(e)); } } private static Resp httpPost(String path, String body, Duration timeout) { long t0 = System.currentTimeMillis(); try { HttpRequest req = HttpRequest.newBuilder() .uri(URI.create(API + path)) .timeout(timeout) .header("Content-Type", "application/json; charset=utf-8") .POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpResponse resp = HTTP.send(req, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); long elapsed = System.currentTimeMillis() - t0; return new Resp(resp.statusCode(), resp.body(), elapsed, null); } catch (Exception e) { return new Resp(0, "", System.currentTimeMillis() - t0, unwrap(e)); } } private static String unwrap(Throwable e) { StringBuilder sb = new StringBuilder(); Throwable cur = e; while (cur != null) { if (sb.length() > 0) { sb.append(" <- "); } sb.append(cur.getClass().getSimpleName()).append(": ").append(cur.getMessage()); cur = cur.getCause(); if (cur == e) { break; } } return sb.toString(); } // ════════ JSON 解析工具(正则,不依赖外部库) ════════ private static final Pattern CODE_NUM = Pattern.compile("\"code\"\\s*:\\s*(\\d+)"); private static final Pattern BOOL_RE = Pattern.compile("\"%s\"\\s*:\\s*(true|false)"); private static final Pattern INT_RE = Pattern.compile("\"%s\"\\s*:\\s*(-?\\d+)"); private static final Pattern STR_RE = Pattern.compile("\"%s\"\\s*:\\s*\"((?:\\\\.|[^\"\\\\])*)\""); private static final Pattern STR_ALL_RE = Pattern.compile("\"%s\"\\s*:\\s*\"((?:\\\\.|[^\"\\\\])*)\""); private static int extractCode(String body) { if (body == null || body.isEmpty()) { return -1; } Matcher m = CODE_NUM.matcher(body); return m.find() ? Integer.parseInt(m.group(1)) : -1; } private static String extractStr(String body, String field) { if (body == null || body.isEmpty()) { return null; } Matcher m = Pattern.compile("\"" + Pattern.quote(field) + "\"\\s*:\\s*\"((?:\\\\.|[^\"\\\\])*)\"").matcher(body); if (m.find()) { return unescape(m.group(1)); } return null; } private static Boolean extractBool(String body, String field) { if (body == null || body.isEmpty()) { return null; } Matcher m = Pattern.compile("\"" + Pattern.quote(field) + "\"\\s*:\\s*(true|false)").matcher(body); return m.find() ? Boolean.parseBoolean(m.group(1)) : null; } private static Integer extractInt(String body, String field) { if (body == null || body.isEmpty()) { return null; } Matcher m = Pattern.compile("\"" + Pattern.quote(field) + "\"\\s*:\\s*(-?\\d+)").matcher(body); return m.find() ? Integer.parseInt(m.group(1)) : null; } private static List extractAllStr(String body, String field) { List out = new ArrayList<>(); if (body == null || body.isEmpty()) { return out; } Matcher m = Pattern.compile("\"" + Pattern.quote(field) + "\"\\s*:\\s*\"((?:\\\\.|[^\"\\\\])*)\"").matcher(body); while (m.find()) { out.add(unescape(m.group(1))); } return out; } private static int countMatches(String body, String regex) { if (body == null || body.isEmpty()) { return 0; } int c = 0; Matcher m = Pattern.compile(regex).matcher(body); while (m.find()) { c++; } return c; } private static String unescape(String s) { if (s == null) { return null; } if (s.indexOf('\\') < 0) { return s; } StringBuilder sb = new StringBuilder(s.length()); for (int i = 0; i < s.length(); i++) { char ch = s.charAt(i); if (ch == '\\' && i + 1 < s.length()) { char n = s.charAt(i + 1); switch (n) { case '"': sb.append('"'); i++; break; case '\\': sb.append('\\'); i++; break; case '/': sb.append('/'); i++; break; case 'n': sb.append('\n'); i++; break; case 'r': sb.append('\r'); i++; break; case 't': sb.append('\t'); i++; break; case 'u': if (i + 5 < s.length()) { try { sb.append((char) Integer.parseInt(s.substring(i + 2, i + 6), 16)); i += 5; } catch (NumberFormatException e) { sb.append(ch); } } else { sb.append(ch); } break; default: sb.append(ch); } } else { sb.append(ch); } } return sb.toString(); } // ════════ JSON 序列化工具 ════════ private static String jsonStr(String s) { if (s == null) { return "null"; } StringBuilder sb = new StringBuilder(s.length() + 8); sb.append('"'); for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); switch (c) { case '"': sb.append("\\\""); break; case '\\': sb.append("\\\\"); break; case '\n': sb.append("\\n"); break; case '\r': sb.append("\\r"); break; case '\t': sb.append("\\t"); break; default: if (c < 0x20) { sb.append(String.format("\\u%04x", (int) c)); } else { sb.append(c); } } } sb.append('"'); return sb.toString(); } private static String truncate(String s, int max) { if (s == null) { return ""; } return s.length() <= max ? s : s.substring(0, max) + "...[truncated]"; } // 内置行业兜底(与后端 LobsterIndustryCatalog 一致,共 28 个) private static List builtinIndustries() { return java.util.Arrays.asList( "liquor", "medical", "travel", "medical_beauty", "education", "insurance", "tea", "real_estate", "automobile", "finance", "retail", "catering", "fitness", "wedding", "home_decoration", "legal", "recruitment", "pet", "mother_baby", "pharmaceutical", "furniture", "logistics", "agriculture", "manufacturing", "beauty_salon", "photography", "saas_b2b", "general"); } // ════════ 报告生成 ════════ private static String buildReportJson(long totalMs) { StringBuilder sb = new StringBuilder(64 * 1024); sb.append("{\n"); sb.append(" \"reportName\": \"full-industry-validation-batch1\",\n"); sb.append(" \"generatedAt\": ") .append(jsonStr(LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME))).append(",\n"); sb.append(" \"backend\": ").append(jsonStr(BASE)).append(",\n"); sb.append(" \"apiBase\": ").append(jsonStr("/api/lobster/admin/validation")).append(",\n"); sb.append(" \"config\": {\n"); sb.append(" \"defaultTimeoutSec\": 30,\n"); sb.append(" \"semanticTimeoutSec\": ").append(SEMANTIC_TIMEOUT.getSeconds()).append(",\n"); sb.append(" \"sensitiveTimeoutSec\": ").append(SENSITIVE_TIMEOUT.getSeconds()).append(",\n"); sb.append(" \"semanticConcurrency\": ").append(SEMANTIC_CONCURRENCY).append(",\n"); sb.append(" \"sensitiveConcurrency\": ").append(SENSITIVE_CONCURRENCY).append(",\n"); sb.append(" \"note\": ") .append(jsonStr("默认请求超时30秒;语义分析为LLM调用(单次40-60s)、敏感词check每词触发多次LLM语义确认,故这两类超时放宽。敏感词check接口实际读取content字段(非message),已按content发送;命中判定以detectedWords非空(关键词级命中)为准,containsSensitive(LLM语义确认)作为附加信号。敏感词检测并发=2且含一次重试,避免后端500。")) .append("\n"); sb.append(" },\n"); // items sb.append(" \"items\": [\n"); for (int i = 0; i < ITEMS.size(); i++) { sb.append(itemToJson(ITEMS.get(i))); if (i < ITEMS.size() - 1) { sb.append(","); } sb.append("\n"); } sb.append(" ],\n"); // summary sb.append(summaryJson(totalMs)); sb.append("\n}\n"); return sb.toString(); } private static String itemToJson(Item it) { StringBuilder sb = new StringBuilder(); sb.append(" {"); sb.append("\"step\":").append(it.step).append(", "); sb.append("\"module\":").append(jsonStr(it.module)).append(", "); sb.append("\"industry\":").append(jsonStr(it.industry)).append(", "); sb.append("\"messageLabel\":").append(jsonStr(it.messageLabel)).append(", "); sb.append("\"sensitiveWord\":").append(jsonStr(it.sensitiveWord)).append(", "); sb.append("\"intent\":").append(jsonStr(it.intent)).append(", "); sb.append("\"containsSensitive\":").append(it.containsSensitive == null ? "null" : it.containsSensitive).append(", "); sb.append("\"input\":").append(jsonStr(it.input)).append(", "); sb.append("\"output\":").append(jsonStr(it.output)).append(", "); sb.append("\"elapsedMs\":").append(it.elapsedMs).append(", "); sb.append("\"passed\":").append(it.passed).append(", "); sb.append("\"error\":").append(jsonStr(it.error)).append(", "); sb.append("\"order\":").append(it.order); sb.append("}"); return sb.toString(); } private static String summaryJson(long totalMs) { int total = ITEMS.size(); int passed = 0; for (Item it : ITEMS) { if (it.passed) { passed++; } } int failed = total - passed; double passRate = total == 0 ? 0 : (passed * 100.0 / total); // 按模块汇总 Map byModule = new LinkedHashMap<>(); for (Item it : ITEMS) { int[] v = byModule.computeIfAbsent(it.module, k -> new int[2]); v[0]++; if (it.passed) { v[1]++; } } // 按步骤汇总 Map byStep = new LinkedHashMap<>(); for (Item it : ITEMS) { int[] v = byStep.computeIfAbsent(it.step, k -> new int[2]); v[0]++; if (it.passed) { v[1]++; } } StringBuilder sb = new StringBuilder(); sb.append(" \"summary\": {\n"); sb.append(" \"totalItems\": ").append(total).append(",\n"); sb.append(" \"passed\": ").append(passed).append(",\n"); sb.append(" \"failed\": ").append(failed).append(",\n"); sb.append(" \"passRate\": ").append(String.format(java.util.Locale.ROOT, "%.2f", passRate)).append(",\n"); sb.append(" \"totalElapsedMs\": ").append(totalMs).append(",\n"); sb.append(" \"byModule\": {\n"); int mi = 0; for (Map.Entry e : byModule.entrySet()) { sb.append(" ").append(jsonStr(e.getKey())).append(": {") .append("\"total\":").append(e.getValue()[0]).append(", ") .append("\"passed\":").append(e.getValue()[1]).append(", ") .append("\"failed\":").append(e.getValue()[0] - e.getValue()[1]).append("}"); if (mi < byModule.size() - 1) { sb.append(","); } sb.append("\n"); mi++; } sb.append(" },\n"); sb.append(" \"byStep\": {\n"); int si = 0; for (Map.Entry e : byStep.entrySet()) { sb.append(" \"step").append(e.getKey()).append("\": {") .append("\"total\":").append(e.getValue()[0]).append(", ") .append("\"passed\":").append(e.getValue()[1]).append(", ") .append("\"failed\":").append(e.getValue()[0] - e.getValue()[1]).append("}"); if (si < byStep.size() - 1) { sb.append(","); } sb.append("\n"); si++; } sb.append(" }\n"); sb.append(" }"); return sb.toString(); } // ════════ 数据结构 ════════ private static class Resp { final int statusCode; final String body; final long elapsedMs; final String error; Resp(int statusCode, String body, long elapsedMs, String error) { this.statusCode = statusCode; this.body = body; this.elapsedMs = elapsedMs; this.error = error; } } private static class Item { int step; String module; String industry; // 语义/行业相关项填写 String messageLabel; // 语义项:消息类别 String sensitiveWord; // 敏感词检测项:被测词 String intent; // 语义项:识别意图 Boolean containsSensitive; // 敏感词检测项:是否命中 String input; String output; long elapsedMs; boolean passed; String error; int order; } }