FullIndustryValidation.java 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921
  1. import java.net.URI;
  2. import java.net.http.HttpClient;
  3. import java.net.http.HttpRequest;
  4. import java.net.http.HttpResponse;
  5. import java.nio.charset.StandardCharsets;
  6. import java.nio.file.Files;
  7. import java.nio.file.Path;
  8. import java.nio.file.Paths;
  9. import java.time.Duration;
  10. import java.time.LocalDateTime;
  11. import java.time.format.DateTimeFormatter;
  12. import java.util.ArrayList;
  13. import java.util.Collections;
  14. import java.util.LinkedHashMap;
  15. import java.util.List;
  16. import java.util.Map;
  17. import java.util.concurrent.ExecutorService;
  18. import java.util.concurrent.Executors;
  19. import java.util.concurrent.Future;
  20. import java.util.concurrent.TimeUnit;
  21. import java.util.concurrent.atomic.AtomicInteger;
  22. import java.util.regex.Matcher;
  23. import java.util.regex.Pattern;
  24. /**
  25. * 龙虾引擎全行业验证 - 第一批基础验证
  26. *
  27. * 通过 HTTP 调用 /api/lobster/admin/validation/** 完成基础验证:
  28. * 第1步:基础信息获取(health / industries / node-capabilities)
  29. * 第2步:语义意图识别(全行业 × 3条典型消息)
  30. * 第3步:敏感词全量验证(list + 逐词 check)
  31. * 第4步:Token 统计(daily / model)
  32. * 第5步:进化指标(evolution / learning)
  33. *
  34. * 技术要求:
  35. * - Java 11+ HttpClient(java.net.http.HttpClient)
  36. * - JSON 使用字符串拼接 + 正则解析,不依赖外部库
  37. * - 默认请求超时 30 秒;语义/敏感词检测为 LLM/缓存预热场景,超时放宽至 150s/60s
  38. *
  39. * 编译运行:
  40. * javac -encoding UTF-8 FullIndustryValidation.java
  41. * java -cp . FullIndustryValidation
  42. *
  43. * 输出:reports/full-industry-validation-batch1.json
  44. */
  45. public class FullIndustryValidation {
  46. // ════════ 配置 ════════
  47. private static final String BASE = "http://localhost:8006";
  48. private static final String API = BASE + "/api/lobster/admin/validation";
  49. private static final String REPORT_DIR = "reports";
  50. private static final String REPORT_FILE = "reports/full-industry-validation-batch1.json";
  51. private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(15);
  52. private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(30);
  53. private static final Duration SEMANTIC_TIMEOUT = Duration.ofSeconds(150);
  54. private static final Duration SENSITIVE_TIMEOUT = Duration.ofSeconds(90);
  55. /** 语义分析并行度(LLM 调用,单次 40~60s;命中缓存/关键词快路径时毫秒级返回) */
  56. private static final int SEMANTIC_CONCURRENCY = 8;
  57. /** 敏感词检测并行度(后端每个 check 会触发多次 LLM 语义确认;高并发会返回 500,故低并发) */
  58. private static final int SENSITIVE_CONCURRENCY = 2;
  59. // 每个行业测试的 3 条典型消息
  60. private static final String[] TEST_MESSAGES = {
  61. "想了解一下你们的服务",
  62. "这个多少钱,怎么购买",
  63. "不需要了,谢谢"
  64. };
  65. private static final String[] MESSAGE_LABELS = {
  66. "咨询类", "购买意向", "拒绝类"
  67. };
  68. private static final HttpClient HTTP = HttpClient.newBuilder()
  69. .connectTimeout(CONNECT_TIMEOUT)
  70. .build();
  71. // 全部验证项(线程安全)
  72. private static final List<Item> ITEMS = Collections.synchronizedList(new ArrayList<>());
  73. // 从第1步解析出的行业代码
  74. private static volatile List<String> INDUSTRY_CODES = new ArrayList<>();
  75. // ════════ 入口 ════════
  76. public static void main(String[] args) throws Exception {
  77. System.out.println("=== 龙虾引擎全行业验证 - 第一批基础验证 ===");
  78. System.out.println("后端服务: " + BASE);
  79. System.out.println("API 基础路径: /api/lobster/admin/validation");
  80. long startAll = System.currentTimeMillis();
  81. // 第1步:基础信息获取
  82. List<Item> step1 = step1BasicInfo();
  83. ITEMS.addAll(step1);
  84. // 第2步:语义意图识别(依赖第1步解析的行业列表;若解析为空则使用内置 28 行业兜底)
  85. List<String> codes = INDUSTRY_CODES;
  86. if (codes == null || codes.isEmpty()) {
  87. codes = builtinIndustries();
  88. System.out.println(" [WARN] 未能从 /industries 解析出行业列表,使用内置 " + codes.size() + " 个行业兜底。");
  89. }
  90. List<Item> step2 = step2Semantic(codes);
  91. ITEMS.addAll(step2);
  92. // 第3步:敏感词全量验证
  93. List<Item> step3 = step3Sensitive();
  94. ITEMS.addAll(step3);
  95. // 第4步:Token 统计
  96. List<Item> step4 = step4TokenStats();
  97. ITEMS.addAll(step4);
  98. // 第5步:进化指标
  99. List<Item> step5 = step5Evolution();
  100. ITEMS.addAll(step5);
  101. long totalMs = System.currentTimeMillis() - startAll;
  102. // 生成报告
  103. Files.createDirectories(Paths.get(REPORT_DIR));
  104. String json = buildReportJson(totalMs);
  105. Path out = Paths.get(REPORT_FILE).toAbsolutePath();
  106. Files.write(out, json.getBytes(StandardCharsets.UTF_8));
  107. System.out.println("\n=== 验证完成 ===");
  108. System.out.println("总耗时: " + totalMs + " ms (" + (totalMs / 1000) + " s)");
  109. System.out.println("报告文件: " + out);
  110. // 控制台汇总
  111. int total = ITEMS.size();
  112. int passed = 0;
  113. for (Item it : ITEMS) {
  114. if (it.passed) {
  115. passed++;
  116. }
  117. }
  118. System.out.println("验证项总数: " + total + ",通过: " + passed + ",失败: " + (total - passed)
  119. + ",通过率: " + String.format("%.1f%%", total == 0 ? 0 : (passed * 100.0 / total)));
  120. }
  121. // ════════ 第1步:基础信息获取 ════════
  122. private static List<Item> step1BasicInfo() {
  123. System.out.println("\n--- 第1步:基础信息获取 ---");
  124. List<Item> list = new ArrayList<>();
  125. // 1.1 health
  126. {
  127. Resp r = httpGet("/health");
  128. Item it = new Item();
  129. it.step = 1;
  130. it.module = "health";
  131. it.input = "GET /health";
  132. it.output = truncate(r.body, 3000);
  133. it.elapsedMs = r.elapsedMs;
  134. if (r.error != null) {
  135. it.passed = false;
  136. it.error = r.error;
  137. } else {
  138. int code = extractCode(r.body);
  139. Boolean healthy = extractBool(r.body, "healthy");
  140. it.passed = (r.statusCode == 200 && code == 200 && Boolean.TRUE.equals(healthy));
  141. if (!it.passed) {
  142. it.error = "http=" + r.statusCode + " code=" + code + " healthy=" + healthy;
  143. }
  144. }
  145. list.add(it);
  146. System.out.println(" [health] passed=" + it.passed + " elapsed=" + it.elapsedMs + "ms"
  147. + (it.error != null ? " err=" + it.error : ""));
  148. }
  149. // 1.2 industries
  150. {
  151. Resp r = httpGet("/industries");
  152. Item it = new Item();
  153. it.step = 1;
  154. it.module = "industries";
  155. it.input = "GET /industries";
  156. it.output = truncate(r.body, 4000);
  157. it.elapsedMs = r.elapsedMs;
  158. if (r.error != null) {
  159. it.passed = false;
  160. it.error = r.error;
  161. } else {
  162. int code = extractCode(r.body);
  163. Integer total = extractInt(r.body, "total");
  164. List<String> codes = extractAllStr(r.body, "code");
  165. INDUSTRY_CODES = codes;
  166. it.passed = (r.statusCode == 200 && code == 200 && total != null && total >= 27);
  167. if (!it.passed) {
  168. it.error = "http=" + r.statusCode + " code=" + code + " total=" + total;
  169. }
  170. }
  171. list.add(it);
  172. System.out.println(" [industries] passed=" + it.passed + " total="
  173. + extractInt(r.body, "total") + " codes=" + INDUSTRY_CODES.size()
  174. + " elapsed=" + it.elapsedMs + "ms");
  175. }
  176. // 1.3 node-capabilities
  177. {
  178. Resp r = httpGet("/node-capabilities");
  179. Item it = new Item();
  180. it.step = 1;
  181. it.module = "node-capabilities";
  182. it.input = "GET /node-capabilities";
  183. it.output = truncate(r.body, 4000);
  184. it.elapsedMs = r.elapsedMs;
  185. if (r.error != null) {
  186. it.passed = false;
  187. it.error = r.error;
  188. } else {
  189. int code = extractCode(r.body);
  190. // capabilities 数组项里含 "maturityStars" 字段,统计其出现次数作为能力数量
  191. int capCount = countMatches(r.body, "\"maturityStars\"\\s*:");
  192. it.passed = (r.statusCode == 200 && code == 200 && capCount > 0);
  193. if (!it.passed) {
  194. it.error = "http=" + r.statusCode + " code=" + code + " capabilities=" + capCount;
  195. }
  196. }
  197. list.add(it);
  198. System.out.println(" [node-capabilities] passed=" + it.passed
  199. + " capabilities=" + countMatches(r.body, "\"maturityStars\"\\s*:")
  200. + " elapsed=" + it.elapsedMs + "ms");
  201. }
  202. return list;
  203. }
  204. // ════════ 第2步:语义意图识别(全行业,并行) ════════
  205. private static List<Item> step2Semantic(List<String> codes) throws Exception {
  206. int total = codes.size() * TEST_MESSAGES.length;
  207. System.out.println("\n--- 第2步:语义意图识别(全行业 " + codes.size()
  208. + " × " + TEST_MESSAGES.length + " 条 = " + total + " 次调用,并行度=" + SEMANTIC_CONCURRENCY + ") ---");
  209. ExecutorService pool = Executors.newFixedThreadPool(SEMANTIC_CONCURRENCY);
  210. List<Future<Item>> futures = new ArrayList<>();
  211. AtomicInteger orderSeq = new AtomicInteger(0);
  212. long t0 = System.currentTimeMillis();
  213. for (String code : codes) {
  214. for (int i = 0; i < TEST_MESSAGES.length; i++) {
  215. final String c = code;
  216. final int mi = i;
  217. final int order = orderSeq.getAndIncrement();
  218. futures.add(pool.submit(() -> runSemantic(c, mi, order)));
  219. }
  220. }
  221. pool.shutdown();
  222. List<Item> list = new ArrayList<>();
  223. int done = 0;
  224. for (Future<Item> f : futures) {
  225. try {
  226. Item it = f.get(SEMANTIC_TIMEOUT.getSeconds() + 30, TimeUnit.SECONDS);
  227. list.add(it);
  228. } catch (Exception e) {
  229. Item it = new Item();
  230. it.step = 2;
  231. it.module = "semantic";
  232. it.passed = false;
  233. it.error = "future-get-error: " + e;
  234. list.add(it);
  235. }
  236. done++;
  237. if (done % 12 == 0 || done == futures.size()) {
  238. long elapsed = System.currentTimeMillis() - t0;
  239. System.out.println(" 语义进度: " + done + "/" + futures.size()
  240. + " 累计耗时=" + (elapsed / 1000) + "s");
  241. }
  242. }
  243. pool.awaitTermination(5, TimeUnit.MINUTES);
  244. // 按 order 排序,保证报告顺序稳定
  245. list.sort((a, b) -> Integer.compare(a.order, b.order));
  246. int pass = 0;
  247. for (Item it : list) {
  248. if (it.passed) {
  249. pass++;
  250. }
  251. }
  252. System.out.println(" 语义完成: " + pass + "/" + list.size() + " 通过");
  253. return list;
  254. }
  255. private static Item runSemantic(String industry, int msgIdx, int order) {
  256. String message = TEST_MESSAGES[msgIdx];
  257. String label = MESSAGE_LABELS[msgIdx];
  258. String body = "{\"message\":" + jsonStr(message)
  259. + ",\"industryType\":" + jsonStr(industry)
  260. + ",\"context\":{}}";
  261. Resp r = httpPost("/semantic/analyze", body, SEMANTIC_TIMEOUT);
  262. Item it = new Item();
  263. it.step = 2;
  264. it.module = "semantic";
  265. it.industry = industry;
  266. it.messageLabel = label;
  267. it.order = order;
  268. it.input = body;
  269. it.output = truncate(r.body, 2500);
  270. it.elapsedMs = r.elapsedMs;
  271. if (r.error != null) {
  272. it.passed = false;
  273. it.error = r.error;
  274. } else {
  275. int code = extractCode(r.body);
  276. String intent = extractStr(r.body, "intent");
  277. it.intent = intent;
  278. it.passed = (r.statusCode == 200 && code == 200 && intent != null && !intent.isEmpty());
  279. if (!it.passed) {
  280. it.error = "http=" + r.statusCode + " code=" + code + " intent=" + intent;
  281. }
  282. }
  283. return it;
  284. }
  285. // ════════ 第3步:敏感词全量验证 ════════
  286. private static List<Item> step3Sensitive() throws Exception {
  287. System.out.println("\n--- 第3步:敏感词全量验证 ---");
  288. List<Item> list = new ArrayList<>();
  289. // 3.1 获取所有敏感词
  290. Resp r = httpGet("/sensitive/list");
  291. Item listIt = new Item();
  292. listIt.step = 3;
  293. listIt.module = "sensitive-list";
  294. listIt.input = "GET /sensitive/list";
  295. listIt.output = truncate(r.body, 4000);
  296. listIt.elapsedMs = r.elapsedMs;
  297. List<String> words = new ArrayList<>();
  298. if (r.error != null) {
  299. listIt.passed = false;
  300. listIt.error = r.error;
  301. } else {
  302. int code = extractCode(r.body);
  303. words = extractAllStr(r.body, "word");
  304. listIt.passed = (r.statusCode == 200 && code == 200 && !words.isEmpty());
  305. if (!listIt.passed) {
  306. listIt.error = "http=" + r.statusCode + " code=" + code + " words=" + words.size();
  307. }
  308. }
  309. list.add(listIt);
  310. System.out.println(" [sensitive-list] passed=" + listIt.passed
  311. + " words=" + words.size() + " elapsed=" + listIt.elapsedMs + "ms");
  312. // 3.2 逐词 check(并行)
  313. if (!words.isEmpty()) {
  314. System.out.println(" 开始逐词检测 " + words.size() + " 个敏感词,并行度=" + SENSITIVE_CONCURRENCY + " ...");
  315. ExecutorService pool = Executors.newFixedThreadPool(SENSITIVE_CONCURRENCY);
  316. List<Future<Item>> futures = new ArrayList<>();
  317. int order = 0;
  318. for (String w : words) {
  319. final String word = w;
  320. futures.add(pool.submit(() -> runSensitiveCheck(word)));
  321. }
  322. pool.shutdown();
  323. long t0 = System.currentTimeMillis();
  324. int done = 0;
  325. for (Future<Item> f : futures) {
  326. try {
  327. list.add(f.get(SENSITIVE_TIMEOUT.getSeconds() + 30, TimeUnit.SECONDS));
  328. } catch (Exception e) {
  329. Item it = new Item();
  330. it.step = 3;
  331. it.module = "sensitive-check";
  332. it.passed = false;
  333. it.error = "future-get-error: " + e;
  334. list.add(it);
  335. }
  336. done++;
  337. if (done % 30 == 0 || done == futures.size()) {
  338. long el = System.currentTimeMillis() - t0;
  339. System.out.println(" 敏感词进度: " + done + "/" + futures.size()
  340. + " 累计耗时=" + (el / 1000) + "s");
  341. }
  342. }
  343. pool.awaitTermination(5, TimeUnit.MINUTES);
  344. }
  345. int pass = 0;
  346. for (Item it : list) {
  347. if (it.passed) {
  348. pass++;
  349. }
  350. }
  351. System.out.println(" 敏感词完成: " + pass + "/" + list.size() + " 通过");
  352. return list;
  353. }
  354. private static Item runSensitiveCheck(String word) {
  355. // API 实际读取 content 字段(controller: body.get("content"));
  356. // 同时附带 message 字段以对齐任务示例。
  357. String body = "{\"content\":" + jsonStr(word)
  358. + ",\"message\":" + jsonStr(word)
  359. + ",\"industryType\":\"general\",\"companyId\":1}";
  360. // 首次失败(500/超时)则重试一次,缓解后端偶发错误
  361. Resp r = httpPost("/sensitive/check", body, SENSITIVE_TIMEOUT);
  362. String retryNote = null;
  363. if (isBadResp(r)) {
  364. Resp r2 = httpPost("/sensitive/check", body, SENSITIVE_TIMEOUT);
  365. retryNote = "retried; first=" + briefResp(r);
  366. r = r2;
  367. }
  368. Item it = new Item();
  369. it.step = 3;
  370. it.module = "sensitive-check";
  371. it.sensitiveWord = word;
  372. it.input = body;
  373. it.output = truncate(r.body, 2500);
  374. it.elapsedMs = r.elapsedMs;
  375. if (r.error != null) {
  376. it.passed = false;
  377. it.error = (retryNote != null ? retryNote + "; " : "") + r.error;
  378. } else {
  379. int code = extractCode(r.body);
  380. Boolean hit = extractBool(r.body, "containsSensitive");
  381. it.containsSensitive = hit;
  382. // 命中以 detectedWords 非空为准(关键词级命中,最可靠);
  383. // containsSensitive 为 LLM 语义确认结果,作为附加信号记录。
  384. boolean detected = r.body != null && r.body.contains("\"detectedWords\":[{");
  385. it.passed = (r.statusCode == 200 && code == 200 && detected);
  386. if (!it.passed) {
  387. it.error = (retryNote != null ? retryNote + "; " : "")
  388. + "http=" + r.statusCode + " code=" + code
  389. + " detected=" + detected + " containsSensitive=" + hit;
  390. }
  391. }
  392. return it;
  393. }
  394. private static boolean isBadResp(Resp r) {
  395. if (r.error != null) {
  396. return true;
  397. }
  398. int code = extractCode(r.body);
  399. return r.statusCode != 200 || code == 500;
  400. }
  401. private static String briefResp(Resp r) {
  402. if (r.error != null) {
  403. return "err:" + r.error;
  404. }
  405. return "http=" + r.statusCode + " code=" + extractCode(r.body);
  406. }
  407. // ════════ 第4步:Token 统计 ════════
  408. private static List<Item> step4TokenStats() {
  409. System.out.println("\n--- 第4步:Token 统计 ---");
  410. List<Item> list = new ArrayList<>();
  411. // 4.1 daily
  412. {
  413. Resp r = httpGet("/token-stats/daily");
  414. Item it = new Item();
  415. it.step = 4;
  416. it.module = "token-stats-daily";
  417. it.input = "GET /token-stats/daily";
  418. it.output = truncate(r.body, 4000);
  419. it.elapsedMs = r.elapsedMs;
  420. if (r.error != null) {
  421. it.passed = false;
  422. it.error = r.error;
  423. } else {
  424. int code = extractCode(r.body);
  425. it.passed = (r.statusCode == 200 && code == 200);
  426. if (!it.passed) {
  427. it.error = "http=" + r.statusCode + " code=" + code;
  428. }
  429. }
  430. list.add(it);
  431. System.out.println(" [token-stats/daily] passed=" + it.passed + " elapsed=" + it.elapsedMs + "ms");
  432. }
  433. // 4.2 model
  434. {
  435. Resp r = httpGet("/token-stats/model");
  436. Item it = new Item();
  437. it.step = 4;
  438. it.module = "token-stats-model";
  439. it.input = "GET /token-stats/model";
  440. it.output = truncate(r.body, 4000);
  441. it.elapsedMs = r.elapsedMs;
  442. if (r.error != null) {
  443. it.passed = false;
  444. it.error = r.error;
  445. } else {
  446. int code = extractCode(r.body);
  447. it.passed = (r.statusCode == 200 && code == 200);
  448. if (!it.passed) {
  449. it.error = "http=" + r.statusCode + " code=" + code;
  450. }
  451. }
  452. list.add(it);
  453. System.out.println(" [token-stats/model] passed=" + it.passed + " elapsed=" + it.elapsedMs + "ms");
  454. }
  455. return list;
  456. }
  457. // ════════ 第5步:进化指标 ════════
  458. private static List<Item> step5Evolution() {
  459. System.out.println("\n--- 第5步:进化指标 ---");
  460. List<Item> list = new ArrayList<>();
  461. // 5.1 evolution/metrics
  462. {
  463. Resp r = httpGet("/evolution/metrics");
  464. Item it = new Item();
  465. it.step = 5;
  466. it.module = "evolution-metrics";
  467. it.input = "GET /evolution/metrics";
  468. it.output = truncate(r.body, 4000);
  469. it.elapsedMs = r.elapsedMs;
  470. if (r.error != null) {
  471. it.passed = false;
  472. it.error = r.error;
  473. } else {
  474. int code = extractCode(r.body);
  475. it.passed = (r.statusCode == 200 && code == 200);
  476. if (!it.passed) {
  477. it.error = "http=" + r.statusCode + " code=" + code;
  478. }
  479. }
  480. list.add(it);
  481. System.out.println(" [evolution/metrics] passed=" + it.passed + " elapsed=" + it.elapsedMs + "ms");
  482. }
  483. // 5.2 learning/metrics
  484. {
  485. Resp r = httpGet("/learning/metrics");
  486. Item it = new Item();
  487. it.step = 5;
  488. it.module = "learning-metrics";
  489. it.input = "GET /learning/metrics";
  490. it.output = truncate(r.body, 4000);
  491. it.elapsedMs = r.elapsedMs;
  492. if (r.error != null) {
  493. it.passed = false;
  494. it.error = r.error;
  495. } else {
  496. int code = extractCode(r.body);
  497. it.passed = (r.statusCode == 200 && code == 200);
  498. if (!it.passed) {
  499. it.error = "http=" + r.statusCode + " code=" + code;
  500. }
  501. }
  502. list.add(it);
  503. System.out.println(" [learning/metrics] passed=" + it.passed + " elapsed=" + it.elapsedMs + "ms");
  504. }
  505. return list;
  506. }
  507. // ════════ HTTP 工具 ════════
  508. private static Resp httpGet(String path) {
  509. return httpGet(path, DEFAULT_TIMEOUT);
  510. }
  511. private static Resp httpGet(String path, Duration timeout) {
  512. long t0 = System.currentTimeMillis();
  513. try {
  514. HttpRequest req = HttpRequest.newBuilder()
  515. .uri(URI.create(API + path))
  516. .timeout(timeout)
  517. .GET()
  518. .build();
  519. HttpResponse<String> resp = HTTP.send(req,
  520. HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
  521. long elapsed = System.currentTimeMillis() - t0;
  522. return new Resp(resp.statusCode(), resp.body(), elapsed, null);
  523. } catch (Exception e) {
  524. return new Resp(0, "", System.currentTimeMillis() - t0, unwrap(e));
  525. }
  526. }
  527. private static Resp httpPost(String path, String body, Duration timeout) {
  528. long t0 = System.currentTimeMillis();
  529. try {
  530. HttpRequest req = HttpRequest.newBuilder()
  531. .uri(URI.create(API + path))
  532. .timeout(timeout)
  533. .header("Content-Type", "application/json; charset=utf-8")
  534. .POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8))
  535. .build();
  536. HttpResponse<String> resp = HTTP.send(req,
  537. HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
  538. long elapsed = System.currentTimeMillis() - t0;
  539. return new Resp(resp.statusCode(), resp.body(), elapsed, null);
  540. } catch (Exception e) {
  541. return new Resp(0, "", System.currentTimeMillis() - t0, unwrap(e));
  542. }
  543. }
  544. private static String unwrap(Throwable e) {
  545. StringBuilder sb = new StringBuilder();
  546. Throwable cur = e;
  547. while (cur != null) {
  548. if (sb.length() > 0) {
  549. sb.append(" <- ");
  550. }
  551. sb.append(cur.getClass().getSimpleName()).append(": ").append(cur.getMessage());
  552. cur = cur.getCause();
  553. if (cur == e) {
  554. break;
  555. }
  556. }
  557. return sb.toString();
  558. }
  559. // ════════ JSON 解析工具(正则,不依赖外部库) ════════
  560. private static final Pattern CODE_NUM = Pattern.compile("\"code\"\\s*:\\s*(\\d+)");
  561. private static final Pattern BOOL_RE = Pattern.compile("\"%s\"\\s*:\\s*(true|false)");
  562. private static final Pattern INT_RE = Pattern.compile("\"%s\"\\s*:\\s*(-?\\d+)");
  563. private static final Pattern STR_RE = Pattern.compile("\"%s\"\\s*:\\s*\"((?:\\\\.|[^\"\\\\])*)\"");
  564. private static final Pattern STR_ALL_RE = Pattern.compile("\"%s\"\\s*:\\s*\"((?:\\\\.|[^\"\\\\])*)\"");
  565. private static int extractCode(String body) {
  566. if (body == null || body.isEmpty()) {
  567. return -1;
  568. }
  569. Matcher m = CODE_NUM.matcher(body);
  570. return m.find() ? Integer.parseInt(m.group(1)) : -1;
  571. }
  572. private static String extractStr(String body, String field) {
  573. if (body == null || body.isEmpty()) {
  574. return null;
  575. }
  576. Matcher m = Pattern.compile("\"" + Pattern.quote(field) + "\"\\s*:\\s*\"((?:\\\\.|[^\"\\\\])*)\"").matcher(body);
  577. if (m.find()) {
  578. return unescape(m.group(1));
  579. }
  580. return null;
  581. }
  582. private static Boolean extractBool(String body, String field) {
  583. if (body == null || body.isEmpty()) {
  584. return null;
  585. }
  586. Matcher m = Pattern.compile("\"" + Pattern.quote(field) + "\"\\s*:\\s*(true|false)").matcher(body);
  587. return m.find() ? Boolean.parseBoolean(m.group(1)) : null;
  588. }
  589. private static Integer extractInt(String body, String field) {
  590. if (body == null || body.isEmpty()) {
  591. return null;
  592. }
  593. Matcher m = Pattern.compile("\"" + Pattern.quote(field) + "\"\\s*:\\s*(-?\\d+)").matcher(body);
  594. return m.find() ? Integer.parseInt(m.group(1)) : null;
  595. }
  596. private static List<String> extractAllStr(String body, String field) {
  597. List<String> out = new ArrayList<>();
  598. if (body == null || body.isEmpty()) {
  599. return out;
  600. }
  601. Matcher m = Pattern.compile("\"" + Pattern.quote(field) + "\"\\s*:\\s*\"((?:\\\\.|[^\"\\\\])*)\"").matcher(body);
  602. while (m.find()) {
  603. out.add(unescape(m.group(1)));
  604. }
  605. return out;
  606. }
  607. private static int countMatches(String body, String regex) {
  608. if (body == null || body.isEmpty()) {
  609. return 0;
  610. }
  611. int c = 0;
  612. Matcher m = Pattern.compile(regex).matcher(body);
  613. while (m.find()) {
  614. c++;
  615. }
  616. return c;
  617. }
  618. private static String unescape(String s) {
  619. if (s == null) {
  620. return null;
  621. }
  622. if (s.indexOf('\\') < 0) {
  623. return s;
  624. }
  625. StringBuilder sb = new StringBuilder(s.length());
  626. for (int i = 0; i < s.length(); i++) {
  627. char ch = s.charAt(i);
  628. if (ch == '\\' && i + 1 < s.length()) {
  629. char n = s.charAt(i + 1);
  630. switch (n) {
  631. case '"': sb.append('"'); i++; break;
  632. case '\\': sb.append('\\'); i++; break;
  633. case '/': sb.append('/'); i++; break;
  634. case 'n': sb.append('\n'); i++; break;
  635. case 'r': sb.append('\r'); i++; break;
  636. case 't': sb.append('\t'); i++; break;
  637. case 'u':
  638. if (i + 5 < s.length()) {
  639. try {
  640. sb.append((char) Integer.parseInt(s.substring(i + 2, i + 6), 16));
  641. i += 5;
  642. } catch (NumberFormatException e) {
  643. sb.append(ch);
  644. }
  645. } else {
  646. sb.append(ch);
  647. }
  648. break;
  649. default: sb.append(ch);
  650. }
  651. } else {
  652. sb.append(ch);
  653. }
  654. }
  655. return sb.toString();
  656. }
  657. // ════════ JSON 序列化工具 ════════
  658. private static String jsonStr(String s) {
  659. if (s == null) {
  660. return "null";
  661. }
  662. StringBuilder sb = new StringBuilder(s.length() + 8);
  663. sb.append('"');
  664. for (int i = 0; i < s.length(); i++) {
  665. char c = s.charAt(i);
  666. switch (c) {
  667. case '"': sb.append("\\\""); break;
  668. case '\\': sb.append("\\\\"); break;
  669. case '\n': sb.append("\\n"); break;
  670. case '\r': sb.append("\\r"); break;
  671. case '\t': sb.append("\\t"); break;
  672. default:
  673. if (c < 0x20) {
  674. sb.append(String.format("\\u%04x", (int) c));
  675. } else {
  676. sb.append(c);
  677. }
  678. }
  679. }
  680. sb.append('"');
  681. return sb.toString();
  682. }
  683. private static String truncate(String s, int max) {
  684. if (s == null) {
  685. return "";
  686. }
  687. return s.length() <= max ? s : s.substring(0, max) + "...[truncated]";
  688. }
  689. // 内置行业兜底(与后端 LobsterIndustryCatalog 一致,共 28 个)
  690. private static List<String> builtinIndustries() {
  691. return java.util.Arrays.asList(
  692. "liquor", "medical", "travel", "medical_beauty", "education", "insurance",
  693. "tea", "real_estate", "automobile", "finance", "retail", "catering",
  694. "fitness", "wedding", "home_decoration", "legal", "recruitment", "pet",
  695. "mother_baby", "pharmaceutical", "furniture", "logistics", "agriculture",
  696. "manufacturing", "beauty_salon", "photography", "saas_b2b", "general");
  697. }
  698. // ════════ 报告生成 ════════
  699. private static String buildReportJson(long totalMs) {
  700. StringBuilder sb = new StringBuilder(64 * 1024);
  701. sb.append("{\n");
  702. sb.append(" \"reportName\": \"full-industry-validation-batch1\",\n");
  703. sb.append(" \"generatedAt\": ")
  704. .append(jsonStr(LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME))).append(",\n");
  705. sb.append(" \"backend\": ").append(jsonStr(BASE)).append(",\n");
  706. sb.append(" \"apiBase\": ").append(jsonStr("/api/lobster/admin/validation")).append(",\n");
  707. sb.append(" \"config\": {\n");
  708. sb.append(" \"defaultTimeoutSec\": 30,\n");
  709. sb.append(" \"semanticTimeoutSec\": ").append(SEMANTIC_TIMEOUT.getSeconds()).append(",\n");
  710. sb.append(" \"sensitiveTimeoutSec\": ").append(SENSITIVE_TIMEOUT.getSeconds()).append(",\n");
  711. sb.append(" \"semanticConcurrency\": ").append(SEMANTIC_CONCURRENCY).append(",\n");
  712. sb.append(" \"sensitiveConcurrency\": ").append(SENSITIVE_CONCURRENCY).append(",\n");
  713. sb.append(" \"note\": ")
  714. .append(jsonStr("默认请求超时30秒;语义分析为LLM调用(单次40-60s)、敏感词check每词触发多次LLM语义确认,故这两类超时放宽。敏感词check接口实际读取content字段(非message),已按content发送;命中判定以detectedWords非空(关键词级命中)为准,containsSensitive(LLM语义确认)作为附加信号。敏感词检测并发=2且含一次重试,避免后端500。"))
  715. .append("\n");
  716. sb.append(" },\n");
  717. // items
  718. sb.append(" \"items\": [\n");
  719. for (int i = 0; i < ITEMS.size(); i++) {
  720. sb.append(itemToJson(ITEMS.get(i)));
  721. if (i < ITEMS.size() - 1) {
  722. sb.append(",");
  723. }
  724. sb.append("\n");
  725. }
  726. sb.append(" ],\n");
  727. // summary
  728. sb.append(summaryJson(totalMs));
  729. sb.append("\n}\n");
  730. return sb.toString();
  731. }
  732. private static String itemToJson(Item it) {
  733. StringBuilder sb = new StringBuilder();
  734. sb.append(" {");
  735. sb.append("\"step\":").append(it.step).append(", ");
  736. sb.append("\"module\":").append(jsonStr(it.module)).append(", ");
  737. sb.append("\"industry\":").append(jsonStr(it.industry)).append(", ");
  738. sb.append("\"messageLabel\":").append(jsonStr(it.messageLabel)).append(", ");
  739. sb.append("\"sensitiveWord\":").append(jsonStr(it.sensitiveWord)).append(", ");
  740. sb.append("\"intent\":").append(jsonStr(it.intent)).append(", ");
  741. sb.append("\"containsSensitive\":").append(it.containsSensitive == null ? "null" : it.containsSensitive).append(", ");
  742. sb.append("\"input\":").append(jsonStr(it.input)).append(", ");
  743. sb.append("\"output\":").append(jsonStr(it.output)).append(", ");
  744. sb.append("\"elapsedMs\":").append(it.elapsedMs).append(", ");
  745. sb.append("\"passed\":").append(it.passed).append(", ");
  746. sb.append("\"error\":").append(jsonStr(it.error)).append(", ");
  747. sb.append("\"order\":").append(it.order);
  748. sb.append("}");
  749. return sb.toString();
  750. }
  751. private static String summaryJson(long totalMs) {
  752. int total = ITEMS.size();
  753. int passed = 0;
  754. for (Item it : ITEMS) {
  755. if (it.passed) {
  756. passed++;
  757. }
  758. }
  759. int failed = total - passed;
  760. double passRate = total == 0 ? 0 : (passed * 100.0 / total);
  761. // 按模块汇总
  762. Map<String, int[]> byModule = new LinkedHashMap<>();
  763. for (Item it : ITEMS) {
  764. int[] v = byModule.computeIfAbsent(it.module, k -> new int[2]);
  765. v[0]++;
  766. if (it.passed) {
  767. v[1]++;
  768. }
  769. }
  770. // 按步骤汇总
  771. Map<Integer, int[]> byStep = new LinkedHashMap<>();
  772. for (Item it : ITEMS) {
  773. int[] v = byStep.computeIfAbsent(it.step, k -> new int[2]);
  774. v[0]++;
  775. if (it.passed) {
  776. v[1]++;
  777. }
  778. }
  779. StringBuilder sb = new StringBuilder();
  780. sb.append(" \"summary\": {\n");
  781. sb.append(" \"totalItems\": ").append(total).append(",\n");
  782. sb.append(" \"passed\": ").append(passed).append(",\n");
  783. sb.append(" \"failed\": ").append(failed).append(",\n");
  784. sb.append(" \"passRate\": ").append(String.format(java.util.Locale.ROOT, "%.2f", passRate)).append(",\n");
  785. sb.append(" \"totalElapsedMs\": ").append(totalMs).append(",\n");
  786. sb.append(" \"byModule\": {\n");
  787. int mi = 0;
  788. for (Map.Entry<String, int[]> e : byModule.entrySet()) {
  789. sb.append(" ").append(jsonStr(e.getKey())).append(": {")
  790. .append("\"total\":").append(e.getValue()[0]).append(", ")
  791. .append("\"passed\":").append(e.getValue()[1]).append(", ")
  792. .append("\"failed\":").append(e.getValue()[0] - e.getValue()[1]).append("}");
  793. if (mi < byModule.size() - 1) {
  794. sb.append(",");
  795. }
  796. sb.append("\n");
  797. mi++;
  798. }
  799. sb.append(" },\n");
  800. sb.append(" \"byStep\": {\n");
  801. int si = 0;
  802. for (Map.Entry<Integer, int[]> e : byStep.entrySet()) {
  803. sb.append(" \"step").append(e.getKey()).append("\": {")
  804. .append("\"total\":").append(e.getValue()[0]).append(", ")
  805. .append("\"passed\":").append(e.getValue()[1]).append(", ")
  806. .append("\"failed\":").append(e.getValue()[0] - e.getValue()[1]).append("}");
  807. if (si < byStep.size() - 1) {
  808. sb.append(",");
  809. }
  810. sb.append("\n");
  811. si++;
  812. }
  813. sb.append(" }\n");
  814. sb.append(" }");
  815. return sb.toString();
  816. }
  817. // ════════ 数据结构 ════════
  818. private static class Resp {
  819. final int statusCode;
  820. final String body;
  821. final long elapsedMs;
  822. final String error;
  823. Resp(int statusCode, String body, long elapsedMs, String error) {
  824. this.statusCode = statusCode;
  825. this.body = body;
  826. this.elapsedMs = elapsedMs;
  827. this.error = error;
  828. }
  829. }
  830. private static class Item {
  831. int step;
  832. String module;
  833. String industry; // 语义/行业相关项填写
  834. String messageLabel; // 语义项:消息类别
  835. String sensitiveWord; // 敏感词检测项:被测词
  836. String intent; // 语义项:识别意图
  837. Boolean containsSensitive; // 敏感词检测项:是否命中
  838. String input;
  839. String output;
  840. long elapsedMs;
  841. boolean passed;
  842. String error;
  843. int order;
  844. }
  845. }