business-workflow.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. const path = require('path');
  2. const { runXiaohongshuTrend } = require('../../tools/xiaohongshu-trend-run');
  3. const { runDouyinTrend } = require('../../tools/douyin-trend-run');
  4. const { runVocIssuePool } = require('../../tools/voc-issue-pool-run');
  5. const { runVocProblemDeepDive } = require('../../tools/voc-problem-deep-dive-run');
  6. const { runVocContentPlan } = require('../../tools/voc-content-plan-run');
  7. const { runVocSpeakingScript } = require('../../tools/voc-speaking-script-run');
  8. function asList(value) {
  9. if (!value) return [];
  10. if (Array.isArray(value)) return value.map(String).map(item => item.trim()).filter(Boolean);
  11. return String(value).split(/[,,、\n\r]+/).map(item => item.trim()).filter(Boolean);
  12. }
  13. function firstText(...values) {
  14. for (const value of values) {
  15. if (value && String(value).trim()) return String(value).trim();
  16. }
  17. return '';
  18. }
  19. function collectEvidence(trendResult) {
  20. const comments = trendResult.data && Array.isArray(trendResult.data.comments)
  21. ? trendResult.data.comments
  22. : [];
  23. const notes = trendResult.data && Array.isArray(trendResult.data.notes)
  24. ? trendResult.data.notes
  25. : [];
  26. const videos = trendResult.data && Array.isArray(trendResult.data.videos)
  27. ? trendResult.data.videos
  28. : [];
  29. const commentEvidence = comments
  30. .slice(0, 12)
  31. .map(comment => comment.text || comment.content)
  32. .filter(Boolean);
  33. const itemEvidence = [...notes, ...videos]
  34. .slice(0, 5)
  35. .map(item => firstText(item.title, item.desc, item.content, item.contentExcerpt))
  36. .filter(Boolean);
  37. return [...commentEvidence, ...itemEvidence].slice(0, 16);
  38. }
  39. function extractIssues(issuePoolResult, fallbackEvidence) {
  40. const issues = issuePoolResult.data && Array.isArray(issuePoolResult.data.issues)
  41. ? issuePoolResult.data.issues
  42. : [];
  43. const titles = issues.map(issue => issue.title || issue.issue || issue.name).filter(Boolean);
  44. return titles.length ? titles : fallbackEvidence.slice(0, 3);
  45. }
  46. function compactSection(title, body, maxLines = 18) {
  47. const lines = String(body || '').split('\n').filter(Boolean);
  48. return [`## ${title}`, '', ...lines.slice(0, maxLines)].join('\n');
  49. }
  50. async function runBusinessWorkflow(input = {}) {
  51. const platform = String(input.platform || input.sourcePlatform || 'douyin').toLowerCase().includes('xiao')
  52. || String(input.platform || '').includes('小红书')
  53. ? 'xiaohongshu'
  54. : 'douyin';
  55. const collectionMode = input.collectionMode || input['collection-mode'] || 'sample';
  56. const project = firstText(input.project, input.brand, input.store, 'VOC经营闭环');
  57. const industry = firstText(input.industry, input.category, '本地生活餐饮');
  58. const brand = firstText(input.brand, input.store, project);
  59. const scenario = firstText(input.scenario, input.scene, '经营诊断');
  60. const audience = firstText(input.audience, input.targetAudience, '目标用户');
  61. const keywords = asList(input.keywords).length ? asList(input.keywords) : asList(input.keyword);
  62. const outputRoot = path.resolve(input.output || path.join('outputs', 'voc-business-workflow', new Date().toISOString().slice(0, 10)));
  63. const trendInput = {
  64. ...input,
  65. project,
  66. industry,
  67. targetAudience: Array.isArray(input.targetAudience) ? input.targetAudience : [audience],
  68. keywords: keywords.length ? keywords : undefined,
  69. collectionMode,
  70. output: path.join(outputRoot, platform === 'xiaohongshu' ? 'xiaohongshu-trend' : 'douyin-trend'),
  71. keywordLimit: input.keywordLimit || input['keyword-limit'] || 2,
  72. notesPerKeyword: input.notesPerKeyword || input['notes-per-keyword'] || 2,
  73. videosPerKeyword: input.videosPerKeyword || input['videos-per-keyword'] || 2,
  74. maxCommentPages: input.maxCommentPages || input['max-comment-pages'] || 1
  75. };
  76. const trendResult = platform === 'xiaohongshu'
  77. ? await runXiaohongshuTrend(trendInput)
  78. : await runDouyinTrend(trendInput);
  79. if (trendResult.status !== 'ok') {
  80. return {
  81. ...trendResult,
  82. assistantMessage: [
  83. '# VOC经营闭环未完成',
  84. '',
  85. '第一步真实声音采集没有拿到可继续分析的样本,先按下面提示处理:',
  86. '',
  87. trendResult.assistantMessage || '',
  88. '',
  89. '处理完成后再说:继续做经营闭环。'
  90. ].filter(Boolean).join('\n')
  91. };
  92. }
  93. const evidence = collectEvidence(trendResult);
  94. const issuePool = await runVocIssuePool({
  95. project,
  96. brand,
  97. industry,
  98. scenario,
  99. audience,
  100. evidence,
  101. reportText: trendResult.assistantMessage
  102. });
  103. const issues = extractIssues(issuePool, evidence);
  104. const topIssue = firstText(input.issue, input.problem, issues[0], '用户顾虑不清晰');
  105. const deepDive = await runVocProblemDeepDive({
  106. project,
  107. brand,
  108. industry,
  109. scenario,
  110. audience,
  111. issue: topIssue,
  112. evidence
  113. });
  114. const contentPlan = await runVocContentPlan({
  115. project,
  116. brand,
  117. industry,
  118. platform: platform === 'xiaohongshu' ? '小红书' : '抖音',
  119. issues: issues.slice(0, 5),
  120. reportText: deepDive.assistantMessage
  121. });
  122. const firstTopic = contentPlan.data && Array.isArray(contentPlan.data.plan)
  123. ? contentPlan.data.plan[0]
  124. : null;
  125. const speakingScript = await runVocSpeakingScript({
  126. project,
  127. brand,
  128. industry,
  129. topic: firstText(input.topic, firstTopic && firstTopic.title, topIssue),
  130. userIssue: firstText(firstTopic && firstTopic.userIssue, topIssue),
  131. evidence: evidence.slice(0, 5),
  132. feedback: input.feedback,
  133. finalize: Boolean(input.finalize)
  134. });
  135. const files = [
  136. ...(trendResult.files || []),
  137. ...(issuePool.files || []),
  138. ...(deepDive.files || []),
  139. ...(contentPlan.files || []),
  140. ...(speakingScript.files || [])
  141. ];
  142. const assistantMessage = [
  143. '# VOC经营闭环(第一轮)',
  144. '',
  145. '你这轮能拿到:市场真实声音、问题优先级、一个单点解决动作、7天内容方向,以及一条可继续改稿的口播稿。',
  146. '',
  147. `- 平台:${platform === 'xiaohongshu' ? '小红书' : '抖音'}`,
  148. `- 模式:${collectionMode}`,
  149. `- 行业/品类:${industry}`,
  150. `- 品牌/门店:${brand}`,
  151. '',
  152. compactSection('1. 市场和用户声音', trendResult.assistantMessage, 16),
  153. '',
  154. compactSection('2. 先改哪个问题', issuePool.assistantMessage, 18),
  155. '',
  156. compactSection('3. Top问题怎么解决', deepDive.assistantMessage, 18),
  157. '',
  158. compactSection('4. 下周账号发什么', contentPlan.assistantMessage, 18),
  159. '',
  160. compactSection('5. 第一条口播稿', speakingScript.assistantMessage, 18),
  161. '',
  162. '## 下一步建议',
  163. '',
  164. `1. 先确认 Top 问题“${topIssue}”是否真的最影响生意。`,
  165. '2. 从 7 天内容计划里选 1 条进入改稿。',
  166. '3. 7 天后反馈哪些动作有效/无效,让记忆继续优化。'
  167. ].join('\n');
  168. return {
  169. status: 'ok',
  170. assistantMessage,
  171. summary: {
  172. workflowStage: 'business_workflow_first_round',
  173. platform,
  174. collectionMode,
  175. trendStatus: trendResult.status,
  176. issueCount: issues.length,
  177. topIssue,
  178. contentTopicCount: contentPlan.summary && contentPlan.summary.topicCount,
  179. speakingScriptStatus: speakingScript.status
  180. },
  181. data: {
  182. trend: trendResult,
  183. issuePool,
  184. deepDive,
  185. contentPlan,
  186. speakingScript
  187. },
  188. files,
  189. nextActions: [
  190. `确认 Top 问题“${topIssue}”是否优先`,
  191. '选择 7 天内容计划中的一条继续改成最终口播稿',
  192. '执行 7 天后反馈有效/无效动作,让记忆继续收敛'
  193. ],
  194. warnings: [
  195. collectionMode === 'sample' ? '本轮使用 sample 演示数据,不是真实采集。' : ''
  196. ].filter(Boolean),
  197. errors: []
  198. };
  199. }
  200. module.exports = {
  201. runBusinessWorkflow
  202. };