live-preflight.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. #!/usr/bin/env node
  2. const fs = require('fs');
  3. const path = require('path');
  4. const { readTihaoToken, readEvidenceProviderToken, readSocialAnalysisToken } = require('../mcp/src/core/credentials');
  5. const { resolveTihaoUserInfo } = require('../mcp/src/core/payment-links');
  6. const { redactSecrets } = require('../mcp/src/core/result-envelope');
  7. async function main() {
  8. const args = parseArgs(process.argv.slice(2));
  9. const strict = Boolean(args.strict);
  10. const allowFailure = Boolean(args.allowFailure || args.allowFail);
  11. const outputRoot = path.resolve(args.output || args.outputRoot || process.env.TIHAO_PREFLIGHT_OUTPUT || path.join(__dirname, '..', 'outputs', `live-preflight-${Date.now()}`));
  12. fs.mkdirSync(outputRoot, { recursive: true });
  13. const token = readTihaoToken({});
  14. const socialAnalysisToken = readSocialAnalysisToken({});
  15. const evidenceToken = readEvidenceProviderToken({});
  16. const companyFromEnv = process.env.TIHAO_COMPANY || process.env.VOC_ECOMMERCE_COMPANY || process.env.COMPANY_OBJECT_ID || '';
  17. const baseUrl = process.env.TIHAO_ECOMMERCE_BASE_URL || 'https://server.fmode.cn/api/voc-e-commerce';
  18. const referenceBaseUrl = process.env.SOCIAL_ANALYSIS_BASE_URL || process.env.TIHAO_REFERENCE_ENRICHMENT_BASE_URL || '';
  19. const homepageBaseUrl = process.env.TIHAO_HOMEPAGE_EVIDENCE_BASE_URL || process.env.HOMEPAGE_EVIDENCE_BASE_URL || '';
  20. const evidenceBaseUrl = process.env.TIHAO_EVIDENCE_BASE_URL || process.env.MULTIMODAL_EVIDENCE_BASE_URL || '';
  21. const videoBaseUrl = process.env.VIDEO_ANALYSIS_BASE_URL || process.env.DOUBAO_VISION_BASE_URL || '';
  22. const videoModel = process.env.VIDEO_ANALYSIS_MODEL || process.env.DOUBAO_VISION_MODEL || 'doubao-seed-2-0-pro-260215';
  23. const checks = [];
  24. checks.push(check('session-token-present', Boolean(token), '需要 TIHAO_SESSION_TOKEN / VOC_ECOMMERCE_TOKEN / VOC_TOKEN 才能跑真实 live。'));
  25. checks.push(check('live-base-url-present', Boolean(baseUrl), '需要 live provider base url。'));
  26. checks.push(check('reference-provider-configured', Boolean(referenceBaseUrl), '未配置参考账号补证 provider;可先跑 brief-only/result-first,但不能证明参考链路真实接入。', 'warn'));
  27. checks.push(check('homepage-provider-configured', Boolean(homepageBaseUrl), '未配置主页最近内容 provider;主页证据会使用 fallback/已有标签。', 'warn'));
  28. checks.push(check('evidence-or-video-provider-configured', Boolean(evidenceBaseUrl || videoBaseUrl), '未配置多模态 evidence 或豆包视频分析 provider;不能证明视频证据提升。', 'warn'));
  29. 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。`));
  30. checks.push(check('reference-token-present-if-provider', !referenceBaseUrl || Boolean(socialAnalysisToken || token), '参考账号补证 provider 已配置,但没有 VOC_SOCIAL_TOKEN/sessionToken。'));
  31. checks.push(check('evidence-token-present-if-provider', !evidenceBaseUrl && !videoBaseUrl || Boolean(evidenceToken), '证据/视频 provider 已配置,但没有 TIHAO_EVIDENCE_TOKEN / VIDEO_ANALYSIS_TOKEN / DOUBAO_VISION_TOKEN。'));
  32. let identity = null;
  33. if (token) {
  34. try {
  35. identity = await resolveTihaoUserInfo(token);
  36. checks.push(check('parse-user-resolved', Boolean(identity && identity.userId), 'Parse /users/me 未返回可识别用户。'));
  37. } catch (error) {
  38. checks.push(check('parse-user-resolved', false, `Parse 用户解析失败:${safeMessage(error)}`));
  39. }
  40. }
  41. const company = companyFromEnv || identity?.companyId || '';
  42. checks.push(check('company-resolved', Boolean(company), '需要 TIHAO_COMPANY / VOC_ECOMMERCE_COMPANY / COMPANY_OBJECT_ID,或 sessionToken 用户上有 company。'));
  43. const strictFailures = checks.filter(item => item.status === 'fail');
  44. const warnings = checks.filter(item => item.status === 'warn');
  45. const readyForLiveAcceptance = strictFailures.length === 0;
  46. const readyForVideoAb = readyForLiveAcceptance && Boolean(videoBaseUrl && evidenceToken);
  47. const summary = {
  48. generatedAt: new Date().toISOString(),
  49. readyForLiveAcceptance,
  50. readyForVideoAb,
  51. strict,
  52. allowFailure,
  53. live: {
  54. baseUrl,
  55. tokenPresent: Boolean(token),
  56. companyResolved: Boolean(company),
  57. parseUserResolved: Boolean(identity && identity.userId)
  58. },
  59. providers: {
  60. referenceConfigured: Boolean(referenceBaseUrl),
  61. homepageConfigured: Boolean(homepageBaseUrl),
  62. evidenceConfigured: Boolean(evidenceBaseUrl),
  63. videoConfigured: Boolean(videoBaseUrl),
  64. videoModel
  65. },
  66. checks,
  67. nextActions: buildNextActions({ readyForLiveAcceptance, readyForVideoAb, warnings, strictFailures })
  68. };
  69. const summaryPath = path.join(outputRoot, 'live-preflight-summary.json');
  70. fs.writeFileSync(summaryPath, JSON.stringify(summary, null, 2), 'utf8');
  71. console.log(JSON.stringify({
  72. outputRoot,
  73. readyForLiveAcceptance,
  74. readyForVideoAb,
  75. failures: strictFailures.length,
  76. warnings: warnings.length,
  77. summary: summaryPath
  78. }, null, 2));
  79. if (strict && strictFailures.length && !allowFailure) process.exitCode = 2;
  80. }
  81. function check(name, pass, note, severity = 'fail') {
  82. return {
  83. name,
  84. status: pass ? 'pass' : severity,
  85. note: pass ? '' : note
  86. };
  87. }
  88. function buildNextActions({ readyForLiveAcceptance, readyForVideoAb, warnings, strictFailures }) {
  89. const actions = [];
  90. if (!readyForLiveAcceptance) {
  91. actions.push('补齐 Parse sessionToken 和 company 后运行 npm run live:preflight -- --strict。');
  92. } else {
  93. actions.push('可运行 npm run live:acceptance 做小规模真实 live 检索。');
  94. }
  95. if (!readyForVideoAb) {
  96. actions.push('补齐视频分析 base URL、模型 doubao-seed-2-0-pro 和运行时 token 后,再运行 npm run acceptance:video-ab。');
  97. } else {
  98. actions.push('可运行 npm run acceptance:video-ab 验证视频证据是否提升提号结果。');
  99. }
  100. if (warnings.length) actions.push('provider 警告不是发布阻断,但不能据此宣称真实 provider 或视频链路已验证。');
  101. if (strictFailures.length) actions.push('严格模式下存在 fail 项,不能启动 overnight live 长跑。');
  102. return actions;
  103. }
  104. function safeMessage(error) {
  105. return redactSecrets(error && error.message ? error.message : String(error)).slice(0, 160);
  106. }
  107. function parseArgs(argv) {
  108. const args = {};
  109. for (let index = 0; index < argv.length; index += 1) {
  110. const raw = argv[index];
  111. if (!raw.startsWith('--')) continue;
  112. const key = raw.slice(2).replace(/-([a-z])/g, (_, char) => char.toUpperCase());
  113. const next = argv[index + 1];
  114. if (!next || next.startsWith('--')) args[key] = true;
  115. else {
  116. args[key] = next;
  117. index += 1;
  118. }
  119. }
  120. return args;
  121. }
  122. main().catch(error => {
  123. console.error(redactSecrets(error && error.stack ? error.stack : String(error)));
  124. process.exit(1);
  125. });