| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215 |
- /**
- * **端到端**验证:真实 company_info → 真实分类接口 → 真实 DMS 两栏目。
- *
- * 与 `verify-company-classify-dms.mjs` 的分工:
- * - 那个脚本**注入假分类响应**,验证「拿到结果之后」的落库链路(不需要后端配合,可反复跑)
- * - 本脚本用**真实载荷**打真实分类接口,验证最后一公里(会真的调用 6 次 LLM、会真的写 DMS)
- *
- * 为什么载荷要从文件读、不内置:
- * 分类接口对**合成/精简的请求体一律返回 422**(2026-09-18 实测:文档里的示例片段、
- * 各种键名、`{"company":{}}` 全被拒;换成真实抓下来的 `company_info` 才 200)。
- * 所以这里不硬编码夹具,由调用方给一份**真实形态**的 company_info JSON。
- *
- * 怎么跑:
- * 1) npm run dev
- * 2) 把 /api/chat 的 result 事件里 data.company_info 对象存成 JSON 文件(整个对象,
- * 不含外层 company_info 键),例如 /tmp/company-info.json
- * 3) node harness/tools/verify-company-classify-e2e.mjs <company-info.json> [--cleanup]
- *
- * ⚠️ **默认不清理**:这份是真实企业数据,落库就是功能的正常行为(幂等,重复跑只更新)。
- * 加 `--cleanup` 会在验证后把该企业在本轮写下的行删掉(调试用)。
- */
- process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
- import { readFileSync } from "node:fs";
- const chatBase = process.env.CHAT_BASE || "https://localhost:8083/chat-api";
- const dmsBase = process.env.DMS_BASE || "https://localhost:8083/dms-api";
- globalThis.VITE_CHAT_API = chatBase;
- globalThis.VITE_DMS_API = dmsBase;
- const payloadPath = process.argv[2];
- const cleanup = process.argv.includes("--cleanup");
- if (!payloadPath) {
- console.error("用法:node verify-company-classify-e2e.mjs <company-info.json> [--cleanup]");
- process.exit(2);
- }
- const {
- classifyCompany,
- applyCompanyClassification,
- applyCompanyHonors,
- extractIdentityFromCompanyInfo,
- parseHonorRecords,
- readHonorsFromCompanyInfo,
- HONOR_NAME_FIELD,
- ANGLE_TO_FIELD,
- DMS_COLUMN_ENTERPRISE,
- DMS_COLUMN_HONOR,
- deleteDmsContent,
- searchDmsContents,
- } = await import("./_company-classify.mjs");
- let pass = 0;
- let fail = 0;
- const check = (name, ok, extra = "") => {
- if (ok) {
- pass++;
- console.log(` ok ${name}`);
- } else {
- fail++;
- console.log(` FAIL ${name}${extra === "" ? "" : ` → ${JSON.stringify(extra)}`}`);
- }
- };
- const companyInfo = JSON.parse(readFileSync(payloadPath, "utf8"));
- const identity = extractIdentityFromCompanyInfo(companyInfo);
- const creditCode = identity?.creditCode;
- console.log(`载荷:${payloadPath}`);
- console.log(`企业:${identity?.name || "?"} / ${creditCode || "?"}\n`);
- if (!creditCode) {
- console.error("载荷里没有统一社会信用代码,无法继续");
- process.exit(2);
- }
- const findEnterprise = () =>
- searchDmsContents(DMS_COLUMN_ENTERPRISE, {
- search: [{ field: "c_credit_code", searchType: 1, content: { value: creditCode } }],
- page: 0,
- pageSize: 10,
- });
- const findHonors = () =>
- searchDmsContents(DMS_COLUMN_HONOR, {
- search: [{ field: "c_credit_code", searchType: 1, content: { value: creditCode } }],
- page: 0,
- pageSize: 200,
- });
- /* ---------------------------------------------------------------- *
- * ① 真实分类接口
- * ---------------------------------------------------------------- */
- console.log("【1】真实分类接口(会调用 6 次 LLM,稍慢)");
- const before = Date.now();
- const classification = await classifyCompany(companyInfo);
- console.log(` (耗时 ${((Date.now() - before) / 1000).toFixed(1)}s)`);
- check("接口返回了结果(非 null)", !!classification);
- check(
- "返回的是 completed/partial(不是 failed)",
- classification?.status === "completed" || classification?.status === "partial",
- classification?.status
- );
- check("credit_code 与载荷一致", classification?.credit_code === creditCode, classification?.credit_code);
- const cats = classification?.categories || {};
- const angles = Object.keys(cats);
- check("六个角度键齐全", angles.length === 6, angles);
- check(
- "每个角度都是数组",
- angles.every((k) => Array.isArray(cats[k])),
- angles.map((k) => `${k}:${Array.isArray(cats[k])}`)
- );
- console.log(` 分类结果:${JSON.stringify(cats)}`);
- /* ---------------------------------------------------------------- *
- * ② 落库(真实 DMS)
- * ---------------------------------------------------------------- */
- console.log("\n【2】落库到 DMS 1888 / 1886");
- await applyCompanyClassification(classification, identity);
- const rows = await findEnterprise();
- check("1888 有且只有一行", rows.length === 1, rows.length);
- const row = rows[0] || {};
- check("企业名/信用代码/法人已写入", row.c_name === identity.name && row.c_credit_code === creditCode && row.c_oper_name === identity.legalRep, {
- c_name: row.c_name,
- c_credit_code: row.c_credit_code,
- c_oper_name: row.c_oper_name,
- });
- /**
- * 库里的标签字段有两种合法形态:
- * - 有标签 → JSON 数组字符串(如 `["第三产业","软件信息"]`)
- * - 无标签 → **字段不存在**(DMS 不落空值;`[]` 也不会被写下去)
- * 所以断言要按「解码后的集合」比,不能按字符串比。
- */
- const decodeTags = (value) => {
- if (typeof value !== "string" || !value.trim()) return [];
- try {
- const parsed = JSON.parse(value);
- return Array.isArray(parsed) ? parsed : [];
- } catch {
- return [];
- }
- };
- const sameSet = (a, b) => a.length === b.length && b.every((x) => a.includes(x));
- for (const angle of angles) {
- const field = ANGLE_TO_FIELD[angle];
- const expectedTags = cats[angle] || [];
- const storedTags = decodeTags(row[field]);
- check(
- `1888 ${angle} → ${field}${expectedTags.length === 0 ? "(空集:字段不落库,语义等同)" : ""}`,
- sameSet(storedTags, expectedTags),
- { stored: row[field], storedTags, expectedTags }
- );
- }
- /* ---------------------------------------------------------------- *
- * ②b 荣誉(数据源 = company_info.honors.records)
- * ---------------------------------------------------------------- */
- console.log("\n【2b】荣誉对齐到 1886(数据源 = company_info.honors.records)");
- const honorParse = parseHonorRecords(readHonorsFromCompanyInfo(companyInfo));
- console.log(` 载荷荣誉记录 ${honorParse.records.length} 条(complete=${honorParse.complete},丢弃 ${honorParse.malformed} 条)`);
- await applyCompanyHonors(readHonorsFromCompanyInfo(companyInfo), identity);
- const honors = await findHonors();
- check("1886 行数 = 载荷荣誉条数", honors.length === honorParse.records.length, {
- rows: honors.length,
- records: honorParse.records.length,
- });
- check(
- "每行都有荣誉名(title 与 c_honor)",
- honors.every((r) => typeof r[HONOR_NAME_FIELD] === "string" && r[HONOR_NAME_FIELD] && r.title === r[HONOR_NAME_FIELD]),
- honors.slice(0, 3).map((r) => ({ title: r.title, tag: r[HONOR_NAME_FIELD] }))
- );
- check(
- "级别/来源已写入",
- honors.every((r) => typeof r.c_level === "string" && typeof r.c_source === "string"),
- honors.slice(0, 3).map((r) => ({ level: r.c_level, source: r.c_source }))
- );
- const sampleRecord = honorParse.records[0];
- check(
- "抽样比对:载荷第一条能在库里按「荣誉名+级别+来源」找到",
- !sampleRecord ||
- honors.some((r) => r[HONOR_NAME_FIELD] === sampleRecord.name && r.c_level === sampleRecord.level && r.c_source === sampleRecord.source),
- sampleRecord
- );
- /* ---------------------------------------------------------------- *
- * ③ 幂等:再落一次
- * ---------------------------------------------------------------- */
- console.log("\n【3】同样结果再落一次 → 零新增");
- await applyCompanyClassification(classification, identity);
- await applyCompanyHonors(readHonorsFromCompanyInfo(companyInfo), identity);
- const rows2 = await findEnterprise();
- check("1888 仍是一行、uuid 未变", rows2.length === 1 && rows2[0]?.id === row.id, rows2.length);
- const honors2 = await findHonors();
- check("1886 行数未变", honors2.length === honors.length, { before: honors.length, after: honors2.length });
- check(
- "1886 行 uuid 集合未变(没有删了又加)",
- JSON.stringify(honors2.map((r) => r.id).sort()) === JSON.stringify(honors.map((r) => r.id).sort())
- );
- /* ---------------------------------------------------------------- *
- * ④ 可选清理
- * ---------------------------------------------------------------- */
- if (cleanup) {
- console.log("\n【4】--cleanup:删除本轮写入的行");
- for (const r of await findEnterprise()) if (typeof r.id === "string") await deleteDmsContent(DMS_COLUMN_ENTERPRISE, r.id);
- for (const r of await findHonors()) if (typeof r.id === "string") await deleteDmsContent(DMS_COLUMN_HONOR, r.id);
- check("1888 残留 0 行", (await findEnterprise()).length === 0);
- check("1886 残留 0 行", (await findHonors()).length === 0);
- } else {
- console.log("\n【4】未加 --cleanup:数据保留(真实企业数据,功能正常行为)");
- }
- console.log(`\n===== 通过 ${pass} 项,失败 ${fail} 项 =====`);
- process.exit(fail ? 1 : 0);
|