#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const ROOT = path.resolve(__dirname, '..'); const OUTPUTS = path.join(ROOT, 'outputs'); function main() { const args = parseArgs(process.argv.slice(2)); const outputDir = path.resolve(args.output || process.env.TIHAO_BUSINESS_PROOF_NEXT_ACTIONS_OUTPUT || path.join(OUTPUTS, `business-proof-next-actions-${Date.now()}`)); const summary = buildNextActions({ root: ROOT, outputsDir: path.resolve(args.outputs || OUTPUTS), limit: Number(args.limit || 8) }); fs.mkdirSync(outputDir, { recursive: true }); const jsonPath = path.join(outputDir, 'business-proof-next-actions-summary.json'); const reportPath = path.join(outputDir, 'business-proof-next-actions-report.md'); const csvPath = path.join(outputDir, 'business-proof-next-actions.csv'); const ownerArtifacts = writeOwnerArtifacts(outputDir, summary.ownerGroups, summary.actions, summary.ownerRepairArtifacts); summary.ownerArtifacts = ownerArtifacts; summary.ownerArtifactCount = ownerArtifacts.length; fs.writeFileSync(jsonPath, JSON.stringify(summary, null, 2), 'utf8'); fs.writeFileSync(reportPath, withBom(renderReport(summary, ownerArtifacts)), 'utf8'); fs.writeFileSync(csvPath, withBom(renderCsv(summary.actions)), 'utf8'); console.log(JSON.stringify({ outputDir, json: jsonPath, report: reportPath, csv: csvPath, ownerArtifacts: ownerArtifacts.length, complete: summary.complete, actionCount: summary.actionCount, topAction: summary.topActions[0]?.title || '' }, null, 2)); if (!summary.complete && args.strict) process.exitCode = 1; } function buildNextActions({ root, outputsDir, limit = 8 }) { const progress = readJson(latestFile(outputsDir, 'business-proof-progress-summary.json')); const closure = readJson(latestFile(outputsDir, 'proof-gap-closure-summary.json')); const intake = readJson(latestFile(outputsDir, 'intake-readiness-summary.json')); const videoReadiness = readJson(latestFile(outputsDir, 'video-resource-readiness-summary.json')); const homepageReadiness = readJson(latestFile(outputsDir, 'homepage-evidence-readiness-summary.json')); const pipeline = readJson(latestFile(outputsDir, 'optimization-pipeline-summary.json')); const completion = readJson(latestFile(outputsDir, 'optimization-completion-summary.json')); const pipelineFreshness = getPipelineFreshnessFromCompletion(completion); const candidates = []; const primarySections = new Set(); const primaryGapIds = new Set(); for (const row of progress?.rows || []) { if (row.status === 'pass') continue; const order = Number(row.order); if (order === 3) { primarySections.add('history'); primaryGapIds.add('historical-dataset'); } if (order === 4) { primarySections.add('video'); primaryGapIds.add('video-real-candidate'); } if (order === 5) primaryGapIds.add('intake-readiness'); if (order === 12) { primarySections.add('review'); primaryGapIds.add('manual-review-and-customer-effect'); } if (order === 14) primaryGapIds.add('manual-review-and-customer-effect'); candidates.push(actionFromProgressRow(row, { intake, videoReadiness })); } for (const row of closure?.rows || []) { if (row.status !== 'open') continue; if (primaryGapIds.has(row.id)) { mergeClosureRowIntoPrimaryAction(candidates, row); continue; } candidates.push(actionFromClosureRow(row)); } for (const issue of intake?.issues || []) { if (primarySections.has(issue.section)) continue; candidates.push(actionFromIntakeIssue(issue)); } if (!pipelineFreshness.stale) { mergePipelineNextActions(candidates, pipeline, { closure, videoReadiness }); } const deduped = compactBusinessGapActions(dedupeActions(candidates)) .sort((a, b) => a.priority - b.priority || a.order - b.order) .slice(0, limit); const ownerGroups = groupByOwner(deduped); const ownerRepairArtifacts = buildOwnerRepairActionArtifacts({ root, outputsDir, intake, videoReadiness, homepageReadiness }); return { generatedAt: new Date().toISOString(), root, outputsDir, complete: Boolean(progress?.complete) && Number(closure?.openCount || 0) === 0, actionCount: deduped.length, ownerGroupCount: ownerGroups.length, ownerRepairArtifactCount: ownerRepairArtifacts.length, topActions: summarizeTopActions(deduped), sourceState: { progressComplete: Boolean(progress?.complete), progressCounts: progress?.counts || {}, proofGapOpenCount: closure ? Number(closure.openCount || 0) : null, intakeFailureCount: intake ? Number(intake.failureCount || 0) : null, videoFailureCount: videoReadiness ? Number(videoReadiness.failureCount || 0) : null, homepageFailureCount: homepageReadiness ? Number(homepageReadiness.failureCount || 0) : null, pipelineReadyForClaim: pipeline ? Boolean(pipeline.readyForClaim) : null, pipelineFailCount: pipeline ? Number(pipeline.counts?.fail || 0) : null, pipelineNextActionCount: pipeline && !pipelineFreshness.stale ? Number(pipeline.nextActions?.length || 0) : 0, pipelineRawNextActionCount: pipeline ? Number(pipeline.nextActions?.length || 0) : null, pipelineSuppressedNextActionCount: pipelineFreshness.stale && pipeline ? Number(pipeline.nextActions?.length || 0) : 0, pipelineStale: pipelineFreshness.stale, pipelineStaleReason: pipelineFreshness.reason, pipelineStaleMissingProofRequirements: pipelineFreshness.missingProofRequirements }, guardrails: [ '行动队列只用于安排下一步补证,不证明客户效果完成。', '不能把 sample、smoke、模板、provider fallback 或接口 200 当成业务证明。', '补证动作不得包含 sessionToken、Authorization、模型 token 或 npm token。', '补齐真实材料后优先运行 npm run round:refresh -- --output-root outputs --strict,按顺序刷新 proof-gap、业务补证进度、证据台账、交接摘要和沉淀审计。', 'stale optimization-pipeline nextActions are not current dispatch evidence; rerun optimization:pipeline after real materials are ready.' ], ownerGroups, ownerRepairArtifacts, actions: deduped }; } function summarizeTopActions(actions, limit = 5) { if (!Array.isArray(actions)) return []; return actions.slice(0, limit).map(action => ({ id: action.id || '', source: action.source || '', order: Number(action.order || 0), priority: Number(action.priority || 0), owner: action.owner || '', title: action.title || '', status: action.status || '', evidence: action.evidence || '', command: action.command || '', expectedArtifact: action.expectedArtifact || '', acceptance: action.acceptance || '', missingProofRequirementCount: Array.isArray(action.missingProofRequirements) ? action.missingProofRequirements.length : 0, missingProofRequirements: Array.isArray(action.missingProofRequirements) ? action.missingProofRequirements : [] })); } function getPipelineFreshnessFromCompletion(completion) { const blocker = Array.isArray(completion?.blockingReasons) ? completion.blockingReasons.find(item => item.id === 'optimization-pipeline') : null; const missingProofRequirements = Array.isArray(blocker?.missingProofRequirements) ? blocker.missingProofRequirements.filter(Boolean).map(item => String(item)) : []; const evidence = String(blocker?.evidence || ''); const stale = evidence.includes('stale=true') || missingProofRequirements.includes('optimizationPipeline.generatedAt>=currentProofSources'); return { stale, reason: stale ? (evidence || 'optimization pipeline is stale') : '', missingProofRequirements }; } function mergePipelineNextActions(candidates, pipeline, context = {}) { if (!pipeline || !Array.isArray(pipeline.nextActions)) return; for (const pipelineAction of pipeline.nextActions) { if (shouldSkipPipelineAction(pipelineAction, context)) continue; const mappedId = pipelineStepActionId(pipelineAction.step, candidates); const existing = candidates.find(action => action.id === mappedId); if (!existing) { candidates.push(actionFromPipelineNextAction(pipelineAction)); continue; } existing.source = `${existing.source}+optimization-pipeline`; const keepPrimaryAction = ['step-03', 'step-04', 'step-12'].includes(existing.id); if (!keepPrimaryAction) { existing.owner = pipelineAction.owner || existing.owner; existing.title = pipelineAction.title || existing.title; existing.priority = Math.min(existing.priority || 99, Number(pipelineAction.priority || 99)); existing.command = pipelineAction.command || existing.command; existing.expectedArtifact = pipelineAction.expectedArtifact || existing.expectedArtifact; existing.acceptance = pipelineAction.acceptance || existing.acceptance; } existing.next = appendIssueMessages(existing.next, 'pipeline 当前动作', [pipelineAction.command]); } } function shouldSkipPipelineAction(pipelineAction, { closure, videoReadiness } = {}) { if (pipelineAction?.step !== 'video-resource-readiness') return false; const videoReady = Boolean(videoReadiness?.acceptance?.readyForVideoAbPreflight) && Number(videoReadiness?.failureCount || 0) === 0; const videoGapClosed = (Array.isArray(closure?.rows) ? closure.rows : []) .some(row => row.id === 'video-real-candidate' && row.status === 'closed'); return videoReady || videoGapClosed; } function pipelineStepActionId(step, candidates = []) { if (step === 'history-audit' && hasAction(candidates, 'step-03')) return 'step-03'; if (step === 'video-resource-readiness' && hasAction(candidates, 'step-04')) return 'step-04'; if (step === 'customer-effect-audit' && hasAction(candidates, 'step-12')) return 'step-12'; const map = { 'intake-readiness': 'step-05', 'history-from-csv': 'step-06', 'history-audit': 'step-07', 'video-resource-readiness': 'step-08', 'longrun-readiness': 'step-09', 'review-metrics': 'step-13', 'customer-effect-audit': 'step-14', 'evidence-index': 'step-15' }; return map[step] || ''; } function hasAction(candidates, id) { return candidates.some(action => action.id === id); } function actionFromPipelineNextAction(action) { return { id: `pipeline-${action.step || action.title}`, source: 'optimization-pipeline', order: 300, priority: Number(action.priority || 30), owner: action.owner || '技术/AI', title: action.title || 'Pipeline 下一步动作', status: 'blocked_by_external_data', evidence: `pipeline step=${action.step || 'unknown'}`, next: action.command || '', command: action.command || '', expectedArtifact: action.expectedArtifact || '', acceptance: action.acceptance || 'pipeline 对应 nextAction 完成。' }; } function actionFromProgressRow(row, { intake, videoReadiness }) { const base = { id: `step-${String(row.order).padStart(2, '0')}`, source: 'business-proof-progress', order: Number(row.order || 999), owner: row.owner || '待分配', title: row.title || `补证步骤 ${row.order}`, status: row.status, evidence: row.evidence || '', next: row.next || '', command: commandForStep(row.order), expectedArtifact: artifactForStep(row.order), acceptance: acceptanceForStep(row.order) }; base.priority = priorityForStep(row.order, row.status); if (row.order === 3 && intake?.history?.issues?.length) { base.next = appendIssueMessages(base.next, '当前历史数据问题', [ ...intake.history.issues.map(item => item.message), ...sectionIssueMessages(intake, 'history') ]); } if (row.order === 4) { base.next = appendIssueMessages(base.next, '当前视频资源问题', [ ...(videoReadiness?.issues || []).map(item => item.message), ...sectionIssueMessages(intake, 'video') ]); } if (row.order === 12) { base.next = appendIssueMessages(base.next, '当前人工复核问题', sectionIssueMessages(intake, 'review')); } return base; } function sectionIssueMessages(intake, section) { return (intake?.issues || []) .filter(issue => issue.section === section) .map(issue => issue.message) .filter(Boolean); } function appendIssueMessages(next, label, messages) { const uniqueMessages = Array.from(new Set(messages.filter(Boolean))); if (!uniqueMessages.length) return next; return `${next} ${label}:${uniqueMessages.join(';')}`; } function actionFromClosureRow(row) { const missingProofRequirements = Array.isArray(row.missingProofRequirements) ? row.missingProofRequirements.filter(Boolean) : []; return { id: `gap-${row.id}`, source: 'proof-gap-closure', order: 100 + gapOrder(row.id), priority: 20 + gapOrder(row.id), owner: ownerForGap(row.id), title: titleForGap(row.id, row.title), status: row.status, evidence: row.evidence || '', missingProofRequirements, next: appendIssueMessages(row.next || row.command || '', '缺失证明', missingProofRequirements), command: row.command || '', expectedArtifact: row.expectedArtifact || '', acceptance: row.required || '对应 proof gap status=closed。' }; } function mergeClosureRowIntoPrimaryAction(candidates, row) { const targetId = primaryActionIdForGap(row.id); const target = candidates.find(action => action.id === targetId); if (!target) return; const gapAction = actionFromClosureRow(row); target.source = mergeSource(target.source, gapAction.source); target.evidence = mergeText(target.evidence || '', gapAction.evidence || ''); target.next = appendIssueMessages(target.next, '缺失证明', gapAction.missingProofRequirements || []); target.next = appendIssueMessages(target.next, 'proof-gap 当前证据', [gapAction.evidence]); target.acceptance = mergeText(target.acceptance, gapAction.acceptance); target.missingProofRequirements = mergeList(target.missingProofRequirements, gapAction.missingProofRequirements); if (!target.expectedArtifact.includes(gapAction.expectedArtifact || '__missing__')) { target.expectedArtifact = [target.expectedArtifact, gapAction.expectedArtifact].filter(Boolean).join(';'); } } function primaryActionIdForGap(id) { const map = { 'intake-readiness': 'step-05', 'historical-dataset': 'step-03', 'video-real-candidate': 'step-04', 'manual-review-and-customer-effect': 'step-12' }; return map[id] || ''; } function actionFromIntakeIssue(issue) { return { id: `intake-${issue.section}-${issue.type}`, source: 'intake-readiness', order: 200, priority: issue.section === 'history' ? 1 : issue.section === 'video' ? 2 : 3, owner: issue.section === 'video' ? '商务/投放' : '商务', title: `修复 intake ${issue.section}:${issue.type}`, status: 'blocked_by_external_data', evidence: issue.message || '', next: issue.message || '', command: 'npm run intake:readiness -- --data-pack outputs\\data-intake-pack-latest --video-pack outputs\\video-intake-pack-latest --output outputs\\intake-readiness-latest', expectedArtifact: 'outputs/intake-readiness-latest/intake-readiness-summary.json', acceptance: 'intake readiness 对应 issue 消失,overallReady=true 且 failureCount=0。' }; } function commandForStep(order) { const commands = { 3: '填写 outputs\\data-intake-pack-latest\\history-data-template.csv', 4: '填写 outputs\\video-intake-pack-latest\\video-resource-template.csv', 5: 'npm run intake:readiness -- --data-pack outputs\\data-intake-pack-latest --video-pack outputs\\video-intake-pack-latest --output outputs\\intake-readiness-latest', 6: 'npm run history:from-csv -- --input <历史数据CSV> --output ', 7: 'npm run history:audit -- --input --output <历史审计输出目录> --strict', 8: 'npm run video:resource-readiness -- --input --output <视频资源审计输出目录> --strict', 9: 'npm run longrun:readiness -- --mode full-matrix --intake-readiness outputs\\intake-readiness-latest\\intake-readiness-summary.json --proof-gap outputs\\proof-gap-request-latest\\proof-gap-request-summary.json --proof-gap-closure outputs\\proof-gap-closure-latest\\proof-gap-closure-summary.json --output outputs\\long-run-readiness-latest', 10: 'npm run optimization:pipeline -- --history-csv <历史数据CSV> --review-csv <已标注CSV> --current-manual-supplement-count <本轮人工补号量> --output <输出目录> --strict', 12: '填写 outputs\\data-intake-pack-latest\\manual-review-template.csv', 13: 'npm run review:metrics -- --input <已标注CSV> --output <复核指标输出目录> --strict', 14: 'npm run customer-effect:audit -- --review-csv <已标注CSV> --history-audit --current-manual-supplement-count <本轮人工补号量> --output <客户效果输出目录> --strict' }; return commands[order] || ''; } function artifactForStep(order) { const artifacts = { 3: 'outputs/data-intake-pack-latest/history-data-template.csv', 4: 'outputs/video-intake-pack-latest/video-resource-template.csv', 5: 'outputs/intake-readiness-latest/intake-readiness-summary.json', 6: '/*.json', 7: '<历史审计输出目录>/historical-dataset-audit.json', 8: '<视频资源审计输出目录>/video-resource-readiness-summary.json', 9: 'outputs/long-run-readiness-latest/long-run-readiness-summary.json', 10: '<输出目录>/optimization-pipeline-summary.json', 12: 'outputs/data-intake-pack-latest/manual-review-template.csv', 13: '<复核指标输出目录>/review-metrics-summary.json', 14: '<客户效果输出目录>/customer-effect-summary.json' }; return artifacts[order] || ''; } function acceptanceForStep(order) { const rules = { 3: 'historyReady=true,真实历史 Brief 数 >= 5,且无模板占位。', 4: 'videoReady=true,至少有真实参考视频和真实候选视频。', 5: 'overallReady=true 且 failureCount=0。', 6: '历史数据集 JSON 成功生成。', 7: 'readyForCustomerEffectProof=true。', 8: 'readyForVideoAbPreflight=true 且 failureCount=0。', 9: 'ready=true 且 fail=0。', 10: 'readyForClaim=true,且软件端重复键为 0、排名连续。', 12: '客户选择、负样本归因和本轮人工补号量齐全。', 13: 'review:metrics overallPass=true,负样本归因覆盖率=100%。', 14: 'customer-effect:audit overallPass=true。' }; return rules[order] || '对应步骤状态为 pass。'; } function priorityForStep(order, status) { if (order === 3) return 1; if (order === 4) return 2; if (order === 5) return 3; if (order === 12) return 4; if (status === 'fail') return 10 + Number(order || 99); if (status === 'blocked_by_external_data') return 20 + Number(order || 99); return 40 + Number(order || 99); } function gapOrder(id) { const order = { 'intake-readiness': 1, 'historical-dataset': 2, 'video-real-candidate': 3, 'video-ab-live-proof': 4, 'live-provider-overnight-proof': 5, 'manual-review-and-customer-effect': 6 }; return order[id] || 99; } function ownerForGap(id) { if (id === 'video-real-candidate') return '商务/投放'; if (id === 'video-ab-live-proof') return '技术/AI'; if (id.includes('manual') || id.includes('customer') || id.includes('historical')) return '商务'; return '技术/AI'; } function titleForGap(id, fallback) { const titles = { 'intake-readiness': '真实材料预审', 'historical-dataset': '真实历史 Brief 数据集', 'video-real-candidate': '真实视频资源表', 'video-ab-live-proof': '真实视频 A/B 验收', 'live-provider-overnight-proof': '真实 live/provider 长跑证明', 'manual-review-and-customer-effect': '商务复核和客户效果' }; return titles[id] || fallback || id; } function dedupeActions(actions) { const seen = new Set(); const result = []; for (const action of actions.filter(Boolean)) { const key = `${action.owner}|${action.title}|${action.command}`; if (seen.has(key)) continue; seen.add(key); result.push(action); } return result; } function compactBusinessGapActions(actions) { const byId = new Map(actions.map(action => [action.id, action])); mergeActionInto(byId, 'step-03', 'step-05', '统一预审'); mergeActionInto(byId, 'step-03', 'step-06', '转换动作'); mergeActionInto(byId, 'step-03', 'step-07', '验证动作'); mergeActionInto(byId, 'step-04', 'step-08', '验证动作'); mergeActionInto(byId, 'step-12', 'step-14', '验证动作'); mergeActionInto(byId, 'step-12', 'step-13', '复核指标'); mergeActionInto(byId, 'gap-live-provider-overnight-proof', 'step-09', '前置门禁'); mergeActionInto(byId, 'gap-live-provider-overnight-proof', 'step-10', '执行动作'); mergeActionInto(byId, 'gap-video-ab-live-proof', 'step-11', '验收动作'); const result = []; const usedGapKeys = new Set(); for (const action of actions) { if (isMergedTechnicalAction(action.id)) continue; const gapKey = businessGapKey(action); if (gapKey) { if (usedGapKeys.has(gapKey)) continue; usedGapKeys.add(gapKey); } result.push(action); } return result; } function mergeActionInto(byId, targetId, sourceId, label) { const target = byId.get(targetId); const source = byId.get(sourceId); if (!target || !source) return; target.source = mergeSource(target.source, source.source); target.next = appendIssueMessages(target.next, label, [ `${source.title}:${source.next || source.command || source.acceptance || ''}` ]); target.acceptance = mergeText(target.acceptance, source.acceptance); target.missingProofRequirements = mergeList(target.missingProofRequirements, source.missingProofRequirements); if (!target.expectedArtifact.includes(source.expectedArtifact || '__missing__')) { target.expectedArtifact = [target.expectedArtifact, source.expectedArtifact].filter(Boolean).join(';'); } } function mergeSource(left, right) { return Array.from(new Set(String(`${left}+${right}`).split('+').filter(Boolean))).join('+'); } function mergeText(left, right) { if (!right || left.includes(right)) return left; return `${left};${right}`; } function mergeList(left, right) { return Array.from(new Set([ ...(Array.isArray(left) ? left : []), ...(Array.isArray(right) ? right : []) ].filter(Boolean))); } function isMergedTechnicalAction(id) { return ['step-01', 'step-02', 'step-05', 'step-06', 'step-07', 'step-08', 'step-09', 'step-10', 'step-11', 'step-13', 'step-14', 'step-15'].includes(id); } function businessGapKey(action) { if (['step-03', 'gap-historical-dataset'].includes(action.id)) return 'historical-dataset'; if (['step-04', 'gap-video-real-candidate'].includes(action.id)) return 'video-real-candidate'; if (['step-12', 'gap-manual-review-and-customer-effect'].includes(action.id)) return 'manual-review-and-customer-effect'; if (['gap-live-provider-overnight-proof'].includes(action.id)) return 'live-provider-overnight-proof'; if (['gap-video-ab-live-proof'].includes(action.id)) return 'video-ab-live-proof'; return ''; } function renderReport(summary, ownerArtifacts = summary.ownerArtifacts || []) { const lines = [ '# 提号补证下一步行动队列', '', `- 生成时间:${summary.generatedAt}`, `- 是否完成:${summary.complete ? '是' : '否'}`, `- 行动数:${summary.actions.length}`, `- progress.complete:${summary.sourceState.progressComplete ? 'true' : 'false'}`, `- proofGap.openCount:${summary.sourceState.proofGapOpenCount ?? 'unknown'}`, `- intake.failureCount:${summary.sourceState.intakeFailureCount ?? 'unknown'}`, `- video.failureCount:${summary.sourceState.videoFailureCount ?? 'unknown'}`, `- homepage.failureCount:${summary.sourceState.homepageFailureCount ?? 'unknown'}`, `- pipeline.readyForClaim:${summary.sourceState.pipelineReadyForClaim ?? 'unknown'}`, `- pipeline.failCount:${summary.sourceState.pipelineFailCount ?? 'unknown'}`, `- pipeline.nextActionCount:${summary.sourceState.pipelineNextActionCount ?? 'unknown'}`, `- pipeline.rawNextActionCount:${summary.sourceState.pipelineRawNextActionCount ?? 'unknown'}`, `- pipeline.suppressedNextActionCount:${summary.sourceState.pipelineSuppressedNextActionCount ?? 0}`, `- pipeline.stale:${summary.sourceState.pipelineStale ? 'true' : 'false'}`, summary.sourceState.pipelineStaleReason ? `- pipeline.staleReason:${summary.sourceState.pipelineStaleReason}` : '', '', '## 按负责人汇总', '', '| 负责人 | 行动数 | 最高优先级 | 重点动作 |', '| --- | --- | --- | --- |', ...summary.ownerGroups.map(group => `| ${escapeCell(group.owner)} | ${group.actionCount} | ${group.topPriority} | ${escapeCell(group.topTitles.join(';'))} |`), '', '## 负责人文件', '', '| 负责人 | Markdown | CSV | 修复清单附件 |', '| --- | --- | --- | --- |', ...ownerArtifacts.map(item => `| ${escapeCell(item.owner)} | ${escapeCell(item.markdown)} | ${escapeCell(item.csv)} | ${escapeCell((item.repairArtifacts || []).map(artifact => `${artifact.title}:${artifact.csv}`).join(';') || '无')} |`), '', '## 行动队列', '', '同一份业务材料的多个字段缺口会合并到主动作的下一步说明中,避免把“填一张表”拆成多条重复任务。', '', '| 优先级 | 负责人 | 动作 | 来源 | 当前证据 | 下一步说明 | 执行命令 | 预期产物 | 通过标准 |', '| --- | --- | --- | --- | --- | --- | --- | --- | --- |', ...summary.actions.map(action => `| ${action.priority} | ${escapeCell(action.owner)} | ${escapeCell(action.title)} | ${escapeCell(action.source)} | ${escapeCell(action.evidence)} | ${escapeCell(action.next)} | \`${escapeCell(action.command)}\` | ${escapeCell(action.expectedArtifact)} | ${escapeCell(action.acceptance)} |`), '', '## 边界', '', ...summary.guardrails.map(item => `- ${item}`) ]; return lines.join('\n'); } function writeOwnerArtifacts(outputDir, ownerGroups, actions, ownerRepairArtifacts = []) { const ownerDir = path.join(outputDir, 'by-owner'); fs.mkdirSync(ownerDir, { recursive: true }); const artifacts = []; for (const group of ownerGroups) { const ownerActions = actions.filter(action => action.owner === group.owner); const repairArtifacts = ownerRepairArtifacts.filter(item => item.owner === group.owner); const slug = slugOwner(group.owner); const markdownRel = path.join('by-owner', `${slug}.md`).replace(/\\/g, '/'); const csvRel = path.join('by-owner', `${slug}.csv`).replace(/\\/g, '/'); fs.writeFileSync(path.join(outputDir, markdownRel), withBom(renderOwnerReport(group, ownerActions, repairArtifacts)), 'utf8'); fs.writeFileSync(path.join(outputDir, csvRel), withBom(renderCsv(ownerActions)), 'utf8'); artifacts.push({ owner: group.owner, actionCount: group.actionCount, topPriority: group.topPriority, markdown: markdownRel, csv: csvRel, repairArtifacts }); } return artifacts; } function renderOwnerReport(group, actions, repairArtifacts = []) { const lines = [ `# ${group.owner}补证行动`, '', `- 行动数:${group.actionCount}`, `- 最高优先级:${group.topPriority}`, '', '## 修复清单附件', '', repairArtifacts.length ? renderRepairArtifactTable(repairArtifacts) : '- 当前没有单独归属到本负责人的修复清单附件。', '', '## 行动明细', '', '| 优先级 | 动作 | 当前证据 | 下一步说明 | 执行命令 | 预期产物 | 通过标准 |', '| --- | --- | --- | --- | --- | --- | --- |', ...actions.map(action => `| ${action.priority} | ${escapeCell(action.title)} | ${escapeCell(action.evidence)} | ${escapeCell(action.next)} | \`${escapeCell(action.command)}\` | ${escapeCell(action.expectedArtifact)} | ${escapeCell(action.acceptance)} |`), '', '## 边界', '', '- 本文件只用于安排下一步补证,不证明客户效果完成。', '- 不要在补证材料中写入 sessionToken、Authorization、模型 token 或 npm token。' ]; return lines.join('\n'); } function groupByOwner(actions) { const map = new Map(); for (const action of actions) { const owner = action.owner || '待分配'; if (!map.has(owner)) { map.set(owner, { owner, actionCount: 0, topPriority: action.priority, topTitles: [] }); } const group = map.get(owner); group.actionCount += 1; group.topPriority = Math.min(group.topPriority, action.priority); if (group.topTitles.length < 3) group.topTitles.push(action.title); } return Array.from(map.values()).sort((a, b) => a.topPriority - b.topPriority || b.actionCount - a.actionCount); } function buildOwnerRepairActionArtifacts({ root, outputsDir, intake, videoReadiness, homepageReadiness }) { const artifacts = []; addRepairActionArtifacts(artifacts, { root, csv: latestFile(outputsDir, 'intake-readiness-repair-actions.csv'), title: '历史数据/商务复核修复清单', source: 'intake-readiness', repairActions: intake?.repairActions || [] }); addRepairActionArtifacts(artifacts, { root, csv: latestFile(outputsDir, 'video-resource-repair-actions.csv'), title: '视频资源修复清单', source: 'video-resource-readiness', repairActions: videoReadiness?.repairActions || [] }); addRepairActionArtifacts(artifacts, { root, csv: latestFile(outputsDir, 'homepage-evidence-repair-actions.csv'), title: '主页近期内容证据修复清单', source: 'homepage-evidence-readiness', repairActions: homepageReadiness?.repairActions || [] }); return artifacts.sort((a, b) => a.topPriority - b.topPriority || a.owner.localeCompare(b.owner, 'zh-CN')); } function addRepairActionArtifacts(artifacts, { root, csv, title, source, repairActions }) { if (!csv || !fs.existsSync(csv) || !Array.isArray(repairActions) || repairActions.length === 0) return; const byOwner = new Map(); for (const action of repairActions) { const owner = action.owner || '待分配'; if (!byOwner.has(owner)) { byOwner.set(owner, { owner, title, source, csv: path.relative(root, csv).replace(/\\/g, '/'), actionCount: 0, topPriority: Number(action.priority || 999), fields: [] }); } const item = byOwner.get(owner); item.actionCount += 1; item.topPriority = Math.min(item.topPriority, Number(action.priority || 999)); if (action.field && item.fields.length < 5 && !item.fields.includes(action.field)) item.fields.push(action.field); } artifacts.push(...byOwner.values()); } function renderRepairArtifactTable(repairArtifacts) { return [ '| 修复清单 | CSV | 动作数 | 最高优先级 | 重点字段 |', '| --- | --- | ---: | ---: | --- |', ...repairArtifacts.map(item => `| ${escapeCell(item.title)} | ${escapeCell(item.csv)} | ${item.actionCount} | ${item.topPriority} | ${escapeCell(item.fields.join(';') || '见 CSV')} |`) ].join('\n'); } function renderCsv(actions) { const header = ['优先级', '负责人', '动作', '来源', '当前状态', '当前证据', '执行命令', '预期产物', '通过标准', '下一步说明']; const rows = actions.map(action => [ action.priority, action.owner, action.title, action.source, action.status, action.evidence, action.command, action.expectedArtifact, action.acceptance, action.next ]); return [header, ...rows].map(row => row.map(csvCell).join(',')).join('\n'); } function latestFile(outputsDir, fileName) { if (!fs.existsSync(outputsDir)) return ''; return fs.readdirSync(outputsDir, { withFileTypes: true }) .filter(entry => entry.isDirectory()) .map(entry => path.join(outputsDir, entry.name, fileName)) .filter(file => fs.existsSync(file)) .sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs)[0] || ''; } function readJson(file) { if (!file || !fs.existsSync(file)) return null; try { return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '')); } catch { return null; } } function parseArgs(argv) { const args = {}; for (let index = 0; index < argv.length; index += 1) { const raw = argv[index]; if (!raw.startsWith('--')) continue; const key = raw.slice(2).replace(/-([a-z])/g, (_, char) => char.toUpperCase()); const next = argv[index + 1]; if (!next || next.startsWith('--')) args[key] = true; else { args[key] = next; index += 1; } } return args; } function escapeCell(value) { return String(value ?? '').replace(/\|/g, '/').replace(/\r?\n/g, ' '); } function csvCell(value) { const text = String(value ?? ''); return /[",\r\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text; } function slugOwner(owner) { const map = { '商务': 'business', '投放/商务': 'media-business', '商务/投放': 'business-media', '技术/AI': 'tech-ai' }; if (map[owner]) return map[owner]; const ascii = String(owner || 'owner').replace(/[^\w]+/g, '-').replace(/^-|-$/g, '').toLowerCase(); return ascii || Buffer.from(String(owner || 'owner')).toString('hex').slice(0, 12); } function withBom(text) { return `\uFEFF${text}`; } if (require.main === module) main(); module.exports = { buildNextActions, renderReport };