/** 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]) => `
${n(count[key])}${unit}
${label}
`).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]) => `
${escape(artifactLabels[type] || type)}${amount}
`).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 `${escape(baseLabels[a[0]])}${n(a[1])}${escape(baseLabels[b[0]])}${n(b[1])}`; }).join('\n'), EXT_ROWS: Object.entries(ext).map(([key, amount]) => `${escape(extLabels[key][0])}${n(amount)}${escape(extLabels[key][1])}`).join('\n'), SKILL_COMMANDS: skillCommands.map(([type, command, detail]) => `${escape(type)}${escape(command)}${escape(detail)}`).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(/