| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100 |
- 'use strict';
- /**
- * 智能问数 · 晨报(确定性产出,零模型)
- * ----------------------------------------------------------------------------
- * 每日自动同步完成后,对当天实体确定性跑一遍 数据体检 + 层间核对 + 环比对比,
- * 组装成带可信度标签的晨报落盘(data/agent/<entity>/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 };
|