|
|
@@ -0,0 +1,585 @@
|
|
|
+#!/usr/bin/env node
|
|
|
+const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
|
|
|
+const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
|
+const { z } = require('zod');
|
|
|
+const { readXiaohongshuToken, readVocToken } = require('./core/credentials');
|
|
|
+const { runXiaohongshuTrend } = require('./tools/xiaohongshu-trend-run');
|
|
|
+const { updateXiaohongshuPreference } = require('./tools/xiaohongshu-preference-update');
|
|
|
+const { runDouyinTrend } = require('./tools/douyin-trend-run');
|
|
|
+const { updateDouyinPreference } = require('./tools/douyin-preference-update');
|
|
|
+const { runVocProblemDeepDive } = require('./tools/voc-problem-deep-dive-run');
|
|
|
+const { runVocIssuePool } = require('./tools/voc-issue-pool-run');
|
|
|
+const { runVocContentPlan } = require('./tools/voc-content-plan-run');
|
|
|
+const { runVocCompetitorMap } = require('./tools/voc-competitor-map-run');
|
|
|
+const { runVocSpeakingScript } = require('./tools/voc-speaking-script-run');
|
|
|
+const { runBusinessWorkflow } = require('./tools/voc-business-workflow-run');
|
|
|
+const { buildVocRechargeInfo, buildMissingTokenMessage } = require('./core/payment-links');
|
|
|
+
|
|
|
+function asToolResult(result) {
|
|
|
+ return {
|
|
|
+ content: [
|
|
|
+ {
|
|
|
+ type: 'text',
|
|
|
+ text: result.assistantMessage || JSON.stringify(result, null, 2)
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ structuredContent: result,
|
|
|
+ isError: result.status !== 'ok'
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+function createServer() {
|
|
|
+ const server = new McpServer({
|
|
|
+ name: 'voc-intelligence',
|
|
|
+ version: '0.3.10'
|
|
|
+ });
|
|
|
+
|
|
|
+ server.registerTool(
|
|
|
+ 'voc_business_workflow_run',
|
|
|
+ {
|
|
|
+ title: 'Run VOC Business Workflow',
|
|
|
+ description: [
|
|
|
+ 'Run the full VOC business workflow from market voices to issue pool, top issue deep dive, 7-day content plan, and one speaking script.',
|
|
|
+ 'Use when the user asks to see what customers care about, decide what to fix first, and know what to post next week in one end-to-end workflow.'
|
|
|
+ ].join(' '),
|
|
|
+ inputSchema: {
|
|
|
+ project: z.string().optional(),
|
|
|
+ brand: z.string().optional(),
|
|
|
+ store: z.string().optional(),
|
|
|
+ industry: z.string().optional(),
|
|
|
+ category: z.string().optional(),
|
|
|
+ platform: z.enum(['douyin', 'xiaohongshu', '抖音', '小红书']).optional(),
|
|
|
+ collectionMode: z.enum(['sample', 'live']).optional(),
|
|
|
+ scenario: z.string().optional(),
|
|
|
+ scene: z.string().optional(),
|
|
|
+ audience: z.string().optional(),
|
|
|
+ targetAudience: z.string().optional(),
|
|
|
+ keywords: z.array(z.string()).optional(),
|
|
|
+ keyword: z.string().optional(),
|
|
|
+ issue: z.string().optional(),
|
|
|
+ problem: z.string().optional(),
|
|
|
+ topic: z.string().optional(),
|
|
|
+ feedback: z.string().optional(),
|
|
|
+ finalize: z.boolean().optional(),
|
|
|
+ output: z.string().optional(),
|
|
|
+ keywordLimit: z.number().int().min(1).max(10).optional(),
|
|
|
+ notesPerKeyword: z.number().int().min(1).max(10).optional(),
|
|
|
+ videosPerKeyword: z.number().int().min(1).max(10).optional(),
|
|
|
+ maxCommentPages: z.number().int().min(0).max(5).optional(),
|
|
|
+ xiaohongshuToken: z.string().optional(),
|
|
|
+ douyinToken: z.string().optional(),
|
|
|
+ vocToken: z.string().optional()
|
|
|
+ },
|
|
|
+ outputSchema: {
|
|
|
+ status: z.string(),
|
|
|
+ assistantMessage: z.string(),
|
|
|
+ summary: z.record(z.any()).optional(),
|
|
|
+ data: z.record(z.any()).optional(),
|
|
|
+ files: z.array(z.string()).optional(),
|
|
|
+ nextActions: z.array(z.string()).optional(),
|
|
|
+ warnings: z.array(z.any()).optional(),
|
|
|
+ errors: z.array(z.any()).optional()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async input => asToolResult(await runBusinessWorkflow(input))
|
|
|
+ );
|
|
|
+
|
|
|
+ server.registerTool(
|
|
|
+ 'voc_speaking_script_run',
|
|
|
+ {
|
|
|
+ title: 'Run VOC Speaking Script Co-Creation',
|
|
|
+ description: [
|
|
|
+ 'Turn one VOC-backed topic into a co-created speaking script.',
|
|
|
+ 'Use after a content plan when the user selects a topic, asks to enter script co-creation, revise a speaking script, finalize a draft, or save script preferences.'
|
|
|
+ ].join(' '),
|
|
|
+ inputSchema: {
|
|
|
+ project: z.string().optional(),
|
|
|
+ brand: z.string().optional(),
|
|
|
+ store: z.string().optional(),
|
|
|
+ industry: z.string().optional(),
|
|
|
+ category: z.string().optional(),
|
|
|
+ topic: z.string().optional(),
|
|
|
+ title: z.string().optional(),
|
|
|
+ selectedTopic: z.string().optional(),
|
|
|
+ userIssue: z.string().optional(),
|
|
|
+ issue: z.string().optional(),
|
|
|
+ problem: z.string().optional(),
|
|
|
+ issues: z.array(z.string()).optional(),
|
|
|
+ problems: z.array(z.string()).optional(),
|
|
|
+ vocIssues: z.array(z.string()).optional(),
|
|
|
+ evidence: z.array(z.string()).optional(),
|
|
|
+ comments: z.array(z.string()).optional(),
|
|
|
+ voices: z.array(z.string()).optional(),
|
|
|
+ vocEvidence: z.array(z.string()).optional(),
|
|
|
+ evidenceText: z.string().optional(),
|
|
|
+ feedback: z.string().optional(),
|
|
|
+ message: z.string().optional(),
|
|
|
+ preferredStyles: z.array(z.string()).optional(),
|
|
|
+ blockedStyles: z.array(z.string()).optional(),
|
|
|
+ finalize: z.boolean().optional(),
|
|
|
+ memory: z.string().optional(),
|
|
|
+ memoryPath: z.string().optional(),
|
|
|
+ output: z.string().optional()
|
|
|
+ },
|
|
|
+ outputSchema: {
|
|
|
+ status: z.string(),
|
|
|
+ assistantMessage: z.string(),
|
|
|
+ summary: z.record(z.any()).optional(),
|
|
|
+ data: z.record(z.any()).optional(),
|
|
|
+ files: z.array(z.string()).optional(),
|
|
|
+ nextActions: z.array(z.string()).optional(),
|
|
|
+ warnings: z.array(z.any()).optional(),
|
|
|
+ errors: z.array(z.any()).optional()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async input => asToolResult(await runVocSpeakingScript(input))
|
|
|
+ );
|
|
|
+
|
|
|
+ server.registerTool(
|
|
|
+ 'voc_content_plan_run',
|
|
|
+ {
|
|
|
+ title: 'Run VOC Content Plan',
|
|
|
+ description: [
|
|
|
+ 'Turn VOC issues, comments, trend reports, or issue-pool findings into a 7-day content plan and speaking scripts.',
|
|
|
+ 'Use when the user asks what to post next week, wants口播脚本, video topics, account content ideas, or wants to turn customer problems into marketing content.'
|
|
|
+ ].join(' '),
|
|
|
+ inputSchema: {
|
|
|
+ project: z.string().optional(),
|
|
|
+ brand: z.string().optional(),
|
|
|
+ store: z.string().optional(),
|
|
|
+ industry: z.string().optional(),
|
|
|
+ category: z.string().optional(),
|
|
|
+ platform: z.string().optional(),
|
|
|
+ days: z.number().int().min(1).max(15).optional(),
|
|
|
+ issues: z.array(z.string()).optional(),
|
|
|
+ problems: z.array(z.string()).optional(),
|
|
|
+ vocIssues: z.array(z.string()).optional(),
|
|
|
+ evidence: z.array(z.string()).optional(),
|
|
|
+ comments: z.array(z.string()).optional(),
|
|
|
+ evidenceText: z.string().optional(),
|
|
|
+ reportText: z.string().optional(),
|
|
|
+ report: z.string().optional(),
|
|
|
+ reportPath: z.string().optional()
|
|
|
+ },
|
|
|
+ outputSchema: {
|
|
|
+ status: z.string(),
|
|
|
+ assistantMessage: z.string(),
|
|
|
+ summary: z.record(z.any()).optional(),
|
|
|
+ data: z.record(z.any()).optional(),
|
|
|
+ files: z.array(z.string()).optional(),
|
|
|
+ nextActions: z.array(z.string()).optional(),
|
|
|
+ warnings: z.array(z.any()).optional(),
|
|
|
+ errors: z.array(z.any()).optional()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async input => asToolResult(await runVocContentPlan(input))
|
|
|
+ );
|
|
|
+
|
|
|
+ server.registerTool(
|
|
|
+ 'voc_competitor_map_run',
|
|
|
+ {
|
|
|
+ title: 'Run VOC Competitor Map',
|
|
|
+ description: [
|
|
|
+ 'Build a competitor map and differentiated opportunity report from category, city, competitors, and VOC evidence.',
|
|
|
+ 'Use when the user asks to look at competitors, competitor analysis, who else is doing well, or how the brand should compete differently.'
|
|
|
+ ].join(' '),
|
|
|
+ inputSchema: {
|
|
|
+ project: z.string().optional(),
|
|
|
+ brand: z.string().optional(),
|
|
|
+ store: z.string().optional(),
|
|
|
+ industry: z.string().optional(),
|
|
|
+ category: z.string().optional(),
|
|
|
+ city: z.string().optional(),
|
|
|
+ region: z.string().optional(),
|
|
|
+ priceBand: z.string().optional(),
|
|
|
+ price: z.string().optional(),
|
|
|
+ scenario: z.string().optional(),
|
|
|
+ scene: z.string().optional(),
|
|
|
+ competitors: z.array(z.string()).optional(),
|
|
|
+ competitorNames: z.array(z.string()).optional(),
|
|
|
+ evidence: z.array(z.string()).optional(),
|
|
|
+ comments: z.array(z.string()).optional(),
|
|
|
+ voices: z.array(z.string()).optional(),
|
|
|
+ reportText: z.string().optional(),
|
|
|
+ report: z.string().optional(),
|
|
|
+ reportPath: z.string().optional()
|
|
|
+ },
|
|
|
+ outputSchema: {
|
|
|
+ status: z.string(),
|
|
|
+ assistantMessage: z.string(),
|
|
|
+ summary: z.record(z.any()).optional(),
|
|
|
+ data: z.record(z.any()).optional(),
|
|
|
+ files: z.array(z.string()).optional(),
|
|
|
+ nextActions: z.array(z.string()).optional(),
|
|
|
+ warnings: z.array(z.any()).optional(),
|
|
|
+ errors: z.array(z.any()).optional()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async input => asToolResult(await runVocCompetitorMap(input))
|
|
|
+ );
|
|
|
+
|
|
|
+ server.registerTool(
|
|
|
+ 'voc_issue_pool_run',
|
|
|
+ {
|
|
|
+ title: 'Run VOC Issue Pool',
|
|
|
+ description: [
|
|
|
+ 'Turn user comments, report text, or VOC snippets into a prioritized issue pool.',
|
|
|
+ 'Use when the user asks which real problems matter most, asks to manage VOC issues, or wants a problem list before deep-diving.'
|
|
|
+ ].join(' '),
|
|
|
+ inputSchema: {
|
|
|
+ project: z.string().optional().describe('Optional project, brand, account, or store name used to isolate issue-pool memory.'),
|
|
|
+ brand: z.string().optional(),
|
|
|
+ store: z.string().optional(),
|
|
|
+ industry: z.string().optional(),
|
|
|
+ category: z.string().optional(),
|
|
|
+ scenario: z.string().optional(),
|
|
|
+ scene: z.string().optional(),
|
|
|
+ businessType: z.string().optional(),
|
|
|
+ audience: z.string().optional(),
|
|
|
+ targetAudience: z.string().optional(),
|
|
|
+ evidence: z.array(z.string()).optional().describe('User comments or VOC snippets.'),
|
|
|
+ comments: z.array(z.string()).optional().describe('User comment snippets.'),
|
|
|
+ voices: z.array(z.string()).optional().describe('User voice snippets.'),
|
|
|
+ issues: z.array(z.string()).optional().describe('Known issue snippets.'),
|
|
|
+ issueTexts: z.array(z.string()).optional().describe('Known issue snippets.'),
|
|
|
+ evidenceText: z.string().optional().describe('Raw comments or evidence text.'),
|
|
|
+ reportText: z.string().optional().describe('Previous report markdown text.'),
|
|
|
+ report: z.string().optional().describe('Optional previous report markdown path.'),
|
|
|
+ reportPath: z.string().optional().describe('Optional previous report markdown path.'),
|
|
|
+ memory: z.string().optional().describe('Optional issue-pool memory JSON path.'),
|
|
|
+ memoryPath: z.string().optional().describe('Optional issue-pool memory JSON path.'),
|
|
|
+ feedback: z.string().optional().describe('User feedback or status notes.'),
|
|
|
+ message: z.string().optional().describe('User feedback or status notes.'),
|
|
|
+ resolvedIssues: z.array(z.string()).optional().describe('Issue names or ids already resolved.'),
|
|
|
+ validatingIssues: z.array(z.string()).optional().describe('Issue names or ids currently validating.'),
|
|
|
+ blockedIssues: z.array(z.string()).optional().describe('Issue names or ids to pause.'),
|
|
|
+ statusUpdates: z.record(z.string()).optional().describe('Map of issue title/id to status.')
|
|
|
+ },
|
|
|
+ outputSchema: {
|
|
|
+ status: z.string(),
|
|
|
+ assistantMessage: z.string(),
|
|
|
+ summary: z.record(z.any()).optional(),
|
|
|
+ data: z.record(z.any()).optional(),
|
|
|
+ files: z.array(z.string()).optional(),
|
|
|
+ nextActions: z.array(z.string()).optional(),
|
|
|
+ warnings: z.array(z.any()).optional(),
|
|
|
+ errors: z.array(z.any()).optional()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async input => {
|
|
|
+ const result = await runVocIssuePool(input);
|
|
|
+ return asToolResult(result);
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ server.registerTool(
|
|
|
+ 'voc_problem_deep_dive_run',
|
|
|
+ {
|
|
|
+ title: 'Run VOC Problem Deep Dive',
|
|
|
+ description: [
|
|
|
+ 'Deep-dive a single VOC problem from a boss/operator perspective.',
|
|
|
+ 'Use when the user says continue digging, what should the boss do, how to solve this problem, or turn this VOC into actions.'
|
|
|
+ ].join(' '),
|
|
|
+ inputSchema: {
|
|
|
+ issue: z.string().optional().describe('Single VOC issue to deep dive, e.g. 排队, 贵, 不好吃, 服务差.'),
|
|
|
+ problem: z.string().optional().describe('Alias for issue.'),
|
|
|
+ topic: z.string().optional().describe('Alias for issue.'),
|
|
|
+ project: z.string().optional().describe('Optional project, brand, or account name used to isolate deep-dive memory.'),
|
|
|
+ brand: z.string().optional().describe('Optional brand name used to isolate deep-dive memory.'),
|
|
|
+ store: z.string().optional().describe('Optional store name used to isolate deep-dive memory.'),
|
|
|
+ industry: z.string().optional(),
|
|
|
+ category: z.string().optional(),
|
|
|
+ scenario: z.string().optional(),
|
|
|
+ scene: z.string().optional(),
|
|
|
+ businessType: z.string().optional(),
|
|
|
+ audience: z.string().optional(),
|
|
|
+ targetAudience: z.string().optional(),
|
|
|
+ evidence: z.array(z.string()).optional().describe('Optional user comments or VOC snippets.'),
|
|
|
+ evidenceText: z.string().optional().describe('Optional text with comments or evidence.'),
|
|
|
+ comments: z.array(z.string()).optional().describe('Optional comment snippets.'),
|
|
|
+ voices: z.array(z.string()).optional().describe('Optional user voice snippets.'),
|
|
|
+ report: z.string().optional().describe('Optional previous report markdown path.'),
|
|
|
+ reportPath: z.string().optional().describe('Optional previous report markdown path.'),
|
|
|
+ memory: z.string().optional().describe('Optional deep-dive memory JSON path.'),
|
|
|
+ memoryPath: z.string().optional().describe('Optional deep-dive memory JSON path.'),
|
|
|
+ feedback: z.string().optional().describe('User feedback from previous iteration.'),
|
|
|
+ message: z.string().optional().describe('User feedback from previous iteration.'),
|
|
|
+ preferredActions: z.array(z.string()).optional().describe('Actions the user prefers.'),
|
|
|
+ blockedActions: z.array(z.string()).optional().describe('Actions the user does not want.'),
|
|
|
+ preferredContentAngles: z.array(z.string()).optional().describe('Content angles the user prefers.'),
|
|
|
+ validatedActions: z.array(z.string()).optional().describe('Actions proven useful in practice.'),
|
|
|
+ rejectedActions: z.array(z.string()).optional().describe('Actions proven ineffective in practice.')
|
|
|
+ },
|
|
|
+ outputSchema: {
|
|
|
+ status: z.string(),
|
|
|
+ assistantMessage: z.string(),
|
|
|
+ summary: z.record(z.any()).optional(),
|
|
|
+ data: z.record(z.any()).optional(),
|
|
|
+ files: z.array(z.string()).optional(),
|
|
|
+ nextActions: z.array(z.string()).optional(),
|
|
|
+ warnings: z.array(z.any()).optional(),
|
|
|
+ errors: z.array(z.any()).optional()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async input => {
|
|
|
+ const result = await runVocProblemDeepDive(input);
|
|
|
+ return asToolResult(result);
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ server.registerTool(
|
|
|
+ 'voc_xiaohongshu_token_check',
|
|
|
+ {
|
|
|
+ title: 'Check Xiaohongshu Token',
|
|
|
+ description: 'Check whether a Xiaohongshu/TikHub API token is configured for live collection.',
|
|
|
+ inputSchema: {
|
|
|
+ xiaohongshuToken: z.string().optional().describe('Optional request-scoped collection token. It is never echoed back.'),
|
|
|
+ vocToken: z.string().optional().describe('Optional VOC social token, same convention as douyin-speaking-daily. It is never echoed back.')
|
|
|
+ },
|
|
|
+ outputSchema: {
|
|
|
+ status: z.string(),
|
|
|
+ assistantMessage: z.string(),
|
|
|
+ configured: z.boolean(),
|
|
|
+ data: z.record(z.any()).optional()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async input => {
|
|
|
+ const token = readXiaohongshuToken(input);
|
|
|
+ const recharge = token ? null : await buildVocRechargeInfo();
|
|
|
+ const result = {
|
|
|
+ status: token ? 'ok' : 'needs_token',
|
|
|
+ configured: Boolean(token),
|
|
|
+ data: recharge ? { recharge } : {},
|
|
|
+ assistantMessage: token
|
|
|
+ ? '小红书 live 采集 Token 已配置,可以尝试小规模真实采集。'
|
|
|
+ : buildMissingTokenMessage(recharge.paymentUrl)
|
|
|
+ };
|
|
|
+ return asToolResult(result);
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ server.registerTool(
|
|
|
+ 'voc_douyin_token_check',
|
|
|
+ {
|
|
|
+ title: 'Check Douyin Token',
|
|
|
+ description: 'Check whether a Douyin/VOC API token is configured for live collection.',
|
|
|
+ inputSchema: {
|
|
|
+ douyinToken: z.string().optional().describe('Optional request-scoped Douyin token. It is never echoed back.'),
|
|
|
+ vocToken: z.string().optional().describe('Optional VOC social token, same convention as xiaohongshu trend.'),
|
|
|
+ xiaohongshuToken: z.string().optional().describe('Optional platform token alias, never echoed back.')
|
|
|
+ },
|
|
|
+ outputSchema: {
|
|
|
+ status: z.string(),
|
|
|
+ assistantMessage: z.string(),
|
|
|
+ configured: z.boolean(),
|
|
|
+ data: z.record(z.any()).optional()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async input => {
|
|
|
+ const token = readVocToken(input);
|
|
|
+ const recharge = token ? null : await buildVocRechargeInfo();
|
|
|
+ const result = {
|
|
|
+ status: token ? 'ok' : 'needs_token',
|
|
|
+ configured: Boolean(token),
|
|
|
+ data: recharge ? { recharge } : {},
|
|
|
+ assistantMessage: token
|
|
|
+ ? '抖音 live 采集 Token 已配置,可以开始小规模真实采集。'
|
|
|
+ : buildMissingTokenMessage(recharge.paymentUrl, recharge, { platformLabel: '抖音' })
|
|
|
+ };
|
|
|
+ return asToolResult(result);
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ server.registerTool(
|
|
|
+ 'voc_xiaohongshu_trend_run',
|
|
|
+ {
|
|
|
+ title: 'Run Xiaohongshu Trend Intelligence',
|
|
|
+ description: [
|
|
|
+ 'Generate first-round Xiaohongshu trend observations and validation questions from an industry profile.',
|
|
|
+ 'P0 supports sample mode for demos and course recording.',
|
|
|
+ 'Returns assistantMessage as the user-facing body; first-round output is a hypothesis that needs user calibration.'
|
|
|
+ ].join(' '),
|
|
|
+ inputSchema: {
|
|
|
+ collectionMode: z.enum(['sample', 'live']).optional().describe('Use sample for P0 demo. Live will be enabled after VOC provider integration.'),
|
|
|
+ profile: z.string().optional().describe('Optional profile JSON path.'),
|
|
|
+ output: z.string().optional().describe('Optional output directory.'),
|
|
|
+ memory: z.string().optional().describe('Optional preference memory JSON path.'),
|
|
|
+ memoryPath: z.string().optional().describe('Optional preference memory JSON path.'),
|
|
|
+ project: z.string().optional(),
|
|
|
+ industry: z.string().optional(),
|
|
|
+ businessType: z.string().optional(),
|
|
|
+ targetAudience: z.array(z.string()).optional(),
|
|
|
+ keywords: z.array(z.string()).optional(),
|
|
|
+ trendQuestions: z.array(z.string()).optional(),
|
|
|
+ mustTrackSignals: z.array(z.string()).optional(),
|
|
|
+ keywordLimit: z.number().int().min(1).max(10).optional(),
|
|
|
+ notesPerKeyword: z.number().int().min(1).max(10).optional(),
|
|
|
+ maxCommentPages: z.number().int().min(0).max(5).optional(),
|
|
|
+ sort: z.enum(['general', 'time_descending', 'popularity_descending']).optional(),
|
|
|
+ noteType: z.enum(['_0', '_1', '_2']).optional(),
|
|
|
+ cacheAssets: z.boolean().optional().describe('Whether to cache evidence images into the output assets directory. Defaults to true.'),
|
|
|
+ assetLimit: z.number().int().min(0).max(30).optional().describe('Maximum number of note cover images to cache.'),
|
|
|
+ xiaohongshuToken: z.string().optional().describe('Optional request-scoped collection token. It is used only for this run and never echoed back.'),
|
|
|
+ vocToken: z.string().optional().describe('Optional VOC social token, same convention as douyin-speaking-daily. It is used only for this run and never echoed back.')
|
|
|
+ },
|
|
|
+ outputSchema: {
|
|
|
+ status: z.string(),
|
|
|
+ assistantMessage: z.string(),
|
|
|
+ summary: z.record(z.any()).optional(),
|
|
|
+ data: z.record(z.any()).optional(),
|
|
|
+ files: z.array(z.string()).optional(),
|
|
|
+ nextActions: z.array(z.string()).optional(),
|
|
|
+ warnings: z.array(z.string()).optional(),
|
|
|
+ errors: z.array(z.any()).optional()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async input => {
|
|
|
+ const result = await runXiaohongshuTrend({
|
|
|
+ ...input,
|
|
|
+ collectionMode: input.collectionMode || 'sample'
|
|
|
+ });
|
|
|
+ return asToolResult(result);
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ server.registerTool(
|
|
|
+ 'voc_douyin_trend_run',
|
|
|
+ {
|
|
|
+ title: 'Run Douyin Trend Intelligence',
|
|
|
+ description: [
|
|
|
+ 'Generate first-round Douyin trend observations and validation questions from an industry profile.',
|
|
|
+ 'P0 supports sample mode for demos and course recording.',
|
|
|
+ 'Returns assistantMessage as the user-facing body; first-round output is a hypothesis that needs user calibration.'
|
|
|
+ ].join(' '),
|
|
|
+ inputSchema: {
|
|
|
+ collectionMode: z.enum(['sample', 'live']).optional().describe('Use sample for P0 demo. Live will be enabled after VOC provider integration.'),
|
|
|
+ profile: z.string().optional().describe('Optional profile JSON path.'),
|
|
|
+ output: z.string().optional().describe('Optional output directory.'),
|
|
|
+ memory: z.string().optional().describe('Optional preference memory JSON path.'),
|
|
|
+ memoryPath: z.string().optional().describe('Optional preference memory JSON path.'),
|
|
|
+ project: z.string().optional(),
|
|
|
+ industry: z.string().optional(),
|
|
|
+ businessType: z.string().optional(),
|
|
|
+ targetAudience: z.array(z.string()).optional(),
|
|
|
+ keywords: z.array(z.string()).optional(),
|
|
|
+ trendQuestions: z.array(z.string()).optional(),
|
|
|
+ mustTrackSignals: z.array(z.string()).optional(),
|
|
|
+ keywordLimit: z.number().int().min(1).max(10).optional(),
|
|
|
+ videosPerKeyword: z.number().int().min(1).max(10).optional(),
|
|
|
+ maxCommentPages: z.number().int().min(0).max(5).optional(),
|
|
|
+ sortType: z.enum(['0', '1', '2']).optional(),
|
|
|
+ publishTime: z.enum(['0', '1', '7', '180']).optional(),
|
|
|
+ filterDuration: z.enum(['0', '0-1', '1-5', '5-10000']).optional(),
|
|
|
+ contentType: z.enum(['0', '1', '2', '3']).optional(),
|
|
|
+ cacheAssets: z.boolean().optional().describe('Whether to cache evidence images into the output assets directory. Defaults to true.'),
|
|
|
+ assetLimit: z.number().int().min(0).max(30).optional().describe('Maximum number of video cover images to cache.'),
|
|
|
+ douyinToken: z.string().optional().describe('Optional request-scoped collection token. It is used only for this run and never echoed back.'),
|
|
|
+ vocToken: z.string().optional().describe('Optional VOC social token, same convention as xiaohongshu trend. It is used only for this run and never echoed back.')
|
|
|
+ },
|
|
|
+ outputSchema: {
|
|
|
+ status: z.string(),
|
|
|
+ assistantMessage: z.string(),
|
|
|
+ summary: z.record(z.any()).optional(),
|
|
|
+ data: z.record(z.any()).optional(),
|
|
|
+ files: z.array(z.string()).optional(),
|
|
|
+ nextActions: z.array(z.string()).optional(),
|
|
|
+ warnings: z.array(z.any()).optional(),
|
|
|
+ errors: z.array(z.any()).optional()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async input => {
|
|
|
+ const result = await runDouyinTrend({
|
|
|
+ ...input,
|
|
|
+ collectionMode: input.collectionMode || 'sample'
|
|
|
+ });
|
|
|
+ return asToolResult(result);
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ server.registerTool(
|
|
|
+ 'voc_xiaohongshu_preference_update',
|
|
|
+ {
|
|
|
+ title: 'Update Xiaohongshu Trend Preference',
|
|
|
+ description: [
|
|
|
+ 'Save user feedback for the next Xiaohongshu trend refinement.',
|
|
|
+ 'Use after the user answers validation questions or says what to keep, block, downgrade, or focus on next time.'
|
|
|
+ ].join(' '),
|
|
|
+ inputSchema: {
|
|
|
+ message: z.string().optional().describe('Natural-language user feedback.'),
|
|
|
+ feedback: z.string().optional().describe('Natural-language user feedback.'),
|
|
|
+ memory: z.string().optional().describe('Optional preference memory JSON path.'),
|
|
|
+ memoryPath: z.string().optional().describe('Optional preference memory JSON path.'),
|
|
|
+ preferredDirections: z.array(z.string()).optional(),
|
|
|
+ blockedDirections: z.array(z.string()).optional(),
|
|
|
+ focusModes: z.array(z.string()).optional()
|
|
|
+ },
|
|
|
+ outputSchema: {
|
|
|
+ status: z.string(),
|
|
|
+ assistantMessage: z.string(),
|
|
|
+ summary: z.record(z.any()).optional(),
|
|
|
+ data: z.record(z.any()).optional(),
|
|
|
+ files: z.array(z.string()).optional(),
|
|
|
+ nextActions: z.array(z.string()).optional(),
|
|
|
+ warnings: z.array(z.string()).optional(),
|
|
|
+ errors: z.array(z.any()).optional()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async input => {
|
|
|
+ const result = await updateXiaohongshuPreference(input);
|
|
|
+ return asToolResult(result);
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ server.registerTool(
|
|
|
+ 'voc_douyin_preference_update',
|
|
|
+ {
|
|
|
+ title: 'Update Douyin Trend Preference',
|
|
|
+ description: [
|
|
|
+ 'Save user feedback for the next Douyin trend refinement.',
|
|
|
+ 'Use after the user answers validation questions or says what to keep, block, downgrade, or focus on next time.'
|
|
|
+ ].join(' '),
|
|
|
+ inputSchema: {
|
|
|
+ message: z.string().optional().describe('Natural-language user feedback.'),
|
|
|
+ feedback: z.string().optional().describe('Natural-language user feedback.'),
|
|
|
+ memory: z.string().optional().describe('Optional preference memory JSON path.'),
|
|
|
+ memoryPath: z.string().optional().describe('Optional preference memory JSON path.'),
|
|
|
+ preferredDirections: z.array(z.string()).optional(),
|
|
|
+ blockedDirections: z.array(z.string()).optional(),
|
|
|
+ focusModes: z.array(z.string()).optional()
|
|
|
+ },
|
|
|
+ outputSchema: {
|
|
|
+ status: z.string(),
|
|
|
+ assistantMessage: z.string(),
|
|
|
+ summary: z.record(z.any()).optional(),
|
|
|
+ data: z.record(z.any()).optional(),
|
|
|
+ files: z.array(z.string()).optional(),
|
|
|
+ nextActions: z.array(z.string()).optional(),
|
|
|
+ warnings: z.array(z.any()).optional(),
|
|
|
+ errors: z.array(z.any()).optional()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async input => {
|
|
|
+ const result = await updateDouyinPreference(input);
|
|
|
+ return asToolResult(result);
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ return server;
|
|
|
+}
|
|
|
+
|
|
|
+async function main() {
|
|
|
+ const server = createServer();
|
|
|
+ const transport = new StdioServerTransport();
|
|
|
+ await server.connect(transport);
|
|
|
+}
|
|
|
+
|
|
|
+if (require.main === module) {
|
|
|
+ main().catch(error => {
|
|
|
+ console.error(error);
|
|
|
+ process.exit(1);
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+module.exports = {
|
|
|
+ createServer
|
|
|
+};
|