|
@@ -0,0 +1,186 @@
|
|
|
|
|
+/**
|
|
|
|
|
+ * **端到端**验证:真实 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,
|
|
|
|
|
+ extractIdentityFromCompanyInfo,
|
|
|
|
|
+ HONOR_SYNC_SOURCE,
|
|
|
|
|
+ HONOR_TAG_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 }
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const honors = await findHonors();
|
|
|
|
|
+const owned = honors.filter((r) => r.c_source === HONOR_SYNC_SOURCE);
|
|
|
|
|
+check("1886 我方行数 = 资质荣誉标签数", owned.length === (cats["资质荣誉"] || []).length, {
|
|
|
|
|
+ owned: owned.length,
|
|
|
|
|
+ tags: cats["资质荣誉"],
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+/* ---------------------------------------------------------------- *
|
|
|
|
|
+ * ③ 幂等:再落一次
|
|
|
|
|
+ * ---------------------------------------------------------------- */
|
|
|
|
|
+console.log("\n【3】同样结果再落一次 → 零新增");
|
|
|
|
|
+await applyCompanyClassification(classification, identity);
|
|
|
|
|
+const rows2 = await findEnterprise();
|
|
|
|
|
+check("1888 仍是一行、uuid 未变", rows2.length === 1 && rows2[0]?.id === row.id, rows2.length);
|
|
|
|
|
+check(
|
|
|
|
|
+ "1886 行数未变",
|
|
|
|
|
+ (await findHonors()).filter((r) => r.c_source === HONOR_SYNC_SOURCE).length === owned.length
|
|
|
|
|
+);
|
|
|
|
|
+
|
|
|
|
|
+/* ---------------------------------------------------------------- *
|
|
|
|
|
+ * ④ 可选清理
|
|
|
|
|
+ * ---------------------------------------------------------------- */
|
|
|
|
|
+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);
|