#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const { buildLocalSeedMaterialIndex, sanitizeCell, sanitizePublicUrl } = require('./local-seed-material-index'); const ROOT = path.resolve(__dirname, '..'); const DEFAULT_OUTPUT = path.join(ROOT, 'outputs', 'local-seed-to-intake-worklist-latest'); function main() { const args = parseArgs(process.argv.slice(2)); const workspaceRoot = path.resolve(args.workspaceRoot || path.join(ROOT, '..')); const outputsDir = path.resolve(args.outputs || path.join(ROOT, 'outputs')); const outputDir = path.resolve(args.output || DEFAULT_OUTPUT); const seedIndex = buildLocalSeedMaterialIndex({ root: ROOT, workspaceRoot, outputsDir }); const summary = buildLocalSeedToIntakeWorklist({ root: ROOT, seedIndex }); fs.mkdirSync(outputDir, { recursive: true }); const summaryPath = path.join(outputDir, 'local-seed-to-intake-worklist-summary.json'); const reportPath = path.join(outputDir, 'local-seed-to-intake-worklist.md'); const csvPath = path.join(outputDir, 'local-seed-to-intake-worklist.csv'); const historyDraftPath = path.join(outputDir, 'history-intake-draft.csv'); const reviewDraftPath = path.join(outputDir, 'manual-review-draft.csv'); const videoWorklistPath = path.join(outputDir, 'video-resource-worklist.csv'); const candidateWorklistPath = path.join(outputDir, 'candidate-seed-worklist.csv'); fs.writeFileSync(summaryPath, JSON.stringify(summary, null, 2), 'utf8'); fs.writeFileSync(reportPath, withBom(renderReport(summary)), 'utf8'); fs.writeFileSync(csvPath, withBom(renderUnifiedCsv(summary)), 'utf8'); fs.writeFileSync(historyDraftPath, withBom(renderHistoryDraftCsv(summary)), 'utf8'); fs.writeFileSync(reviewDraftPath, withBom(renderReviewDraftCsv(summary)), 'utf8'); fs.writeFileSync(videoWorklistPath, withBom(renderVideoWorklistCsv(summary)), 'utf8'); fs.writeFileSync(candidateWorklistPath, withBom(renderCandidateWorklistCsv(summary)), 'utf8'); console.log(JSON.stringify({ outputDir, summary: summaryPath, report: reportPath, csv: csvPath, historyDraft: historyDraftPath, reviewDraft: reviewDraftPath, videoWorklist: videoWorklistPath, candidateWorklist: candidateWorklistPath, historyDraftRowCount: summary.historyDraftRows.length, reviewDraftRowCount: summary.reviewDraftRows.length, referenceSeedRowCount: summary.referenceSeedRows.length, candidateSeedRowCount: summary.candidateSeedRows.length, canCloseProofGap: summary.canCloseProofGap }, null, 2)); if (args.strict && summary.directCustomerProof !== false) process.exitCode = 1; } function buildLocalSeedToIntakeWorklist({ root, seedIndex }) { const references = extractReferences(seedIndex.materials || []); const candidates = extractCandidates(seedIndex.materials || []); const videoCandidates = candidates.filter(item => /视频/i.test(item.suggestedFormat || '')); const briefSeed = buildDhaBriefSeed(seedIndex, references); const videoWorklistRows = buildVideoWorklistRows({ briefSeed, references, videoCandidates }); const candidateSeedRows = candidates.slice(0, 12).map((item, index) => ({ order: index + 1, briefId: briefSeed?.briefId || 'dha-xhs-mom-baby-seed-001', platform: '小红书', creatorName: item.creatorName, xhsId: item.xhsId, sourceSheet: item.sourceSheet, sourceWorkbook: item.sourceWorkbook, recommendationLevel: item.recommendationLevel, region: item.region, fans: item.fans, imageQuote: item.imageQuote, videoQuote: item.videoQuote, suggestedFormat: item.suggestedFormat, suggestedQuote: item.suggestedQuote, readMedian: item.readMedian, interactionMedian: item.interactionMedian, readUnitCost: item.readUnitCost, targetFile: 'outputs/data-intake-pack-latest/manual-review-template.csv', missingForProof: [ '主页链接或真实可访问 profile URL', '商务人工复核标签', '客户最终选中/拒绝', '拒绝原因或负样本归因', '本轮人工补号量' ] })); const historyDraftRows = buildHistoryDraftRows({ briefSeed, candidateSeedRows }); const reviewDraftRows = buildReviewDraftRows({ briefSeed, candidateSeedRows }); const proofGapMaterialBridge = buildProofGapMaterialBridge({ historyDraftRows, reviewDraftRows, referenceSeedRows: references, candidateSeedRows, videoWorklistRows, videoCandidates }); return { generatedAt: new Date().toISOString(), root, proofLevel: 'not_business_proof', materialType: 'seed_to_intake_worklist', directCustomerProof: false, canCloseProofGap: false, complete: false, sourceSeedIndex: { proofLevel: seedIndex.proofLevel || 'not_business_proof', directCustomerProof: Boolean(seedIndex.directCustomerProof), canCloseProofGap: Boolean(seedIndex.canCloseProofGap), materialCount: Number(seedIndex.materialCount || 0), existingMaterialCount: Number(seedIndex.existingMaterialCount || 0) }, counts: { historyDraftRows: historyDraftRows.length, reviewDraftRows: reviewDraftRows.length, referenceSeedRows: references.length, candidateSeedRows: candidateSeedRows.length, videoWorklistRows: videoWorklistRows.length, videoCandidateNeedsResourceRows: videoCandidates.length }, historyDraftRows, reviewDraftRows, referenceSeedRows: references, candidateSeedRows, videoWorklistRows, proofGapMaterialBridge, missingProofRequirements: [ '至少 5 个真实历史 Brief', '历史人工补号量基线', '人工最终名单中的真实博主名称和主页链接', '客户最终选中/拒绝记录', '拒绝原因或商务复核标签', '至少 1 个真实候选视频 URL、封面、ASR/字幕、帧图或正文证据', '本轮人工补号量', 'history:audit readyForCustomerEffectProof=true', 'video:resource-readiness readyForVideoAbPreflight=true', 'customer-effect:audit overallPass=true' ], targetFiles: [ 'outputs/data-intake-pack-latest/history-data-template.csv', 'outputs/video-intake-pack-latest/video-resource-template.csv', 'outputs/data-intake-pack-latest/manual-review-template.csv' ], guardrails: [ '本 worklist 只把本地 DHA Brief、PoC 候选和参考账号解析转成补表草稿,不写入正式 intake 模板。', '空字段表示仍需商务/投放补真实材料;不得用“未知”“0”或示例值补齐客户选择、拒绝原因或人工补号量。', '候选账号来自 PoC/参考分析种子,不等同于人工最终名单或客户选中名单。', '参考笔记链接和候选账号只能作为线索;缺真实候选视频 URL、封面、ASR/字幕或帧图时,不得启动视频 A/B 证明。', '本 worklist 的 proofLevel 固定为 not_business_proof,不能关闭 proof-gap。' ], nextActions: [ { owner: '商务', title: '把 history-intake-draft.csv 扩成 5-10 个真实 Brief', action: '保留 DHA 行作为本地种子草稿,同时补齐其他真实历史 Brief、历史人工补号量基线、人工最终名单和客户选择/拒绝。', acceptance: 'intake:readiness historyReady=true,history:audit readyForCustomerEffectProof=true。' }, { owner: '商务/投放', title: '按 video-resource-worklist.csv 补真实候选视频', action: '优先为建议形式为视频的候选补视频链接、封面、ASR/字幕、帧图或正文证据。', acceptance: 'video:resource-readiness readyForVideoAbPreflight=true,realCandidateRows>=1。' }, { owner: '商务', title: '按 candidate-seed-worklist.csv 做人工复核', action: '只把候选种子作为复核池,补真实主页链接、人工复核标签、客户选择、拒绝原因和本轮人工补号量。', acceptance: 'review:metrics 与 customer-effect:audit 在真实标注后通过。' } ] }; } function buildDhaBriefSeed(seedIndex, references) { const briefMaterial = (seedIndex.materials || []).find(item => item.id === 'dha-brief-workbook' && item.exists); if (!briefMaterial) return null; const referenceLinks = references .filter(item => item.finalLink && !/\/404\b/.test(item.finalLink)) .slice(0, 4) .map(item => item.finalLink); return { briefId: 'dha-xhs-mom-baby-seed-001', projectName: 'DHA 小红书母婴提号', category: '母婴', customerBrief: [ '平台:小红书', '产品:DHA', '账号类型:干货科普(营养师/育婴师/妈妈为主)、618合集(6个月-7岁孩子妈妈博主)', '预算:1000-5000', '粉丝画像:女性为主,25-44岁占比70%以上;兴趣需命中母婴/萌娃/记录生活中至少 2 个' ].join(';'), historicalManualSupplementBaseline: '', referenceAccountOrVideo: referenceLinks.join('|'), platform: '小红书', manualFinalCreatorName: '', manualFinalProfileUrl: '', manualReviewLabel: '', customerDecision: '', rejectReason: '', sourcePath: briefMaterial.path, canWritePartialFields: true, missingForProof: [ '历史人工补号量基线', '人工最终名单博主名称', '人工最终名单主页链接', '客户最终选中/拒绝', '拒绝原因或商务复核标签', '仍需至少 4 个额外真实 Brief 才达到 minBriefs=5' ] }; } function buildHistoryDraftRows({ briefSeed, candidateSeedRows }) { if (!briefSeed) return []; const rows = (candidateSeedRows || []).slice(0, 12).map(candidate => ({ ...briefSeed, manualFinalCreatorName: candidate.creatorName || '', manualFinalProfileUrl: profileHint(candidate.xhsId), manualReviewLabel: '', customerDecision: '', rejectReason: '', sourcePath: `${candidate.sourceWorkbook} / ${candidate.sourceSheet}`, candidateSeedLevel: candidate.recommendationLevel || '', missingForProof: [ '客户最终选中/拒绝记录', '拒绝原因或商务复核标签', '历史人工补号量基线', '真实主页链接复核', '仍需至少 5 个真实历史 Brief 才能满足 minBriefs=5' ] })); return rows.length ? rows : [briefSeed]; } function buildReviewDraftRows({ briefSeed, candidateSeedRows }) { const briefId = briefSeed?.briefId || 'dha-xhs-mom-baby-seed-001'; return (candidateSeedRows || []).map((candidate, index) => ({ briefId, strategy: strategyFromCandidate(candidate), rank: index + 1, platform: candidate.platform || '小红书', creatorName: candidate.creatorName || '', totalScore: '', briefMatchScore: '', referenceStyleScore: '', homepageEvidenceScore: '', visualQualityScore: '', toneFitScore: '', evidenceBonus: '', evidenceRiskPenalty: '', reason: `PoC候选:${candidate.recommendationLevel || '待复核'};建议形式:${candidate.suggestedFormat || '待补'};建议报价:${candidate.suggestedQuote || '待补'}`, risk: '需商务复核真实主页链接、报价有效期、内容调性、客户最终选择和本轮人工补号量。', profileUrl: profileHint(candidate.xhsId), manualReviewLabel: '', customerDecision: '', attributionType: '', feedbackReason: '', currentManualSupplementCount: '', source: `${candidate.sourceWorkbook} / ${candidate.sourceSheet}`, proofBoundary: 'seed_only_not_customer_effect_proof' })); } function buildProofGapMaterialBridge({ historyDraftRows, reviewDraftRows, referenceSeedRows, candidateSeedRows, videoWorklistRows, videoCandidates }) { return [ { id: 'historical-dataset', materialFound: historyDraftRows.length > 0, connectedDraft: 'history-intake-draft.csv', draftRowCount: historyDraftRows.length, canCloseProofGap: false, whyNotClosed: [ '当前只接入 DHA/PoC 种子,未达到 5 个真实历史 Brief。', '客户选择/拒绝、拒绝原因和历史人工补号量基线仍为空,不能通过 readyForCustomerEffectProof。' ] }, { id: 'video-ab-live-proof', materialFound: referenceSeedRows.length > 0 || videoWorklistRows.length > 0, connectedDraft: 'video-resource-worklist.csv', referenceSeedRowCount: referenceSeedRows.length, videoCandidateSeedRowCount: videoCandidates.length, canCloseProofGap: false, whyNotClosed: [ '资源线索不等于 acceptance:video-ab 的 live proofContext。', '仍需要 live runtime credential、company、VOC social provider token、video analysis token 后运行 npm run acceptance:video-ab。' ] }, { id: 'manual-review-and-customer-effect', materialFound: candidateSeedRows.length > 0 || reviewDraftRows.length > 0, connectedDraft: 'manual-review-draft.csv', draftRowCount: reviewDraftRows.length, canCloseProofGap: false, whyNotClosed: [ '候选种子已转成商务复核草表,但人工复核标签、客户选择、归因、反馈原因和本轮人工补号量仍需真实填写。', 'customer-effect:audit 还必须绑定通过的历史审计 JSON。' ] } ]; } function profileHint(xhsId) { return xhsId ? `小红书号:${xhsId}` : ''; } function strategyFromCandidate(candidate) { const source = `${candidate.sourceWorkbook || ''} ${candidate.sourceSheet || ''}`.toLowerCase(); if (source.includes('参考分析')) return 'reference-account'; if (/主推|main|strong/i.test(candidate.recommendationLevel || candidate.sourceSheet || '')) return 'result-first-broad'; return 'candidate-seed'; } function extractReferences(materials) { const rows = []; for (const material of materials || []) { for (const sheet of material.workbook?.sheets || []) { if (!/参考.*(解析|链接)/.test(sheet.name || '')) continue; for (const row of sheet.sampleRows || []) { const referenceType = clean(row[0]); const shortLink = sanitizePublicUrl(clean(row[1])); const noteId = clean(row[2]); const authorName = clean(row[3]); const xhsId = clean(row[5]); const status = clean(row[8]); const finalLink = sanitizePublicUrl(clean(row[9])); if (!referenceType && !shortLink && !finalLink) continue; rows.push({ referenceType, shortLink, noteId, authorName, xhsId, status, finalLink, sourceWorkbook: material.path, sourceSheet: sheet.name, usableFor: /已解析/.test(status) && finalLink && !/\/404\b/.test(finalLink) ? '可作为参考账号/参考内容线索转写' : '需人工重新打开短链或替换为可访问参考资源', missingForProof: [ '是否真实参考视频需投放复核', '不能替代真实候选视频', '不能替代客户选择或人工补号量' ] }); } } } return uniqueBy(rows, item => item.finalLink || item.shortLink || `${item.referenceType}:${item.authorName}`); } function extractCandidates(materials) { const rows = []; for (const material of materials || []) { for (const sheet of material.workbook?.sheets || []) { if (!/主推账号|备选/.test(sheet.name || '')) continue; for (const row of sheet.sampleRows || []) { const creatorName = clean(row[1]); if (!creatorName) continue; rows.push({ recommendationLevel: clean(row[0]), creatorName, xhsId: clean(row[2]), region: clean(row[3]), fans: clean(row[4]), imageQuote: clean(row[5]), videoQuote: clean(row[6]), suggestedFormat: clean(row[7]), suggestedQuote: clean(row[8]), readMedian: clean(row[9]), interactionMedian: clean(row[10]), readUnitCost: clean(row[11]), sourceWorkbook: material.path, sourceSheet: sheet.name }); } } } return uniqueBy(rows, item => item.xhsId || item.creatorName); } function buildVideoWorklistRows({ briefSeed, references, videoCandidates }) { const briefId = briefSeed?.briefId || 'dha-xhs-mom-baby-seed-001'; const referenceRows = references .filter(item => /video|视频/i.test(item.referenceType || '') || /视频/.test(item.usableFor || '')) .map((item, index) => ({ order: index + 1, briefId, projectName: briefSeed?.projectName || 'DHA 小红书母婴提号', platform: '小红书', role: '参考视频', creatorName: item.authorName, profileOrHomepage: item.xhsId ? `小红书号:${item.xhsId}` : '', videoLink: item.finalLink || item.shortLink, source: `${item.sourceWorkbook} / ${item.sourceSheet}`, resourceStatus: item.finalLink && !/\/404\b/.test(item.finalLink) ? '参考线索可访问,需投放复核是否为真实视频资源' : '短链或最终链接不可用,需替换', requiredFill: '补封面、ASR/字幕、帧图、标题、发布时间、内容摘要和真实资源确认', canMarkRealResourceNow: false })); const candidateRows = videoCandidates.map((item, index) => ({ order: referenceRows.length + index + 1, briefId, projectName: briefSeed?.projectName || 'DHA 小红书母婴提号', platform: '小红书', role: '候选视频', creatorName: item.creatorName, profileOrHomepage: item.xhsId ? `小红书号:${item.xhsId}` : '', videoLink: '', source: `${item.sourceWorkbook} / ${item.sourceSheet}`, resourceStatus: '候选账号种子,缺真实候选视频 URL', requiredFill: '补真实候选视频 URL、封面、ASR/字幕、帧图或正文证据后,才能在 video-resource-template.csv 标为真实资源', canMarkRealResourceNow: false })); return [...referenceRows, ...candidateRows]; } function renderReport(summary) { return [ '# 本地种子转 intake 补表 worklist', '', `- 生成时间:${summary.generatedAt}`, `- proofLevel:${summary.proofLevel}`, `- materialType:${summary.materialType}`, `- directCustomerProof:${summary.directCustomerProof}`, `- canCloseProofGap:${summary.canCloseProofGap}`, `- 历史草稿行:${summary.counts.historyDraftRows}`, `- 商务复核草表行:${summary.counts.reviewDraftRows}`, `- 参考线索:${summary.counts.referenceSeedRows}`, `- 候选种子:${summary.counts.candidateSeedRows}`, `- 视频补表行:${summary.counts.videoWorklistRows}`, '', '## 边界', '', ...summary.guardrails.map(item => `- ${item}`), '', '## 目标文件', '', ...summary.targetFiles.map(item => `- ${item}`), '', '## 历史数据草稿', '', '| brief编号 | 项目 | 类目 | 参考链接 | 可直接转写 | 仍缺证明 |', '| --- | --- | --- | --- | --- | --- |', ...summary.historyDraftRows.map(item => tableRow([ item.briefId, item.projectName, item.category, item.referenceAccountOrVideo || '缺失', item.canWritePartialFields, item.missingForProof.join(';') ])), '', '## 参考账号/内容线索', '', '| 类型 | 作者 | 状态 | 最终链接 | 来源 | 用途 | 仍缺证明 |', '| --- | --- | --- | --- | --- | --- | --- |', ...summary.referenceSeedRows.map(item => tableRow([ item.referenceType, item.authorName || '缺失', item.status || '缺失', item.finalLink || item.shortLink || '缺失', `${item.sourceWorkbook} / ${item.sourceSheet}`, item.usableFor, item.missingForProof.join(';') ])), '', '## 候选种子', '', '| 序号 | 账号 | 红书号 | 推荐等级 | 建议形式 | 报价 | 来源 | 仍缺证明 |', '| ---: | --- | --- | --- | --- | --- | --- | --- |', ...summary.candidateSeedRows.map(item => tableRow([ item.order, item.creatorName, item.xhsId, item.recommendationLevel, item.suggestedFormat, item.suggestedQuote, `${item.sourceWorkbook} / ${item.sourceSheet}`, item.missingForProof.join(';') ])), '', '## 视频资源补表清单', '', '| 序号 | 角色 | 博主 | 视频链接 | 当前状态 | 需补字段 | 可标真实资源 |', '| ---: | --- | --- | --- | --- | --- | --- |', ...summary.videoWorklistRows.map(item => tableRow([ item.order, item.role, item.creatorName || '缺失', item.videoLink || '缺失', item.resourceStatus, item.requiredFill, item.canMarkRealResourceNow ])), '', '## Proof Gap Material Bridge', '', '| Gap | Material found | Connected draft | Rows | Can close | Why not closed |', '| --- | --- | --- | ---: | --- | --- |', ...summary.proofGapMaterialBridge.map(item => tableRow([ item.id, item.materialFound, item.connectedDraft, item.draftRowCount ?? item.referenceSeedRowCount ?? 0, item.canCloseProofGap, item.whyNotClosed.join('; ') ])), '', '## 仍缺证明', '', ...summary.missingProofRequirements.map(item => `- ${item}`), '', '## 下一步', '', '| 负责人 | 动作 | 验收 |', '| --- | --- | --- |', ...summary.nextActions.map(item => tableRow([item.owner, `${item.title}:${item.action}`, item.acceptance])), '' ].join('\n'); } function renderUnifiedCsv(summary) { const rows = [ ['类型', '负责人', '目标文件', '字段/对象', '建议值/动作', '来源', '可直接转写', '仍缺证明', '边界'], ...summary.historyDraftRows.flatMap(item => [ ['history-draft', '商务', 'outputs/data-intake-pack-latest/history-data-template.csv', '客户原始Brief', item.customerBrief, item.sourcePath, '是', item.missingForProof.join(';'), '不是业务证明'], ['history-draft', '商务', 'outputs/data-intake-pack-latest/history-data-template.csv', '历史人工补号量基线', '补真实历史人工补号量基线', item.sourcePath, '否', '历史人工补号量基线', '不得用 0 或示例值代替'], ['history-draft', '商务', 'outputs/data-intake-pack-latest/history-data-template.csv', '客户选择/拒绝原因', '补客户最终选中/拒绝和拒绝原因', item.sourcePath, '否', '客户最终选择和拒绝原因', '不得用主观判断代替客户反馈'] ]), ...summary.reviewDraftRows.map(item => [ 'manual-review-draft', '商务', 'outputs/data-intake-pack-latest/manual-review-template.csv', item.creatorName, '补人工复核标签、客户选择、归因类型、反馈原因和本轮人工补号量', item.source, '部分字段可转写', '客户选择、归因、反馈原因、本轮人工补号量', '候选草表不是客户效果证明' ]), ...summary.videoWorklistRows.map(item => [ 'video-resource-worklist', item.role === '候选视频' ? '商务/投放' : '商务/投放', 'outputs/video-intake-pack-latest/video-resource-template.csv', `${item.role}:${item.creatorName}`, item.requiredFill, item.source, item.videoLink ? '部分字段可转写' : '否', '真实视频 URL、封面、ASR/字幕、帧图或正文证据', '补齐前不得标为真实候选视频' ]), ...summary.candidateSeedRows.map(item => [ 'candidate-seed', '商务', 'outputs/data-intake-pack-latest/manual-review-template.csv', item.creatorName, `复核候选:${item.recommendationLevel},建议形式 ${item.suggestedFormat || '缺失'}`, `${item.sourceWorkbook} / ${item.sourceSheet}`, '部分字段可转写', item.missingForProof.join(';'), '候选种子不是人工最终名单或客户选择' ]) ]; return rows.map(row => row.map(csvCell).join(',')).join('\n'); } function renderHistoryDraftCsv(summary) { const rows = [ ['brief编号', '项目名称', '类目', '客户原始Brief', '历史人工补号量基线', '参考账号或视频', '平台', '博主名称', '主页链接', '人工复核标签', '客户选择', '拒绝原因'], ...summary.historyDraftRows.map(item => [ item.briefId, item.projectName, item.category, item.customerBrief, item.historicalManualSupplementBaseline, item.referenceAccountOrVideo, item.platform, item.manualFinalCreatorName, item.manualFinalProfileUrl, item.manualReviewLabel, item.customerDecision, item.rejectReason ]) ]; return rows.map(row => row.map(csvCell).join(',')).join('\n'); } function renderReviewDraftCsv(summary) { const rows = [ ['brief编号', '策略', '排名', '平台', '博主名称', '综合分', 'brief匹配分', '参考风格分', '主页证据分', '视觉质感分', '调性一致分', '证据加分', '证据风险扣分', '推荐理由', '风险提示', '主页链接', '人工复核标签', '客户选择', '归因类型', '反馈原因', '本轮人工补号量', '来源', '证明边界'], ...summary.reviewDraftRows.map(item => [ item.briefId, item.strategy, item.rank, item.platform, item.creatorName, item.totalScore, item.briefMatchScore, item.referenceStyleScore, item.homepageEvidenceScore, item.visualQualityScore, item.toneFitScore, item.evidenceBonus, item.evidenceRiskPenalty, item.reason, item.risk, item.profileUrl, item.manualReviewLabel, item.customerDecision, item.attributionType, item.feedbackReason, item.currentManualSupplementCount, item.source, item.proofBoundary ]) ]; return rows.map(row => row.map(csvCell).join(',')).join('\n'); } function renderVideoWorklistCsv(summary) { const rows = [ ['序号', 'brief编号', '项目名称', '平台', '资源角色', '博主名称', '主页或红书号', '视频链接', '当前状态', '需补字段', '来源', '是否可直接标真实资源'], ...summary.videoWorklistRows.map(item => [ item.order, item.briefId, item.projectName, item.platform, item.role, item.creatorName, item.profileOrHomepage, item.videoLink, item.resourceStatus, item.requiredFill, item.source, item.canMarkRealResourceNow ? '是' : '否' ]) ]; return rows.map(row => row.map(csvCell).join(',')).join('\n'); } function renderCandidateWorklistCsv(summary) { const rows = [ ['序号', 'brief编号', '平台', '账号', '红书号', '推荐等级', '地区', '粉丝数', '图文报价', '视频报价', '建议形式', '建议报价', '阅读中位', '互动中位', '阅读单价', '来源', '仍缺证明'], ...summary.candidateSeedRows.map(item => [ item.order, item.briefId, item.platform, item.creatorName, item.xhsId, item.recommendationLevel, item.region, item.fans, item.imageQuote, item.videoQuote, item.suggestedFormat, item.suggestedQuote, item.readMedian, item.interactionMedian, item.readUnitCost, `${item.sourceWorkbook} / ${item.sourceSheet}`, item.missingForProof.join(';') ]) ]; return rows.map(row => row.map(csvCell).join(',')).join('\n'); } function clean(value) { return sanitizeCell(value == null ? '' : String(value)); } function uniqueBy(rows, keyFn) { const seen = new Set(); const result = []; for (const row of rows) { const key = keyFn(row); if (!key || seen.has(key)) continue; seen.add(key); result.push(row); } return result; } function parseArgs(argv) { const result = {}; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; if (!arg.startsWith('--')) continue; const key = arg.slice(2).replace(/-([a-z])/g, (_, char) => char.toUpperCase()); const next = argv[index + 1]; if (!next || next.startsWith('--')) result[key] = true; else { result[key] = next; index += 1; } } return result; } function withBom(text) { return `\uFEFF${text}`; } function tableRow(values) { return `| ${values.map(value => escapeCell(value)).join(' | ')} |`; } function escapeCell(value) { return String(value ?? '').replace(/\|/g, '\\|').replace(/\r?\n/g, '
'); } function csvCell(value) { return `"${String(value ?? '').replace(/"/g, '""')}"`; } if (require.main === module) main(); module.exports = { buildLocalSeedToIntakeWorklist, extractReferences, extractCandidates, buildVideoWorklistRows };