| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017 |
- #!/usr/bin/env node
- const path = require('path');
- const os = require('os');
- const fs = require('fs');
- const { runXiaohongshuTrend } = require('../mcp/src/tools/xiaohongshu-trend-run');
- const { updateXiaohongshuPreference } = require('../mcp/src/tools/xiaohongshu-preference-update');
- const { normalizeNote } = require('../mcp/src/features/xiaohongshu-trend/live-collector');
- const { buildTrendReport } = require('../mcp/src/features/xiaohongshu-trend/report');
- const { XiaohongshuApi } = require('../mcp/src/providers/xiaohongshu-api');
- const { runDouyinTrend } = require('../mcp/src/tools/douyin-trend-run');
- const { updateDouyinPreference } = require('../mcp/src/tools/douyin-preference-update');
- const { runVocIssuePool } = require('../mcp/src/tools/voc-issue-pool-run');
- const { runVocProblemDeepDive } = require('../mcp/src/tools/voc-problem-deep-dive-run');
- const { runVocContentPlan } = require('../mcp/src/tools/voc-content-plan-run');
- const { runVocSpeakingScript } = require('../mcp/src/tools/voc-speaking-script-run');
- const { runVocCompetitorMap } = require('../mcp/src/tools/voc-competitor-map-run');
- const { runBusinessWorkflow } = require('../mcp/src/tools/voc-business-workflow-run');
- const {
- normalizeVideo,
- normalizeSearchKeyword,
- keywordValidationIssue,
- buildKeywordRepairCandidates
- } = require('../mcp/src/features/douyin-trend/live-collector');
- const { DouyinApi } = require('../mcp/src/providers/douyin-api');
- const { readVocToken } = require('../mcp/src/core/credentials');
- const { smokeCrossIndustry } = require('./smoke-cross-industry');
- const { Client } = require('@modelcontextprotocol/sdk/client/index.js');
- const { StdioClientTransport } = require('@modelcontextprotocol/sdk/client/stdio.js');
- const { version } = require('../package.json');
- const ROOT = path.resolve(__dirname, '..');
- const OUTPUT_ROOT = path.resolve(ROOT, 'outputs', 'claude-code-package-smoke');
- const MEMORY_PATH = path.join(OUTPUT_ROOT, 'xiaohongshu-trend-memory.json');
- const DOUYIN_MEMORY_PATH = path.join(OUTPUT_ROOT, 'douyin-trend-memory.json');
- async function smokeMcpTools() {
- const client = new Client({
- name: 'voc-intelligence-package-smoke',
- version
- });
- const transport = new StdioClientTransport({
- command: process.execPath,
- args: ['mcp/src/server.js'],
- cwd: ROOT,
- stderr: 'pipe'
- });
- 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 (!imageTool || !imageSchema.includes('imagePath') || !imageSchema.includes('fmodeToken')) {
- throw new Error('Expected fmode_image_analysis to accept imagePath and fmodeToken inputs');
- }
- const xhsTrendTool = tools.tools.find(tool => tool.name === 'voc_xiaohongshu_trend_run');
- const xhsTrendSchema = JSON.stringify(xhsTrendTool?.inputSchema || {});
- if (!xhsTrendTool || !xhsTrendSchema.includes('xiaohongshuToken') || !xhsTrendSchema.includes('vocToken')) {
- throw new Error('Expected voc_xiaohongshu_trend_run to accept simple request token inputs xiaohongshuToken and vocToken');
- }
- const douyinTrendTool = tools.tools.find(tool => tool.name === 'voc_douyin_trend_run');
- const douyinTrendSchema = JSON.stringify(douyinTrendTool?.inputSchema || {});
- if (!douyinTrendTool || !douyinTrendSchema.includes('douyinToken') || !douyinTrendSchema.includes('vocToken')) {
- throw new Error('Expected voc_douyin_trend_run to accept simple request token inputs douyinToken and vocToken');
- }
- const deepDiveTool = tools.tools.find(tool => tool.name === 'voc_problem_deep_dive_run');
- const deepDiveSchema = JSON.stringify(deepDiveTool?.inputSchema || {});
- if (!deepDiveTool || !deepDiveSchema.includes('issue') || !deepDiveSchema.includes('evidence')) {
- throw new Error('Expected voc_problem_deep_dive_run to accept issue and evidence inputs');
- }
- const issuePoolTool = tools.tools.find(tool => tool.name === 'voc_issue_pool_run');
- const issuePoolSchema = JSON.stringify(issuePoolTool?.inputSchema || {});
- if (!issuePoolTool || !issuePoolSchema.includes('evidence') || !issuePoolSchema.includes('resolvedIssues')) {
- throw new Error('Expected voc_issue_pool_run to accept evidence and status inputs');
- }
- return toolNames;
- } finally {
- await client.close();
- }
- }
- async function smokeNoTokenRechargePrompt() {
- const originalCwd = process.cwd();
- const originalEnv = {
- USERPROFILE: process.env.USERPROFILE,
- HOME: process.env.HOME,
- VOC_TOKEN: process.env.VOC_TOKEN,
- VOC_SOCIAL_TOKEN: process.env.VOC_SOCIAL_TOKEN,
- XIAOHONGSHU_API_TOKEN: process.env.XIAOHONGSHU_API_TOKEN,
- TIKHUB_API_TOKEN: process.env.TIKHUB_API_TOKEN,
- FMODE_XIAOHONGSHU_TOKEN: process.env.FMODE_XIAOHONGSHU_TOKEN,
- FMODE_SOCIAL_TOKEN: process.env.FMODE_SOCIAL_TOKEN,
- DOUYIN_API_TOKEN: process.env.DOUYIN_API_TOKEN,
- DOUYIN_TOKEN: process.env.DOUYIN_TOKEN,
- VOC_DOUYIN_TOKEN: process.env.VOC_DOUYIN_TOKEN
- };
- const tempHome = path.join(os.tmpdir(), 'claude-voc-no-token-smoke');
- const tempWorkspace = path.join(os.tmpdir(), 'claude-voc-no-token-workspace-smoke');
- fs.mkdirSync(tempHome, { recursive: true });
- fs.mkdirSync(tempWorkspace, { recursive: true });
- try {
- process.chdir(tempWorkspace);
- process.env.USERPROFILE = tempHome;
- process.env.HOME = tempHome;
- [
- 'VOC_TOKEN',
- 'VOC_SOCIAL_TOKEN',
- 'XIAOHONGSHU_API_TOKEN',
- 'TIKHUB_API_TOKEN',
- 'FMODE_XIAOHONGSHU_TOKEN',
- 'FMODE_SOCIAL_TOKEN',
- 'DOUYIN_API_TOKEN',
- 'DOUYIN_TOKEN',
- 'VOC_DOUYIN_TOKEN'
- ].forEach(key => delete process.env[key]);
- const result = await runXiaohongshuTrend({
- collectionMode: 'live',
- keywords: '全屋定制',
- output: path.join(OUTPUT_ROOT, 'no-token')
- });
- const body = String(result.assistantMessage || '');
- const hasPaymentUrl = /apig-pay/.test(body) && /Vo3ROWEvDy/.test(body);
- const hasEarlyPaymentUrl = /apig-pay/.test(body.slice(0, 180));
- const hasNextActionUrl = (result.nextActions || []).some(action => /apig-pay/.test(String(action)));
- if (result.status !== 'needs_token' || !hasPaymentUrl || !hasEarlyPaymentUrl || !hasNextActionUrl || (result.errors || []).length) {
- throw new Error('no-token live path did not return a clean recharge prompt');
- }
- } finally {
- Object.entries(originalEnv).forEach(([key, value]) => {
- if (value === undefined) {
- delete process.env[key];
- } else {
- process.env[key] = value;
- }
- });
- process.chdir(originalCwd);
- }
- }
- async function smokeDouyinNoTokenRechargePrompt() {
- const originalCwd = process.cwd();
- const originalEnv = {
- USERPROFILE: process.env.USERPROFILE,
- HOME: process.env.HOME,
- VOC_TOKEN: process.env.VOC_TOKEN,
- VOC_SOCIAL_TOKEN: process.env.VOC_SOCIAL_TOKEN,
- XIAOHONGSHU_API_TOKEN: process.env.XIAOHONGSHU_API_TOKEN,
- TIKHUB_API_TOKEN: process.env.TIKHUB_API_TOKEN,
- FMODE_XIAOHONGSHU_TOKEN: process.env.FMODE_XIAOHONGSHU_TOKEN,
- FMODE_SOCIAL_TOKEN: process.env.FMODE_SOCIAL_TOKEN,
- DOUYIN_API_TOKEN: process.env.DOUYIN_API_TOKEN,
- DOUYIN_TOKEN: process.env.DOUYIN_TOKEN,
- VOC_DOUYIN_TOKEN: process.env.VOC_DOUYIN_TOKEN
- };
- const tempHome = path.join(os.tmpdir(), 'claude-voc-douyin-no-token-smoke');
- const tempWorkspace = path.join(os.tmpdir(), 'claude-voc-douyin-no-token-workspace-smoke');
- fs.mkdirSync(tempHome, { recursive: true });
- fs.mkdirSync(tempWorkspace, { recursive: true });
- try {
- process.chdir(tempWorkspace);
- process.env.USERPROFILE = tempHome;
- process.env.HOME = tempHome;
- [
- 'VOC_TOKEN',
- 'VOC_SOCIAL_TOKEN',
- 'XIAOHONGSHU_API_TOKEN',
- 'TIKHUB_API_TOKEN',
- 'FMODE_XIAOHONGSHU_TOKEN',
- 'FMODE_SOCIAL_TOKEN',
- 'DOUYIN_API_TOKEN',
- 'DOUYIN_TOKEN',
- 'VOC_DOUYIN_TOKEN'
- ].forEach(key => delete process.env[key]);
- const result = await runDouyinTrend({
- collectionMode: 'live',
- keywords: '全屋定制',
- output: path.join(OUTPUT_ROOT, 'douyin-no-token')
- });
- const body = String(result.assistantMessage || '');
- const hasPaymentUrl = /apig-pay/.test(body) && /Vo3ROWEvDy/.test(body);
- const hasEarlyPaymentUrl = /apig-pay/.test(body.slice(0, 180));
- const hasNextActionUrl = (result.nextActions || []).some(action => /apig-pay/.test(String(action)));
- if (result.status !== 'needs_token' || !hasPaymentUrl || !hasEarlyPaymentUrl || !hasNextActionUrl || (result.errors || []).length) {
- throw new Error('douyin no-token live path did not return a clean recharge prompt');
- }
- } finally {
- Object.entries(originalEnv).forEach(([key, value]) => {
- if (value === undefined) {
- delete process.env[key];
- } else {
- process.env[key] = value;
- }
- });
- process.chdir(originalCwd);
- }
- }
- function smokeWorkspaceEnvLocalToken() {
- const originalCwd = process.cwd();
- const originalEnv = {
- USERPROFILE: process.env.USERPROFILE,
- HOME: process.env.HOME,
- VOC_TOKEN: process.env.VOC_TOKEN,
- VOC_SOCIAL_TOKEN: process.env.VOC_SOCIAL_TOKEN,
- XIAOHONGSHU_API_TOKEN: process.env.XIAOHONGSHU_API_TOKEN,
- TIKHUB_API_TOKEN: process.env.TIKHUB_API_TOKEN,
- FMODE_XIAOHONGSHU_TOKEN: process.env.FMODE_XIAOHONGSHU_TOKEN,
- FMODE_SOCIAL_TOKEN: process.env.FMODE_SOCIAL_TOKEN,
- DOUYIN_API_TOKEN: process.env.DOUYIN_API_TOKEN,
- DOUYIN_TOKEN: process.env.DOUYIN_TOKEN,
- VOC_DOUYIN_TOKEN: process.env.VOC_DOUYIN_TOKEN
- };
- const tempHome = path.join(os.tmpdir(), 'claude-voc-env-local-home-smoke');
- const tempWorkspace = path.join(os.tmpdir(), 'claude-voc-env-local-workspace-smoke');
- fs.mkdirSync(tempHome, { recursive: true });
- fs.mkdirSync(tempWorkspace, { recursive: true });
- try {
- process.chdir(tempWorkspace);
- process.env.USERPROFILE = tempHome;
- process.env.HOME = tempHome;
- [
- 'VOC_TOKEN',
- 'VOC_SOCIAL_TOKEN',
- 'XIAOHONGSHU_API_TOKEN',
- 'TIKHUB_API_TOKEN',
- 'FMODE_XIAOHONGSHU_TOKEN',
- 'FMODE_SOCIAL_TOKEN',
- 'DOUYIN_API_TOKEN',
- 'DOUYIN_TOKEN',
- 'VOC_DOUYIN_TOKEN'
- ].forEach(key => delete process.env[key]);
- fs.writeFileSync(path.join(tempWorkspace, '.env.local'), 'VOC_TOKEN=workspace-smoke-token\n');
- const token = readVocToken({});
- if (token !== 'workspace-smoke-token') {
- throw new Error('workspace .env.local token was not used');
- }
- } finally {
- Object.entries(originalEnv).forEach(([key, value]) => {
- if (value === undefined) {
- delete process.env[key];
- } else {
- process.env[key] = value;
- }
- });
- process.chdir(originalCwd);
- }
- }
- function smokeAncestorEnvLocalToken() {
- const originalCwd = process.cwd();
- const originalEnv = {
- USERPROFILE: process.env.USERPROFILE,
- HOME: process.env.HOME,
- VOC_TOKEN: process.env.VOC_TOKEN,
- VOC_SOCIAL_TOKEN: process.env.VOC_SOCIAL_TOKEN,
- XIAOHONGSHU_API_TOKEN: process.env.XIAOHONGSHU_API_TOKEN,
- TIKHUB_API_TOKEN: process.env.TIKHUB_API_TOKEN,
- FMODE_XIAOHONGSHU_TOKEN: process.env.FMODE_XIAOHONGSHU_TOKEN,
- FMODE_SOCIAL_TOKEN: process.env.FMODE_SOCIAL_TOKEN,
- DOUYIN_API_TOKEN: process.env.DOUYIN_API_TOKEN,
- DOUYIN_TOKEN: process.env.DOUYIN_TOKEN,
- VOC_DOUYIN_TOKEN: process.env.VOC_DOUYIN_TOKEN
- };
- const tempHome = path.join(os.tmpdir(), 'claude-voc-ancestor-env-home-smoke');
- const tempWorkspace = path.join(os.tmpdir(), 'claude-voc-ancestor-env-workspace-smoke');
- const nested = path.join(tempWorkspace, 'nested', 'plugin');
- fs.mkdirSync(tempHome, { recursive: true });
- fs.mkdirSync(nested, { recursive: true });
- try {
- process.chdir(nested);
- process.env.USERPROFILE = tempHome;
- process.env.HOME = tempHome;
- [
- 'VOC_TOKEN',
- 'VOC_SOCIAL_TOKEN',
- 'XIAOHONGSHU_API_TOKEN',
- 'TIKHUB_API_TOKEN',
- 'FMODE_XIAOHONGSHU_TOKEN',
- 'FMODE_SOCIAL_TOKEN',
- 'DOUYIN_API_TOKEN',
- 'DOUYIN_TOKEN',
- 'VOC_DOUYIN_TOKEN'
- ].forEach(key => delete process.env[key]);
- fs.writeFileSync(path.join(tempWorkspace, '.env.local'), 'VOC_DOUYIN_TOKEN=ancestor-smoke-token\n');
- const token = readVocToken({});
- if (token !== 'ancestor-smoke-token') {
- throw new Error('ancestor .env.local token was not used');
- }
- } finally {
- Object.entries(originalEnv).forEach(([key, value]) => {
- if (value === undefined) {
- delete process.env[key];
- } else {
- process.env[key] = value;
- }
- });
- process.chdir(originalCwd);
- }
- }
- async function smokeIssuePoolReportEvidenceFiltering() {
- const reportPath = path.join(OUTPUT_ROOT, 'issue-pool-report-filter-smoke.md');
- const memoryPath = path.join(OUTPUT_ROOT, 'issue-pool-report-filter-memory.json');
- fs.writeFileSync(reportPath, [
- '# 盛焰铁板烧竞品图谱',
- '',
- '## 品牌背景',
- '- 品牌:盛焰铁板烧',
- '- 城市:南昌',
- '',
- '## 当前经营痛点',
- '1. 日常营销能够带来 TC,但是活动折扣大。',
- '',
- '## live 采集真实样本',
- '1. [南昌铁板烧样本](https://example.test/note)',
- ' - 关键评论:多少钱啊',
- ' - 可解读点:用户第一反应不是口味,而是价格。',
- '2. [南昌排队样本](https://example.test/note2)',
- ' - 高赞评论:排队太久了,下次不想等',
- '',
- '## 下一步建议',
- '- 先选一家做深度拆解。'
- ].join('\n'));
- const result = await runVocIssuePool({
- project: '盛焰铁板烧',
- industry: '餐饮',
- scenario: '门店经营',
- audience: '到店顾客',
- report: reportPath,
- memory: memoryPath
- });
- const issues = result.data?.issues || [];
- const evidence = issues.flatMap(issue => issue.evidence || []);
- if (!issues.some(issue => issue.id === 'price') || !issues.some(issue => issue.id === 'waiting')) {
- throw new Error('issue pool did not classify filtered report comments');
- }
- if (issues.some(issue => issue.id === 'other' && issue.count > 0)) {
- throw new Error('issue pool classified generated report prose as other evidence');
- }
- if (evidence.some(item => /^#|品牌:|城市:|当前经营痛点|下一步建议/.test(String(item)))) {
- throw new Error(`issue pool leaked report headings/prose into evidence: ${JSON.stringify(evidence)}`);
- }
- }
- function smokeAccessibleUrlNormalization() {
- const note = normalizeNote({
- id: '69f72f860000000035038a78',
- title: '自制多功能收纳抽屉课题',
- desc: '把柜内空间重新分区,适合小户型收纳。',
- user: { nickname: '小熊居家' },
- liked_count: '16.3w',
- collected_count: '15.9w',
- comments_count: '1.8k',
- mini_program_info: {
- webpage_url: 'https://www.xiaohongshu.com/discovery/item/69f72f860000000035038a78?xsec_source=app_share&xsec_token=sample-xsec-token'
- },
- share_info: {
- link: 'https://www.xiaohongshu.com/discovery/item/69f72f860000000035038a78?xsec_source=app_share&xsec_token=sample-share-token'
- },
- image_list: [
- { url: 'https://img.example.com/xhs-cover.jpg' }
- ]
- }, '全屋定制');
- if (!note.sourceUrlVerified || !/xsec_token=sample-xsec-token/.test(note.sourceUrl || '')) {
- throw new Error(`expected xsec_token sourceUrl, got ${note.sourceUrl}`);
- }
- if (/\/explore\//.test(note.sourceUrl || '') || /\/explore\//.test(note.url || '')) {
- throw new Error('normalizeNote should not use /explore/ as the primary source URL');
- }
- if (note.coverUrl !== 'https://img.example.com/xhs-cover.jpg') {
- throw new Error(`expected coverUrl from image_list, got ${note.coverUrl}`);
- }
- const unverified = normalizeNote({
- id: '69f72f860000000035038a79',
- title: '没有 xsec 的候选链接不应直接展示',
- desc: '避免把可能 404 的候选链接当原帖。',
- user: { nickname: '链接测试' },
- webpage_url: 'https://www.xiaohongshu.com/discovery/item/69f72f860000000035038a79',
- display_image_info: {
- url_list: ['https://img.example.com/xhs-display-cover.webp']
- }
- }, '全屋定制');
- if (unverified.sourceUrl || unverified.url || unverified.sourceUrlVerified) {
- throw new Error(`unverified xiaohongshu URL should not become clickable sourceUrl: ${unverified.sourceUrl}`);
- }
- if (!/xiaohongshu\.com\/discovery\/item/.test(unverified.candidateUrl || '')) {
- throw new Error(`expected candidateUrl to preserve raw fallback for debugging, got ${unverified.candidateUrl}`);
- }
- if (unverified.coverUrl !== 'https://img.example.com/xhs-display-cover.webp') {
- throw new Error(`expected coverUrl from display_image_info, got ${unverified.coverUrl}`);
- }
- const report = buildTrendReport({
- profile: {
- industry: '测试行业',
- businessType: '测试业务',
- targetAudience: ['测试人群'],
- keywords: ['全屋定制']
- },
- dataset: { notes: [unverified], comments: [] },
- collectionMode: 'live',
- memory: {}
- });
- const body = String(report.assistantMessage || '');
- if (body.includes('[打开原帖') || body.includes('链接待核验')) {
- throw new Error('unverified Xiaohongshu links should not be rendered as clickable evidence links');
- }
- if (!body.includes('平台未返回可核验原帖链接')) {
- throw new Error('unverified Xiaohongshu links should explain why no clickable original link is shown');
- }
- }
- function smokeDouyinUrlNormalization() {
- const video = normalizeVideo({
- aweme_id: '7448118827402972455',
- desc: '全屋定制视频开头怎么讲更容易留人?',
- author: { nickname: '抖音家装观察员', uid: 'u001', sec_uid: 'sec001' },
- statistics: {
- digg_count: '1.2w',
- comment_count: '860',
- share_count: '520',
- play_count: '23.5w'
- },
- video: {
- cover: { url_list: ['https://img.example.com/dy-cover.jpg'] }
- },
- share_info: {
- link: 'https://www.douyin.com/video/7448118827402972455?from=sample'
- }
- }, '全屋定制');
- if (!video.sourceUrlVerified || !/douyin\.com\/video\//.test(video.sourceUrl || '')) {
- throw new Error(`expected douyin sourceUrl, got ${video.sourceUrl}`);
- }
- if (video.coverUrl !== 'https://img.example.com/dy-cover.jpg') {
- throw new Error(`expected coverUrl from video.cover, got ${video.coverUrl}`);
- }
- }
- function smokeEvidenceCardReport(result) {
- const body = String(result.assistantMessage || '');
- const required = ['## 证据样本', '原帖', '原文摘录', '高赞评论'];
- const missing = required.filter(item => !body.includes(item));
- if (missing.length) {
- throw new Error(`sample report missing evidence card fields: ${missing.join(', ')}`);
- }
- if (/xiaohongshu\.com\/explore\//.test(body)) {
- throw new Error('sample report should not contain /explore/ Xiaohongshu links');
- }
- if (!body.includes('## 证据质量门槛') || !result.summary?.evidenceQuality?.grade) {
- throw new Error('sample report did not include evidence quality gate');
- }
- const assetFiles = (result.files || []).filter(file => /[\\/]assets[\\/]/.test(file));
- if (!assetFiles.length) {
- throw new Error('sample report did not return cached asset files');
- }
- const missingAsset = assetFiles.find(file => !fs.existsSync(file));
- if (missingAsset) {
- throw new Error(`cached asset file missing: ${missingAsset}`);
- }
- }
- function smokeDouyinEvidenceCardReport(result) {
- const body = String(result.assistantMessage || '');
- const required = ['## 证据样本', '原视频', '视频文案', '高赞评论'];
- const missing = required.filter(item => !body.includes(item));
- if (missing.length) {
- throw new Error(`douyin sample report missing evidence card fields: ${missing.join(', ')}`);
- }
- if (!/douyin\.com\/video\//.test(body)) {
- throw new Error('douyin sample report should contain a Douyin video link');
- }
- if (!body.includes('## 证据质量门槛') || !result.summary?.evidenceQuality?.grade) {
- throw new Error('douyin sample report did not include evidence quality gate');
- }
- const assetFiles = (result.files || []).filter(file => /[\\/]assets[\\/]/.test(file));
- if (!assetFiles.length) {
- throw new Error('douyin sample report did not return cached asset files');
- }
- const missingAsset = assetFiles.find(file => !fs.existsSync(file));
- if (missingAsset) {
- throw new Error(`cached douyin asset file missing: ${missingAsset}`);
- }
- }
- function sectionBetween(text, startTitle, endTitle) {
- const body = String(text || '');
- const start = body.indexOf(startTitle);
- if (start < 0) return '';
- const end = body.indexOf(endTitle, start + startTitle.length);
- return end >= 0 ? body.slice(start, end) : body.slice(start);
- }
- function smokeDeepDiveMemoryBehavior(result, blockedTerm) {
- const body = String(result.assistantMessage || '');
- const actionSection = sectionBetween(
- body,
- '### 今天就能做',
- '## \u53ef\u8f6c\u5316\u5185\u5bb9\u9009\u9898'
- );
- const memorySection = sectionBetween(
- body,
- '## \u5df2\u5e94\u7528\u7684\u4f7f\u7528\u8bb0\u5fc6',
- '## \u4ecd\u9700\u786e\u8ba4\u7684\u95ee\u9898'
- );
- if (!actionSection) {
- throw new Error('VOC problem deep dive action section was not found');
- }
- if (actionSection.includes(blockedTerm)) {
- throw new Error('VOC problem deep dive leaked a blocked action into executable actions');
- }
- if (!memorySection.includes(blockedTerm)) {
- throw new Error('VOC problem deep dive did not show blocked action in applied memory');
- }
- }
- async function smokeBusinessStatusErrors() {
- const originalFetch = global.fetch;
- const makeBusinessErrorFetch = () => async () => ({
- ok: true,
- status: 200,
- text: async () => JSON.stringify({
- code: 403,
- message: 'insufficient balance'
- })
- });
- try {
- global.fetch = makeBusinessErrorFetch();
- await new XiaohongshuApi({
- token: 'r:smoke-token',
- baseUrl: 'https://example.test/xiaohongshu/app'
- }).searchNotes({ keyword: 'smoke' });
- throw new Error('xiaohongshu business status 403 did not throw');
- } catch (error) {
- if (error.message === 'xiaohongshu business status 403 did not throw') {
- throw error;
- }
- if (Number(error.httpStatus) !== 403) {
- throw new Error(`expected xiaohongshu business httpStatus 403, got ${error.httpStatus}`);
- }
- } finally {
- global.fetch = originalFetch;
- }
- try {
- global.fetch = makeBusinessErrorFetch();
- await new DouyinApi({
- token: 'r:smoke-token',
- baseUrl: 'https://example.test/douyin'
- }).searchVideos({ keyword: 'smoke' });
- throw new Error('douyin business status 403 did not throw');
- } catch (error) {
- if (error.message === 'douyin business status 403 did not throw') {
- throw error;
- }
- if (Number(error.httpStatus) !== 403) {
- throw new Error(`expected douyin business httpStatus 403, got ${error.httpStatus}`);
- }
- } finally {
- global.fetch = originalFetch;
- }
- }
- async function smokeDouyinSearchInputErrors() {
- const normalized = normalizeSearchKeyword('茶饮18元22元');
- if (normalized !== '茶饮 18元 22元') {
- throw new Error(`expected douyin keyword spacing normalization, got ${normalized}`);
- }
- const repairCandidates = buildKeywordRepairCandidates('茶饮 18元 22元', {
- industry: '茶饮',
- targetAudience: ['新客']
- });
- if (!repairCandidates.includes('茶饮 性价比 新客')) {
- throw new Error(`expected douyin keyword repair candidates, got ${repairCandidates.join(', ')}`);
- }
- if (!keywordValidationIssue('茶饮'.repeat(40))) {
- throw new Error('expected overlong douyin keyword to be rejected before search');
- }
- const overlong = await runDouyinTrend({
- collectionMode: 'live',
- vocToken: 'r:smoke-token',
- keywords: '茶饮'.repeat(40),
- output: path.join(OUTPUT_ROOT, 'douyin-overlong-keyword')
- });
- if (overlong.status !== 'needs_keyword_fix') {
- throw new Error(`expected overlong keyword to return needs_keyword_fix, got ${overlong.status}`);
- }
- const originalFetch = global.fetch;
- try {
- global.fetch = async () => ({
- ok: false,
- status: 500,
- text: async () => JSON.stringify({
- code: 500,
- message: '社交平台接口请求错误'
- })
- });
- const result = await runDouyinTrend({
- collectionMode: 'live',
- vocToken: 'r:smoke-token',
- keywords: '茶饮 18元 22元',
- keywordLimit: 1,
- videosPerKeyword: 1,
- maxCommentPages: 0,
- output: path.join(OUTPUT_ROOT, 'douyin-search-input-error')
- });
- if (result.status !== 'upstream_unstable') {
- throw new Error(`expected persistent upstream search error to return upstream_unstable, got ${result.status}`);
- }
- if (/充值链接|apig-pay/.test(String(result.assistantMessage || ''))) {
- throw new Error('upstream search error was misreported as recharge/balance');
- }
- if (result.status === 'needs_keyword_fix') {
- throw new Error('upstream search error must not be misreported as a keyword problem');
- }
- } finally {
- global.fetch = originalFetch;
- }
- let callCount = 0;
- try {
- global.fetch = async () => {
- callCount += 1;
- if (callCount === 1) {
- return {
- ok: false,
- status: 500,
- text: async () => JSON.stringify({
- code: 500,
- message: '社交平台接口请求错误'
- })
- };
- }
- return {
- ok: true,
- status: 200,
- text: async () => JSON.stringify({
- code: 0,
- data: {
- data: {
- business_data: [
- {
- data: {
- aweme_info: {
- aweme_id: 'douyin-repair-smoke-1',
- desc: '茶饮性价比新客点单避坑',
- author: { nickname: 'smoke' },
- statistics: { digg_count: 1, comment_count: 0, share_count: 0 }
- }
- }
- }
- ]
- }
- }
- })
- };
- };
- const repaired = await runDouyinTrend({
- collectionMode: 'live',
- vocToken: 'r:smoke-token',
- industry: '茶饮',
- targetAudience: '新客',
- keywords: '茶饮 18元 22元',
- keywordLimit: 1,
- videosPerKeyword: 1,
- maxCommentPages: 0,
- cacheAssets: false,
- output: path.join(OUTPUT_ROOT, 'douyin-auto-keyword-repair')
- });
- if (repaired.status !== 'ok' || repaired.summary?.videoCount !== 1) {
- throw new Error(`expected transient upstream error to auto-recover via request retry, got ${repaired.status}`);
- }
- } finally {
- global.fetch = originalFetch;
- }
- }
- async function main() {
- const sample = await runXiaohongshuTrend({
- collectionMode: 'sample',
- profile: path.join(ROOT, 'memory-templates', 'xiaohongshu-trend-profile.json'),
- output: path.join(OUTPUT_ROOT, 'sample')
- });
- if (sample.status !== 'ok') {
- throw new Error(`sample failed: ${sample.status}`);
- }
- smokeEvidenceCardReport(sample);
- smokeAccessibleUrlNormalization();
- const douyinSample = await runDouyinTrend({
- collectionMode: 'sample',
- profile: path.join(ROOT, 'memory-templates', 'douyin-trend-profile.json'),
- output: path.join(OUTPUT_ROOT, 'douyin-sample')
- });
- if (douyinSample.status !== 'ok') {
- throw new Error(`douyin sample failed: ${douyinSample.status}`);
- }
- smokeDouyinEvidenceCardReport(douyinSample);
- smokeDouyinUrlNormalization();
- const preference = await updateXiaohongshuPreference({
- message: '保留奶油风和全屋定制翻车方向,不要纯风格美图,下一个版本更偏门店转化话术。',
- memory: MEMORY_PATH
- });
- if (preference.status !== 'ok') {
- throw new Error(`preference failed: ${preference.status}`);
- }
- const withMemory = await runXiaohongshuTrend({
- collectionMode: 'sample',
- profile: path.join(ROOT, 'memory-templates', 'xiaohongshu-trend-profile.json'),
- memory: MEMORY_PATH,
- output: path.join(OUTPUT_ROOT, 'with-memory')
- });
- if (withMemory.status !== 'ok' || !withMemory.summary.preferenceMemoryApplied) {
- throw new Error('sample-with-memory did not apply preference memory');
- }
- const douyinPreference = await updateDouyinPreference({
- message: '保留口播开头和评论区问题,不要只做纯素材剪辑,下一个版本更偏门店转化话术。',
- memory: DOUYIN_MEMORY_PATH
- });
- if (douyinPreference.status !== 'ok') {
- throw new Error(`douyin preference failed: ${douyinPreference.status}`);
- }
- const douyinWithMemory = await runDouyinTrend({
- collectionMode: 'sample',
- profile: path.join(ROOT, 'memory-templates', 'douyin-trend-profile.json'),
- memory: DOUYIN_MEMORY_PATH,
- output: path.join(OUTPUT_ROOT, 'douyin-with-memory')
- });
- if (douyinWithMemory.status !== 'ok' || !douyinWithMemory.summary.preferenceMemoryApplied) {
- throw new Error('douyin sample-with-memory did not apply preference memory');
- }
- const deepDive = await runVocProblemDeepDive({
- issue: '排队',
- industry: '本地生活餐饮',
- scenario: '门店经营',
- audience: '到店顾客',
- memory: path.join(OUTPUT_ROOT, 'voc-problem-memory.json'),
- evidence: [
- '想吃就得排队。',
- '排了很久以后,如果菜品普通就会觉得不值。'
- ]
- });
- const deepDiveBody = String(deepDive.assistantMessage || '');
- if (deepDive.status !== 'ok' ||
- !deepDiveBody.includes('## 老板视角影响') ||
- !deepDiveBody.includes('## 可执行解决动作') ||
- !deepDiveBody.includes('## 下轮验证指标')) {
- throw new Error('VOC problem deep dive did not return boss-oriented action output');
- }
- if (!deepDiveBody.includes('## 优先级判断') || !deepDiveBody.includes('## 7 天验证计划')) {
- throw new Error('VOC problem deep dive did not return priority and validation plan');
- }
- if (!deepDive.summary.memoryPath || !(deepDive.files || []).includes(deepDive.summary.memoryPath)) {
- throw new Error('VOC problem deep dive did not persist memory');
- }
- if (!JSON.stringify(deepDive.nextActions || []).includes('live')) {
- throw new Error('VOC problem deep dive next actions did not hand off to live collection');
- }
- const deepDiveWithMemory = await runVocProblemDeepDive({
- issue: '排队',
- industry: '本地生活餐饮',
- scenario: '门店经营',
- audience: '到店顾客',
- memory: deepDive.summary.memoryPath,
- feedback: '老板不想做等位饮品,更想先改评论区回复和预点单',
- blockedActions: ['等位饮品'],
- preferredActions: ['评论区回复', '预点单']
- });
- const memoryBody = String(deepDiveWithMemory.assistantMessage || '');
- if (!deepDiveWithMemory.summary.memoryApplied || !memoryBody.includes('## 已应用的使用记忆')) {
- throw new Error('VOC problem deep dive did not apply iterative memory');
- }
- smokeDeepDiveMemoryBehavior(deepDiveWithMemory, '等位饮品');
- const issuePool = await runVocIssuePool({
- project: '样板门店A',
- industry: '本地生活餐饮',
- scenario: '门店经营',
- audience: '到店顾客',
- evidence: [
- '排队久,但是不知道值不值得等。',
- '等位时间不透明,到了现场才知道要等多久。',
- '人均有点高,第一次来不知道怎么点不踩雷。',
- '服务忙起来没人理。',
- '视频很种草,实际到店体验有落差。'
- ]
- });
- const issuePoolBody = String(issuePool.assistantMessage || '');
- if (issuePool.status !== 'ok' ||
- !issuePoolBody.includes('# VOC 问题池') ||
- !issuePoolBody.includes('## 问题优先级') ||
- !issuePool.summary.memoryPath ||
- !issuePool.data?.issues?.length) {
- throw new Error('VOC issue pool did not return prioritized issue output');
- }
- if (!issuePool.data.issues.some(issue => issue.title === '排队/等待')) {
- throw new Error('VOC issue pool did not classify waiting issue');
- }
- const issuePoolUpdated = await runVocIssuePool({
- project: '样板门店A',
- industry: '本地生活餐饮',
- scenario: '门店经营',
- audience: '到店顾客',
- memory: issuePool.summary.memoryPath,
- evidence: ['排队问题已经开始做预点单验证。'],
- validatingIssues: ['排队/等待']
- });
- if (!issuePoolUpdated.data?.issues?.some(issue => issue.title === '排队/等待' && issue.status === '验证中')) {
- throw new Error('VOC issue pool did not apply issue status update');
- }
- const waitingTrend = issuePoolUpdated.data?.issues?.find(issue => issue.title === '排队/等待');
- if (!waitingTrend || waitingTrend.previousCount < 1 || waitingTrend.delta >= 0 || waitingTrend.trend !== 'down') {
- throw new Error('VOC issue pool did not compare issue trend with previous run');
- }
- if (!String(issuePoolUpdated.assistantMessage || '').includes('## 问题变化')) {
- throw new Error('VOC issue pool did not output issue change section');
- }
- const parsedFeedbackDeepDive = await runVocProblemDeepDive({
- issue: '排队',
- project: '样板门店A',
- industry: '本地生活餐饮',
- scenario: '门店经营',
- audience: '到店顾客',
- feedback: '这个店不想做等位饮品,更想先改评论区回复和预点单'
- });
- smokeDeepDiveMemoryBehavior(parsedFeedbackDeepDive, '等位饮品');
- if (!String(parsedFeedbackDeepDive.summary.memoryPath || '').includes('样板门店a')) {
- throw new Error('VOC problem deep dive did not use scoped default memory path');
- }
- const parsedMemory = parsedFeedbackDeepDive.data?.memory || {};
- if (!parsedMemory.blockedActions?.includes('等位饮品') ||
- !parsedMemory.preferredActions?.includes('评论区回复') ||
- !parsedMemory.preferredActions?.includes('预点单')) {
- throw new Error('VOC problem deep dive did not parse natural-language feedback into memory');
- }
- const actionWeightedDeepDive = await runVocProblemDeepDive({
- issue: '排队',
- project: '样板门店A',
- industry: '本地生活餐饮',
- scenario: '门店经营',
- audience: '到店顾客',
- memory: parsedFeedbackDeepDive.summary.memoryPath,
- validatedActions: ['预点单'],
- rejectedActions: ['等位饮品'],
- feedback: '预点单有效,等位饮品无效'
- });
- const actionWeightedBody = String(actionWeightedDeepDive.assistantMessage || '');
- if (!actionWeightedBody.includes('为什么优先推荐这些动作')) {
- throw new Error('VOC problem deep dive did not explain action-weight memory');
- }
- const actionStats = actionWeightedDeepDive.data?.memory?.actionStats || {};
- if (!actionStats['预点单']?.validatedCount || !actionStats['等位饮品']?.rejectedCount) {
- throw new Error('VOC problem deep dive did not persist action validation stats');
- }
- const contentPlan = await runVocContentPlan({
- brand: '样板门店A',
- industry: '本地生活餐饮',
- issues: ['第一次来怕踩雷', '觉得人均贵', '怕排队久']
- });
- const contentPlanBody = String(contentPlan.assistantMessage || '');
- if (contentPlan.status !== 'ok' ||
- !contentPlanBody.includes('7 天 VOC 内容选题') ||
- !contentPlanBody.includes('前 3 秒钩子') ||
- !contentPlanBody.includes('口播脚本') ||
- contentPlan.data?.plan?.length !== 7) {
- throw new Error('VOC content plan did not return 7-day script output');
- }
- const speakingScript = await runVocSpeakingScript({
- brand: '样板门店A',
- industry: '本地生活餐饮',
- topic: '第一次来怎么点不踩雷',
- userIssue: '第一次来怕点错',
- evidence: ['第一次来不知道怎么点,怕踩雷'],
- feedback: '开头更狠一点,老板视角,少讲概念,多给具体场景',
- memory: path.join(OUTPUT_ROOT, 'voc-speaking-script-memory.json'),
- finalize: true
- });
- const speakingScriptBody = String(speakingScript.assistantMessage || '');
- if (speakingScript.status !== 'ok' ||
- !speakingScriptBody.includes('VOC 口播脚本共创') ||
- !speakingScriptBody.includes('60 秒口播稿') ||
- !speakingScriptBody.includes('30 秒压缩版') ||
- !speakingScript.data?.memory?.finalizedScripts?.length ||
- !speakingScript.data?.memory?.preferredStyles?.includes('老板视角')) {
- throw new Error('VOC speaking script did not return co-creation output with memory');
- }
- const competitorMap = await runVocCompetitorMap({
- brand: '样板门店A',
- city: '南昌',
- category: '餐饮'
- });
- const competitorMapBody = String(competitorMap.assistantMessage || '');
- if (competitorMap.status !== 'ok' ||
- !competitorMapBody.includes('竞品图谱') ||
- !competitorMapBody.includes('错位竞争') ||
- !competitorMap.data?.competitors?.length) {
- throw new Error('VOC competitor map did not return competitor opportunity output');
- }
- const businessWorkflow = await runBusinessWorkflow({
- brand: '样板门店A',
- industry: '本地生活餐饮',
- platform: 'douyin',
- collectionMode: 'sample',
- keywords: ['南昌菜', '第一次来怎么点', '排队']
- });
- const businessWorkflowBody = String(businessWorkflow.assistantMessage || '');
- if (businessWorkflow.status !== 'ok' ||
- !businessWorkflowBody.includes('VOC经营闭环') ||
- !businessWorkflowBody.includes('市场和用户声音') ||
- !businessWorkflowBody.includes('先改哪个问题') ||
- !businessWorkflowBody.includes('第一条口播稿') ||
- businessWorkflow.summary?.workflowStage !== 'business_workflow_first_round') {
- throw new Error('VOC business workflow did not return end-to-end workflow output');
- }
- await smokeNoTokenRechargePrompt();
- await smokeDouyinNoTokenRechargePrompt();
- smokeWorkspaceEnvLocalToken();
- smokeAncestorEnvLocalToken();
- await smokeIssuePoolReportEvidenceFiltering();
- await smokeBusinessStatusErrors();
- await smokeDouyinSearchInputErrors();
- await smokeCrossIndustry({ root: ROOT });
- const toolNames = await smokeMcpTools();
- const requiredTools = [
- 'fmode_image_analysis',
- 'voc_business_workflow_run',
- 'voc_xiaohongshu_token_check',
- 'voc_xiaohongshu_trend_run',
- 'voc_xiaohongshu_preference_update',
- 'voc_douyin_token_check',
- 'voc_douyin_trend_run',
- 'voc_douyin_preference_update',
- 'voc_issue_pool_run',
- 'voc_problem_deep_dive_run',
- 'voc_content_plan_run',
- 'voc_speaking_script_run',
- 'voc_competitor_map_run',
- 'voc_api_search',
- 'voc_api_doc',
- 'voc_api_call'
- ];
- for (const tool of requiredTools) {
- if (!toolNames.includes(tool)) {
- throw new Error(`missing MCP tool: ${tool}`);
- }
- }
- console.log(JSON.stringify({
- status: 'ok',
- checks: [
- 'sample report',
- 'evidence cards',
- 'local asset cache',
- 'xsec URL normalization',
- 'preference memory',
- 'sample report with memory',
- 'no-token recharge prompt',
- 'workspace .env.local token source',
- 'ancestor .env.local token source',
- 'VOC issue pool report evidence filtering',
- 'douyin sample report',
- 'douyin sample report with memory',
- 'VOC issue pool',
- 'VOC issue pool status memory',
- 'VOC issue pool trend comparison',
- 'VOC problem deep dive',
- 'VOC problem deep dive scoped memory',
- 'VOC problem deep dive feedback parser',
- 'VOC problem deep dive blocked-action guard',
- 'VOC problem deep dive action-weight memory',
- 'VOC content plan',
- 'VOC speaking script co-creation',
- 'VOC competitor map',
- 'VOC business workflow',
- 'douyin no-token recharge prompt',
- 'business-status recharge guard',
- 'douyin search input error guard',
- 'cross-industry sample smoke',
- 'MCP tools'
- ],
- mcpTools: toolNames,
- reportPreview: String(withMemory.assistantMessage || '').split('\n').slice(0, 8).join('\n'),
- files: withMemory.files
- }, null, 2));
- }
- main().catch(error => {
- console.error(JSON.stringify({
- status: 'error',
- message: error.message
- }, null, 2));
- process.exit(1);
- });
|