check-policy-sources.mjs 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. /**
  2. * 对比政策库的**两个来源**,确认「开发环境本地优先」这个开关还有没有必要。
  3. *
  4. * 背景(2026-09-17):
  5. * 政策详情面板的「政策名称」跳转地址由 `data.市级政策id` 拼出
  6. * (见 src/components/Chat/policy-library-utils.ts 的 buildPolicyDetailUrl)。
  7. * 但两个来源的字段并不一致:
  8. * - 本地 public/merchant-agent/policies_public.v1.json → 有 市级政策id
  9. * - 远端 VITE_DOWNLOAD_URL → 327 条全无该字段
  10. * 而 index.html 原本是**远端优先**,于是浏览器里取不到 id,「政策名称」是纯文本。
  11. * 为此加了 VITE_POLICY_LOCAL_FIRST(仅 .env.development 为 true)。
  12. *
  13. * 这个脚本干什么:
  14. * 把两个来源的条数与 市级政策id 覆盖率打出来,并判断那个开关是否仍然需要。
  15. *
  16. * 怎么跑(在项目根目录):
  17. * node harness/tools/check-policy-sources.mjs
  18. *
  19. * 退出码:0 = 与预期一致;1 = 情况变了,需要有人看一眼(见输出末尾的提示)
  20. */
  21. import { readFileSync } from 'node:fs';
  22. const LOCAL_PATH = 'public/merchant-agent/policies_public.v1.json';
  23. const ENV_PATH = '.env.development';
  24. /** 从 .env.development 读 VITE_DOWNLOAD_URL,避免把地址写死在脚本里 */
  25. const readDownloadUrl = () => {
  26. const env = readFileSync(ENV_PATH, 'utf8');
  27. const line = env.split(/\r?\n/).find((l) => l.trim().startsWith('VITE_DOWNLOAD_URL'));
  28. const raw = line?.split('=').slice(1).join('=').trim().replace(/^["']|["']$/g, '');
  29. return raw ? raw + 'merchant-agent/policies_public.v1.json' : '';
  30. };
  31. const toList = (json) => (Array.isArray(json) ? json : json?.data) || [];
  32. /** 统计 data.市级政策id 的覆盖率(buildPolicyDetailUrl 读的就是这个字段) */
  33. const countCityId = (list) => {
  34. const missing = [];
  35. let has = 0;
  36. for (const item of list) {
  37. const value = (item?.data || {})['市级政策id'];
  38. if (value && String(value).trim()) has++;
  39. else missing.push(item?.name || '(无名)');
  40. }
  41. return { has, total: list.length, missing };
  42. };
  43. const localList = toList(JSON.parse(readFileSync(LOCAL_PATH, 'utf8')));
  44. const localId = countCityId(localList);
  45. console.log('本地 ' + LOCAL_PATH);
  46. console.log(' 条数: ' + localList.length + ' | 市级政策id: ' + localId.has + '/' + localId.total);
  47. let remoteList = [];
  48. let remoteId = { has: 0, total: 0, missing: [] };
  49. let remoteError = '';
  50. const url = readDownloadUrl();
  51. console.log('\n远端 ' + (url || '(未在 ' + ENV_PATH + ' 找到 VITE_DOWNLOAD_URL)'));
  52. if (url) {
  53. try {
  54. const res = await fetch(url);
  55. if (!res.ok) throw new Error('HTTP ' + res.status);
  56. remoteList = toList(await res.json());
  57. remoteId = countCityId(remoteList);
  58. console.log(' 条数: ' + remoteList.length + ' | 市级政策id: ' + remoteId.has + '/' + remoteId.total);
  59. } catch (err) {
  60. remoteError = err.message;
  61. console.log(' 拉取失败: ' + remoteError + '(跳过远端对比)');
  62. }
  63. }
  64. const localOk = localId.has > 0;
  65. const remoteHasId = remoteId.has > 0;
  66. console.log('\n结论:');
  67. console.log(' 本地这份带 市级政策id:' + (localOk ? '是' : '否'));
  68. if (url && !remoteError) {
  69. console.log(' 远端这份带 市级政策id:' + (remoteHasId ? '是' : '否'));
  70. }
  71. let exitCode = 0;
  72. if (!localOk) {
  73. console.log(' ⚠️ 本地这份也没有 id 了 —— 「本地优先」这个开关失去意义,需重新找 id 来源');
  74. exitCode = 1;
  75. } else if (url && !remoteError && remoteHasId) {
  76. console.log(' ⚠️ 远端现在也带 id 了 —— 「开发环境本地优先」已无必要,可以去掉 VITE_POLICY_LOCAL_FIRST');
  77. exitCode = 1;
  78. } else {
  79. console.log(' ✅ 与预期一致:远端无 id,开发环境仍需本地优先');
  80. }
  81. if (remoteList.length && localList.length !== remoteList.length) {
  82. const localNames = new Set(localList.map((x) => x.name));
  83. const remoteNames = new Set(remoteList.map((x) => x.name));
  84. console.log(
  85. '\n条数差异(本地少 ' +
  86. (remoteList.length - localList.length) +
  87. ' 条):仅远端有的政策名 ' +
  88. [...remoteNames].filter((n) => !localNames.has(n)).length +
  89. ' 个,仅本地有的 ' +
  90. [...localNames].filter((n) => !remoteNames.has(n)).length +
  91. ' 个'
  92. );
  93. console.log(' ⚠️ 开发环境用本地这份意味着比远端少 ' + (remoteList.length - localList.length) + ' 条申报事项');
  94. }
  95. process.exit(exitCode);