/** * 对比政策库的**两个来源**,确认「开发环境本地优先」这个开关还有没有必要。 * * 背景(2026-09-17): * 政策详情面板的「政策名称」跳转地址由 `data.市级政策id` 拼出 * (见 src/components/Chat/policy-library-utils.ts 的 buildPolicyDetailUrl)。 * 但两个来源的字段并不一致: * - 本地 public/merchant-agent/policies_public.v1.json → 有 市级政策id * - 远端 VITE_DOWNLOAD_URL → 327 条全无该字段 * 而 index.html 原本是**远端优先**,于是浏览器里取不到 id,「政策名称」是纯文本。 * 为此加了 VITE_POLICY_LOCAL_FIRST(仅 .env.development 为 true)。 * * 这个脚本干什么: * 把两个来源的条数与 市级政策id 覆盖率打出来,并判断那个开关是否仍然需要。 * * 怎么跑(在项目根目录): * node harness/tools/check-policy-sources.mjs * * 退出码:0 = 与预期一致;1 = 情况变了,需要有人看一眼(见输出末尾的提示) */ import { readFileSync } from 'node:fs'; const LOCAL_PATH = 'public/merchant-agent/policies_public.v1.json'; const ENV_PATH = '.env.development'; /** 从 .env.development 读 VITE_DOWNLOAD_URL,避免把地址写死在脚本里 */ const readDownloadUrl = () => { const env = readFileSync(ENV_PATH, 'utf8'); const line = env.split(/\r?\n/).find((l) => l.trim().startsWith('VITE_DOWNLOAD_URL')); const raw = line?.split('=').slice(1).join('=').trim().replace(/^["']|["']$/g, ''); return raw ? raw + 'merchant-agent/policies_public.v1.json' : ''; }; const toList = (json) => (Array.isArray(json) ? json : json?.data) || []; /** 统计 data.市级政策id 的覆盖率(buildPolicyDetailUrl 读的就是这个字段) */ const countCityId = (list) => { const missing = []; let has = 0; for (const item of list) { const value = (item?.data || {})['市级政策id']; if (value && String(value).trim()) has++; else missing.push(item?.name || '(无名)'); } return { has, total: list.length, missing }; }; const localList = toList(JSON.parse(readFileSync(LOCAL_PATH, 'utf8'))); const localId = countCityId(localList); console.log('本地 ' + LOCAL_PATH); console.log(' 条数: ' + localList.length + ' | 市级政策id: ' + localId.has + '/' + localId.total); let remoteList = []; let remoteId = { has: 0, total: 0, missing: [] }; let remoteError = ''; const url = readDownloadUrl(); console.log('\n远端 ' + (url || '(未在 ' + ENV_PATH + ' 找到 VITE_DOWNLOAD_URL)')); if (url) { try { const res = await fetch(url); if (!res.ok) throw new Error('HTTP ' + res.status); remoteList = toList(await res.json()); remoteId = countCityId(remoteList); console.log(' 条数: ' + remoteList.length + ' | 市级政策id: ' + remoteId.has + '/' + remoteId.total); } catch (err) { remoteError = err.message; console.log(' 拉取失败: ' + remoteError + '(跳过远端对比)'); } } const localOk = localId.has > 0; const remoteHasId = remoteId.has > 0; console.log('\n结论:'); console.log(' 本地这份带 市级政策id:' + (localOk ? '是' : '否')); if (url && !remoteError) { console.log(' 远端这份带 市级政策id:' + (remoteHasId ? '是' : '否')); } let exitCode = 0; if (!localOk) { console.log(' ⚠️ 本地这份也没有 id 了 —— 「本地优先」这个开关失去意义,需重新找 id 来源'); exitCode = 1; } else if (url && !remoteError && remoteHasId) { console.log(' ⚠️ 远端现在也带 id 了 —— 「开发环境本地优先」已无必要,可以去掉 VITE_POLICY_LOCAL_FIRST'); exitCode = 1; } else { console.log(' ✅ 与预期一致:远端无 id,开发环境仍需本地优先'); } if (remoteList.length && localList.length !== remoteList.length) { const localNames = new Set(localList.map((x) => x.name)); const remoteNames = new Set(remoteList.map((x) => x.name)); console.log( '\n条数差异(本地少 ' + (remoteList.length - localList.length) + ' 条):仅远端有的政策名 ' + [...remoteNames].filter((n) => !localNames.has(n)).length + ' 个,仅本地有的 ' + [...localNames].filter((n) => !remoteNames.has(n)).length + ' 个' ); console.log(' ⚠️ 开发环境用本地这份意味着比远端少 ' + (remoteList.length - localList.length) + ' 条申报事项'); } process.exit(exitCode);