#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const { runTihaoSourcing } = require('../mcp/src/features/tihao-sourcing/sourcing-workflow'); const { redactSecrets } = require('../mcp/src/core/result-envelope'); const STATUS_STRONG = '\u5f3a\u63a8\u8350'; const STATUS_BACKUP = '\u5907\u9009'; const STATUS_REVIEW = '\u9700\u590d\u6838'; const STATUS_EXCLUDED = '\u5df2\u5254\u9664'; async function main() { const root = path.resolve(__dirname, '..'); const fixturesDir = path.resolve(process.env.TIHAO_QUALITY_FIXTURES || path.join(root, 'fixtures', 'quality-briefs')); const fixtures = loadFixtures(fixturesDir); if (!fixtures.length) throw new Error(`No quality fixtures found in ${fixturesDir}`); const credentials = readCredentials(); const liveEnabled = process.env.TIHAO_OVERNIGHT_LIVE === 'false' ? false : process.env.TIHAO_OVERNIGHT_LIVE === 'true' || Boolean(credentials.runtimeCredential && credentials.company); const outputRoot = path.resolve(process.env.TIHAO_OVERNIGHT_OUTPUT || path.join(root, 'outputs', `overnight-quality-${Date.now()}`)); fs.mkdirSync(outputRoot, { recursive: true }); const selectedFixtureIds = csvSet(process.env.TIHAO_OVERNIGHT_FIXTURES); const selectedVariantIds = csvSet(process.env.TIHAO_OVERNIGHT_VARIANTS); const activeFixtures = selectedFixtureIds.size ? fixtures.filter(item => selectedFixtureIds.has(item.id)) : fixtures; const variants = buildVariants({ liveEnabled, credentials }).filter(item => !selectedVariantIds.size || selectedVariantIds.has(item.id)); const options = readRunOptions({ liveEnabled, credentials }); const manifest = { startedAt: new Date().toISOString(), fixturesDir, outputRoot, liveEnabled, videoAvailable: Boolean(liveEnabled && credentials.vocSocialToken && credentials.videoToken), fixtureCount: activeFixtures.length, variants: variants.map(item => ({ id: item.id, description: item.description, gateRole: item.gateRole || 'release' })), options: publicRunOptions(options) }; writeJson(path.join(outputRoot, 'manifest.json'), manifest); const runs = []; const failures = []; let attemptedRuns = 0; for (const fixture of activeFixtures) { for (const variant of variants) { if (options.maxRuns && attemptedRuns >= options.maxRuns) { process.stderr.write(`overnight-quality: max runs reached (${options.maxRuns})\n`); break; } const runDir = path.join(outputRoot, 'runs', fixture.id, variant.id); fs.mkdirSync(runDir, { recursive: true }); const summaryFile = path.join(runDir, 'quality-summary.json'); if (options.resume && fs.existsSync(summaryFile)) { process.stderr.write(`overnight-quality: resume ${fixture.id}/${variant.id}\n`); runs.push({ ...JSON.parse(fs.readFileSync(summaryFile, 'utf8')), gateRole: variant.gateRole || 'release' }); continue; } process.stderr.write(`overnight-quality: ${fixture.id}/${variant.id}\n`); attemptedRuns += 1; try { const startedAt = Date.now(); const input = buildRunInput({ fixture, variant, runDir, credentials }); const result = await runSourcingWithRetries(input, options.runRetries); const summary = summarizeRun({ fixture, variant, result, runDir, durationMs: Date.now() - startedAt }); runs.push(summary); writeJson(summaryFile, summary); } catch (error) { const failure = { fixtureId: fixture.id, variant: variant.id, message: redactSecrets(error && error.stack ? error.stack : String(error)) }; failures.push(failure); writeJson(path.join(runDir, 'failure.json'), failure); } if (options.delayMs > 0) await sleep(options.delayMs); } if (options.maxRuns && attemptedRuns >= options.maxRuns) break; } const aggregate = buildAggregate({ manifest, runs, failures, fixtures: activeFixtures, variants, options }); writeJson(path.join(outputRoot, 'aggregate-summary.json'), aggregate); writeJson(path.join(outputRoot, 'failures.json'), failures); writeText(path.join(outputRoot, 'aggregate-report.md'), renderAggregateReportClean(aggregate)); writeText(path.join(outputRoot, 'manual-review-sample.csv'), buildManualReviewSample(runs)); scanForLeaks(outputRoot, credentials.secrets); if (options.failOnGates && !aggregate.acceptance.overallPass) { process.exitCode = 2; } console.log(redactSecrets(JSON.stringify({ outputRoot, liveEnabled, fixtures: activeFixtures.length, variants: variants.length, runs: runs.length, failures: failures.length, gatePass: aggregate.acceptance.overallPass, aggregateReport: path.join(outputRoot, 'aggregate-report.md') }, null, 2))); } function readCredentials() { const runtimeCredential = process.env.TIHAO_SESSION_TOKEN || process.env.VOC_ECOMMERCE_TOKEN || process.env.VOC_TOKEN || ''; const company = process.env.TIHAO_COMPANY || process.env.VOC_ECOMMERCE_COMPANY || process.env.COMPANY_OBJECT_ID || ''; const vocSocialToken = process.env.VOC_SOCIAL_TOKEN || runtimeCredential; const videoToken = process.env.VIDEO_ANALYSIS_TOKEN || process.env.DOUBAO_VISION_TOKEN || ''; return { runtimeCredential, company, vocSocialToken, videoToken, videoBaseUrl: process.env.VIDEO_ANALYSIS_BASE_URL || process.env.DOUBAO_VISION_BASE_URL || 'https://api.fmode.cn', videoModel: process.env.VIDEO_ANALYSIS_MODEL || process.env.DOUBAO_VISION_MODEL || 'doubao-seed-2-0-pro', secrets: [runtimeCredential, vocSocialToken, videoToken].filter(Boolean) }; } function readRunOptions({ liveEnabled, credentials }) { const videoAvailable = Boolean(liveEnabled && credentials.vocSocialToken && credentials.videoToken); return { maxRuns: intEnv('TIHAO_OVERNIGHT_MAX_RUNS', 0), delayMs: intEnv('TIHAO_OVERNIGHT_DELAY_MS', liveEnabled ? 1500 : 0), runRetries: intEnv('TIHAO_OVERNIGHT_RUN_RETRIES', liveEnabled ? 1 : 0), resume: process.env.TIHAO_OVERNIGHT_RESUME === 'true', failOnGates: process.env.TIHAO_OVERNIGHT_FAIL_ON_GATES === 'true', gates: { minEvidenceCoverage: numberEnv('TIHAO_GATE_MIN_EVIDENCE_COVERAGE', videoAvailable ? 0.8 : 0), maxNegativeRiskRate: numberEnv('TIHAO_GATE_MAX_NEGATIVE_RISK_RATE', numberEnv('TIHAO_GATE_MAX_OFF_TOPIC_RATE', 0.1)), maxScoreDrop: numberEnv('TIHAO_GATE_MAX_SCORE_DROP', liveEnabled ? 1 : 999), requireLiveRecall: process.env.TIHAO_GATE_REQUIRE_LIVE_RECALL === 'false' ? false : liveEnabled, requireReferenceProvider: process.env.TIHAO_GATE_REQUIRE_REFERENCE_PROVIDER === 'true', requireHomepageProvider: process.env.TIHAO_GATE_REQUIRE_HOMEPAGE_PROVIDER === 'true', requireResultFirstNoStrongDrop: process.env.TIHAO_GATE_REQUIRE_NO_STRONG_DROP === 'false' ? false : true } }; } function publicRunOptions(options) { return { maxRuns: options.maxRuns, delayMs: options.delayMs, runRetries: options.runRetries, resume: options.resume, failOnGates: options.failOnGates, gates: options.gates }; } async function runSourcingWithRetries(input, retries) { let lastResult = null; for (let attempt = 0; attempt <= retries; attempt++) { lastResult = await runTihaoSourcing(input); if (!shouldRetryRun(lastResult) || attempt === retries) return lastResult; await sleep(1000 * (attempt + 1)); } return lastResult; } function shouldRetryRun(result) { return result && ['live_network_error'].includes(result.status); } function buildVariants({ liveEnabled, credentials }) { const mode = liveEnabled ? 'live' : 'sample'; const canUseVideo = Boolean(liveEnabled && credentials.vocSocialToken && credentials.videoToken); const canUseReference = Boolean(liveEnabled && credentials.vocSocialToken); return [ { id: 'baseline-live', gateRole: 'baseline', description: liveEnabled ? 'Brief-only low-volume live baseline' : 'Sample-mode baseline', input: { collectionMode: mode, keywordLimit: 1, pagesPerKeyword: 1 } }, { id: 'reference-account', gateRole: 'release', description: 'Reference account/link enrichment and similarity-aware recall', input: { collectionMode: mode, keywordLimit: liveEnabled ? 2 : 1, pagesPerKeyword: 1, enableReferenceEnrichment: true, enableVocSocialReferenceEnrichment: canUseReference, useVocSocialReferenceEnrichment: canUseReference, referencePostsLimit: 20 } }, { id: 'homepage-evidence', gateRole: 'release', description: 'Recent homepage content evidence and quality scoring', input: { collectionMode: mode, keywordLimit: liveEnabled ? 2 : 1, pagesPerKeyword: 1, homepageEvidenceLimit: 20, recentContentsLimit: 20, homepageEvidenceCreatorsLimit: liveEnabled ? 20 : 8 } }, { id: 'video-enhanced', gateRole: 'diagnostic', description: 'Reference video enrichment with bounded evidence analysis', input: { collectionMode: mode, keywordLimit: liveEnabled ? 2 : 1, pagesPerKeyword: 1, evidenceCreatorsLimit: liveEnabled ? 10 : 6, resultFirstMode: liveEnabled, optimizeForResults: liveEnabled, enableVocSocialReferenceEnrichment: canUseVideo, videoEnabled: canUseVideo } }, { id: 'result-first', gateRole: 'release', description: 'Quality-first broad recall and batched evidence', input: { collectionMode: mode, keywordLimit: liveEnabled ? 2 : 1, pagesPerKeyword: 1, evidenceCreatorsLimit: liveEnabled ? 10 : 6, resultFirstMode: liveEnabled, optimizeForResults: liveEnabled, enableVocSocialReferenceEnrichment: canUseVideo, videoEnabled: canUseVideo } }, { id: 'result-first-risk', gateRole: 'diagnostic', description: 'Quality-first mode with evidence risk visibility', input: { collectionMode: mode, keywordLimit: liveEnabled ? 2 : 1, pagesPerKeyword: 1, evidenceCreatorsLimit: liveEnabled ? 10 : 6, resultFirstMode: liveEnabled, optimizeForResults: liveEnabled, evidenceBatchSize: 5, enableVocSocialReferenceEnrichment: canUseVideo, videoEnabled: canUseVideo } }, { id: 'result-first-broad', gateRole: 'release', description: 'Quality-first mode with broader live recall', input: { collectionMode: mode, keywordLimit: liveEnabled ? 4 : 1, pagesPerKeyword: liveEnabled ? 2 : 1, resultFirstMode: liveEnabled, optimizeForResults: liveEnabled, evidenceBatchSize: 5, enableVocSocialReferenceEnrichment: canUseVideo, videoEnabled: canUseVideo } } ]; } function buildRunInput({ fixture, variant, runDir, credentials }) { const input = { ...variant.input, ...(fixture.input || {}), briefText: fixture.briefText || '', referenceLinks: fixture.referenceLinks || [], targetCount: fixture.expected?.targetCount, output: runDir }; if (variant.input.collectionMode === 'live') { input.tihaoToken = credentials.runtimeCredential; input.company = credentials.company; } if (variant.input.videoEnabled) { input.vocSocialToken = credentials.vocSocialToken; input.videoAnalysisBaseUrl = credentials.videoBaseUrl; input.videoAnalysisModel = credentials.videoModel; input.videoAnalysisToken = credentials.videoToken; } delete input.videoEnabled; return input; } function summarizeRun({ fixture, variant, result, runDir, durationMs }) { const data = result.data || {}; const criteria = data.criteria || {}; const candidates = data.candidates || []; const top10 = candidates.slice(0, 10); const recallRecords = criteria.recallRecords || []; const offTopic = top10.filter(item => hasOffTopicHit(item, fixture.expected?.offTopicTerms || [])); const negativeRiskRate = ratio(offTopic.length, Math.max(top10.length, 1)); const evidenceHitCount = top10.filter(item => item.evidenceCard || (item.evidenceSignals || []).length).length; const softwareTable = inspectSoftwareTable(path.join(runDir, 'tihao-sourcing-client-list.csv')); return { briefId: fixture.id, briefName: fixture.name, variant: variant.id, gateRole: variant.gateRole || 'release', status: result.status, durationMs, recallRequestCount: recallRecords.length, liveRecallCount: recallRecords.reduce((sum, item) => sum + Number(item.normalizedCount || 0), 0), uniqueCandidateCount: candidates.length, clientListCount: candidates.length, strongCount: countStatus(candidates, STATUS_STRONG), backupCount: countStatus(candidates, STATUS_BACKUP), reviewCount: countStatus(candidates, STATUS_REVIEW), excludedCount: countStatus(candidates, STATUS_EXCLUDED), top10EvidenceCoverage: ratio(evidenceHitCount, Math.max(top10.length, 1)), avgTop10BriefFitScore: avg(top10.map(item => item.briefFitScore)), avgTop10ReferenceStyleFitScore: avg(top10.map(item => item.referenceStyleFitScore || item.referenceSimilarity)), avgTop10TotalScore: avg(top10.map(item => item.score)), avgEvidenceBoost: avg(top10.map(item => item.evidenceScoreBoost)), avgEvidenceRiskPenalty: avg(top10.map(item => item.evidenceRiskPenalty)), offTopicRate: negativeRiskRate, negativeRiskRate, missingKeyConditionRate: ratio(top10.filter(item => (item.missingKeyConditions || []).length).length, Math.max(top10.length, 1)), softwareTable, providerStatus: { reference: criteria.referenceEvidenceStatus || {}, fingerprint: criteria.referenceFingerprintStatus || {}, evidence: criteria.evidenceStatus || {}, homepage: criteria.homepageEvidenceStatus || {} }, top10: top10.map(item => ({ rank: item.rank, platform: item.platform, displayName: item.displayName, recommendStatus: item.recommendStatus, score: item.score, briefFitScore: item.briefFitScore, referenceStyleFitScore: item.referenceStyleFitScore || item.referenceSimilarity || 0, recentContentFitScore: item.recentContentFitScore || 0, visualQualityScore: item.visualQualityScore || 0, toneConsistencyScore: item.toneConsistencyScore || 0, evidenceScoreBoost: item.evidenceScoreBoost || 0, evidenceRiskPenalty: item.evidenceRiskPenalty || 0, homepageEvidenceHitPoints: (item.homepageEvidenceHitPoints || []).slice(0, 5), referenceStyleHitPoints: (item.referenceStyleHitPoints || []).slice(0, 5), referenceFallbackHitPoints: (item.referenceFallbackHitPoints || []).slice(0, 5), referenceEvidenceSource: item.referenceEvidenceSource || '', referenceEvidenceConcrete: Boolean(item.referenceEvidenceConcrete), reason: item.recommendReason, riskNote: item.riskNote, profileUrl: item.profileUrl || '', offTopicHits: findOffTopicHits(item, fixture.expected?.offTopicTerms || []) })), warnings: result.warnings || [], files: result.files || listRunFiles(runDir) }; } function buildAggregate({ manifest, runs, failures, fixtures, variants, options }) { const byBrief = fixtures.map(fixture => { const fixtureRuns = runs.filter(item => item.briefId === fixture.id); const baseline = fixtureRuns.find(item => item.variant === 'baseline-live'); const best = fixtureRuns.slice().sort((a, b) => qualityScore(b, baseline) - qualityScore(a, baseline))[0] || null; return { briefId: fixture.id, briefName: fixture.name, baseline, best, runs: fixtureRuns.map(item => ({ variant: item.variant, gateRole: item.gateRole || 'release', status: item.status, strongCount: item.strongCount, uniqueCandidateCount: item.uniqueCandidateCount, liveRecallCount: item.liveRecallCount, top10EvidenceCoverage: item.top10EvidenceCoverage, avgTop10TotalScore: item.avgTop10TotalScore, avgTop10ReferenceStyleFitScore: item.avgTop10ReferenceStyleFitScore, offTopicRate: item.offTopicRate, negativeRiskRate: item.negativeRiskRate ?? item.offTopicRate, softwareDuplicateKeyCount: item.softwareTable?.duplicateKeyCount ?? 0, softwareRankContinuous: Boolean(item.softwareTable?.rankContinuous), referenceProviderStatus: item.providerStatus?.reference?.status || item.providerStatus?.reference?.providerStatus || 'missing', homepageProviderStatus: item.providerStatus?.homepage?.status || 'missing', gates: evaluateRunGates({ run: item, baseline, manifest, options }) })) }; }); return { manifest, finishedAt: new Date().toISOString(), runCount: runs.length, failureCount: failures.length, fixtureCount: fixtures.length, variantCount: variants.length, byBrief, runs, failures, acceptance: evaluateAggregate({ byBrief, failures, manifest, options }) }; } function evaluateAggregate({ byBrief, failures, manifest, options }) { const comparable = byBrief.filter(item => item.baseline && item.best); const resultFirstWins = comparable.filter(item => item.best.variant !== 'baseline-live' && item.best.strongCount >= item.baseline.strongCount).length; const gates = byBrief.flatMap(brief => brief.runs.flatMap(run => run.gates || [])); const failedGates = gates.filter(gate => gate.status === 'fail' && gate.gateRole !== 'diagnostic'); const warningGates = gates.filter(gate => gate.status === 'warn'); const diagnosticFailures = gates.filter(gate => gate.status === 'fail' && gate.gateRole === 'diagnostic'); const releaseBriefs = byBrief.map(brief => { const releaseRuns = brief.runs.filter(run => run.gateRole === 'release'); const bestRelease = releaseRuns.slice().sort((a, b) => runGateScore(b) - runGateScore(a))[0] || null; return { briefId: brief.briefId, bestReleaseVariant: bestRelease?.variant || '', bestReleasePass: bestRelease ? !(bestRelease.gates || []).some(gate => gate.status === 'fail') : false }; }); const releaseCoveragePass = releaseBriefs.every(item => item.bestReleasePass); return { noUnhandledFailures: failures.length === 0, comparableBriefs: comparable.length, resultFirstWinRate: ratio(resultFirstWins, Math.max(comparable.length, 1)), overallPass: failures.length === 0 && failedGates.length === 0 && releaseCoveragePass, releaseCoveragePass, releaseBriefs, failedGateCount: failedGates.length, warningGateCount: warningGates.length + diagnosticFailures.length, gates: { liveEnabled: manifest.liveEnabled, videoAvailable: manifest.videoAvailable, thresholds: options.gates, failed: failedGates, warnings: [...warningGates, ...diagnosticFailures.map(gate => ({ ...gate, status: 'warn', originalStatus: 'fail' }))] }, tokenLeak: false, notes: ['Automated quality gates are directional; business review sample remains required before release decisions.'] }; } function evaluateRunGates({ run, baseline, manifest, options }) { if (!run) return []; const gates = []; const isResultFirst = /^result-first/.test(run.variant) || run.variant === 'video-enhanced'; if (options.gates.requireLiveRecall && run.variant !== 'baseline-live') { gates.push(makeGate({ name: 'live-recall', status: run.liveRecallCount > 0 ? 'pass' : 'fail', value: run.liveRecallCount, target: '> 0', note: 'Live variants must return real recalled candidates.' })); } if (manifest.videoAvailable && run.variant !== 'baseline-live') { gates.push(makeGate({ name: 'evidence-coverage', status: run.top10EvidenceCoverage >= options.gates.minEvidenceCoverage ? 'pass' : 'fail', value: run.top10EvidenceCoverage, target: `>= ${options.gates.minEvidenceCoverage}`, note: 'Video/evidence-enabled runs should cover most top candidates.' })); } gates.push(makeGate({ name: 'negative-risk-rate', status: negativeRiskRateOf(run) <= options.gates.maxNegativeRiskRate ? 'pass' : 'fail', value: negativeRiskRateOf(run), target: `<= ${options.gates.maxNegativeRiskRate}`, note: 'Top list must avoid negative samples such as off-brief, tone mismatch, weak homepage quality, or unlike reference accounts.' })); gates.push(makeGate({ name: 'software-table-dedup', status: (run.softwareTable?.duplicateKeyCount || 0) === 0 ? 'pass' : 'fail', value: run.softwareTable?.duplicateKeyCount || 0, target: '= 0', note: 'Software handoff table must not contain duplicate creator keys.' })); gates.push(makeGate({ name: 'software-rank-continuous', status: run.softwareTable?.rankContinuous ? 'pass' : 'fail', value: run.softwareTable?.rankSequence || '', target: '1..n', note: 'Software handoff table ranks must be continuous after dedupe and exclusion.' })); gates.push(makeGate({ name: 'homepage-evidence-status', status: run.providerStatus?.homepage?.status ? 'pass' : 'fail', value: run.providerStatus?.homepage?.status || 'missing', target: 'present', note: 'Each run must expose homepage evidence provider or fallback status.' })); if (options.gates.requireReferenceProvider && run.variant === 'reference-account') { const referenceStatus = run.providerStatus?.reference?.status || run.providerStatus?.reference?.providerStatus || 'missing'; gates.push(makeGate({ name: 'reference-provider-ok', status: referenceStatus === 'ok' ? 'pass' : 'fail', value: referenceStatus, target: 'ok', note: 'reference-account strategy must prove real reference enrichment when strict provider gate is enabled.' })); } if (options.gates.requireHomepageProvider && run.variant === 'homepage-evidence') { const homepageStatus = run.providerStatus?.homepage?.status || 'missing'; gates.push(makeGate({ name: 'homepage-provider-ok', status: homepageStatus === 'ok' ? 'pass' : 'fail', value: homepageStatus, target: 'ok', note: 'homepage-evidence strategy must prove real recent homepage evidence when strict provider gate is enabled.' })); } if (baseline && isResultFirst) { if (options.gates.requireResultFirstNoStrongDrop) { gates.push(makeGate({ name: 'strong-count-not-degraded', status: run.strongCount >= baseline.strongCount ? 'pass' : 'fail', value: run.strongCount, target: `>= baseline ${baseline.strongCount}`, note: 'Optimization variants must not lose strong candidates versus baseline.' })); } gates.push(makeGate({ name: 'top10-score-not-degraded', status: run.avgTop10TotalScore >= baseline.avgTop10TotalScore - options.gates.maxScoreDrop ? 'pass' : 'fail', value: run.avgTop10TotalScore, target: `>= baseline ${round(baseline.avgTop10TotalScore - options.gates.maxScoreDrop)}`, note: 'Optimization variants must not buy evidence at the cost of lower list quality.' })); } return gates.map(gate => ({ ...gate, briefId: run.briefId, variant: run.variant, gateRole: run.gateRole || 'release' })); } function runGateScore(run) { if (!run) return -Infinity; const failCount = (run.gates || []).filter(gate => gate.status === 'fail').length; return run.strongCount * 100 + run.avgTop10TotalScore * 2 + run.top10EvidenceCoverage * 20 - failCount * 1000; } function makeGate({ name, status, value, target, note }) { return { name, status, value, target, note }; } function renderAggregateReport(aggregate) { const lines = [ '# 提号 overnight 质量验证报告', '', `- 开始时间:${aggregate.manifest.startedAt}`, `- 结束时间:${aggregate.finishedAt}`, `- 是否 live:${aggregate.manifest.liveEnabled}`, `- 运行次数:${aggregate.runCount}`, `- 程序失败数:${aggregate.failureCount}`, `- 总门禁是否通过:${aggregate.acceptance.overallPass}`, `- 发布策略覆盖是否通过:${aggregate.acceptance.releaseCoveragePass}`, `- 发布门禁失败数:${aggregate.acceptance.failedGateCount}`, `- 诊断 warning 数:${aggregate.acceptance.warningGateCount}`, `- result-first 胜率:${Math.round(aggregate.acceptance.resultFirstWinRate * 100)}%`, '', '## 验收门禁', '', `- 是否要求 live 召回:${aggregate.acceptance.gates.thresholds.requireLiveRecall}`, `- 最低证据覆盖:${pct(aggregate.acceptance.gates.thresholds.minEvidenceCoverage)}`, `- 最高负样本风险率:${pct(aggregate.acceptance.gates.thresholds.maxNegativeRiskRate)}`, `- top-10 均分最大允许下降:${aggregate.acceptance.gates.thresholds.maxScoreDrop}`, '', '| 状态 | 角色 | brief | 策略 | 门禁 | 当前值 | 目标 |', '| --- | --- | --- | --- | --- | ---: | --- |', ...renderGateRows(aggregate), '', '## 各 brief 最佳策略', '', '| Brief | 最佳策略 | 强推数 | 候选数 | 证据覆盖 | 平均分 | 平均参考风格分 | 负样本风险率 |', '| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |', ...aggregate.byBrief.map(item => { const best = item.best || {}; return `| ${escapeCell(item.briefName)} | ${best.variant || 'n/a'} | ${best.strongCount || 0} | ${best.uniqueCandidateCount || 0} | ${pct(best.top10EvidenceCoverage)} | ${best.avgTop10TotalScore || 0} | ${best.avgTop10ReferenceStyleFitScore || 0} | ${pct(negativeRiskRateOf(best))} |`; }), '', '## 各策略指标', '' ]; for (const brief of aggregate.byBrief) { lines.push(`### ${brief.briefName}`, ''); lines.push('| 策略 | 角色 | 强推数 | 候选数 | 召回数 | 证据覆盖 | 平均分 | 平均参考分 | 负样本风险率 | 门禁失败数 |'); lines.push('| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |'); for (const run of aggregate.runs.filter(item => item.briefId === brief.briefId)) { const briefRun = brief.runs.find(item => item.variant === run.variant) || {}; const failCount = (briefRun.gates || []).filter(gate => gate.status === 'fail').length; lines.push(`| ${run.variant} | ${run.gateRole || 'release'} | ${run.strongCount} | ${run.uniqueCandidateCount} | ${run.liveRecallCount} | ${pct(run.top10EvidenceCoverage)} | ${run.avgTop10TotalScore} | ${run.avgTop10ReferenceStyleFitScore} | ${pct(negativeRiskRateOf(run))} | ${failCount} |`); } lines.push(''); } lines.push('## 程序失败', ''); if (!aggregate.failures.length) lines.push('- 无'); for (const failure of aggregate.failures) { lines.push(`- ${failure.fixtureId}/${failure.variant}: ${escapeCell(failure.message).slice(0, 240)}`); } lines.push('', '## 人工复核说明', ''); lines.push('- 打开 `manual-review-sample.csv` 标注人工复核标签:可直接发客户 / 商务复核 / 可投但需补证 / 跑偏 / 硬性规则违规 / 调性不符 / 主页质感不符 / 参考账号不像。'); lines.push('- 负样本必须填写“归因类型”,可选:需求解析错 / 隐性规则漏 / 召回关键词错 / 主页证据不足 / 视频证据误判 / 排序权重错 / 输出解释错 / 软件端表重复或排名不连续。'); lines.push('- 有客户反馈时填写“客户选择”:客户选中 / 客户拒绝 / 待客户反馈。模型证据只用于辅助筛选,不能替代最终合规、报价和主页有效性确认。'); return lines.join('\n'); } function renderGateRows(aggregate) { const gates = [ ...(aggregate.acceptance.gates.failed || []), ...(aggregate.acceptance.gates.warnings || []) ]; if (!gates.length) return ['| n/a | n/a | n/a | n/a | n/a | 0 | n/a |']; return gates .slice(0, 80) .map(gate => `| ${gate.status} | ${escapeCell(gate.gateRole || 'release')} | ${escapeCell(gate.briefId)} | ${escapeCell(gate.variant)} | ${escapeCell(gate.name)} | ${escapeCell(gate.value)} | ${escapeCell(gate.target)} |`); } function renderAggregateReportClean(aggregate) { const lines = [ '# 提号 overnight 质量验证报告', '', `- 开始时间:${aggregate.manifest.startedAt}`, `- 结束时间:${aggregate.finishedAt}`, `- 是否 live:${aggregate.manifest.liveEnabled}`, `- 运行次数:${aggregate.runCount}`, `- 程序失败数:${aggregate.failureCount}`, `- 总门禁是否通过:${aggregate.acceptance.overallPass}`, `- 发布策略覆盖是否通过:${aggregate.acceptance.releaseCoveragePass}`, `- 发布门禁失败数:${aggregate.acceptance.failedGateCount}`, `- warning 数:${aggregate.acceptance.warningGateCount}`, '', '## 新增硬验收', '', '- 软件端表重复键必须为 0。', '- 软件端表排名必须从 1 开始连续。', '- 每个 run 必须输出主页证据状态,状态可以是真实 provider,也可以是 fallback/not_requested。', '', '## 各 Brief 最佳策略', '', '| Brief | 最佳策略 | 强推荐数 | 候选数 | 软件重复键 | 排名连续 | 参考补证状态 | 主页证据状态 | 证据覆盖 | 平均分 | 负样本风险率 |', '| --- | --- | ---: | ---: | ---: | --- | --- | --- | ---: | ---: | ---: |' ]; for (const item of aggregate.byBrief) { const best = item.best || {}; lines.push(`| ${escapeCell(item.briefName)} | ${best.variant || 'n/a'} | ${best.strongCount || 0} | ${best.uniqueCandidateCount || 0} | ${best.softwareTable?.duplicateKeyCount ?? 0} | ${best.softwareTable?.rankContinuous ? '是' : '否'} | ${best.providerStatus?.reference?.status || best.providerStatus?.reference?.providerStatus || 'missing'} | ${best.providerStatus?.homepage?.status || 'missing'} | ${pct(best.top10EvidenceCoverage)} | ${best.avgTop10TotalScore || 0} | ${pct(negativeRiskRateOf(best))} |`); } lines.push('', '## 各策略指标', ''); for (const brief of aggregate.byBrief) { lines.push(`### ${brief.briefName}`, ''); lines.push('| 策略 | 角色 | 强推荐数 | 候选数 | 软件重复键 | 排名连续 | 参考补证状态 | 主页证据状态 | 召回数 | 证据覆盖 | 平均分 | 参考风格分 | 负样本风险率 | 失败门禁数 |'); lines.push('| --- | --- | ---: | ---: | ---: | --- | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |'); for (const run of aggregate.runs.filter(item => item.briefId === brief.briefId)) { const briefRun = brief.runs.find(item => item.variant === run.variant) || {}; const failCount = (briefRun.gates || []).filter(gate => gate.status === 'fail').length; lines.push(`| ${run.variant} | ${run.gateRole || 'release'} | ${run.strongCount} | ${run.uniqueCandidateCount} | ${run.softwareTable?.duplicateKeyCount ?? 0} | ${run.softwareTable?.rankContinuous ? '是' : '否'} | ${run.providerStatus?.reference?.status || run.providerStatus?.reference?.providerStatus || 'missing'} | ${run.providerStatus?.homepage?.status || 'missing'} | ${run.liveRecallCount} | ${pct(run.top10EvidenceCoverage)} | ${run.avgTop10TotalScore} | ${run.avgTop10ReferenceStyleFitScore} | ${pct(negativeRiskRateOf(run))} | ${failCount} |`); } lines.push(''); } lines.push('## 门禁失败与警告', ''); const gates = [...(aggregate.acceptance.gates.failed || []), ...(aggregate.acceptance.gates.warnings || [])]; if (!gates.length) lines.push('- 无'); for (const gate of gates.slice(0, 80)) { lines.push(`- [${gate.status}] ${gate.briefId}/${gate.variant}/${gate.name}: ${gate.value},目标 ${gate.target}`); } lines.push('', '## 程序失败', ''); if (!aggregate.failures.length) lines.push('- 无'); for (const failure of aggregate.failures) { lines.push(`- ${failure.fixtureId}/${failure.variant}: ${escapeCell(failure.message).slice(0, 240)}`); } lines.push('', '## 人工复核说明', ''); lines.push('- 打开 `manual-review-sample.csv` 标注人工复核标签:可直接发客户 / 商务复核 / 可投但需补证 / 跑偏 / 硬性规则违规 / 调性不符 / 主页质感不符 / 参考账号不像。'); lines.push('- 负样本必须填写“归因类型”,可选:需求解析错 / 隐性规则漏 / 召回关键词错 / 主页证据不足 / 视频证据误判 / 排序权重错 / 输出解释错 / 软件端表重复或排名不连续。'); lines.push('- 有客户反馈时填写“客户选择”:客户选中 / 客户拒绝 / 待客户反馈。自动化门禁只证明结构和方向,真实客户选中率仍需要客户最终选择数据验证。'); return lines.join('\n'); } function buildManualReviewSample(runs) { const header = [ 'brief编号', '策略', '排名', '平台', '博主名称', '综合分', 'brief匹配分', '参考风格分', '主页证据分', '视觉质感分', '调性一致分', '证据加分', '证据风险扣分', '推荐理由', '风险提示', '主页链接', '人工复核标签', '客户选择', '归因类型', '反馈原因' ]; const best = new Map(); for (const run of runs.filter(item => item.variant !== 'baseline-live')) { for (const item of (run.top10 || []).slice(0, 10)) { const platform = item.platform || ''; const profileUrl = item.profileUrl || ''; const displayName = item.displayName || ''; const key = profileUrl ? `${run.briefId}|${platform}|url:${profileUrl.toLowerCase()}` : `${run.briefId}|${platform}|name:${displayName.toLowerCase()}`; const row = { briefId: run.briefId, variant: run.variant, platform, displayName, score: Number(item.score || 0), briefFitScore: Number(item.briefFitScore || 0), referenceStyleFitScore: Number(item.referenceStyleFitScore || 0), recentContentFitScore: Number(item.recentContentFitScore || 0), visualQualityScore: Number(item.visualQualityScore || 0), toneConsistencyScore: Number(item.toneConsistencyScore || 0), evidenceScoreBoost: Number(item.evidenceScoreBoost || 0), evidenceRiskPenalty: Number(item.evidenceRiskPenalty || 0), reason: item.reason || '', riskNote: item.riskNote || '', profileUrl }; const previous = best.get(key); if (!previous || reviewRowPriority(row) > reviewRowPriority(previous)) best.set(key, row); } } const rows = rankReviewRowsWithinBrief([...best.values()] .sort((a, b) => String(a.briefId).localeCompare(String(b.briefId)) || b.score - a.score || b.briefFitScore - a.briefFitScore || String(a.displayName).localeCompare(String(b.displayName))) .slice(0, 60)) .map((item) => [ item.briefId, item.variant, item.rank, item.platform, item.displayName, item.score, item.briefFitScore, item.referenceStyleFitScore, item.recentContentFitScore, item.visualQualityScore, item.toneConsistencyScore, item.evidenceScoreBoost, item.evidenceRiskPenalty, item.reason, item.riskNote, item.profileUrl, '', '', '', '' ]); return [header.join(','), ...rows.map(row => row.map(csvCell).join(','))].join('\n'); } function rankReviewRowsWithinBrief(rows) { const counters = new Map(); return rows.map(row => { const briefId = String(row.briefId || '未命名brief'); const next = (counters.get(briefId) || 0) + 1; counters.set(briefId, next); return { ...row, rank: next }; }); } function reviewRowPriority(row) { return Number(row.score || 0) * 10000 + Number(row.briefFitScore || 0) * 100 + Number(row.referenceStyleFitScore || 0); } function loadFixtures(dir) { return fs.readdirSync(dir) .filter(name => name.endsWith('.json')) .sort() .map(name => JSON.parse(fs.readFileSync(path.join(dir, name), 'utf8'))); } function csvSet(value) { return new Set(String(value || '').split(',').map(item => item.trim()).filter(Boolean)); } function intEnv(name, defaultValue) { const value = Number.parseInt(process.env[name] || '', 10); return Number.isFinite(value) && value >= 0 ? value : defaultValue; } function numberEnv(name, defaultValue) { const value = Number.parseFloat(process.env[name] || ''); return Number.isFinite(value) ? value : defaultValue; } function qualityScore(run, baseline) { if (!run) return -Infinity; const baseScore = baseline ? baseline.avgTop10TotalScore : 0; return run.strongCount * 8 + run.avgTop10TotalScore * 2 + run.avgTop10ReferenceStyleFitScore + run.top10EvidenceCoverage * 20 - negativeRiskRateOf(run) * 40 + Math.max(0, run.avgTop10TotalScore - baseScore) * 3; } function negativeRiskRateOf(run) { if (!run) return 0; return run.negativeRiskRate ?? run.offTopicRate ?? 0; } function hasOffTopicHit(item, terms) { return findOffTopicHits(item, terms).length > 0; } function findOffTopicHits(item, terms) { const text = [ item.displayName, item.recommendReason, item.riskNote, ...(item.contentTags || []), ...(item.personaTags || []), ...(item.evidenceSignals || []), ...(item.evidenceRiskHints || []) ].join(' '); return (terms || []).filter(term => term && text.includes(term)); } function countStatus(candidates, status) { return candidates.filter(item => item.recommendStatus === status).length; } function avg(values) { const nums = values.map(Number).filter(Number.isFinite); return nums.length ? round(nums.reduce((sum, value) => sum + value, 0) / nums.length) : 0; } function ratio(numerator, denominator) { return denominator ? round(Number(numerator || 0) / Number(denominator)) : 0; } function round(value) { return Math.round(Number(value || 0) * 100) / 100; } function pct(value) { return `${Math.round(Number(value || 0) * 100)}%`; } function writeJson(file, value) { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, JSON.stringify(value, null, 2)); } function writeText(file, value) { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, withExcelBom(file, value), 'utf8'); } function withExcelBom(file, value) { const text = String(value ?? ''); if (!/\.(csv|md)$/i.test(String(file || ''))) return text; return text.startsWith('\uFEFF') ? text : `\uFEFF${text}`; } function listRunFiles(dir) { if (!fs.existsSync(dir)) return []; return fs.readdirSync(dir).map(name => path.join(dir, name)); } function inspectSoftwareTable(csvPath) { const expectedHeader = 'brief编号,策略,排名,平台,博主名称,综合分,brief匹配分,参考风格分,主页证据分,视觉质感分,调性一致分,证据加分,证据风险扣分,推荐理由,风险提示,主页链接,人工复核标签'; if (!fs.existsSync(csvPath)) { return { exists: false, headerOk: false, rowCount: 0, duplicateKeyCount: 0, rankContinuous: false, rankSequence: '' }; } const text = fs.readFileSync(csvPath, 'utf8').replace(/^\uFEFF/, ''); const lines = text.trim().split(/\r?\n/).filter(Boolean); const rows = lines.slice(1).map(parseCsvLine); const seen = new Set(); let duplicateKeyCount = 0; for (const row of rows) { const key = row[15] ? `${row[0]}|${row[3]}|${row[15]}` : `${row[0]}|${row[3]}|${row[4]}`; if (seen.has(key)) duplicateKeyCount += 1; seen.add(key); } const ranks = rows.map(row => Number(row[2])); const rankContinuous = ranksContinuousWithinBrief(rows); return { exists: true, headerOk: lines[0] === expectedHeader, rowCount: rows.length, duplicateKeyCount, rankContinuous, rankSequence: ranks.join(',') }; } function ranksContinuousWithinBrief(rows) { const counters = new Map(); for (const row of rows) { const briefId = row[0] || '未命名brief'; const expected = (counters.get(briefId) || 0) + 1; if (Number(row[2]) !== expected) return false; counters.set(briefId, expected); } return true; } function parseCsvLine(line) { const cells = []; let current = ''; let quoted = false; for (let i = 0; i < line.length; i += 1) { const char = line[i]; if (char === '"' && quoted && line[i + 1] === '"') { current += '"'; i += 1; } else if (char === '"') { quoted = !quoted; } else if (char === ',' && !quoted) { cells.push(current); current = ''; } else { current += char; } } cells.push(current); return cells; } function scanForLeaks(root, secrets) { const realSecrets = [...new Set((secrets || []).filter(Boolean))]; for (const file of listFiles(root)) { const text = fs.readFileSync(file, 'utf8'); for (const secret of realSecrets) { if (text.includes(secret)) throw new Error(`Secret leaked into ${file}`); } if (/Authorization\s*:/i.test(text)) throw new Error(`Authorization header leaked into ${file}`); } } function listFiles(root) { return fs.readdirSync(root, { withFileTypes: true }).flatMap(entry => { const full = path.join(root, entry.name); return entry.isDirectory() ? listFiles(full) : [full]; }); } function csvCell(value) { const text = String(value ?? ''); return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text; } function escapeCell(value) { return String(value || '').replace(/\|/g, '/').replace(/\n/g, ' '); } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } main().catch(error => { console.error(redactSecrets(error && error.stack ? error.stack : String(error))); process.exit(1); });