#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const { readTihaoToken, readEvidenceProviderToken, readSocialAnalysisToken } = require('../mcp/src/core/credentials'); const { resolveTihaoUserInfo } = require('../mcp/src/core/payment-links'); const { redactSecrets } = require('../mcp/src/core/result-envelope'); async function main() { const args = parseArgs(process.argv.slice(2)); const strict = Boolean(args.strict); const allowFailure = Boolean(args.allowFailure || args.allowFail); const outputRoot = path.resolve(args.output || args.outputRoot || process.env.TIHAO_PREFLIGHT_OUTPUT || path.join(__dirname, '..', 'outputs', `live-preflight-${Date.now()}`)); fs.mkdirSync(outputRoot, { recursive: true }); const token = readTihaoToken({}); const socialAnalysisToken = readSocialAnalysisToken({}); const evidenceToken = readEvidenceProviderToken({}); const companyFromEnv = process.env.TIHAO_COMPANY || process.env.VOC_ECOMMERCE_COMPANY || process.env.COMPANY_OBJECT_ID || ''; const baseUrl = process.env.TIHAO_ECOMMERCE_BASE_URL || 'https://server.fmode.cn/api/voc-e-commerce'; const referenceBaseUrl = process.env.SOCIAL_ANALYSIS_BASE_URL || process.env.TIHAO_REFERENCE_ENRICHMENT_BASE_URL || ''; const homepageBaseUrl = process.env.TIHAO_HOMEPAGE_EVIDENCE_BASE_URL || process.env.HOMEPAGE_EVIDENCE_BASE_URL || ''; const evidenceBaseUrl = process.env.TIHAO_EVIDENCE_BASE_URL || process.env.MULTIMODAL_EVIDENCE_BASE_URL || ''; const videoBaseUrl = process.env.VIDEO_ANALYSIS_BASE_URL || process.env.DOUBAO_VISION_BASE_URL || ''; const videoModel = process.env.VIDEO_ANALYSIS_MODEL || process.env.DOUBAO_VISION_MODEL || 'doubao-seed-2-0-pro-260215'; const checks = []; checks.push(check('session-token-present', Boolean(token), '需要 TIHAO_SESSION_TOKEN / VOC_ECOMMERCE_TOKEN / VOC_TOKEN 才能跑真实 live。')); checks.push(check('live-base-url-present', Boolean(baseUrl), '需要 live provider base url。')); checks.push(check('reference-provider-configured', Boolean(referenceBaseUrl), '未配置参考账号补证 provider;可先跑 brief-only/result-first,但不能证明参考链路真实接入。', 'warn')); checks.push(check('homepage-provider-configured', Boolean(homepageBaseUrl), '未配置主页最近内容 provider;主页证据会使用 fallback/已有标签。', 'warn')); checks.push(check('evidence-or-video-provider-configured', Boolean(evidenceBaseUrl || videoBaseUrl), '未配置多模态 evidence 或豆包视频分析 provider;不能证明视频证据提升。', 'warn')); checks.push(check('video-model-doubao-seed-2-0-pro-260215', !videoBaseUrl || videoModel === 'doubao-seed-2-0-pro-260215', `当前视频模型为 ${videoModel},用户指定模型应为 doubao-seed-2-0-pro-260215。`)); checks.push(check('reference-token-present-if-provider', !referenceBaseUrl || Boolean(socialAnalysisToken || token), '参考账号补证 provider 已配置,但没有 VOC_SOCIAL_TOKEN/sessionToken。')); checks.push(check('evidence-token-present-if-provider', !evidenceBaseUrl && !videoBaseUrl || Boolean(evidenceToken), '证据/视频 provider 已配置,但没有 TIHAO_EVIDENCE_TOKEN / VIDEO_ANALYSIS_TOKEN / DOUBAO_VISION_TOKEN。')); let identity = null; if (token) { try { identity = await resolveTihaoUserInfo(token); checks.push(check('parse-user-resolved', Boolean(identity && identity.userId), 'Parse /users/me 未返回可识别用户。')); } catch (error) { checks.push(check('parse-user-resolved', false, `Parse 用户解析失败:${safeMessage(error)}`)); } } const company = companyFromEnv || identity?.companyId || ''; checks.push(check('company-resolved', Boolean(company), '需要 TIHAO_COMPANY / VOC_ECOMMERCE_COMPANY / COMPANY_OBJECT_ID,或 sessionToken 用户上有 company。')); const strictFailures = checks.filter(item => item.status === 'fail'); const warnings = checks.filter(item => item.status === 'warn'); const readyForLiveAcceptance = strictFailures.length === 0; const readyForVideoAb = readyForLiveAcceptance && Boolean(videoBaseUrl && evidenceToken); const summary = { generatedAt: new Date().toISOString(), readyForLiveAcceptance, readyForVideoAb, strict, allowFailure, live: { baseUrl, tokenPresent: Boolean(token), companyResolved: Boolean(company), parseUserResolved: Boolean(identity && identity.userId) }, providers: { referenceConfigured: Boolean(referenceBaseUrl), homepageConfigured: Boolean(homepageBaseUrl), evidenceConfigured: Boolean(evidenceBaseUrl), videoConfigured: Boolean(videoBaseUrl), videoModel }, checks, nextActions: buildNextActions({ readyForLiveAcceptance, readyForVideoAb, warnings, strictFailures }) }; const summaryPath = path.join(outputRoot, 'live-preflight-summary.json'); fs.writeFileSync(summaryPath, JSON.stringify(summary, null, 2), 'utf8'); console.log(JSON.stringify({ outputRoot, readyForLiveAcceptance, readyForVideoAb, failures: strictFailures.length, warnings: warnings.length, summary: summaryPath }, null, 2)); if (strict && strictFailures.length && !allowFailure) process.exitCode = 2; } function check(name, pass, note, severity = 'fail') { return { name, status: pass ? 'pass' : severity, note: pass ? '' : note }; } function buildNextActions({ readyForLiveAcceptance, readyForVideoAb, warnings, strictFailures }) { const actions = []; if (!readyForLiveAcceptance) { actions.push('补齐 Parse sessionToken 和 company 后运行 npm run live:preflight -- --strict。'); } else { actions.push('可运行 npm run live:acceptance 做小规模真实 live 检索。'); } if (!readyForVideoAb) { actions.push('补齐视频分析 base URL、模型 doubao-seed-2-0-pro 和运行时 token 后,再运行 npm run acceptance:video-ab。'); } else { actions.push('可运行 npm run acceptance:video-ab 验证视频证据是否提升提号结果。'); } if (warnings.length) actions.push('provider 警告不是发布阻断,但不能据此宣称真实 provider 或视频链路已验证。'); if (strictFailures.length) actions.push('严格模式下存在 fail 项,不能启动 overnight live 长跑。'); return actions; } function safeMessage(error) { return redactSecrets(error && error.message ? error.message : String(error)).slice(0, 160); } 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; } main().catch(error => { console.error(redactSecrets(error && error.stack ? error.stack : String(error))); process.exit(1); });