| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337 |
- #!/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);
- if (!toolNames.includes('fmode_image_analysis')) {
- throw new Error(`Expected tool fmode_image_analysis, got: ${toolNames.join(', ')}`);
- }
- const imageTool = tools.tools.find(tool => tool.name === 'fmode_image_analysis');
- const imageSchema = JSON.stringify(imageTool?.inputSchema || {});
- if (!imageSchema.includes('imagePath') || !imageSchema.includes('fmodeToken')) {
- throw new Error('Expected fmode_image_analysis to accept imagePath and fmodeToken inputs');
- }
- 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 imageNoToken = await client.callTool({
- name: 'fmode_image_analysis',
- arguments: {
- images: ['data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII='],
- prompt: '识别这张测试图片',
- allowEnvToken: false
- }
- });
- if (!imageNoToken.structuredContent || imageNoToken.structuredContent.status !== 'needs_token') {
- throw new Error('image analysis no-token path returned unexpected structured content');
- }
- if (Array.isArray(imageNoToken.structuredContent.errors) && imageNoToken.structuredContent.errors.length !== 0) {
- throw new Error('image analysis no-token path should keep errors empty');
- }
- 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 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)}`);
- }
- for (const tool of ['voc_api_search', 'voc_api_doc', 'voc_api_call']) {
- if (!toolNames.includes(tool)) {
- throw new Error(`Expected catalog tool ${tool}, got: ${toolNames.join(', ')}`);
- }
- }
- const apiSearchResult = await client.callTool({
- name: 'voc_api_search',
- arguments: { platform: 'douyin' }
- });
- const apiSearchStructured = apiSearchResult.structuredContent || {};
- if (apiSearchStructured.status !== 'ok' || !apiSearchStructured.data?.endpoints?.length) {
- throw new Error(`Expected voc_api_search to list douyin endpoints, got: ${JSON.stringify(apiSearchStructured)}`);
- }
- const apiDocResult = await client.callTool({
- name: 'voc_api_doc',
- arguments: { id: 'douyin.search_general' }
- });
- const apiDocStructured = apiDocResult.structuredContent || {};
- if (apiDocStructured.status !== 'ok' ||
- !String(apiDocStructured.data?.endpoint?.proxyPath || '').includes('fetch_general_search_v2')) {
- throw new Error(`Expected voc_api_doc to return douyin search doc, got: ${JSON.stringify(apiDocStructured)}`);
- }
- const apiCallSentinel = 'sk-SMOKE_WRONG_TOKEN_TYPE';
- const apiCallResult = await client.callTool({
- name: 'voc_api_call',
- arguments: { id: 'douyin.search_general', params: { keyword: '律师' }, newapiToken: apiCallSentinel, vocToken: 'r:SMOKE_INVALID_SESSION_TOKEN' }
- });
- const apiCallStructured = apiCallResult.structuredContent || {};
- if (apiCallStructured.status !== 'needs_valid_token') {
- throw new Error(`Expected voc_api_call to map invalid sk-/r: tokens to needs_valid_token (token problem, not 余额不足), got: ${JSON.stringify(apiCallStructured)}`);
- }
- if (JSON.stringify(apiCallStructured).includes(apiCallSentinel) || JSON.stringify(apiCallStructured).includes('r:SMOKE_INVALID_SESSION_TOKEN')) {
- throw new Error('voc_api_call leaked the request-scoped token');
- }
- 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);
- });
|