| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214 |
- const path = require('path');
- const { runXiaohongshuTrend } = require('../../tools/xiaohongshu-trend-run');
- const { runDouyinTrend } = require('../../tools/douyin-trend-run');
- const { runVocIssuePool } = require('../../tools/voc-issue-pool-run');
- const { runVocProblemDeepDive } = require('../../tools/voc-problem-deep-dive-run');
- const { runVocContentPlan } = require('../../tools/voc-content-plan-run');
- const { runVocSpeakingScript } = require('../../tools/voc-speaking-script-run');
- function asList(value) {
- if (!value) return [];
- if (Array.isArray(value)) return value.map(String).map(item => item.trim()).filter(Boolean);
- return String(value).split(/[,,、\n\r]+/).map(item => item.trim()).filter(Boolean);
- }
- function firstText(...values) {
- for (const value of values) {
- if (value && String(value).trim()) return String(value).trim();
- }
- return '';
- }
- function collectEvidence(trendResult) {
- const comments = trendResult.data && Array.isArray(trendResult.data.comments)
- ? trendResult.data.comments
- : [];
- const notes = trendResult.data && Array.isArray(trendResult.data.notes)
- ? trendResult.data.notes
- : [];
- const videos = trendResult.data && Array.isArray(trendResult.data.videos)
- ? trendResult.data.videos
- : [];
- const commentEvidence = comments
- .slice(0, 12)
- .map(comment => comment.text || comment.content)
- .filter(Boolean);
- const itemEvidence = [...notes, ...videos]
- .slice(0, 5)
- .map(item => firstText(item.title, item.desc, item.content, item.contentExcerpt))
- .filter(Boolean);
- return [...commentEvidence, ...itemEvidence].slice(0, 16);
- }
- function extractIssues(issuePoolResult, fallbackEvidence) {
- const issues = issuePoolResult.data && Array.isArray(issuePoolResult.data.issues)
- ? issuePoolResult.data.issues
- : [];
- const titles = issues.map(issue => issue.title || issue.issue || issue.name).filter(Boolean);
- return titles.length ? titles : fallbackEvidence.slice(0, 3);
- }
- function compactSection(title, body, maxLines = 18) {
- const lines = String(body || '').split('\n').filter(Boolean);
- return [`## ${title}`, '', ...lines.slice(0, maxLines)].join('\n');
- }
- async function runBusinessWorkflow(input = {}) {
- const platform = String(input.platform || input.sourcePlatform || 'douyin').toLowerCase().includes('xiao')
- || String(input.platform || '').includes('小红书')
- ? 'xiaohongshu'
- : 'douyin';
- const collectionMode = input.collectionMode || input['collection-mode'] || 'sample';
- const project = firstText(input.project, input.brand, input.store, 'VOC经营闭环');
- const industry = firstText(input.industry, input.category, '本地生活餐饮');
- const brand = firstText(input.brand, input.store, project);
- const scenario = firstText(input.scenario, input.scene, '经营诊断');
- const audience = firstText(input.audience, input.targetAudience, '目标用户');
- const keywords = asList(input.keywords).length ? asList(input.keywords) : asList(input.keyword);
- const outputRoot = path.resolve(input.output || path.join('outputs', 'voc-business-workflow', new Date().toISOString().slice(0, 10)));
- const trendInput = {
- ...input,
- project,
- industry,
- targetAudience: Array.isArray(input.targetAudience) ? input.targetAudience : [audience],
- keywords: keywords.length ? keywords : undefined,
- collectionMode,
- output: path.join(outputRoot, platform === 'xiaohongshu' ? 'xiaohongshu-trend' : 'douyin-trend'),
- keywordLimit: input.keywordLimit || input['keyword-limit'] || 2,
- notesPerKeyword: input.notesPerKeyword || input['notes-per-keyword'] || 2,
- videosPerKeyword: input.videosPerKeyword || input['videos-per-keyword'] || 2,
- maxCommentPages: input.maxCommentPages || input['max-comment-pages'] || 1
- };
- const trendResult = platform === 'xiaohongshu'
- ? await runXiaohongshuTrend(trendInput)
- : await runDouyinTrend(trendInput);
- if (trendResult.status !== 'ok') {
- return {
- ...trendResult,
- assistantMessage: [
- '# VOC经营闭环未完成',
- '',
- '第一步真实声音采集没有拿到可继续分析的样本,先按下面提示处理:',
- '',
- trendResult.assistantMessage || '',
- '',
- '处理完成后再说:继续做经营闭环。'
- ].filter(Boolean).join('\n')
- };
- }
- const evidence = collectEvidence(trendResult);
- const issuePool = await runVocIssuePool({
- project,
- brand,
- industry,
- scenario,
- audience,
- evidence,
- reportText: trendResult.assistantMessage
- });
- const issues = extractIssues(issuePool, evidence);
- const topIssue = firstText(input.issue, input.problem, issues[0], '用户顾虑不清晰');
- const deepDive = await runVocProblemDeepDive({
- project,
- brand,
- industry,
- scenario,
- audience,
- issue: topIssue,
- evidence
- });
- const contentPlan = await runVocContentPlan({
- project,
- brand,
- industry,
- platform: platform === 'xiaohongshu' ? '小红书' : '抖音',
- issues: issues.slice(0, 5),
- reportText: deepDive.assistantMessage
- });
- const firstTopic = contentPlan.data && Array.isArray(contentPlan.data.plan)
- ? contentPlan.data.plan[0]
- : null;
- const speakingScript = await runVocSpeakingScript({
- project,
- brand,
- industry,
- topic: firstText(input.topic, firstTopic && firstTopic.title, topIssue),
- userIssue: firstText(firstTopic && firstTopic.userIssue, topIssue),
- evidence: evidence.slice(0, 5),
- feedback: input.feedback,
- finalize: Boolean(input.finalize)
- });
- const files = [
- ...(trendResult.files || []),
- ...(issuePool.files || []),
- ...(deepDive.files || []),
- ...(contentPlan.files || []),
- ...(speakingScript.files || [])
- ];
- const assistantMessage = [
- '# VOC经营闭环(第一轮)',
- '',
- '你这轮能拿到:市场真实声音、问题优先级、一个单点解决动作、7天内容方向,以及一条可继续改稿的口播稿。',
- '',
- `- 平台:${platform === 'xiaohongshu' ? '小红书' : '抖音'}`,
- `- 模式:${collectionMode}`,
- `- 行业/品类:${industry}`,
- `- 品牌/门店:${brand}`,
- '',
- compactSection('1. 市场和用户声音', trendResult.assistantMessage, 16),
- '',
- compactSection('2. 先改哪个问题', issuePool.assistantMessage, 18),
- '',
- compactSection('3. Top问题怎么解决', deepDive.assistantMessage, 18),
- '',
- compactSection('4. 下周账号发什么', contentPlan.assistantMessage, 18),
- '',
- compactSection('5. 第一条口播稿', speakingScript.assistantMessage, 18),
- '',
- '## 下一步建议',
- '',
- `1. 先确认 Top 问题“${topIssue}”是否真的最影响生意。`,
- '2. 从 7 天内容计划里选 1 条进入改稿。',
- '3. 7 天后反馈哪些动作有效/无效,让记忆继续优化。'
- ].join('\n');
- return {
- status: 'ok',
- assistantMessage,
- summary: {
- workflowStage: 'business_workflow_first_round',
- platform,
- collectionMode,
- trendStatus: trendResult.status,
- issueCount: issues.length,
- topIssue,
- contentTopicCount: contentPlan.summary && contentPlan.summary.topicCount,
- speakingScriptStatus: speakingScript.status
- },
- data: {
- trend: trendResult,
- issuePool,
- deepDive,
- contentPlan,
- speakingScript
- },
- files,
- nextActions: [
- `确认 Top 问题“${topIssue}”是否优先`,
- '选择 7 天内容计划中的一条继续改成最终口播稿',
- '执行 7 天后反馈有效/无效动作,让记忆继续收敛'
- ],
- warnings: [
- collectionMode === 'sample' ? '本轮使用 sample 演示数据,不是真实采集。' : ''
- ].filter(Boolean),
- errors: []
- };
- }
- module.exports = {
- runBusinessWorkflow
- };
|