| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921 |
- 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<Item> ITEMS = Collections.synchronizedList(new ArrayList<>());
- // 从第1步解析出的行业代码
- private static volatile List<String> 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<Item> step1 = step1BasicInfo();
- ITEMS.addAll(step1);
- // 第2步:语义意图识别(依赖第1步解析的行业列表;若解析为空则使用内置 28 行业兜底)
- List<String> codes = INDUSTRY_CODES;
- if (codes == null || codes.isEmpty()) {
- codes = builtinIndustries();
- System.out.println(" [WARN] 未能从 /industries 解析出行业列表,使用内置 " + codes.size() + " 个行业兜底。");
- }
- List<Item> step2 = step2Semantic(codes);
- ITEMS.addAll(step2);
- // 第3步:敏感词全量验证
- List<Item> step3 = step3Sensitive();
- ITEMS.addAll(step3);
- // 第4步:Token 统计
- List<Item> step4 = step4TokenStats();
- ITEMS.addAll(step4);
- // 第5步:进化指标
- List<Item> 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<Item> step1BasicInfo() {
- System.out.println("\n--- 第1步:基础信息获取 ---");
- List<Item> 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<String> 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<Item> step2Semantic(List<String> 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<Future<Item>> 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<Item> list = new ArrayList<>();
- int done = 0;
- for (Future<Item> 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<Item> step3Sensitive() throws Exception {
- System.out.println("\n--- 第3步:敏感词全量验证 ---");
- List<Item> 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<String> 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<Future<Item>> 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<Item> 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<Item> step4TokenStats() {
- System.out.println("\n--- 第4步:Token 统计 ---");
- List<Item> 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<Item> step5Evolution() {
- System.out.println("\n--- 第5步:进化指标 ---");
- List<Item> 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<String> 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<String> 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<String> extractAllStr(String body, String field) {
- List<String> 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<String> 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<String, int[]> 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<Integer, int[]> 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<String, int[]> 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<Integer, int[]> 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;
- }
- }
|