claude-voc.js 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812
  1. #!/usr/bin/env node
  2. const fs = require('fs');
  3. const os = require('os');
  4. const path = require('path');
  5. const { spawnSync } = require('child_process');
  6. const SOURCE_ROOT = path.resolve(__dirname, '..');
  7. const WORKSPACE_ROOT = process.cwd();
  8. const DEFAULT_TARGET = path.join(os.homedir(), '.claude', 'plugins', 'voc-intelligence');
  9. const WORKSPACE_TARGET = path.join(WORKSPACE_ROOT, '.claude', 'plugins', 'voc-intelligence');
  10. const WORKSPACE_PLUGINS_ROOT = path.join(WORKSPACE_ROOT, '.claude', 'plugins');
  11. const MIN_NODE_MAJOR = 18;
  12. const EXCLUDED_DIRS = new Set([
  13. 'node_modules',
  14. 'memory',
  15. 'outputs',
  16. '.npm-cache-acceptance',
  17. '.tmp',
  18. '.git',
  19. '.claude'
  20. ]);
  21. const EXCLUDED_FILES = new Set([
  22. '.env',
  23. '.env.local',
  24. '.npmrc'
  25. ]);
  26. function usage() {
  27. return [
  28. 'Claude VOC skill package installer',
  29. '',
  30. 'Usage:',
  31. ' claude-voc install',
  32. ' claude-voc workspace',
  33. ' claude-voc install --workspace --smoke',
  34. ' claude-voc check',
  35. ' claude-voc smoke',
  36. ' claude-voc image --image-path <file> --prompt <text>',
  37. ' claude-voc path',
  38. '',
  39. 'npx:',
  40. ' npx @vocmarket/voc-skill install',
  41. ' npx @vocmarket/voc-skill workspace --smoke',
  42. '',
  43. 'Options:',
  44. ' --workspace Install into ./.claude/plugins/voc-intelligence',
  45. ' --target <dir> Install into a custom directory',
  46. ' --force Allow overwriting an existing custom target',
  47. ' --skip-install Skip npm install after copying files',
  48. ' --smoke Run sample/preference/MCP smoke checks after install',
  49. ' --help, -h Show help'
  50. ].join('\n');
  51. }
  52. function expandHome(value) {
  53. return value.replace(/^~(?=$|[\\/])/, os.homedir());
  54. }
  55. function parseArgs(argv) {
  56. const first = argv[0] && !argv[0].startsWith('--') ? argv[0] : 'install';
  57. const opts = {
  58. command: first,
  59. target: DEFAULT_TARGET,
  60. skipInstall: false,
  61. smoke: false,
  62. force: false,
  63. help: false
  64. };
  65. if (first === 'workspace' || first === 'install-workspace') {
  66. opts.command = 'install';
  67. opts.target = WORKSPACE_TARGET;
  68. }
  69. for (let i = first === argv[0] ? 1 : 0; i < argv.length; i++) {
  70. const token = argv[i];
  71. if (token === '--target') {
  72. opts.target = argv[++i];
  73. } else if (token.startsWith('--target=')) {
  74. opts.target = token.slice('--target='.length);
  75. } else if (token === '--workspace') {
  76. opts.target = WORKSPACE_TARGET;
  77. } else if (token === '--force') {
  78. opts.force = true;
  79. } else if (token === '--skip-install') {
  80. opts.skipInstall = true;
  81. } else if (token === '--smoke') {
  82. opts.smoke = true;
  83. } else if (token === '--help' || token === '-h') {
  84. opts.help = true;
  85. }
  86. }
  87. opts.target = path.resolve(expandHome(opts.target));
  88. return opts;
  89. }
  90. function run(command, args, cwd) {
  91. const useCmd = process.platform === 'win32' && command === 'npm';
  92. const executable = useCmd ? 'cmd.exe' : command;
  93. const finalArgs = useCmd ? ['/d', '/s', '/c', 'npm', ...args] : args;
  94. const child = spawnSync(executable, finalArgs, {
  95. cwd,
  96. encoding: 'utf8',
  97. stdio: 'inherit',
  98. maxBuffer: 1024 * 1024 * 100
  99. });
  100. if (child.status !== 0) {
  101. const detail = child.error ? `: ${child.error.message}` : '';
  102. throw new Error(`${command} ${args.join(' ')} failed with exit ${child.status}${detail}`);
  103. }
  104. }
  105. function checkNodeVersion() {
  106. const major = Number(process.versions.node.split('.')[0]);
  107. if (!Number.isFinite(major) || major < MIN_NODE_MAJOR) {
  108. throw new Error(`Node.js ${MIN_NODE_MAJOR}+ is required. Current version: ${process.version}`);
  109. }
  110. }
  111. function ensureDir(dirPath) {
  112. fs.mkdirSync(dirPath, { recursive: true });
  113. }
  114. function isInside(parentDir, childDir) {
  115. const relative = path.relative(path.resolve(parentDir), path.resolve(childDir));
  116. return relative === '' || (!!relative && !relative.startsWith('..') && !path.isAbsolute(relative));
  117. }
  118. function isSafeDefaultTarget(targetDir) {
  119. return path.resolve(targetDir) === path.resolve(DEFAULT_TARGET);
  120. }
  121. function isSafeWorkspaceTarget(targetDir) {
  122. return isInside(WORKSPACE_PLUGINS_ROOT, targetDir);
  123. }
  124. function canOverwriteTarget(targetDir, opts) {
  125. return opts.force || isSafeDefaultTarget(targetDir) || isSafeWorkspaceTarget(targetDir);
  126. }
  127. function shouldCopyEntry(entry) {
  128. if (entry.isDirectory()) return !EXCLUDED_DIRS.has(entry.name);
  129. if (entry.isFile()) {
  130. if (EXCLUDED_FILES.has(entry.name)) return false;
  131. if (entry.name.endsWith('.tgz')) return false;
  132. return true;
  133. }
  134. return false;
  135. }
  136. function copyDirRecursive(srcDir, destDir) {
  137. ensureDir(destDir);
  138. let count = 0;
  139. for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
  140. if (!shouldCopyEntry(entry)) continue;
  141. const src = path.join(srcDir, entry.name);
  142. const dest = path.join(destDir, entry.name);
  143. if (entry.isDirectory()) {
  144. count += copyDirRecursive(src, dest);
  145. } else if (entry.isFile()) {
  146. ensureDir(path.dirname(dest));
  147. fs.copyFileSync(src, dest);
  148. count++;
  149. }
  150. }
  151. return count;
  152. }
  153. function assertInstalled(targetDir) {
  154. const required = [
  155. 'package.json',
  156. 'install.js',
  157. '.claude-plugin/plugin.json',
  158. '.mcp.json',
  159. 'skills/xiaohongshu-trend-intelligence/SKILL.md',
  160. 'skills/douyin-trend-intelligence/SKILL.md',
  161. 'skills/voc-issue-pool/SKILL.md',
  162. 'skills/voc-problem-deep-dive/SKILL.md',
  163. 'skills/voc-content-plan/SKILL.md',
  164. 'skills/voc-speaking-script/SKILL.md',
  165. 'skills/voc-competitor-map/SKILL.md',
  166. 'skills/voc-business-workflow/SKILL.md',
  167. 'skills/fmode-image-analysis/SKILL.md',
  168. 'skills/voc-api-catalog/SKILL.md',
  169. 'mcp/src/tools/fmode-image-analysis.js',
  170. 'mcp/src/tools/voc-api-catalog-run.js',
  171. 'mcp/catalog/voc-social-endpoints.json',
  172. 'mcp/src/server.js'
  173. ];
  174. const missing = required.filter(rel => !fs.existsSync(path.join(targetDir, rel)));
  175. if (missing.length) {
  176. throw new Error(`Missing required files in install target: ${missing.join(', ')}`);
  177. }
  178. }
  179. function writeMcpConfig(targetDir) {
  180. const serverPath = path.join(targetDir, 'mcp', 'src', 'server.js');
  181. const mcpConfig = {
  182. mcpServers: {
  183. voc: {
  184. command: 'node',
  185. args: [serverPath],
  186. cwd: targetDir
  187. }
  188. }
  189. };
  190. fs.writeFileSync(path.join(targetDir, '.mcp.json'), `${JSON.stringify(mcpConfig, null, 2)}\n`, 'utf8');
  191. }
  192. function readJsonIfExists(filePath) {
  193. if (!fs.existsSync(filePath)) return {};
  194. return JSON.parse(fs.readFileSync(filePath, 'utf8'));
  195. }
  196. function writeWorkspaceMcpConfig(targetDir) {
  197. const mcpConfigPath = path.join(WORKSPACE_ROOT, '.mcp.json');
  198. const existing = readJsonIfExists(mcpConfigPath);
  199. const mcpConfig = {
  200. ...existing,
  201. mcpServers: {
  202. ...(existing.mcpServers || {}),
  203. voc: {
  204. command: 'node',
  205. args: [path.join(targetDir, 'mcp', 'src', 'server.js')],
  206. cwd: targetDir
  207. }
  208. }
  209. };
  210. fs.writeFileSync(mcpConfigPath, `${JSON.stringify(mcpConfig, null, 2)}\n`, 'utf8');
  211. }
  212. function asForwardSlash(filePath) {
  213. return path.resolve(filePath).replace(/\\/g, '/');
  214. }
  215. function quotedPath(filePath) {
  216. return `"${asForwardSlash(filePath)}"`;
  217. }
  218. function workspaceTrendSkillText(options) {
  219. const trendRunner = path.join(options.targetDir, 'mcp', 'src', 'tools', options.trendTool);
  220. const preferenceUpdater = path.join(options.targetDir, 'mcp', 'src', 'tools', options.preferenceTool);
  221. const profilePath = path.join(options.targetDir, 'memory-templates', options.profileFile);
  222. const defaultMemoryPath = path.join(WORKSPACE_ROOT, 'outputs', options.memoryDir, options.memoryFile);
  223. const liveLimits = options.platform === '抖音'
  224. ? '--keyword-limit 1 --videos-per-keyword 1 --max-comment-pages 1'
  225. : '--keyword-limit 1 --notes-per-keyword 1 --max-comment-pages 1';
  226. return [
  227. '---',
  228. `name: ${options.skillName}`,
  229. `description: ${options.description}`,
  230. 'allowed-tools: Read Write Bash(node *)',
  231. '---',
  232. '',
  233. `# ${options.platform}趋势情报官`,
  234. '',
  235. `你是面向业务用户的“${options.platform}趋势情报官”。用户要的是第一轮样本观察、机会假设和待确认问题,不是技术日志。`,
  236. '',
  237. '## 必须执行',
  238. '',
  239. `- 用户要求${options.platform}趋势情报、内容趋势、评论分析、真实采集、live采集或社媒 VOC 时,必须运行本技能包的工具或 MCP,不要只凭经验回答。`,
  240. '- 用户说“live / 真实采集 / 进入 live / 实时采集 / 抓真实数据”时,必须运行 live 模式工具。不要回复“我不能实时抓取外部数据”。本技能包已经提供本地 VOC 采集入口。',
  241. '- live 模式返回 `needs_token`、`needs_recharge` 或 `needs_valid_token` 时,必须把工具输出里的 `assistantMessage` 原样转述给用户,尤其不能省略充值/开通链接。',
  242. '- 如果工具输出里有 `nextActions`,至少转述其中带 `apig-pay` 的充值链接。',
  243. '- 第一轮只说“初步判断 / 机会假设 / 待校准”,不要说成最终结论。',
  244. '- 后续用户说“保留、不要、更偏、这个不准”等反馈时,运行偏好更新命令沉淀记忆。',
  245. '',
  246. '## MANDATORY Live Rule',
  247. '',
  248. '- If the user asks for live collection, real collection, or entering live mode, run the local VOC tool in live mode.',
  249. '- Do not answer that you cannot fetch external data. The local VOC tool is the approved collection entry.',
  250. '- If the tool returns `needs_token`, `needs_recharge`, or `needs_valid_token`, paste its `assistantMessage` to the user and keep the payment URL visible.',
  251. '- The payment URL must be shown when there is no token or insufficient balance: `https://app.fmode.cn/dev/apig-pay/?apigid=Vo3ROWEvDy&fun_id=HOkkX72PMF`',
  252. '',
  253. '## sample 命令',
  254. '',
  255. '```bash',
  256. `node ${quotedPath(trendRunner)} --collection-mode sample --profile ${quotedPath(profilePath)} --output ${JSON.stringify(options.sampleOutput)} --assistant-message-only`,
  257. '```',
  258. '',
  259. '## live 命令',
  260. '',
  261. '```bash',
  262. `node ${quotedPath(trendRunner)} --collection-mode live --profile ${quotedPath(profilePath)} --output ${JSON.stringify(options.liveOutput)} ${liveLimits} --assistant-message-only`,
  263. '```',
  264. '',
  265. '运行后把命令输出正文直接发给用户。不要展示 JSON、summary、data、files 或调试日志。',
  266. '',
  267. '## 偏好更新命令',
  268. '',
  269. '```bash',
  270. `node ${quotedPath(preferenceUpdater)} --message "<用户反馈原文>" --memory ${quotedPath(defaultMemoryPath)} --result-prefix ${options.resultPrefix}`,
  271. '```',
  272. '',
  273. '## 充值链接口径',
  274. '',
  275. '没有 token 或余额不足时,用户必须看到:',
  276. '',
  277. '```text',
  278. 'https://app.fmode.cn/dev/apig-pay/?apigid=Vo3ROWEvDy&fun_id=HOkkX72PMF',
  279. '```',
  280. '',
  281. '套餐入口可作为补充:',
  282. '',
  283. '```text',
  284. 'https://app.fmode.cn/dev/apig-pay/#/workshop/99',
  285. '```',
  286. ''
  287. ].join('\n');
  288. }
  289. function workspaceSkillText(targetDir) {
  290. return workspaceTrendSkillText({
  291. targetDir,
  292. skillName: 'xiaohongshu-trend-intelligence',
  293. platform: '小红书',
  294. description: 'Build Xiaohongshu trend intelligence reports. Use when the user asks for 小红书趋势情报、行业趋势、内容选题、销售话术、真实采集、live采集、用户顾虑 or social VOC analysis.',
  295. trendTool: 'xiaohongshu-trend-run.js',
  296. preferenceTool: 'xiaohongshu-preference-update.js',
  297. profileFile: 'xiaohongshu-trend-profile.json',
  298. memoryDir: 'claude-code-xhs-memory',
  299. memoryFile: 'xiaohongshu-trend-memory.json',
  300. sampleOutput: 'outputs/claude-code-xhs-sample',
  301. liveOutput: 'outputs/claude-code-xhs-live',
  302. resultPrefix: 'XHS_PREF_RESULT'
  303. });
  304. const trendRunner = path.join(targetDir, 'mcp', 'src', 'tools', 'xiaohongshu-trend-run.js');
  305. const preferenceUpdater = path.join(targetDir, 'mcp', 'src', 'tools', 'xiaohongshu-preference-update.js');
  306. const profilePath = path.join(targetDir, 'memory-templates', 'xiaohongshu-trend-profile.json');
  307. const defaultMemoryPath = path.join(WORKSPACE_ROOT, 'outputs', 'claude-code-xhs-memory', 'xiaohongshu-trend-memory.json');
  308. return [
  309. '---',
  310. 'name: xiaohongshu-trend-intelligence',
  311. 'description: Build Xiaohongshu trend intelligence reports from industry direction, audience, keywords, notes, and comments. Use when the user asks for 小红书趋势情报、行业趋势、女性客户消费决策、设计元素洞察、家装全屋定制趋势、内容选题 or social VOC analysis.',
  312. 'allowed-tools: Read Write Bash(node *)',
  313. '---',
  314. '',
  315. '# 小红书趋势情报官',
  316. '',
  317. '你是面向业务用户的“小红书趋势情报官”。用户要的是第一轮样本观察、机会假设和待确认问题,不是技术日志。',
  318. '',
  319. '## 必须执行',
  320. '',
  321. '- 当用户要求做小红书趋势情报、行业趋势、女性客户消费决策、家装全屋定制趋势、设计元素洞察或内容选题时,优先运行下面的 sample 工具命令。',
  322. '- 不要只凭经验回答,也不要只返回文件路径。',
  323. '- 工具输出里会有 `assistantMessage`,请把它的正文直接发给用户。',
  324. '- 第一轮只能说“初步判断 / 机会假设 / 待校准”,不要说成最终结论。',
  325. '- 后续用户说“保留、不要、更偏、这个不准”等反馈时,运行偏好更新命令沉淀记忆。',
  326. '',
  327. '## sample 趋势情报命令',
  328. '',
  329. '```bash',
  330. `node ${quotedPath(trendRunner)} --collection-mode sample --profile ${quotedPath(profilePath)} --output "outputs/claude-code-xhs-sample" --assistant-message-only`,
  331. '```',
  332. '',
  333. '运行后把命令输出正文直接发给用户。不要展示 JSON、summary、data、files 或调试日志。',
  334. '',
  335. '## 偏好更新命令',
  336. '',
  337. '```bash',
  338. `node ${quotedPath(preferenceUpdater)} --message "<用户反馈原文>" --memory ${quotedPath(defaultMemoryPath)} --result-prefix XHS_PREF_RESULT`,
  339. '```',
  340. '',
  341. '## 推荐用户启动话术',
  342. '',
  343. '```text',
  344. '帮我做一份家装全屋定制行业的小红书趋势情报。',
  345. '先用演示样例跑通流程,不要真实采集。',
  346. '在聊天里给我第一轮样本观察和待确认问题。',
  347. '```',
  348. ''
  349. ].join('\n');
  350. }
  351. function workspaceDouyinSkillText(targetDir) {
  352. return workspaceTrendSkillText({
  353. targetDir,
  354. skillName: 'douyin-trend-intelligence',
  355. platform: '抖音',
  356. description: 'Build Douyin trend intelligence reports. Use when the user asks for 抖音趋势情报、视频评论、口播选题、短视频开头、内容趋势、真实采集、live采集、用户顾虑 or social VOC analysis.',
  357. trendTool: 'douyin-trend-run.js',
  358. preferenceTool: 'douyin-preference-update.js',
  359. profileFile: 'douyin-trend-profile.json',
  360. memoryDir: 'claude-code-douyin-memory',
  361. memoryFile: 'douyin-trend-memory.json',
  362. sampleOutput: 'outputs/claude-code-douyin-sample',
  363. liveOutput: 'outputs/claude-code-douyin-live',
  364. resultPrefix: 'DOUYIN_PREF_RESULT'
  365. });
  366. const trendRunner = path.join(targetDir, 'mcp', 'src', 'tools', 'douyin-trend-run.js');
  367. const preferenceUpdater = path.join(targetDir, 'mcp', 'src', 'tools', 'douyin-preference-update.js');
  368. const profilePath = path.join(targetDir, 'memory-templates', 'douyin-trend-profile.json');
  369. const defaultMemoryPath = path.join(WORKSPACE_ROOT, 'outputs', 'claude-code-douyin-memory', 'douyin-trend-memory.json');
  370. return [
  371. '---',
  372. 'name: douyin-trend-intelligence',
  373. 'description: Build Douyin trend intelligence reports from industry direction, audience, keywords, videos, and comments. Use when the user asks for 抖音趋势情报、视频评论、口播选题、短视频开头、内容趋势 or social VOC analysis.',
  374. 'allowed-tools: Read Write Bash(node *)',
  375. '---',
  376. '',
  377. '# 抖音趋势情报官',
  378. '',
  379. '你是面向业务用户的“抖音趋势情报官”。用户要的是第一轮样本观察、机会假设和待确认问题,不是技术日志。',
  380. '',
  381. '## 必须执行',
  382. '',
  383. '- 当用户要求做抖音趋势情报、视频评论、口播选题、短视频开头或内容趋势时,优先运行下面的 sample 工具命令。',
  384. '- 不要只凭经验回答,也不要只返回文件路径。',
  385. '- 工具输出里会有 `assistantMessage`,请把它的正文直接发给用户。',
  386. '- 第一轮只能说“初步判断 / 机会假设 / 待校准”,不要说成最终结论。',
  387. '- 后续用户说“保留、不要、更偏、这个不准”等反馈时,运行偏好更新命令沉淀记忆。',
  388. '',
  389. '## sample 趋势情报命令',
  390. '',
  391. '```bash',
  392. `node ${quotedPath(trendRunner)} --collection-mode sample --profile ${quotedPath(profilePath)} --output "outputs/claude-code-douyin-sample" --assistant-message-only`,
  393. '```',
  394. '',
  395. '运行后把命令输出正文直接发给用户。不要展示 JSON、summary、data、files 或调试日志。',
  396. '',
  397. '## 偏好更新命令',
  398. '',
  399. '```bash',
  400. `node ${quotedPath(preferenceUpdater)} --message "<用户反馈原文>" --memory ${quotedPath(defaultMemoryPath)} --result-prefix DOUYIN_PREF_RESULT`,
  401. '```',
  402. '',
  403. '## 推荐用户启动话术',
  404. '',
  405. '```text',
  406. '帮我做一份家装全屋定制行业的抖音趋势情报。',
  407. '先用演示样例跑通流程,不要真实采集。',
  408. '在聊天里给我第一轮样本观察和待确认问题。',
  409. '```',
  410. ''
  411. ].join('\n');
  412. }
  413. function workspaceProblemDeepDiveSkillText(targetDir) {
  414. const deepDiveRunner = path.join(targetDir, 'mcp', 'src', 'tools', 'voc-problem-deep-dive-run.js');
  415. return [
  416. '---',
  417. 'name: voc-problem-deep-dive',
  418. 'description: Deep-dive a single VOC problem into root cause, boss/operator impact, concrete actions, content ideas, and validation metrics. Use when the user says 继续挖、深入分析、老板怎么解决、这个问题怎么办、怎么改门店、怎么转成内容、单点VOC or VOC深挖.',
  419. 'allowed-tools: Read Write Bash(node *)',
  420. '---',
  421. '',
  422. '# 单点 VOC 深挖官',
  423. '',
  424. '你是面向老板和门店经营者的“单点 VOC 深挖官”。用户不需要学习提示词。用户只要说“这个问题继续挖”“老板怎么解决”“这个差评背后是什么”,你就要把真实用户声音转成可执行经营动作。',
  425. '',
  426. '## 必须执行',
  427. '',
  428. '- 当用户说“继续挖、深入分析、老板怎么解决、这个问题怎么办、怎么改、怎么转成内容、单点 VOC”时,必须运行单点深挖工具。',
  429. '- 不要输出提示词教学,不要让用户自己设计分析维度。',
  430. '- 输出必须站在老板视角:这件事影响新客、复购、客单、口碑,还是现场效率。',
  431. '- 每次只深挖一个问题点。如果用户给了多个问题,先选择最影响转化的一个。',
  432. '- 如果上一轮趋势报告里有证据样本或高赞评论,要把相关评论作为 evidence 传入工具。',
  433. '- 要使用记忆能力:用户说“这个动作不适合”“我们已经试过”“这个有效”“下次更偏内容/门店动作”时,把原话作为 `feedback` 传入;如果能明确识别,也同步传 `blockedActions`、`validatedActions`、`rejectedActions` 或 `preferredActions`。',
  434. '- 后续多轮迭代时要带上 `project`/`brand`/`store`、`industry`、`scenario`、`audience`。未显式传 `memory` 时,工具会按这些字段自动隔离记忆,避免不同行业、不同客户串味。',
  435. '- 如果用户没有指定记忆文件,不要强行要求用户理解路径;让工具使用默认分桶记忆即可。只有在同一个客户需要固定沉淀时,才显式传同一个 `memory` 路径。',
  436. '',
  437. '## 命令',
  438. '',
  439. '```bash',
  440. `node ${quotedPath(deepDiveRunner)} --issue "怎么选" --industry "家居定制" --scenario "线上获客" --audience "意向用户" --assistant-message-only`,
  441. '```',
  442. '',
  443. '## 带记忆的多轮迭代',
  444. '',
  445. '```bash',
  446. `node ${quotedPath(deepDiveRunner)} --issue "怎么选" --industry "家居定制" --scenario "线上获客" --audience "意向用户" --memory "outputs/voc-problem-memory.json" --feedback "老板不想做直播带看,更想先改评论区回复和方案说明" --blocked-actions "直播带看" --preferred-actions "评论区回复,方案说明" --assistant-message-only`,
  447. '```',
  448. '',
  449. '自然语言反馈也可以直接传入 `feedback`,工具会尝试自动提取偏好和屏蔽动作:',
  450. '',
  451. '```bash',
  452. `node ${quotedPath(deepDiveRunner)} --issue "怎么选" --project "示例品牌A" --industry "家居定制" --scenario "线上获客" --audience "意向用户" --feedback "这个品牌不想做直播带看,更想先改评论区回复和方案说明" --assistant-message-only`,
  453. '```',
  454. '',
  455. '运行后把命令输出正文直接发给用户。不要展示 JSON、summary、data、files 或调试日志。',
  456. ''
  457. ].join('\n');
  458. }
  459. function workspaceIssuePoolSkillText(targetDir) {
  460. const issuePoolRunner = path.join(targetDir, 'mcp', 'src', 'tools', 'voc-issue-pool-run.js');
  461. return [
  462. '---',
  463. 'name: voc-issue-pool',
  464. 'description: Turn VOC comments, report evidence, or user feedback into a prioritized issue pool with status, evidence, business impact, next actions, and follow-up deep-dive suggestions. Use when the user asks 哪些问题最影响生意、问题池、问题清单、用户都在吐槽什么、先改哪个、VOC问题管理、问题优先级、把评论整理成问题.',
  465. 'allowed-tools: Read Write Bash(node *)',
  466. '---',
  467. '',
  468. '# VOC 问题池官',
  469. '',
  470. '你是面向老板、门店经营者和营销负责人的 VOC 问题池官。用户不需要知道分类法,也不需要学习提示词。用户只要说“这些评论里哪些问题最重要”“先改哪个”“整理成问题池”,你就把真实用户声音整理成可排序、可跟进、可继续深挖的问题列表。',
  471. '',
  472. '## 必须执行',
  473. '',
  474. '- 当用户说“问题池、问题清单、用户吐槽什么、哪些问题最影响生意、先改哪个、VOC 问题管理、把评论整理成问题”时,优先运行 `voc_issue_pool_run`。',
  475. '- 如果上一轮小红书/抖音报告里有证据样本或高赞评论,把相关评论作为 `evidence`、`evidenceText` 或 `reportPath` 传入。',
  476. '- 输出必须站在业务视角:这个问题影响新客、复购、客单、口碑、内容信任,还是现场效率。',
  477. '- 不要只做词频统计。每个问题都要包含证据样本、影响环节、建议动作和下一步。',
  478. '- 如果用户说某个问题已解决、验证中、暂缓,要通过 `resolvedIssues`、`validatingIssues`、`blockedIssues` 或 `statusUpdates` 更新问题状态。',
  479. '- 后续深挖时,引导用户直接说“继续深挖「问题名」”,然后转入 `voc-problem-deep-dive`。',
  480. '',
  481. '## 命令',
  482. '',
  483. '```bash',
  484. `node ${quotedPath(issuePoolRunner)} --project "示例品牌A" --industry "家居定制" --scenario "线上获客" --audience "意向用户" --evidence "第一次买不知道怎么选,怕踩雷。,价格有点高,不确定值不值。,咨询的时候没人理。" --assistant-message-only`,
  485. '```',
  486. '',
  487. '带报告文件:',
  488. '',
  489. '```bash',
  490. `node ${quotedPath(issuePoolRunner)} --project "示例品牌A" --industry "家居定制" --report "outputs/douyin-trend-report.md" --assistant-message-only`,
  491. '```',
  492. '',
  493. '运行后把命令输出正文直接发给用户。不要展示 JSON、summary、data、files 或调试日志。',
  494. ''
  495. ].join('\n');
  496. }
  497. function workspaceContentPlanSkillText(targetDir) {
  498. const runner = path.join(targetDir, 'mcp', 'src', 'tools', 'voc-content-plan-run.js');
  499. return [
  500. '---',
  501. 'name: voc-content-plan',
  502. 'description: Turn VOC issues into a 7-day content plan with short-video topics and speaking scripts. Use when the user asks 下周账号发什么、口播脚本、短视频选题、把用户问题变成内容、7天内容计划.',
  503. 'allowed-tools: Read Write Bash(node *)',
  504. '---',
  505. '',
  506. '# VOC 内容选题官',
  507. '',
  508. '当用户说“下周发什么、7 天选题、口播脚本、短视频脚本、账号内容、把问题变成内容”时,运行本工具。',
  509. '',
  510. '必须把内容绑定到 VOC 用户问题,不要输出空泛爆款标题。不要覆盖创始人故事。',
  511. '',
  512. '## 命令',
  513. '',
  514. '```bash',
  515. `node ${quotedPath(runner)} --brand "示例品牌A" --industry "家居定制" --issues "第一次买怕踩雷;觉得价格贵;怕质量不稳定" --assistant-message-only`,
  516. '```',
  517. '',
  518. '运行后把正文直接发给用户,不要展示 JSON。',
  519. ''
  520. ].join('\n');
  521. }
  522. function workspaceSpeakingScriptSkillText(targetDir) {
  523. const runner = path.join(targetDir, 'mcp', 'src', 'tools', 'voc-speaking-script-run.js');
  524. return [
  525. '---',
  526. 'name: voc-speaking-script',
  527. 'description: Turn one VOC-backed topic into a co-created speaking script with 60-second draft, 30-second compressed version, teleprompter text, revision feedback, finalization, and script memory. Use when the user says 选题2进入脚本共创、改稿、定稿、口播稿、提词器、开头更狠、老板视角、少讲概念.',
  528. 'allowed-tools: Read Write Bash(node *)',
  529. '---',
  530. '',
  531. '# VOC 口播脚本共创',
  532. '',
  533. '当用户从 7 天内容计划里选择某条选题,或说“进入脚本共创、改稿、定稿、提词器版、开头更狠、老板视角”时,运行本工具。',
  534. '',
  535. '口播必须来自 VOC 用户问题、趋势证据、问题池 Top 问题、单点深挖动作或竞品评论差异。没有真实证据时要标注低证据风险。',
  536. '',
  537. '## 命令',
  538. '',
  539. '```bash',
  540. `node ${quotedPath(runner)} --brand "示例品牌A" --industry "家居定制" --topic "第一次买怎么选不踩雷" --user-issue "第一次买怕选错" --evidence "第一次买不知道怎么选,怕踩雷" --assistant-message-only`,
  541. '```',
  542. '',
  543. '改稿:',
  544. '',
  545. '```bash',
  546. `node ${quotedPath(runner)} --brand "示例品牌A" --topic "第一次买怎么选不踩雷" --user-issue "第一次买怕选错" --evidence "第一次买不知道怎么选,怕踩雷" --feedback "开头更狠一点,老板视角,少讲概念,多给具体场景" --assistant-message-only`,
  547. '```',
  548. '',
  549. '定稿时传 `--finalize true`,工具会写入脚本记忆。运行后把正文直接发给用户,不要展示 JSON。',
  550. ''
  551. ].join('\n');
  552. }
  553. function workspaceCompetitorMapSkillText(targetDir) {
  554. const runner = path.join(targetDir, 'mcp', 'src', 'tools', 'voc-competitor-map-run.js');
  555. return [
  556. '---',
  557. 'name: voc-competitor-map',
  558. 'description: Build a competitor map and differentiated opportunity report. Use when the user asks 看竞品、竞品分析、错位竞争、别人为什么吸引顾客、我不知道竞品是谁.',
  559. 'allowed-tools: Read Write Bash(node *)',
  560. '---',
  561. '',
  562. '# VOC 竞品图谱官',
  563. '',
  564. '当用户说“看竞品、竞品分析、错位竞争、别人怎么做、我不知道竞品是谁”时,运行本工具。',
  565. '',
  566. '如果用户没有明确竞品,先按同城、同品类、同价格带、同消费场景、平台声量生成候选竞品类型。不要覆盖创始人故事。',
  567. '',
  568. '## 命令',
  569. '',
  570. '```bash',
  571. `node ${quotedPath(runner)} --brand "示例品牌A" --city "本地" --category "家居定制" --assistant-message-only`,
  572. '```',
  573. '',
  574. '运行后把正文直接发给用户,不要展示 JSON。',
  575. ''
  576. ].join('\n');
  577. }
  578. function workspaceBusinessWorkflowSkillText(targetDir) {
  579. const runner = path.join(targetDir, 'mcp', 'src', 'tools', 'voc-business-workflow-run.js');
  580. return [
  581. '---',
  582. 'name: voc-business-workflow',
  583. 'description: Run the full VOC business workflow from market voices to issue pool, top issue action, 7-day content plan, and one speaking script. Use when the user asks 看顾客关心什么、先改哪里、下周发什么、完整VOC闭环、经营诊断.',
  584. 'allowed-tools: Read Write Bash(node *)',
  585. '---',
  586. '',
  587. '# VOC经营闭环',
  588. '',
  589. '当用户想一次拿到“看市场、看问题、找动作、做内容”的结果时,运行本工具。不要让用户理解 MCP、schema 或多个工具名。',
  590. '',
  591. '## 命令',
  592. '',
  593. '```bash',
  594. `node ${quotedPath(runner)} --brand "示例品牌A" --industry "家居定制" --platform douyin --collection-mode sample --keywords "家居定制怎么选,第一次买怎么对比,售后" --assistant-message-only`,
  595. '```',
  596. '',
  597. '真实采集时把 `--collection-mode sample` 换成 `--collection-mode live`。如果工具提示没有 token、余额不足或没有样本,直接把工具正文发给用户,并保留充值/降级建议。',
  598. '',
  599. '## 交付物',
  600. '',
  601. '必须包含五段:市场和用户声音、先改哪个问题、Top问题怎么解决、下周账号发什么、第一条口播稿。',
  602. ''
  603. ].join('\n');
  604. }
  605. function writeWorkspaceSkillEntries(targetDir) {
  606. const xhsSkillDir = path.join(WORKSPACE_ROOT, '.claude', 'skills', 'xiaohongshu-trend-intelligence');
  607. const douyinSkillDir = path.join(WORKSPACE_ROOT, '.claude', 'skills', 'douyin-trend-intelligence');
  608. const issuePoolSkillDir = path.join(WORKSPACE_ROOT, '.claude', 'skills', 'voc-issue-pool');
  609. const deepDiveSkillDir = path.join(WORKSPACE_ROOT, '.claude', 'skills', 'voc-problem-deep-dive');
  610. const contentPlanSkillDir = path.join(WORKSPACE_ROOT, '.claude', 'skills', 'voc-content-plan');
  611. const speakingScriptSkillDir = path.join(WORKSPACE_ROOT, '.claude', 'skills', 'voc-speaking-script');
  612. const competitorMapSkillDir = path.join(WORKSPACE_ROOT, '.claude', 'skills', 'voc-competitor-map');
  613. const businessWorkflowSkillDir = path.join(WORKSPACE_ROOT, '.claude', 'skills', 'voc-business-workflow');
  614. const fmodeImageSkillDir = path.join(WORKSPACE_ROOT, '.claude', 'skills', 'fmode-image-analysis');
  615. const apiCatalogSkillDir = path.join(WORKSPACE_ROOT, '.claude', 'skills', 'voc-api-catalog');
  616. ensureDir(xhsSkillDir);
  617. ensureDir(douyinSkillDir);
  618. ensureDir(issuePoolSkillDir);
  619. ensureDir(deepDiveSkillDir);
  620. ensureDir(contentPlanSkillDir);
  621. ensureDir(speakingScriptSkillDir);
  622. ensureDir(competitorMapSkillDir);
  623. ensureDir(businessWorkflowSkillDir);
  624. ensureDir(fmodeImageSkillDir);
  625. ensureDir(apiCatalogSkillDir);
  626. fs.writeFileSync(path.join(xhsSkillDir, 'SKILL.md'), workspaceSkillText(targetDir), 'utf8');
  627. fs.writeFileSync(path.join(douyinSkillDir, 'SKILL.md'), workspaceDouyinSkillText(targetDir), 'utf8');
  628. fs.writeFileSync(path.join(issuePoolSkillDir, 'SKILL.md'), workspaceIssuePoolSkillText(targetDir), 'utf8');
  629. fs.writeFileSync(path.join(deepDiveSkillDir, 'SKILL.md'), workspaceProblemDeepDiveSkillText(targetDir), 'utf8');
  630. fs.writeFileSync(path.join(contentPlanSkillDir, 'SKILL.md'), workspaceContentPlanSkillText(targetDir), 'utf8');
  631. fs.writeFileSync(path.join(speakingScriptSkillDir, 'SKILL.md'), workspaceSpeakingScriptSkillText(targetDir), 'utf8');
  632. fs.writeFileSync(path.join(competitorMapSkillDir, 'SKILL.md'), workspaceCompetitorMapSkillText(targetDir), 'utf8');
  633. fs.writeFileSync(path.join(businessWorkflowSkillDir, 'SKILL.md'), workspaceBusinessWorkflowSkillText(targetDir), 'utf8');
  634. fs.copyFileSync(path.join(targetDir, 'skills', 'fmode-image-analysis', 'SKILL.md'), path.join(fmodeImageSkillDir, 'SKILL.md'));
  635. fs.copyFileSync(path.join(targetDir, 'skills', 'voc-api-catalog', 'SKILL.md'), path.join(apiCatalogSkillDir, 'SKILL.md'));
  636. }
  637. function writeWorkspaceActivation(targetDir) {
  638. if (!isSafeWorkspaceTarget(targetDir)) return;
  639. writeWorkspaceMcpConfig(targetDir);
  640. writeWorkspaceSkillEntries(targetDir);
  641. console.log('Wrote workspace MCP config: .\\.mcp.json');
  642. console.log('Wrote workspace skill entry: .\\.claude\\skills\\xiaohongshu-trend-intelligence\\SKILL.md');
  643. console.log('Wrote workspace skill entry: .\\.claude\\skills\\douyin-trend-intelligence\\SKILL.md');
  644. console.log('Wrote workspace skill entry: .\\.claude\\skills\\voc-issue-pool\\SKILL.md');
  645. console.log('Wrote workspace skill entry: .\\.claude\\skills\\voc-problem-deep-dive\\SKILL.md');
  646. console.log('Wrote workspace skill entry: .\\.claude\\skills\\voc-content-plan\\SKILL.md');
  647. console.log('Wrote workspace skill entry: .\\.claude\\skills\\voc-speaking-script\\SKILL.md');
  648. console.log('Wrote workspace skill entry: .\\.claude\\skills\\voc-competitor-map\\SKILL.md');
  649. console.log('Wrote workspace skill entry: .\\.claude\\skills\\voc-business-workflow\\SKILL.md');
  650. console.log('Wrote workspace skill entry: .\\.claude\\skills\\fmode-image-analysis\\SKILL.md');
  651. console.log('Wrote workspace skill entry: .\\.claude\\skills\\voc-api-catalog\\SKILL.md');
  652. }
  653. function printNextSteps(targetDir) {
  654. const workspaceMode = isSafeWorkspaceTarget(targetDir);
  655. console.log('');
  656. console.log('Install complete.');
  657. console.log('');
  658. if (workspaceMode) {
  659. console.log('VSCode workspace mode is ready.');
  660. console.log('');
  661. console.log('Generated project files:');
  662. console.log(' .\\.mcp.json');
  663. console.log(' .\\.claude\\skills\\xiaohongshu-trend-intelligence\\SKILL.md');
  664. console.log(' .\\.claude\\skills\\douyin-trend-intelligence\\SKILL.md');
  665. console.log(' .\\.claude\\skills\\voc-issue-pool\\SKILL.md');
  666. console.log(' .\\.claude\\skills\\voc-problem-deep-dive\\SKILL.md');
  667. console.log(' .\\.claude\\skills\\voc-content-plan\\SKILL.md');
  668. console.log(' .\\.claude\\skills\\voc-speaking-script\\SKILL.md');
  669. console.log(' .\\.claude\\skills\\voc-competitor-map\\SKILL.md');
  670. console.log(' .\\.claude\\skills\\voc-business-workflow\\SKILL.md');
  671. console.log(' .\\.claude\\skills\\fmode-image-analysis\\SKILL.md');
  672. console.log(' .\\.claude\\plugins\\voc-intelligence');
  673. console.log('');
  674. console.log('Restart the VSCode Claude Code session if it was already open.');
  675. } else {
  676. console.log('Claude Code CLI launch command:');
  677. console.log(` claude --plugin-dir "${targetDir}"`);
  678. }
  679. console.log('');
  680. console.log('Try this prompt in Claude Code:');
  681. console.log(' 帮我看一下家装全屋定制最近用户在关心什么,并告诉我先改哪里、下周发什么。');
  682. console.log('');
  683. console.log('Or try:');
  684. console.log(' 帮我做一份家装全屋定制行业的小红书趋势情报。');
  685. console.log(' 先用演示样例跑通流程,不要真实采集。');
  686. console.log(' 在聊天里给我第一轮样本观察和待确认问题。');
  687. console.log('');
  688. console.log('Or try:');
  689. console.log(' 帮我做一份家装全屋定制行业的抖音趋势情报。');
  690. console.log(' 先用演示样例跑通流程,不要真实采集。');
  691. console.log(' 在聊天里给我第一轮样本观察和待确认问题。');
  692. console.log('');
  693. console.log(`Install target: ${targetDir}`);
  694. }
  695. function install(opts) {
  696. checkNodeVersion();
  697. if (path.resolve(SOURCE_ROOT) !== path.resolve(opts.target)) {
  698. if (fs.existsSync(opts.target)) {
  699. if (!canOverwriteTarget(opts.target, opts)) {
  700. throw new Error(`Target already exists. Use --force to overwrite custom target: ${opts.target}`);
  701. }
  702. fs.rmSync(opts.target, { recursive: true, force: true });
  703. }
  704. ensureDir(opts.target);
  705. const copied = copyDirRecursive(SOURCE_ROOT, opts.target);
  706. console.log(`Copied skill package files: ${copied}`);
  707. }
  708. assertInstalled(opts.target);
  709. writeMcpConfig(opts.target);
  710. writeWorkspaceActivation(opts.target);
  711. if (!opts.skipInstall) {
  712. console.log('Installing runtime dependencies...');
  713. run('npm', ['install', '--omit=dev', '--ignore-scripts'], opts.target);
  714. }
  715. if (opts.smoke) {
  716. console.log('Running install smoke checks...');
  717. run(process.execPath, ['install.js', '--smoke', '--skip-install', '--no-next'], opts.target);
  718. } else {
  719. run(process.execPath, ['install.js', '--check', '--no-next'], opts.target);
  720. }
  721. printNextSteps(opts.target);
  722. }
  723. function check(opts) {
  724. assertInstalled(opts.target);
  725. run(process.execPath, ['install.js', '--check', '--no-next'], opts.target);
  726. }
  727. function smoke(opts) {
  728. assertInstalled(opts.target);
  729. run(process.execPath, ['install.js', '--smoke', '--skip-install', '--no-next'], opts.target);
  730. }
  731. function printPath(opts) {
  732. console.log(opts.target);
  733. if (isSafeWorkspaceTarget(opts.target)) {
  734. console.log('.\\.claude\\plugins\\voc-intelligence');
  735. } else {
  736. console.log(`claude --plugin-dir "${opts.target}"`);
  737. }
  738. }
  739. function main() {
  740. const directCommand = process.argv[2];
  741. if (directCommand === 'image' || directCommand === 'fmode-image') {
  742. run(process.execPath, ['mcp/src/tools/fmode-image-analysis.js', ...process.argv.slice(3)], SOURCE_ROOT);
  743. return;
  744. }
  745. const opts = parseArgs(process.argv.slice(2));
  746. if (opts.help || opts.command === 'help') {
  747. console.log(usage());
  748. return;
  749. }
  750. if (opts.command === 'install') return install(opts);
  751. if (opts.command === 'check') return check(opts);
  752. if (opts.command === 'smoke') return smoke(opts);
  753. if (opts.command === 'path') return printPath(opts);
  754. throw new Error(`Unknown command: ${opts.command}\n\n${usage()}`);
  755. }
  756. try {
  757. main();
  758. } catch (error) {
  759. console.error('');
  760. console.error(`claude-voc failed: ${error.message}`);
  761. process.exit(1);
  762. }