| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302 |
- #!/usr/bin/env node
- const path = require('path');
- const { Client } = require('@modelcontextprotocol/sdk/client/index.js');
- const { StdioClientTransport } = require('@modelcontextprotocol/sdk/client/stdio.js');
- const { version } = require('../package.json');
- async function main() {
- const cwd = path.resolve(__dirname, '..');
- const client = new Client({
- name: 'voc-intelligence-smoke',
- version
- });
- const transport = new StdioClientTransport({
- command: process.execPath,
- args: ['mcp/src/server.js'],
- cwd,
- stderr: 'pipe'
- });
- const stderr = transport.stderr;
- if (stderr) {
- stderr.on('data', chunk => {
- process.stderr.write(chunk);
- });
- }
- await client.connect(transport);
- try {
- const tools = await client.listTools();
- const toolNames = tools.tools.map(tool => tool.name);
- const trendTool = tools.tools.find(tool => tool.name === 'voc_xiaohongshu_trend_run');
- const trendSchema = JSON.stringify(trendTool?.inputSchema || {});
- if (!trendTool || !trendSchema.includes('xiaohongshuToken') || !trendSchema.includes('vocToken')) {
- throw new Error('Expected voc_xiaohongshu_trend_run to accept simple request token inputs xiaohongshuToken and vocToken');
- }
- if (!toolNames.includes('voc_xiaohongshu_trend_run')) {
- throw new Error(`Expected tool voc_xiaohongshu_trend_run, got: ${toolNames.join(', ')}`);
- }
- if (!toolNames.includes('voc_xiaohongshu_preference_update')) {
- throw new Error(`Expected tool voc_xiaohongshu_preference_update, got: ${toolNames.join(', ')}`);
- }
- if (!toolNames.includes('voc_problem_deep_dive_run')) {
- throw new Error(`Expected tool voc_problem_deep_dive_run, got: ${toolNames.join(', ')}`);
- }
- if (!toolNames.includes('voc_issue_pool_run')) {
- throw new Error(`Expected tool voc_issue_pool_run, got: ${toolNames.join(', ')}`);
- }
- if (!toolNames.includes('voc_content_plan_run')) {
- throw new Error(`Expected tool voc_content_plan_run, got: ${toolNames.join(', ')}`);
- }
- if (!toolNames.includes('voc_speaking_script_run')) {
- throw new Error(`Expected tool voc_speaking_script_run, got: ${toolNames.join(', ')}`);
- }
- if (!toolNames.includes('voc_competitor_map_run')) {
- throw new Error(`Expected tool voc_competitor_map_run, got: ${toolNames.join(', ')}`);
- }
- if (!toolNames.includes('voc_business_workflow_run')) {
- throw new Error(`Expected tool voc_business_workflow_run, got: ${toolNames.join(', ')}`);
- }
- const issuePoolTool = tools.tools.find(tool => tool.name === 'voc_issue_pool_run');
- const issuePoolSchema = JSON.stringify(issuePoolTool?.inputSchema || {});
- if (!issuePoolSchema.includes('evidence') || !issuePoolSchema.includes('resolvedIssues')) {
- throw new Error('Expected voc_issue_pool_run to expose evidence and status inputs');
- }
- const deepDiveTool = tools.tools.find(tool => tool.name === 'voc_problem_deep_dive_run');
- const deepDiveSchema = JSON.stringify(deepDiveTool?.inputSchema || {});
- if (!deepDiveSchema.includes('project') || !deepDiveSchema.includes('feedback') || !deepDiveSchema.includes('blockedActions')) {
- throw new Error('Expected voc_problem_deep_dive_run to expose scoped memory and feedback inputs');
- }
- const preferenceResult = await client.callTool({
- name: 'voc_xiaohongshu_preference_update',
- arguments: {
- message: '保留奶油风和全屋定制翻车方向,不要纯风格美图,下一个版本更偏门店转化话术。',
- memory: '../outputs/claude-code-xhs-mcp-smoke/xiaohongshu-trend-memory.json'
- }
- });
- const preferenceStructured = preferenceResult.structuredContent || {};
- if (preferenceStructured.status !== 'ok') {
- throw new Error(`Expected ok preference result, got: ${JSON.stringify(preferenceStructured)}`);
- }
- const sentinelToken = 'SMOKE_REQUEST_TOKEN_VALUE';
- const tokenCheckResult = await client.callTool({
- name: 'voc_xiaohongshu_token_check',
- arguments: {
- vocToken: sentinelToken
- }
- });
- const tokenStructured = tokenCheckResult.structuredContent || {};
- const tokenText = JSON.stringify(tokenStructured);
- if (tokenStructured.status !== 'ok' || tokenStructured.configured !== true) {
- throw new Error(`Expected request-scoped token to be accepted, got: ${tokenText}`);
- }
- if (tokenText.includes(sentinelToken)) {
- throw new Error('Request-scoped token leaked in MCP token check result');
- }
- const result = await client.callTool({
- name: 'voc_xiaohongshu_trend_run',
- arguments: {
- collectionMode: 'sample',
- profile: 'memory-templates/xiaohongshu-trend-profile.json',
- memory: '../outputs/claude-code-xhs-mcp-smoke/xiaohongshu-trend-memory.json',
- output: '../outputs/claude-code-xhs-mcp-smoke'
- }
- });
- const structured = result.structuredContent || {};
- if (structured.status !== 'ok') {
- throw new Error(`Expected ok result, got: ${JSON.stringify(structured)}`);
- }
- const assistantMessage = String(structured.assistantMessage || '');
- const evidenceFields = ['## 证据样本', '原帖', '原文摘录', '高赞评论'];
- const missingEvidenceFields = evidenceFields.filter(field => !assistantMessage.includes(field));
- if (missingEvidenceFields.length) {
- throw new Error(`Expected evidence card fields in sample report, missing: ${missingEvidenceFields.join(', ')}`);
- }
- if (/xiaohongshu\.com\/explore\//.test(assistantMessage)) {
- throw new Error('Sample report should not contain /explore/ Xiaohongshu links');
- }
- const assetFiles = (structured.files || []).filter(file => /[\\/]assets[\\/]/.test(file));
- if (!assetFiles.length) {
- throw new Error('Expected cached asset files in sample report result');
- }
- const issuePoolResult = await client.callTool({
- name: 'voc_issue_pool_run',
- arguments: {
- project: 'MCP样板门店',
- industry: '本地生活餐饮',
- scenario: '门店经营',
- audience: '到店顾客',
- evidence: [
- '排队久,但是不知道值不值得等。',
- '人均有点高,第一次来不知道怎么点不踩雷。',
- '服务忙起来没人理。'
- ]
- }
- });
- const issuePoolStructured = issuePoolResult.structuredContent || {};
- if (issuePoolStructured.status !== 'ok' ||
- !String(issuePoolStructured.assistantMessage || '').includes('## 问题优先级') ||
- !issuePoolStructured.data?.issues?.length) {
- throw new Error(`Expected issue pool result, got: ${JSON.stringify(issuePoolStructured)}`);
- }
- const deepDiveResult = await client.callTool({
- name: 'voc_problem_deep_dive_run',
- arguments: {
- issue: '排队',
- project: 'MCP样板门店',
- industry: '本地生活餐饮',
- scenario: '门店经营',
- audience: '到店顾客',
- evidence: ['排队久,但是不知道值不值得等。'],
- feedback: '老板不想做等位饮品,更想先改评论区回复和预点单'
- }
- });
- const deepDiveStructured = deepDiveResult.structuredContent || {};
- const deepDiveMessage = String(deepDiveStructured.assistantMessage || '');
- if (deepDiveStructured.status !== 'ok' ||
- !deepDiveMessage.includes('## 可执行解决动作') ||
- !deepDiveMessage.includes('## 优先级判断') ||
- !deepDiveStructured.summary?.memoryPath) {
- throw new Error(`Expected boss-oriented deep dive result, got: ${JSON.stringify(deepDiveStructured)}`);
- }
- if (!deepDiveStructured.data?.memory?.blockedActions?.includes('等位饮品') ||
- !deepDiveStructured.data?.memory?.preferredActions?.includes('预点单')) {
- throw new Error('Expected deep dive MCP call to parse feedback into scoped memory');
- }
- if (!JSON.stringify(deepDiveStructured.nextActions || []).includes('live')) {
- throw new Error('Expected deep dive next actions to hand off to live collection instead of trapping the workflow');
- }
- const contentPlanResult = await client.callTool({
- name: 'voc_content_plan_run',
- arguments: {
- brand: 'MCP样板门店',
- industry: '本地生活餐饮',
- issues: ['第一次来怕踩雷', '觉得人均贵', '怕排队久']
- }
- });
- const contentPlanStructured = contentPlanResult.structuredContent || {};
- const contentPlanMessage = String(contentPlanStructured.assistantMessage || '');
- if (contentPlanStructured.status !== 'ok' ||
- !contentPlanMessage.includes('7 天 VOC 内容选题') ||
- !contentPlanMessage.includes('前 3 秒钩子') ||
- !contentPlanStructured.data?.plan?.length) {
- throw new Error(`Expected VOC content plan result, got: ${JSON.stringify(contentPlanStructured)}`);
- }
- const teaContentPlanResult = await client.callTool({
- name: 'voc_content_plan_run',
- arguments: {
- brand: '茶饮新客转化',
- industry: '茶饮',
- issues: [
- '第一次来怎么点最稳',
- '18-22 元贵在哪/值在哪',
- '券后价和套餐怎么买最划算',
- '茶底/甜度/配料怎么选',
- '哪几款不容易踩雷',
- '外卖到手怎么避免翻车',
- '买一送一/团购规则怎么讲清楚'
- ],
- evidenceText: '小红书评论:波霸奶茶用的什么茶底?我要避雷!!下午喝的,晚上睁眼到天亮。抖音线索:9.9 新品、11.9 单人下午茶、21 元买一送一能触发注意,但还需要解释规则、口味和是否有坑。'
- }
- });
- const teaContentPlanStructured = teaContentPlanResult.structuredContent || {};
- const teaContentPlanMessage = String(teaContentPlanStructured.assistantMessage || '');
- if (teaContentPlanStructured.status !== 'ok' ||
- !teaContentPlanMessage.includes('茶底怎么选') ||
- !teaContentPlanMessage.includes('买一送一到底怎么算') ||
- teaContentPlanMessage.includes('招牌菜') ||
- teaContentPlanMessage.includes('下饭菜')) {
- throw new Error(`Expected tea-specific VOC content plan result, got: ${JSON.stringify(teaContentPlanStructured)}`);
- }
- const speakingScriptResult = await client.callTool({
- name: 'voc_speaking_script_run',
- arguments: {
- brand: 'MCP样板门店',
- industry: '本地生活餐饮',
- topic: '第一次来怎么点不踩雷',
- userIssue: '第一次来怕点错',
- evidence: ['第一次来不知道怎么点,怕踩雷'],
- feedback: '开头更狠一点,老板视角,少讲概念,多给具体场景',
- memory: '../outputs/claude-code-xhs-mcp-smoke/voc-speaking-script-memory.json',
- finalize: true
- }
- });
- const speakingScriptStructured = speakingScriptResult.structuredContent || {};
- const speakingScriptMessage = String(speakingScriptStructured.assistantMessage || '');
- if (speakingScriptStructured.status !== 'ok' ||
- !speakingScriptMessage.includes('VOC 口播脚本共创') ||
- !speakingScriptMessage.includes('60 秒口播稿') ||
- !speakingScriptMessage.includes('30 秒压缩版') ||
- !speakingScriptStructured.data?.memory?.finalizedScripts?.length ||
- !speakingScriptStructured.data?.memory?.preferredStyles?.includes('老板视角')) {
- throw new Error(`Expected VOC speaking script result, got: ${JSON.stringify(speakingScriptStructured)}`);
- }
- const competitorMapResult = await client.callTool({
- name: 'voc_competitor_map_run',
- arguments: {
- brand: 'MCP样板门店',
- city: '南昌',
- category: '餐饮'
- }
- });
- const competitorMapStructured = competitorMapResult.structuredContent || {};
- const competitorMapMessage = String(competitorMapStructured.assistantMessage || '');
- if (competitorMapStructured.status !== 'ok' ||
- !competitorMapMessage.includes('竞品图谱') ||
- !competitorMapMessage.includes('错位竞争') ||
- !competitorMapStructured.data?.competitors?.length) {
- throw new Error(`Expected VOC competitor map result, got: ${JSON.stringify(competitorMapStructured)}`);
- }
- const businessWorkflowResult = await client.callTool({
- name: 'voc_business_workflow_run',
- arguments: {
- brand: 'MCP样板门店',
- industry: '本地生活餐饮',
- platform: 'douyin',
- collectionMode: 'sample',
- keywords: ['南昌菜', '第一次来怎么点', '排队']
- }
- });
- const businessWorkflowStructured = businessWorkflowResult.structuredContent || {};
- const businessWorkflowMessage = String(businessWorkflowStructured.assistantMessage || '');
- if (businessWorkflowStructured.status !== 'ok' ||
- !businessWorkflowMessage.includes('VOC经营闭环') ||
- businessWorkflowStructured.summary?.workflowStage !== 'business_workflow_first_round') {
- throw new Error(`Expected VOC business workflow result, got: ${JSON.stringify(businessWorkflowStructured)}`);
- }
- console.log(JSON.stringify({
- status: 'ok',
- tools: toolNames,
- assistantMessagePreview: assistantMessage.slice(0, 120),
- deepDivePreview: deepDiveMessage.slice(0, 120),
- contentPlanPreview: contentPlanMessage.slice(0, 120),
- speakingScriptPreview: speakingScriptMessage.slice(0, 120),
- competitorMapPreview: competitorMapMessage.slice(0, 120),
- businessWorkflowPreview: businessWorkflowMessage.slice(0, 120),
- preferenceMemoryApplied: Boolean(structured.summary && structured.summary.preferenceMemoryApplied),
- files: structured.files || []
- }, null, 2));
- } finally {
- await client.close();
- }
- }
- main().catch(error => {
- console.error(error);
- process.exit(1);
- });
|