| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055 |
- #!/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 { callVocApi } = require('../mcp/src/tools/voc-api-catalog-run');
- 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,
- FMODE_API_KEY: process.env.FMODE_API_KEY,
- FMODE_API_TOKEN: process.env.FMODE_API_TOKEN,
- NEWAPI_TOKEN: process.env.NEWAPI_TOKEN,
- NEW_API_TOKEN: process.env.NEW_API_TOKEN,
- ANTHROPIC_AUTH_TOKEN: process.env.ANTHROPIC_AUTH_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',
- 'FMODE_API_KEY',
- 'FMODE_API_TOKEN',
- 'NEWAPI_TOKEN',
- 'NEW_API_TOKEN',
- 'ANTHROPIC_AUTH_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 pointsToCredentialRecovery = /settings\.json|Session Token|newapiToken/i.test(body)
- || (result.nextActions || []).some(action => /settings\.json|Session Token|newapiToken/i.test(String(action)));
- if (result.status !== 'needs_token' || !pointsToCredentialRecovery || /balance=fmodeapi|apig-pay/.test(body) || (result.errors || []).length) {
- throw new Error('no-token live path did not return a clean credential 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,
- FMODE_API_KEY: process.env.FMODE_API_KEY,
- FMODE_API_TOKEN: process.env.FMODE_API_TOKEN,
- NEWAPI_TOKEN: process.env.NEWAPI_TOKEN,
- NEW_API_TOKEN: process.env.NEW_API_TOKEN,
- ANTHROPIC_AUTH_TOKEN: process.env.ANTHROPIC_AUTH_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',
- 'FMODE_API_KEY',
- 'FMODE_API_TOKEN',
- 'NEWAPI_TOKEN',
- 'NEW_API_TOKEN',
- 'ANTHROPIC_AUTH_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 pointsToCredentialRecovery = /settings\.json|Session Token|newapiToken/i.test(body)
- || (result.nextActions || []).some(action => /settings\.json|Session Token|newapiToken/i.test(String(action)));
- if (result.status !== 'needs_token' || !pointsToCredentialRecovery || /balance=fmodeapi|apig-pay/.test(body) || (result.errors || []).length) {
- throw new Error('douyin no-token live path did not return a clean credential 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,
- };
- 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',
- ].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,
- };
- 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 });
- fs.writeFileSync(path.join(tempWorkspace, '.env.local'), 'VOC_TOKEN=ancestor-smoke-token\n');
- try {
- process.chdir(nested);
- process.env.USERPROFILE = tempHome;
- process.env.HOME = tempHome;
- [
- 'VOC_TOKEN',
- 'VOC_SOCIAL_TOKEN',
- ].forEach(key => delete process.env[key]);
- 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, [
- '# 示例品牌竞品图谱',
- '',
- '## 品牌背景',
- '- 品牌:示例品牌A',
- '- 城市:本地',
- '',
- '## 当前经营痛点',
- '1. 日常营销能够带来线索,但是活动折扣大。',
- '',
- '## live 采集真实样本',
- '1. [价格样本](https://example.test/note)',
- ' - 关键评论:多少钱啊',
- ' - 可解读点:用户第一反应不是质量,而是价格。',
- '2. [选择样本](https://example.test/note2)',
- ' - 高赞评论:第一次买不知道怎么选,怕踩雷',
- '',
- '## 下一步建议',
- '- 先选一个问题做深度拆解。'
- ].join('\n'));
- const result = await runVocIssuePool({
- project: '示例品牌A',
- 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 === 'decision')) {
- 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 smokeTaobaoFreshnessAndBusinessCodes() {
- const originalFetch = global.fetch;
- try {
- let collectAttempts = 0;
- global.fetch = async () => {
- collectAttempts += 1;
- return {
- ok: true,
- status: 200,
- text: async () => JSON.stringify({
- code: 200,
- data: { code: 301, data: null, message: 'COLLECT FAILED, SEND REQUEST AGAIN', recordTime: null }
- })
- };
- };
- const collectFailure = await callVocApi({
- id: 'taobao.get_item_detail_v7',
- params: { itemId: 'smoke-item' },
- newapiToken: 'sk-smoke-token',
- baseUrl: 'https://example.test/ecommerce',
- retries: 1
- });
- if (collectFailure.status !== 'upstream_unstable' || collectAttempts !== 2 || Number(collectFailure.summary?.httpStatus) !== 301) {
- throw new Error(`expected Taobao nested code 301 to retry then fail as upstream_unstable, got ${JSON.stringify(collectFailure)}`);
- }
- global.fetch = async () => ({
- ok: true,
- status: 200,
- text: async () => JSON.stringify({
- code: 200,
- data: { code: 202, data: null, message: 'NOT SUPPORTED', recordTime: null }
- })
- });
- const unsupported = await callVocApi({
- id: 'taobao.get_item_detail_v7',
- params: { itemId: 'smoke-item' },
- newapiToken: 'sk-smoke-token',
- baseUrl: 'https://example.test/ecommerce',
- retries: 0
- });
- if (unsupported.status !== 'not_supported' || Number(unsupported.summary?.httpStatus) !== 202) {
- throw new Error(`expected Taobao nested code 202 to return not_supported, got ${JSON.stringify(unsupported)}`);
- }
- global.fetch = async () => ({
- ok: true,
- status: 200,
- text: async () => JSON.stringify({
- code: 200,
- data: {
- code: 0,
- data: { subject: 'stale smoke item', price: 10, skuVoList: [] },
- message: null,
- recordTime: '2020-01-01T00:00:00'
- }
- })
- });
- const stale = await callVocApi({
- id: 'taobao.get_item_detail_v7',
- params: { itemId: 'smoke-item' },
- newapiToken: 'sk-smoke-token',
- baseUrl: 'https://example.test/ecommerce',
- retries: 0
- });
- if (stale.status !== 'stale_data' || !stale.summary?.stale || Number(stale.summary?.maxAgeHours) !== 48) {
- throw new Error(`expected old Taobao recordTime to return stale_data, got ${JSON.stringify(stale)}`);
- }
- } 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 decision 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 smokeTaobaoFreshnessAndBusinessCodes();
- 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',
- 'Taobao V7 freshness and nested business-code 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);
- });
|