| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786 |
- #!/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 <history-dataset目录>',
- 7: 'npm run history:audit -- --input <history-dataset目录> --output <历史审计输出目录> --strict',
- 8: 'npm run video:resource-readiness -- --input <video-resource-template.csv> --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 <historical-dataset-audit.json> --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: '<history-dataset目录>/*.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
- };
|