| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970 |
- /** Build an offline report from an explicitly captured public snapshot. No network requests. */
- import fs from 'node:fs';
- import path from 'node:path';
- import { fileURLToPath } from 'node:url';
- const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
- const folder = path.join(root, 'docs/workshop-report');
- const snapshot = JSON.parse(fs.readFileSync(path.join(folder, 'snapshot-20261103.json'), 'utf8'));
- const args = process.argv.slice(2);
- if (args.length && (args.length !== 2 || args[0] !== '--out' || !path.isAbsolute(args[1]))) throw Error('Use --out ABSOLUTE_HTML_FILE');
- const out = args[1] || path.join(folder, 'report-20261104.html');
- const escape = v => String(v).replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"');
- const n = v => Number(v).toLocaleString('en-US');
- const count = snapshot.publicSnapshot.counts;
- const modes = snapshot.publicSnapshot.distributions.artifact.dataMode;
- const base = snapshot.demo.baseline, ext = snapshot.demo.extension;
- const total = o => Object.values(o).reduce((sum, v) => sum + v, 0);
- if (total(base) !== 334 || total(ext) !== 16446 || !snapshot.publicSnapshot.complete) throw Error('UNEXPECTED_SNAPSHOT');
- const artifactLabels = {
- opportunity_card: '机会卡类成果', pain_list: '痛点清单', competitor_report: '竞品研究',
- concept: '产品概念', assessment_report: '验证评估', category_report: '品类研究', voc_report: '用户声音',
- };
- const baseLabels = { groups: '小组', members: '成员', ideas: '创意', sources: '来源', evidence: '基础引文', opportunities: '机会卡',
- projects: '项目', conceptVersions: '方案版本', reports: '报告', questions: '未决问题', validationTasks: '验证任务', assignments: '作业' };
- const extLabels = { voices: ['虚构用户声音', '人群、场景、口味与条件检索'], observations: ['商品观察', '产品与规格比较'],
- prices: ['价格快照', '同一商品多个时间与条件'], citations: ['新增引文', '同源原文与结论回查'], researchTasks: ['研究任务', '补证、负责人、执行状态'],
- collaborationRecords: ['协作记录', '多人分工、评论与接力'] };
- const skillCommands = [
- ['员工与会话', 'workshop-start', '绑定当前员工、Agent、小组和工作坊会话'],
- ['称呼', 'employee-name', '读取、保存或修改本人称呼;记住明确跳过'],
- ['可选通讯录', 'employee-read', '只读查询通讯录姓名;依赖管理员配置,当前未接通'],
- ['阶段成果', 'workshop-save', '保存报告、材料关联、未决问题,可生成机会卡'],
- ['项目', 'project-create / project-progress', '从机会建立项目,记录阶段推进、受阻或课堂完成'],
- ['概念方案', 'concept-save / project-status', '保存方案版本;回读项目成果、方案和事件'],
- ['资料入库', 'workshop-import', '按合同保存原料、工艺、竞品、证据等资料'],
- ['查重与检索', 'workshop-dedup / workshop-search', '精确查重、检索已有资料与报告'],
- ['回读与合同', 'workshop-read / workshop-contract', '按真实编号回读;核对服务支持字段'],
- ['正式报告', 'report-preview / report-submit', '建立预览;本人完成服务端确认后提交'],
- ['兼容入口', 'ingest / status / demo / connection', '沿用原文导入、状态、示例和连接检查'],
- ];
- const baseEntries = Object.entries(base);
- const localTime = new Intl.DateTimeFormat('zh-CN', { timeZone: 'Asia/Shanghai', dateStyle: 'medium', timeStyle: 'short', hour12: false }).format(new Date(snapshot.generatedAt));
- const replacements = {
- SNAPSHOT_TIME: escape(localTime + '(北京时间)'),
- PUBLIC_KPIS: [['artifact', '份', '阶段成果'], ['project', '个', '项目'], ['opportunity', '张', '独立机会卡'], ['source', '个', '来源'], ['session', '个', '工作坊会话'], ['insight', '条', '洞察']]
- .map(([key, unit, label]) => `<article class="card metric"><div class="val">${n(count[key])}<span class="unit">${unit}</span></div><div class="name">${label}</div></article>`).join('\n'),
- DATA_QUALITY: `本次 ${count.artifact} 份成果中,取得模式标为样例的 ${modes.sample || 0} 份、标为 live 的 ${modes.live || 0} 份,另有 ${modes['未提供'] || 0} 份公开记录未提供该字段。live 只表示记录的取得模式,不能单独证明研究结论已经核验。`,
- MEMBER_COUNT: snapshot.publicSnapshot.modelMembers,
- SOURCE_COUNT: count.source, INSIGHT_COUNT: count.insight, ARTIFACT_COUNT: count.artifact, OPPORTUNITY_COUNT: count.opportunity,
- PROJECT_COUNT: count.project, SESSION_COUNT: count.session,
- OPPORTUNITY_REPORTS: snapshot.publicSnapshot.distributions.artifact.artifactType.opportunity_card,
- REPORT_BARS: Object.entries(snapshot.publicSnapshot.distributions.artifact.artifactType).map(([type, amount]) => `<div class="bar-row"><span>${escape(artifactLabels[type] || type)}</span><div class="bar-track" role="img" aria-label="${escape(artifactLabels[type] || type)} ${amount} 份,占成果总量 ${Math.round(amount / count.artifact * 100)}%"><div class="bar-fill" style="--pct:${(amount / count.artifact * 100).toFixed(2)}%"></div></div><b>${amount}</b></div>`).join('\n'),
- BASELINE_TOTAL: n(total(base)), EXT_TOTAL: n(total(ext)), EXT_CITATIONS: ext.citations,
- BASELINE_ROWS: Array.from({ length: Math.ceil(baseEntries.length / 2) }, (_, i) => {
- const a = baseEntries[i * 2], b = baseEntries[i * 2 + 1];
- return `<tr><td>${escape(baseLabels[a[0]])}</td><td><b>${n(a[1])}</b></td><td>${escape(baseLabels[b[0]])}</td><td><b>${n(b[1])}</b></td></tr>`;
- }).join('\n'),
- EXT_ROWS: Object.entries(ext).map(([key, amount]) => `<tr><td>${escape(extLabels[key][0])}</td><td><b>${n(amount)}</b></td><td>${escape(extLabels[key][1])}</td></tr>`).join('\n'),
- SKILL_COMMANDS: skillCommands.map(([type, command, detail]) => `<tr><td>${escape(type)}</td><td><code>${escape(command)}</code></td><td>${escape(detail)}</td></tr>`).join('\n'),
- SNAPSHOT_JSON: JSON.stringify(snapshot).replaceAll('<', '\\u003c'),
- };
- let html = fs.readFileSync(path.join(folder, 'report.template.html'), 'utf8');
- html = html.replace(/__([A-Z_]+)__/g, (match, key) => {
- if (!Object.hasOwn(replacements, key)) throw Error('UNKNOWN_PLACEHOLDER:' + match);
- return String(replacements[key]);
- });
- if (/__[A-Z_]+__/.test(html) || /https?:\/\//.test(html.match(/<script>([\s\S]*?)<\/script>/)?.[1] || '')) throw Error('BAD_REPORT_OUTPUT');
- fs.mkdirSync(path.dirname(out), { recursive: true });
- fs.writeFileSync(out, html);
- console.log(JSON.stringify({ ok: true, file: out, bytes: Buffer.byteLength(html), sections: 10, offline: true, externalResources: 0, networkRequests: 0, productionWrites: 0 }));
|