'use strict'; /** * 智能问数 · 晨报(确定性产出,零模型) * ---------------------------------------------------------------------------- * 每日自动同步完成后,对当天实体确定性跑一遍 数据体检 + 层间核对 + 环比对比, * 组装成带可信度标签的晨报落盘(data/agent//digests.json,cap 30), * 问数页顶部横幅展示。无钉钉推送——推送通道后续按需接。 * 确定性口径与问数工具同源(直接复用工具 handler),数字不许两套算法。 */ const fs = require('fs'); const path = require('path'); const trace = require('./trace.js'); const nlQuery = require('../../nl-query.js'); const lake = require('./tools/lake.js'); const cross = require('./tools/cross.js'); const compare = require('./tools/compare.js'); function digestsFile(entityKey) { return path.join(trace.AGENT_DIR, trace.sanitizeKey(entityKey), 'digests.json'); } function loadDigests(entityKey) { try { return JSON.parse(fs.readFileSync(digestsFile(entityKey), 'utf8')).digests || []; } catch { return []; } } function saveDigests(entityKey, digests) { const file = digestsFile(entityKey); fs.mkdirSync(path.dirname(file), { recursive: true }); const temp = `${file}.${process.pid}.tmp`; fs.writeFileSync(temp, JSON.stringify({ digests }), 'utf8'); fs.renameSync(temp, file); } function buildAndSaveDigest(entity, svc) { const key = trace.sanitizeKey(entity); const meta = svc.store.getEntity(key) || {}; const dateRange = nlQuery.lakeDateRange(key, svc); const ctx = { svc, entityKey: key, meta, shops: meta.shops || [], dateRange }; const sections = []; let verdicts = []; const runSection = (name, fn) => { try { const out = fn(); sections.push({ tool: name, ok: true, summary: String(out.summary || '').slice(0, 1200) }); if (out.data && Array.isArray(out.data.verdicts)) { verdicts = verdicts.concat(out.data.verdicts.map((v) => ({ ...v, tool: name }))); } return out; } catch (e) { sections.push({ tool: name, ok: false, summary: String(e.message || e).slice(0, 300) }); return null; } }; const quality = runSection('data_quality', () => lake.qualityHandler(ctx, lake.qualitySanitize({}, ctx))); runSection('cross_check', () => cross.crossHandler(ctx, cross.crossSanitize({}, ctx))); const cmp = runSection('compare_periods', () => compare.compareHandler(ctx, compare.compareSanitize({}, ctx))); // 头条:环比一句话 + 最差校验结论 let headline; const worst = verdicts.reduce((acc, v) => (v.status === 'fail' ? 'fail' : (v.status === 'warn' && acc !== 'fail' ? 'warn' : acc)), 'ok'); if (!dateRange || svc.store.datesInLake(key).length === 0) { headline = '湖内暂无数据,待同步后再出晨报'; } else if (cmp && cmp.data && !cmp.data.empty) { const gross = (cmp.data.deltas || []).find((d) => d.key === 'grossAmt'); if (gross) { const pct = gross.deltaPct == null ? '' : `,环比 ${gross.deltaPct >= 0 ? '+' : ''}${(gross.deltaPct * 100).toFixed(1)}%`; headline = `近 7 天营业额 ${Math.round(gross.cur).toLocaleString('zh-CN')} 元${pct}(可比口径)`; } else { headline = '近 7 天经营数据已就绪'; } } else { headline = '经营数据已就绪'; } if (worst === 'fail') headline = `⚠ ${headline};数据体检发现问题,详见下方校验`; else if (worst === 'warn') headline = `${headline};部分校验有警告`; const digest = { id: trace.newId('dg'), entity: key, entityName: meta.name || key, at: new Date().toISOString(), status: worst, headline, sections, verdicts, reportUrl: `/api/reports/report-html?${new URLSearchParams({ entity: key, from: dateRange.from, to: dateRange.to }).toString()}`, }; const digests = loadDigests(key); digests.unshift(digest); if (digests.length > 30) digests.length = 30; saveDigests(key, digests); return digest; } module.exports = { buildAndSaveDigest, loadDigests };