/** Shared by the Skill, save service and dashboard. No credentials or runtime dependencies. */ export const DELIVERY_SCHEMA = 'ksf.workshop-delivery.v1'; export const CORE_DELIVERABLES = Object.freeze([ { key: 'market', name: '市场分析', anchor: 'market-analysis', modules: ['D1-01', 'D1-03'] }, { key: 'social', name: '社媒场景需求', anchor: 'social-needs', modules: [] }, { key: 'evidence', name: '用户与竞品证言池', anchor: 'evidence-pool', modules: [] }, { key: 'opportunities', name: '三方机会卡', anchor: 'opportunity-cards', modules: ['D2-04'] }, { key: 'validation', name: '产品论证机会清单', anchor: 'opportunity-validation', modules: ['D2-05', 'D2-06'] }, { key: 'value', name: '场景买点方案', anchor: 'buying-points', modules: ['D2-07'] }, { key: 'proposal', name: '产品概念提案', anchor: 'product-proposal', modules: [] }, ]); export const DELIVERY_KINDS = Object.freeze({ market: { category: 'category', types: ['category_report'] }, social: { category: 'social', types: ['voc_report'] }, evidence: { category: 'evidence', types: ['voc_report'] }, opportunities: { category: 'opportunities', types: ['opportunity_card'] }, validation: { category: 'assessment', types: ['assessment_report'] }, value: { category: 'value', types: ['concept'] }, proposal: { category: 'concept', types: ['concept'] }, competitor: { category: 'competitor', types: ['competitor_report'] }, pain: { category: 'pain', types: ['pain_list'] }, research: { category: 'voc', types: ['voc_report'] }, method: { category: 'method', types: ['category_report', 'pain_list', 'assessment_report'] }, }); export const FALLBACK_REPORT_TYPES = Object.freeze({ category_report: 'category', competitor_report: 'competitor', voc_report: 'voc', pain_list: 'pain', opportunity_card: 'opportunities', assessment_report: 'assessment', concept: 'concept', }); export const REPORT_ATTACHMENT_TYPES = Object.freeze({ 'text/html': ['html', 'htm'], 'text/markdown': ['md'], 'text/plain': ['txt'], 'application/json': ['json'], 'application/pdf': ['pdf'], 'image/png': ['png'], 'image/jpeg': ['jpg', 'jpeg'], 'image/webp': ['webp'], }); export const REPORT_ATTACHMENT_LIMITS = Object.freeze({ count: 8, perFileBytes: 1024 * 1024, totalBytes: 2 * 1024 * 1024 }); const deliveryText = x => typeof x === 'string' && Boolean(x.trim()); const deliveryChapters = report => Array.isArray(report?.chapters) ? report.chapters : []; const deliveryValue = x => typeof x === 'string' ? deliveryText(x) : typeof x === 'number' ? Number.isFinite(x) : Array.isArray(x) ? x.some(deliveryValue) : x && typeof x === 'object' ? Object.values(x).some(deliveryValue) : false; export function chapterHasContent(chapter) { return Array.isArray(chapter?.sections) && chapter.sections.some(s => { if (!s || typeof s !== 'object') return false; if (s.kind === 'para' || s.kind === 'longpara') return deliveryText(s.text); if (s.kind === 'table' || s.kind === 'matrix') return Array.isArray(s.data?.rows) && s.data.rows.some(deliveryValue); return deliveryValue(s.data); }); } export function reportBodyStatus(artifact) { if (deliveryChapters(artifact?.report).some(chapterHasContent)) return 'report'; return deliveryText(artifact?.rawText) ? 'text' : 'none'; } export function validateReportDelivery(report, artifactType) { const d = report?.delivery; if (d === undefined) return null; // Historical clients remain readable and writable. const reject = message => { throw Object.assign(new Error(message), { code: 'VALIDATION_FAILED' }); }; if (!d || typeof d !== 'object' || Array.isArray(d) || Object.keys(d).some(k => !['schema', 'moduleId', 'kind', 'contentStatus', 'outputs'].includes(k))) reject('报告交付标记字段无效'); if (d.schema !== DELIVERY_SCHEMA || !Object.hasOwn(DELIVERY_KINDS, d.kind) || !DELIVERY_KINDS[d.kind].types.includes(artifactType)) reject('报告分类与成果类型不匹配'); if (!/^(D1-(0[1-4]|N)|D2-0[1-8])$/.test(d.moduleId || '')) reject('报告环节标记无效'); if (!['draft', 'ready'].includes(d.contentStatus) || !Array.isArray(d.outputs) || new Set(d.outputs).size !== d.outputs.length) reject('报告交付状态无效'); for (const key of d.outputs) { const output = CORE_DELIVERABLES.find(o => o.key === key); if (!output || !deliveryChapters(report).some(ch => ch?.id === output.anchor && chapterHasContent(ch))) reject('交付物须对应有实质正文的稳定章节:' + key); } if (d.contentStatus === 'ready' && (!deliveryChapters(report).some(chapterHasContent) || CORE_DELIVERABLES.some(o => o.key === d.kind) && !d.outputs.includes(d.kind))) reject('已提交报告缺少对应的交付物正文'); return d; } export function reportCategoryOf(artifact) { const type = artifact?.type || artifact?.artifactType; try { const d = validateReportDelivery(artifact?.report, type); if (d) return DELIVERY_KINDS[d.kind].category; } catch { /* Do not trust malformed legacy metadata; retain its artifact type. */ } // Only unambiguous, substantive legacy anchors refine a broad category. const keys = CORE_DELIVERABLES.filter(o => deliveryChapters(artifact?.report).some(ch => ch?.id === o.anchor && chapterHasContent(ch))); const compatible = keys.filter(o => DELIVERY_KINDS[o.key].types.includes(type)); if (compatible.length === 1) return DELIVERY_KINDS[compatible[0].key].category; return Object.hasOwn(FALLBACK_REPORT_TYPES, type) ? FALLBACK_REPORT_TYPES[type] : ''; }