/** * 审计:`company_info` 里的字段,有多少真的落进了 DMS 的企业两栏目。 * * 为什么要有这个脚本:`company_info`(企查查原始结构)字段很多,而 1888 的列 * 是照着它建的(47 列里大部分是工商信息),**但我们的同步只写其中一小部分**。 * 字段一多,靠记忆或靠文档核对必然漏;这个脚本把三边对齐: * ① 真实载荷里实际有哪些字段(`company / profile.data / profile.source / honors`) * ② DMS 模型里实际有哪些列(**实时** `getModelById`,不用记录值) * ③ 我们的代码实际写哪些列(在脚本里显式列出,与 src 对齐) * * 怎么跑(需要 dev server 起着,代理注入 DMS token): * node harness/tools/audit-company-info-coverage.mjs [dms 代理地址] * * 载荷怎么来:把 `/api/chat` 的 `result` 事件里 `data.company_info` 对象存成 JSON 文件 * (整个对象,不含外层 company_info 键)。注意用 node 写文件,别用 shell 里的 curl -d 传中文。 */ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; import { readFileSync } from 'node:fs'; const payloadPath = process.argv[2]; const dmsBase = process.argv[3] || process.env.DMS_BASE || 'https://localhost:8083/dms-api'; if (!payloadPath) { console.error('用法:node audit-company-info-coverage.mjs [dms 代理地址]'); process.exit(2); } globalThis.VITE_DMS_API = dmsBase; // ⚠️ 注意取的是 **modelId**(2036/2032),不是 columnId(1888/1886)—— // 传错会把别的模型拉回来(踩过:1888 当 modelId 用,拉回来一个带 c_milvus_chunk_ids 的知识库模型) const { DMS_MODEL_ENTERPRISE, DMS_MODEL_HONOR, PROFILE_FIELD_MAP, } = await import('./_company-classify.mjs'); /* ------------------------------------------------------------------ * * 我们的代码实际写入的列(与 src 对齐,改代码时记得同步这里) * 来源:src/network/api/dms/company-info-sync.ts 与 classification-sync-utils.ts * ------------------------------------------------------------------ */ const WE_WRITE_1888 = [ // 工商信息:**直接从代码的映射表派生**,避免这份清单与 src 漂移 ...Object.values(PROFILE_FIELD_MAP), 'c_created_at', // 仅新增时 // 六个分类角度标签(JSON 数组字符串) 'c_tag_basic', 'c_tag_honor', 'c_tag_sector', 'c_tag_operation', 'c_tag_industry', 'c_tag_license', ]; const WE_WRITE_1886 = [ 'c_id', // 只传 0(must 字段,DMS 自动填) 'c_credit_code', 'c_name', 'c_honor', // 荣誉名(并写系统字段 title) 'c_level', 'c_source', 'c_publish_office', 'c_publish_date', 'c_beging_date', 'c_dead_line', 'c_certificate_code', 'c_created_at', ]; /** company_info 字段 → 1888 列(语义对应;null = DMS 没有这一列) */ const MAP_PROFILE_DATA = { Name: 'c_name', CreditCode: 'c_credit_code', OperName: 'c_oper_name', BelongOrg: 'c_belong_org', OperId: 'c_oper_id', EntType: 'c_ent_type', EconKind: 'c_econ_kind', Status: 'c_status', Province: 'c_province', AreaCode: 'c_area_code', Address: 'c_address', StartDate: 'c_start_date', EndDate: 'c_end_date', TermStart: 'c_term_start', TermEnd: 'c_term_end', CheckDate: 'c_check_date', UpdatedDate: 'c_updated_date', RegistCapi: 'c_regist_capi', RegisteredCapital: 'c_registered_capital', RegisteredCapitalUnit: 'c_registered_capital_unit', RegisteredCapitalCCY: 'c_registered_capital_ccy', RecCap: 'c_rec_cap', PaidUpCapital: 'c_paid_up_capital', PaidUpCapitalUnit: 'c_paid_up_capital_unit', PaidUpCapitalCCY: 'c_paid_up_capital_ccy', IsOnStock: 'c_is_on_stock', StockNumber: 'c_stock_number', StockType: 'c_stock_type', OrgNo: 'c_org_no', No: 'c_no', ImageUrl: 'c_image_url', Scope: 'c_scope', DesignatedRepresentativeList: 'c_designated_representative_list', OriginalName: 'c_original_name', RevokeInfo: 'c_revoke_info', Area: 'c_area', }; const MAP_COMPANY = { KeyNo: null, Name: 'c_name', CreditCode: 'c_credit_code', StartDate: 'c_start_date', OperName: 'c_oper_name', Status: 'c_status', No: 'c_no', Address: 'c_address', }; const MAP_HONOR_RECORD = { Name: 'c_honor(并写系统 title)', Level: 'c_level', Source: 'c_source', PublishOffice: 'c_publish_office', PublishDate: 'c_publish_date', BegingDate: 'c_beging_date', DeadLine: 'c_dead_line', CertificateCode: 'c_certificate_code', }; /* ------------------------------------------------------------------ * * 读输入 * ------------------------------------------------------------------ */ const payload = JSON.parse(readFileSync(payloadPath, 'utf8')); const fetchModelFields = async (modelId, expectAlias) => { const form = new URLSearchParams({ modelId: String(modelId) }).toString(); const res = await fetch(`${dmsBase}/model/getModelById`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: form, }); const json = await res.json().catch(() => null); if (!json || json.code !== 200) { throw new Error(`取模型 ${modelId} 失败:${json ? `${json.code} ${json.message}` : res.status}`); } // 防呆:modelId 传错时 DMS 不会报错、只会返回另一个模型,所以校验别名 if (json.content.modelAlias !== expectAlias) { throw new Error( `模型 ${modelId} 的别名是「${json.content.modelAlias}」,期望「${expectAlias}」——modelId 传错了吧?` ); } const list = JSON.parse(json.content.fieldList); return Object.values(list) .filter((f) => f.name) // 个别条目 name 为空 .sort((a, b) => a.sequence - b.sequence); }; const fields1888 = await fetchModelFields(DMS_MODEL_ENTERPRISE, '企业基础信息'); const fields1886 = await fetchModelFields(DMS_MODEL_HONOR, '企业荣誉信息'); const cols1888 = fields1888.map((f) => f.name); const cols1886 = fields1886.map((f) => f.name); const out = []; const line = (s = '') => out.push(s); line(`载荷:${payloadPath}`); line(`DMS 模型:1888 企业基础信息 ${cols1888.length} 字段 / 1886 企业荣誉信息 ${cols1886.length} 字段`); line(); line('===== 载荷结构 ====='); line(`顶层键:${Object.keys(payload).join(', ')}`); line(`company:${Object.keys(payload.company || {}).join(', ')}`); const profileData = payload.profile?.data || {}; line(`profile.data(${Object.keys(profileData).length}):${Object.keys(profileData).join(', ')}`); line(`profile.source:${Object.keys(payload.profile?.source || {}).join(', ')}`); const honorRecordData = payload.honors?.records?.[0]?.data || {}; line(`honors:${Object.keys(payload.honors || {}).join(', ')}`); line(`honors.records[].data:${Object.keys(honorRecordData).join(', ')}`); line(`honors.sources[]:${Object.keys(payload.honors?.sources?.[0] || {}).join(', ')}`); line(); line('===== ① profile.data → 1888:已写 / 未写 ====='); const wrote = []; const notWrote = []; const noColumn = []; for (const key of Object.keys(profileData)) { const col = MAP_PROFILE_DATA[key]; if (col === undefined) noColumn.push(key); else if (col === null) noColumn.push(key); else if (WE_WRITE_1888.includes(col)) wrote.push([key, col]); else notWrote.push([key, col]); } line(`【已写入 DMS】${wrote.length} 个:`); for (const [k, c] of wrote) line(` profile.data.${k.padEnd(30)} → ${c}`); line(`【未写入(DMS 里已有现成的列,只是同步没写)】${notWrote.length} 个:`); for (const [k, c] of notWrote) line(` profile.data.${k.padEnd(30)} → ${c}`); line(`【DMS 没有对应列】${noColumn.length} 个:${noColumn.join(', ') || '(无)'}`); line(); line('===== ② company 段 ====='); for (const key of Object.keys(payload.company || {})) { const col = MAP_COMPANY[key]; const state = col === null || col === undefined ? '**DMS 无对应列**' : WE_WRITE_1888.includes(col) ? `已写(${col},走 profile 同名字段)` : `未写(${col})`; line(` company.${key.padEnd(16)} → ${state}`); } line(); line('===== ③ honors → 1886 ====='); line('honors.records[].data:'); for (const [k, c] of Object.entries(MAP_HONOR_RECORD)) { const ok = c.startsWith('c_honor') ? WE_WRITE_1886.includes('c_honor') : WE_WRITE_1886.includes(c); line(` ${k.padEnd(16)} → ${c} ${ok ? '【已写】' : '【未写】'}`); } line(` honors.complete / error、honors.sources[](${Object.keys(payload.honors?.sources?.[0] || {}).join('/')}):**不写**(只用于判断能否删除)`); line(); line('===== ④ profile.source ====='); line(` ${Object.keys(payload.profile?.source || {}).join(', ')}`); line(' → **全部不写**:1888 没有对应列,属溯源元信息(荣誉那边的 c_source 是"来源通知名",不是这个)'); line(); line('===== ⑤ 汇总:1888 的 47 列里,我们在写哪些 ====='); const unused1888 = cols1888.filter((c) => !WE_WRITE_1888.includes(c)); line(` 在写 ${WE_WRITE_1888.length} 列`); line(` 没写 ${unused1888.length} 列:${unused1888.join(', ')}`); line(); line('===== ⑥ 汇总:1886 的 13 列 ====='); const unused1886 = cols1886.filter((c) => !WE_WRITE_1886.includes(c)); line(` 在写 ${WE_WRITE_1886.length} 列`); line(` 没写 ${unused1886.length} 列:${unused1886.join(', ') || '(无)'}`); const report = out.join('\n'); console.log(report); if (process.env.WRITE_REPORT) { const { writeFileSync } = await import('node:fs'); writeFileSync(process.env.WRITE_REPORT, report, 'utf8'); console.log(`\n(已写入 ${process.env.WRITE_REPORT})`); }