Просмотр исходного кода

docs: summarize workshop workflow, skill, data and board delivery

pengzhikang 23 часов назад
Родитель
Сommit
2278290d2c

Разница между файлами не показана из-за своего большого размера
+ 9 - 0
docs/workshop-report/report-20261104.html


Разница между файлами не показана из-за своего большого размера
+ 9 - 0
docs/workshop-report/report.template.html


+ 192 - 0
docs/workshop-report/snapshot-20261103.json

@@ -0,0 +1,192 @@
+{
+  "generatedAt": "2026-11-03T15:57:40.519Z",
+  "origin": "https://kangshifu.fmode.cn",
+  "commit": "a68bfe1",
+  "publicSnapshot": {
+    "endpoint": "/api/functions",
+    "function": "ksfGetPublicBoard",
+    "http": 200,
+    "complete": true,
+    "asOf": "2026-11-03T15:57:20.176Z",
+    "counts": {
+      "session": 14,
+      "artifact": 47,
+      "project": 13,
+      "opportunity": 12,
+      "source": 25,
+      "insight": 4
+    },
+    "modelMembers": 17,
+    "distributions": {
+      "session": {
+        "dataMode": {
+          "未提供": 14
+        },
+        "reviewState": {
+          "未提供": 14
+        }
+      },
+      "artifact": {
+        "dataMode": {
+          "未提供": 26,
+          "sample": 12,
+          "live": 9
+        },
+        "reviewState": {
+          "未提供": 47
+        },
+        "artifactType": {
+          "opportunity_card": 19,
+          "pain_list": 12,
+          "competitor_report": 7,
+          "concept": 3,
+          "assessment_report": 3,
+          "category_report": 2,
+          "voc_report": 1
+        },
+        "reportType": {
+          "未提供": 26,
+          "workshop_auxiliary": 21
+        },
+        "stage": {
+          "未提供": 26,
+          "S3": 8,
+          "S1": 4,
+          "S5": 3,
+          "S2": 3,
+          "S4": 3
+        }
+      },
+      "project": {
+        "dataMode": {
+          "未提供": 10,
+          "sample": 2,
+          "live": 1
+        },
+        "reviewState": {
+          "未提供": 13
+        },
+        "stage": {
+          "未提供": 10,
+          "S5": 3
+        },
+        "state": {
+          "未提供": 13
+        }
+      },
+      "opportunity": {
+        "dataMode": {
+          "未提供": 9,
+          "sample": 2,
+          "live": 1
+        },
+        "reviewState": {
+          "未提供": 12
+        },
+        "status": {
+          "approved": 7,
+          "candidate": 3,
+          "rejected": 2
+        }
+      },
+      "source": {
+        "dataMode": {
+          "sample": 12,
+          "live": 9,
+          "未提供": 4
+        },
+        "reviewState": {
+          "未提供": 25
+        },
+        "sourceKind": {
+          "dataset": 21,
+          "未提供": 4
+        }
+      },
+      "insight": {
+        "dataMode": {
+          "未提供": 4
+        },
+        "reviewState": {
+          "未提供": 4
+        }
+      }
+    },
+    "nickname": [
+      {
+        "name": "小智",
+        "artifacts": 15,
+        "projects": 2,
+        "caption": "本人提供的称呼 · 未经通讯录核验",
+        "source": "self_reported",
+        "nameVerified": false
+      }
+    ],
+    "examples": [
+      {
+        "title": "健身人群高蛋白面 - opportunity_card",
+        "artifactType": "opportunity_card",
+        "stageCode": null,
+        "dataMode": null
+      },
+      {
+        "title": "【样例】待检索选择的机会卡",
+        "artifactType": "opportunity_card",
+        "stageCode": "S3",
+        "dataMode": "sample"
+      },
+      {
+        "title": "夜宵场景低卡高蛋白拌面 - pain_list",
+        "artifactType": "pain_list",
+        "stageCode": null,
+        "dataMode": null
+      },
+      {
+        "title": "【样例】新品方案与待验证事项",
+        "artifactType": "concept",
+        "stageCode": "S5",
+        "dataMode": "sample"
+      },
+      {
+        "title": "早餐快速汤面 - competitor_report",
+        "artifactType": "competitor_report",
+        "stageCode": null,
+        "dataMode": null
+      }
+    ],
+    "verification": "生产公开API与前端模型核对",
+    "browserVerified": false
+  },
+  "demo": {
+    "baseline": {
+      "groups": 6,
+      "members": 36,
+      "ideas": 72,
+      "sources": 6,
+      "evidence": 6,
+      "opportunities": 24,
+      "projects": 12,
+      "conceptVersions": 8,
+      "reports": 44,
+      "questions": 72,
+      "validationTasks": 30,
+      "assignments": 18
+    },
+    "extension": {
+      "voices": 12000,
+      "observations": 720,
+      "prices": 2880,
+      "citations": 354,
+      "researchTasks": 180,
+      "collaborationRecords": 312
+    },
+    "version": "ksf.synthetic-extension.20261103.v2"
+  },
+  "checks": {
+    "testsPassed": 114,
+    "staticChecksPassed": 13,
+    "skillHashesPassed": 10,
+    "productionDeployment": "运维001回执确认,当前公开API可读",
+    "browserVerified": false
+  }
+}

+ 20 - 0
docs/workshop-report/validation-20261104.json

@@ -0,0 +1,20 @@
+{
+  "ok": true,
+  "htmlElementsBalanced": true,
+  "duplicateIds": 0,
+  "sectionCount": 10,
+  "fragmentLinksVerified": 12,
+  "externalResourceCount": 0,
+  "inlineScriptSyntax": "passed",
+  "networkCallsInScript": 0,
+  "dataTotals": {
+    "publicRows": 115,
+    "artifacts": 47,
+    "demoBaseline": 334,
+    "demoExtension": 16446
+  },
+  "hasMobileAndPrintStyles": true,
+  "browserVerified": false,
+  "sha256": "8a0efb331b836301e0045fa982b9e63d023387e0c5a48a5bb30fb6c6aef8a0a6",
+  "bytes": 71605
+}

+ 70 - 0
scripts/build-workshop-report.mjs

@@ -0,0 +1,70 @@
+/** 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('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;');
+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 }));

Некоторые файлы не были показаны из-за большого количества измененных файлов