sourcing-workflow.js 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. const path = require('path');
  2. const { okResult } = require('../../core/result-envelope');
  3. const { parseBrief } = require('./brief-parser');
  4. const { SAMPLE_CREATORS } = require('./sample-data');
  5. const { searchLiveCreators } = require('./live-provider');
  6. const { applyMemory } = require('./preference-memory');
  7. const { attachReferenceBaselines } = require('./reference-baselines');
  8. const { enrichReferenceBaselines } = require('./reference-enrichment-provider');
  9. const { attachReferenceStyleFingerprints } = require('./reference-style-fingerprint');
  10. const { attachEvidenceCards, loadMultimodalEvidence } = require('./multimodal-evidence');
  11. const { enrichCriteriaWithExperience } = require('./experience-rules');
  12. const { enrichHomepageEvidence } = require('./homepage-evidence-provider');
  13. const { rankCreators } = require('./ranker');
  14. const { buildSourcingReport } = require('./report');
  15. const { createReporter } = require('./progress-reporter');
  16. async function runTihaoSourcing(input = {}) {
  17. const collectionMode = resolveCollectionMode(input);
  18. const effectiveInput = collectionMode === 'live' ? withLiveSmallRunDefaults(input) : input;
  19. const outputDir = path.resolve(input.output || input.outputDir || `outputs/tihao-sourcing-${Date.now()}`);
  20. const reporter = createReporter(input, { outputDir });
  21. try {
  22. return await runTihaoSourcingInner(effectiveInput, collectionMode, outputDir, reporter);
  23. } catch (error) {
  24. await reporter.fail(error);
  25. throw error;
  26. }
  27. }
  28. async function runTihaoSourcingInner(input, collectionMode, outputDir, reporter) {
  29. const effectiveInput = input;
  30. let criteria = parseBrief(input);
  31. criteria = applyMemory(criteria, effectiveInput.memory || effectiveInput.memoryPath);
  32. await reporter.start(criteria, input);
  33. const enrichment = await enrichReferenceBaselines(criteria, effectiveInput);
  34. criteria = attachReferenceBaselines(criteria, {
  35. ...effectiveInput,
  36. referenceBaselines: [
  37. ...(Array.isArray(effectiveInput.referenceBaselines) ? effectiveInput.referenceBaselines : []),
  38. ...enrichment.baselines
  39. ],
  40. referenceProviderStatus: enrichment.status
  41. });
  42. criteria = attachReferenceStyleFingerprints(criteria, effectiveInput, {
  43. status: enrichment.status,
  44. referenceStyleFingerprints: enrichment.referenceStyleFingerprints || [],
  45. referenceResources: enrichment.referenceResources || []
  46. });
  47. criteria = enrichCriteriaWithExperience(criteria);
  48. let creators = SAMPLE_CREATORS;
  49. let warnings = [...(enrichment.warnings || [])];
  50. let recallRecords = [];
  51. if (collectionMode === 'live') {
  52. const liveResult = await searchLiveCreators(criteria, effectiveInput);
  53. if (liveResult.status !== 'ok') {
  54. await reporter.blocked(liveResult.status || 'live_error', '真实检索未通过', 20);
  55. return liveResult;
  56. }
  57. creators = liveResult.creators.length ? liveResult.creators : [];
  58. recallRecords = liveResult.recallRecords || [];
  59. warnings = [...warnings, ...(liveResult.warnings || [])];
  60. if (creators.length === 0) {
  61. warnings.push('live 模式没有返回候选,建议放宽关键词、检查 company,或先用 sample 模式校准 brief。');
  62. if (!effectiveInput.allowSampleFallback) {
  63. await reporter.blocked('no_live_samples', 'live 检索无候选', 30);
  64. return okResult({
  65. status: 'no_live_samples',
  66. assistantMessage: [
  67. '真实 live 检索已完成,但没有返回可用候选。',
  68. '',
  69. `检索平台:${criteria.platforms.map(labelPlatform).join('、')}`,
  70. `关键词:${criteria.keywords.join('、') || '未解析到关键词'}`,
  71. '',
  72. '建议下一步:',
  73. '- 确认 company objectId 是否需要传入;',
  74. '- 换一个更宽的关键词,例如品类词而不是品牌词;',
  75. '- 先用 sample 模式校准 Brief,再扩大 live 检索页数。'
  76. ].join('\n'),
  77. summary: {
  78. brand: criteria.brand,
  79. collectionMode,
  80. total: 0,
  81. targetCount: criteria.targetCount || 0
  82. },
  83. data: { criteria: { ...criteria, recallRecords } },
  84. warnings,
  85. errors: [],
  86. nextActions: [
  87. '传入 company 后重跑 live',
  88. '改用更宽关键词重跑 live',
  89. '设置 allowSampleFallback=true 生成演示名单'
  90. ]
  91. });
  92. }
  93. creators = SAMPLE_CREATORS.filter(item => criteria.platforms.includes(item.platform));
  94. }
  95. }
  96. await reporter.step('search', '候选检索完成', 45);
  97. const homepageEvidence = await enrichHomepageEvidence(criteria, creators, effectiveInput);
  98. creators = homepageEvidence.creators || creators;
  99. warnings = [...warnings, ...(homepageEvidence.warnings || [])];
  100. criteria = {
  101. ...criteria,
  102. homepageEvidenceStatus: buildHomepageEvidenceStatus(homepageEvidence.status || {})
  103. };
  104. const preliminaryCandidates = rankCreators(creators, criteria);
  105. const multimodalEvidence = await loadMultimodalEvidence(criteria, preliminaryCandidates, effectiveInput);
  106. warnings = [...warnings, ...(multimodalEvidence.warnings || [])];
  107. criteria = {
  108. ...criteria,
  109. evidenceCards: multimodalEvidence.evidenceCards || [],
  110. evidenceStatus: buildEvidenceStatus(multimodalEvidence.evidenceCards || [], multimodalEvidence.providerStatus),
  111. recallRecords,
  112. referenceResources: [
  113. ...(criteria.referenceResources || []),
  114. ...(multimodalEvidence.referenceResources || [])
  115. ]
  116. };
  117. criteria = attachReferenceStyleFingerprints(criteria, effectiveInput, {
  118. providerStatus: multimodalEvidence.providerStatus,
  119. referenceStyleFingerprints: multimodalEvidence.referenceStyleFingerprints || [],
  120. referenceResources: multimodalEvidence.referenceResources || []
  121. });
  122. criteria = enrichCriteriaWithExperience(criteria);
  123. await reporter.step('rank', '排序与证据完成', 75);
  124. const candidates = attachEvidenceCards(preliminaryCandidates, criteria.evidenceCards);
  125. const report = buildSourcingReport({
  126. criteria,
  127. candidates,
  128. collectionMode,
  129. outputDir,
  130. warnings
  131. });
  132. await reporter.complete(report);
  133. return okResult({
  134. assistantMessage: report.assistantMessage,
  135. summary: report.summary,
  136. data: report.data,
  137. files: report.files,
  138. warnings,
  139. nextActions: report.nextActions || [
  140. '商务复核强推荐账号的主页、报价有效期和近期内容',
  141. '把客户剔除/偏好反馈写入 tihao_preference_update',
  142. collectionMode === 'sample' ? '确认 SOP 后切换 live 小规模检索' : '确认额度后逐步扩大 keywordLimit/pagesPerKeyword'
  143. ]
  144. });
  145. }
  146. function resolveCollectionMode(input = {}) {
  147. const explicitMode = input.collectionMode || input.collection_mode;
  148. return explicitMode === 'sample' ? 'sample' : 'live';
  149. }
  150. function withLiveSmallRunDefaults(input = {}) {
  151. return {
  152. ...input,
  153. keywordLimit: input.keywordLimit || input.keyword_limit || 1,
  154. pagesPerKeyword: input.pagesPerKeyword || input.pages_per_keyword || 1
  155. };
  156. }
  157. function buildEvidenceStatus(evidenceCards, providerStatus = {}) {
  158. return {
  159. cardCount: evidenceCards.length,
  160. status: evidenceCards.length ? 'evidence_cards_loaded' : 'no_multimodal_evidence',
  161. providerRequested: Boolean(providerStatus.requested),
  162. providerStatus: providerStatus.status || 'not_requested',
  163. providerHttpStatus: providerStatus.httpStatus || 0,
  164. providerWarning: providerStatus.warning || '',
  165. note: providerStatus.warning || (evidenceCards.length
  166. ? '已加载多模态证据卡,可用于帧图/ASR/视觉复核。'
  167. : '本轮未加载多模态证据卡。')
  168. };
  169. }
  170. function buildHomepageEvidenceStatus(status = {}) {
  171. return {
  172. requested: Boolean(status.requested),
  173. status: status.status || 'not_requested',
  174. providerStatus: status.status || 'not_requested',
  175. source: status.source || '',
  176. httpStatus: status.httpStatus || 0,
  177. evidenceCount: status.evidenceCount || 0,
  178. note: status.note || status.warning || ''
  179. };
  180. }
  181. function labelPlatform(platform) {
  182. return {
  183. xiaohongshu: '小红书',
  184. douyin: '抖音',
  185. bilibili: 'B站'
  186. }[platform] || platform;
  187. }
  188. module.exports = {
  189. runTihaoSourcing,
  190. resolveCollectionMode,
  191. withLiveSmallRunDefaults
  192. };