digest.js 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. 'use strict';
  2. /**
  3. * 智能问数 · 晨报(确定性产出,零模型)
  4. * ----------------------------------------------------------------------------
  5. * 每日自动同步完成后,对当天实体确定性跑一遍 数据体检 + 层间核对 + 环比对比,
  6. * 组装成带可信度标签的晨报落盘(data/agent/<entity>/digests.json,cap 30),
  7. * 问数页顶部横幅展示。无钉钉推送——推送通道后续按需接。
  8. * 确定性口径与问数工具同源(直接复用工具 handler),数字不许两套算法。
  9. */
  10. const fs = require('fs');
  11. const path = require('path');
  12. const trace = require('./trace.js');
  13. const nlQuery = require('../../nl-query.js');
  14. const lake = require('./tools/lake.js');
  15. const cross = require('./tools/cross.js');
  16. const compare = require('./tools/compare.js');
  17. function digestsFile(entityKey) {
  18. return path.join(trace.AGENT_DIR, trace.sanitizeKey(entityKey), 'digests.json');
  19. }
  20. function loadDigests(entityKey) {
  21. try { return JSON.parse(fs.readFileSync(digestsFile(entityKey), 'utf8')).digests || []; }
  22. catch { return []; }
  23. }
  24. function saveDigests(entityKey, digests) {
  25. const file = digestsFile(entityKey);
  26. fs.mkdirSync(path.dirname(file), { recursive: true });
  27. const temp = `${file}.${process.pid}.tmp`;
  28. fs.writeFileSync(temp, JSON.stringify({ digests }), 'utf8');
  29. fs.renameSync(temp, file);
  30. }
  31. function buildAndSaveDigest(entity, svc) {
  32. const key = trace.sanitizeKey(entity);
  33. const meta = svc.store.getEntity(key) || {};
  34. const dateRange = nlQuery.lakeDateRange(key, svc);
  35. const ctx = { svc, entityKey: key, meta, shops: meta.shops || [], dateRange };
  36. const sections = [];
  37. let verdicts = [];
  38. const runSection = (name, fn) => {
  39. try {
  40. const out = fn();
  41. sections.push({ tool: name, ok: true, summary: String(out.summary || '').slice(0, 1200) });
  42. if (out.data && Array.isArray(out.data.verdicts)) {
  43. verdicts = verdicts.concat(out.data.verdicts.map((v) => ({ ...v, tool: name })));
  44. }
  45. return out;
  46. } catch (e) {
  47. sections.push({ tool: name, ok: false, summary: String(e.message || e).slice(0, 300) });
  48. return null;
  49. }
  50. };
  51. const quality = runSection('data_quality', () => lake.qualityHandler(ctx, lake.qualitySanitize({}, ctx)));
  52. runSection('cross_check', () => cross.crossHandler(ctx, cross.crossSanitize({}, ctx)));
  53. const cmp = runSection('compare_periods', () => compare.compareHandler(ctx, compare.compareSanitize({}, ctx)));
  54. // 头条:环比一句话 + 最差校验结论
  55. let headline;
  56. const worst = verdicts.reduce((acc, v) => (v.status === 'fail' ? 'fail' : (v.status === 'warn' && acc !== 'fail' ? 'warn' : acc)), 'ok');
  57. if (!dateRange || svc.store.datesInLake(key).length === 0) {
  58. headline = '湖内暂无数据,待同步后再出晨报';
  59. } else if (cmp && cmp.data && !cmp.data.empty) {
  60. const gross = (cmp.data.deltas || []).find((d) => d.key === 'grossAmt');
  61. if (gross) {
  62. const pct = gross.deltaPct == null ? '' : `,环比 ${gross.deltaPct >= 0 ? '+' : ''}${(gross.deltaPct * 100).toFixed(1)}%`;
  63. headline = `近 7 天营业额 ${Math.round(gross.cur).toLocaleString('zh-CN')} 元${pct}(可比口径)`;
  64. } else {
  65. headline = '近 7 天经营数据已就绪';
  66. }
  67. } else {
  68. headline = '经营数据已就绪';
  69. }
  70. if (worst === 'fail') headline = `⚠ ${headline};数据体检发现问题,详见下方校验`;
  71. else if (worst === 'warn') headline = `${headline};部分校验有警告`;
  72. const digest = {
  73. id: trace.newId('dg'),
  74. entity: key,
  75. entityName: meta.name || key,
  76. at: new Date().toISOString(),
  77. status: worst,
  78. headline,
  79. sections,
  80. verdicts,
  81. reportUrl: `/api/reports/report-html?${new URLSearchParams({ entity: key, from: dateRange.from, to: dateRange.to }).toString()}`,
  82. };
  83. const digests = loadDigests(key);
  84. digests.unshift(digest);
  85. if (digests.length > 30) digests.length = 30;
  86. saveDigests(key, digests);
  87. return digest;
  88. }
  89. module.exports = { buildAndSaveDigest, loadDigests };