@vocmarket/voc-skill 0.3.29 → 0.3.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "voc-intelligence",
3
3
  "description": "VOC intelligence skills for Claude Code: end-to-end VOC business workflows, Xiaohongshu and Douyin trend reports, live social VOC collection, Fmode Doubao image analysis, token/recharge checks, evidence-card outputs, prioritized issue pools, single-problem deep dives, competitor maps, 7-day content plans, VOC-backed speaking script co-creation, boss/operator action plans, and multi-turn memory.",
4
- "version": "0.3.29",
4
+ "version": "0.3.31",
5
5
  "author": {
6
6
  "name": "nkkj-BrainHack"
7
7
  }
package/bin/claude-voc.js CHANGED
@@ -3,12 +3,18 @@ const fs = require('fs');
3
3
  const os = require('os');
4
4
  const path = require('path');
5
5
  const { spawnSync } = require('child_process');
6
+ const { ensureActivated } = require('../mcp/src/core/activation');
7
+ const { readNewApiToken } = require('../mcp/src/core/credentials');
6
8
 
7
9
  const SOURCE_ROOT = path.resolve(__dirname, '..');
8
10
  const WORKSPACE_ROOT = process.cwd();
9
11
  const DEFAULT_TARGET = path.join(os.homedir(), '.claude', 'plugins', 'voc-intelligence');
10
12
  const WORKSPACE_TARGET = path.join(WORKSPACE_ROOT, '.claude', 'plugins', 'voc-intelligence');
11
13
  const WORKSPACE_PLUGINS_ROOT = path.join(WORKSPACE_ROOT, '.claude', 'plugins');
14
+ const CODEX_PROJECT_TARGET = path.join(WORKSPACE_ROOT, '.codex', 'plugins', 'voc-intelligence');
15
+ const CODEX_GLOBAL_TARGET = path.join(os.homedir(), '.codex', 'plugins', 'voc-intelligence');
16
+ const PORTABLE_WORKSPACE_TARGET = path.join(WORKSPACE_ROOT, '.vocmarket', 'plugins', 'voc-intelligence');
17
+ const VOCMARKET_CREDENTIALS = path.join(os.homedir(), '.vocmarket', 'credentials.json');
12
18
  const MIN_NODE_MAJOR = 18;
13
19
 
14
20
  const EXCLUDED_DIRS = new Set([
@@ -34,6 +40,7 @@ function usage() {
34
40
  'Usage:',
35
41
  ' claude-voc install',
36
42
  ' claude-voc workspace',
43
+ ' claude-voc workspace --activate --channel <name>',
37
44
  ' claude-voc install --workspace --smoke',
38
45
  ' claude-voc check',
39
46
  ' claude-voc smoke',
@@ -43,6 +50,7 @@ function usage() {
43
50
  'npx:',
44
51
  ' npx @vocmarket/voc-skill install',
45
52
  ' npx @vocmarket/voc-skill workspace --smoke',
53
+ ' npx @vocmarket/voc-skill workspace --activate (其他 AI 编程器渠道专用:安装并按状态弹付费)',
46
54
  '',
47
55
  'Options:',
48
56
  ' --workspace Install into ./.claude/plugins/voc-intelligence',
@@ -50,6 +58,9 @@ function usage() {
50
58
  ' --force Allow overwriting an existing custom target',
51
59
  ' --skip-install Skip npm install after copying files',
52
60
  ' --smoke Run sample/preference/MCP smoke checks after install',
61
+ ' --activate 安装后按 token 状态激活:无 token 弹付费/登录页,有 token 直接放行(外部 AI IDE 专用,不影响 --smoke)',
62
+ ' --channel <name> 安装渠道(claude-code/codex/workbuddy/fmode-studio)',
63
+ ' --scope <name> Codex 安装范围(project/global,默认 project)',
53
64
  ' --help, -h Show help'
54
65
  ].join('\n');
55
66
  }
@@ -65,6 +76,11 @@ function parseArgs(argv) {
65
76
  target: DEFAULT_TARGET,
66
77
  skipInstall: false,
67
78
  smoke: false,
79
+ activate: false,
80
+ channel: 'external',
81
+ scope: 'project',
82
+ targetExplicit: false,
83
+ workspaceRequested: first === 'workspace' || first === 'install-workspace',
68
84
  force: false,
69
85
  help: false
70
86
  };
@@ -78,25 +94,67 @@ function parseArgs(argv) {
78
94
  const token = argv[i];
79
95
  if (token === '--target') {
80
96
  opts.target = argv[++i];
97
+ opts.targetExplicit = true;
81
98
  } else if (token.startsWith('--target=')) {
82
99
  opts.target = token.slice('--target='.length);
100
+ opts.targetExplicit = true;
83
101
  } else if (token === '--workspace') {
84
102
  opts.target = WORKSPACE_TARGET;
103
+ opts.workspaceRequested = true;
85
104
  } else if (token === '--force') {
86
105
  opts.force = true;
87
106
  } else if (token === '--skip-install') {
88
107
  opts.skipInstall = true;
89
108
  } else if (token === '--smoke') {
90
109
  opts.smoke = true;
110
+ } else if (token === '--activate') {
111
+ opts.activate = true;
112
+ } else if (token === '--channel') {
113
+ opts.channel = argv[++i];
114
+ } else if (token.startsWith('--channel=')) {
115
+ opts.channel = token.slice('--channel='.length);
116
+ } else if (token === '--scope') {
117
+ opts.scope = argv[++i];
118
+ } else if (token.startsWith('--scope=')) {
119
+ opts.scope = token.slice('--scope='.length);
91
120
  } else if (token === '--help' || token === '-h') {
92
121
  opts.help = true;
93
122
  }
94
123
  }
95
124
 
125
+ opts.channel = normalizeInstallChannel(opts.channel);
126
+ opts.scope = normalizeInstallScope(opts.channel, opts.scope);
127
+ if (opts.workspaceRequested && !opts.targetExplicit) {
128
+ opts.target = defaultTargetFor(opts.channel, opts.scope);
129
+ }
96
130
  opts.target = path.resolve(expandHome(opts.target));
97
131
  return opts;
98
132
  }
99
133
 
134
+ function normalizeInstallChannel(value) {
135
+ const channel = String(value || 'claude-code').trim().toLowerCase();
136
+ if (channel === 'external') return 'claude-code';
137
+ if (!['claude-code', 'codex', 'workbuddy', 'fmode-studio'].includes(channel)) {
138
+ throw new Error(`Unsupported install channel: ${channel}`);
139
+ }
140
+ return channel;
141
+ }
142
+
143
+ function normalizeInstallScope(channel, value) {
144
+ if (channel !== 'codex') return 'project';
145
+ const scope = String(value || 'project').trim().toLowerCase();
146
+ if (!['project', 'global'].includes(scope)) {
147
+ throw new Error(`Unsupported Codex install scope: ${scope}`);
148
+ }
149
+ return scope;
150
+ }
151
+
152
+ function defaultTargetFor(channel, scope) {
153
+ if (channel === 'codex') return scope === 'global' ? CODEX_GLOBAL_TARGET : CODEX_PROJECT_TARGET;
154
+ if (channel === 'workbuddy') return PORTABLE_WORKSPACE_TARGET;
155
+ return WORKSPACE_TARGET;
156
+ }
157
+
100
158
  function run(command, args, cwd) {
101
159
  const useCmd = process.platform === 'win32' && command === 'npm';
102
160
  const executable = useCmd ? 'cmd.exe' : command;
@@ -171,8 +229,13 @@ function isSafeWorkspaceTarget(targetDir) {
171
229
  return isInside(WORKSPACE_PLUGINS_ROOT, targetDir);
172
230
  }
173
231
 
232
+ function isSafeManagedTarget(targetDir) {
233
+ return [WORKSPACE_TARGET, CODEX_PROJECT_TARGET, CODEX_GLOBAL_TARGET, PORTABLE_WORKSPACE_TARGET]
234
+ .some(candidate => path.resolve(candidate) === path.resolve(targetDir));
235
+ }
236
+
174
237
  function canOverwriteTarget(targetDir, opts) {
175
- return opts.force || isSafeDefaultTarget(targetDir) || isSafeWorkspaceTarget(targetDir);
238
+ return opts.force || isSafeDefaultTarget(targetDir) || isSafeWorkspaceTarget(targetDir) || isSafeManagedTarget(targetDir);
176
239
  }
177
240
 
178
241
  function shouldCopyEntry(entry) {
@@ -249,9 +312,8 @@ function readJsonIfExists(filePath) {
249
312
  return JSON.parse(fs.readFileSync(filePath, 'utf8'));
250
313
  }
251
314
 
252
- function writeWorkspaceMcpConfig(targetDir) {
253
- const mcpConfigPath = path.join(WORKSPACE_ROOT, '.mcp.json');
254
- const existing = readJsonIfExists(mcpConfigPath);
315
+ function writeJsonMcpConfig(configPath, targetDir) {
316
+ const existing = readJsonIfExists(configPath);
255
317
  const mcpConfig = {
256
318
  ...existing,
257
319
  mcpServers: {
@@ -263,7 +325,99 @@ function writeWorkspaceMcpConfig(targetDir) {
263
325
  }
264
326
  }
265
327
  };
266
- fs.writeFileSync(mcpConfigPath, `${JSON.stringify(mcpConfig, null, 2)}\n`, 'utf8');
328
+ ensureDir(path.dirname(configPath));
329
+ fs.writeFileSync(configPath, `${JSON.stringify(mcpConfig, null, 2)}\n`, 'utf8');
330
+ }
331
+
332
+ function writeWorkspaceMcpConfig(targetDir) {
333
+ const mcpConfigPath = path.join(WORKSPACE_ROOT, '.mcp.json');
334
+ writeJsonMcpConfig(mcpConfigPath, targetDir);
335
+ }
336
+
337
+ function tomlString(value) {
338
+ return JSON.stringify(String(value));
339
+ }
340
+
341
+ function upsertCodexProjectMcp(targetDir, workspaceRoot = WORKSPACE_ROOT) {
342
+ const configPath = path.join(workspaceRoot, '.codex', 'config.toml');
343
+ const original = fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf8') : '';
344
+ const lines = original.replace(/\r\n/g, '\n').split('\n');
345
+ const kept = [];
346
+ let skipping = false;
347
+ for (const line of lines) {
348
+ const table = line.match(/^\s*\[([^\]]+)]\s*(?:#.*)?$/);
349
+ if (table) {
350
+ skipping = table[1] === 'mcp_servers.voc' || table[1].startsWith('mcp_servers.voc.');
351
+ }
352
+ if (!skipping) kept.push(line);
353
+ }
354
+ while (kept.length && !kept[kept.length - 1].trim()) kept.pop();
355
+ const serverPath = path.join(targetDir, 'mcp', 'src', 'server.js');
356
+ const block = [
357
+ '[mcp_servers.voc]',
358
+ 'command = "node"',
359
+ `args = [${tomlString(serverPath)}]`,
360
+ `cwd = ${tomlString(targetDir)}`
361
+ ];
362
+ ensureDir(path.dirname(configPath));
363
+ fs.writeFileSync(configPath, `${kept.concat(kept.length ? [''] : [], block).join('\n')}\n`, 'utf8');
364
+ return configPath;
365
+ }
366
+
367
+ function runCapture(command, args, cwd = WORKSPACE_ROOT) {
368
+ const useCmd = process.platform === 'win32';
369
+ const executable = useCmd ? 'cmd.exe' : command;
370
+ const finalArgs = useCmd ? ['/d', '/s', '/c', command, ...args] : args;
371
+ return spawnSync(executable, finalArgs, {
372
+ cwd,
373
+ encoding: 'utf8',
374
+ stdio: ['ignore', 'pipe', 'pipe'],
375
+ maxBuffer: 1024 * 1024 * 10
376
+ });
377
+ }
378
+
379
+ function registerCodexGlobalMcp(targetDir) {
380
+ const getResult = runCapture('codex', ['mcp', 'get', 'voc']);
381
+ if (!getResult.error && getResult.status === 0) {
382
+ const removeResult = runCapture('codex', ['mcp', 'remove', 'voc']);
383
+ if (removeResult.error || removeResult.status !== 0) {
384
+ throw new Error('Unable to update the existing Codex VOC MCP registration');
385
+ }
386
+ }
387
+ const serverPath = path.join(targetDir, 'mcp', 'src', 'server.js');
388
+ const addResult = runCapture('codex', ['mcp', 'add', 'voc', '--', 'node', serverPath]);
389
+ if (addResult.error || addResult.status !== 0) {
390
+ const detail = String(addResult.stderr || addResult.error?.message || '').trim();
391
+ throw new Error(`Codex MCP registration failed${detail ? `: ${detail}` : ''}`);
392
+ }
393
+ }
394
+
395
+ function copyPackagedSkills(targetDir, destinationRoot) {
396
+ const sourceRoot = path.join(targetDir, 'skills');
397
+ ensureDir(destinationRoot);
398
+ let copied = 0;
399
+ for (const entry of fs.readdirSync(sourceRoot, { withFileTypes: true })) {
400
+ if (!entry.isDirectory()) continue;
401
+ const destination = path.join(destinationRoot, entry.name);
402
+ removeDirResilient(destination);
403
+ copied += copyDirRecursive(path.join(sourceRoot, entry.name), destination);
404
+ }
405
+ return copied;
406
+ }
407
+
408
+ function persistNeutralCredential(credentialsPath = VOCMARKET_CREDENTIALS) {
409
+ const token = readNewApiToken();
410
+ if (!token) return false;
411
+ const existing = readJsonIfExists(credentialsPath);
412
+ ensureDir(path.dirname(credentialsPath));
413
+ fs.writeFileSync(credentialsPath, `${JSON.stringify({ ...existing, fmodeApiKey: token }, null, 2)}\n`, {
414
+ encoding: 'utf8',
415
+ mode: 0o600
416
+ });
417
+ try {
418
+ fs.chmodSync(credentialsPath, 0o600);
419
+ } catch {}
420
+ return true;
267
421
  }
268
422
 
269
423
  function asForwardSlash(filePath) {
@@ -604,11 +758,10 @@ function writeWorkspaceSkillEntries(targetDir) {
604
758
  fs.copyFileSync(path.join(targetDir, 'skills', 'voc-api-catalog', 'SKILL.md'), path.join(apiCatalogSkillDir, 'SKILL.md'));
605
759
  }
606
760
 
607
- function writeWorkspaceActivation(targetDir) {
608
- if (!isSafeWorkspaceTarget(targetDir)) return;
761
+ function writeClaudeWorkspaceActivation(targetDir) {
609
762
  writeWorkspaceMcpConfig(targetDir);
610
763
  writeWorkspaceSkillEntries(targetDir);
611
- console.log('Wrote workspace MCP config: .\\.mcp.json');
764
+ console.log('Wrote Claude/Fmode workspace MCP config: .\\.mcp.json');
612
765
  console.log('Wrote workspace skill entry: .\\.claude\\skills\\xiaohongshu-trend-intelligence\\SKILL.md');
613
766
  console.log('Wrote workspace skill entry: .\\.claude\\skills\\douyin-trend-intelligence\\SKILL.md');
614
767
  console.log('Wrote workspace skill entry: .\\.claude\\skills\\voc-issue-pool\\SKILL.md');
@@ -621,26 +774,71 @@ function writeWorkspaceActivation(targetDir) {
621
774
  console.log('Wrote workspace skill entry: .\\.claude\\skills\\voc-api-catalog\\SKILL.md');
622
775
  }
623
776
 
624
- function printNextSteps(targetDir) {
625
- const workspaceMode = isSafeWorkspaceTarget(targetDir);
777
+ function writeWorkspaceActivation(targetDir, opts) {
778
+ if (!opts.workspaceRequested) return;
779
+ if (opts.channel === 'codex') {
780
+ const skillRoot = opts.scope === 'global'
781
+ ? path.join(os.homedir(), '.codex', 'skills')
782
+ : path.join(WORKSPACE_ROOT, '.codex', 'skills');
783
+ copyPackagedSkills(targetDir, skillRoot);
784
+ if (opts.scope === 'global') {
785
+ registerCodexGlobalMcp(targetDir);
786
+ console.log('Registered VOC MCP with Codex user config.');
787
+ console.log(`Installed Codex user skills: ${skillRoot}`);
788
+ } else {
789
+ const configPath = upsertCodexProjectMcp(targetDir);
790
+ console.log(`Registered VOC MCP with Codex project config: ${configPath}`);
791
+ console.log(`Installed Codex project skills: ${skillRoot}`);
792
+ }
793
+ return;
794
+ }
795
+ if (opts.channel === 'workbuddy') {
796
+ writeWorkspaceMcpConfig(targetDir);
797
+ const skillRoot = path.join(WORKSPACE_ROOT, '.workbuddy', 'skills');
798
+ copyPackagedSkills(targetDir, skillRoot);
799
+ console.log('Wrote portable WorkBuddy MCP registration: .\\.mcp.json');
800
+ console.log('Installed WorkBuddy workspace skills: .\\.workbuddy\\skills');
801
+ return;
802
+ }
803
+ writeClaudeWorkspaceActivation(targetDir);
804
+ }
805
+
806
+ async function verifyVocToolDiscovery(targetDir) {
807
+ const { Client } = require('@modelcontextprotocol/sdk/client/index.js');
808
+ const { StdioClientTransport } = require('@modelcontextprotocol/sdk/client/stdio.js');
809
+ const { version } = require('../package.json');
810
+ const client = new Client({ name: 'voc-installer-verification', version });
811
+ const transport = new StdioClientTransport({
812
+ command: process.execPath,
813
+ args: [path.join(targetDir, 'mcp', 'src', 'server.js')],
814
+ cwd: targetDir,
815
+ stderr: 'pipe'
816
+ });
817
+ await client.connect(transport);
818
+ try {
819
+ const result = await client.listTools();
820
+ const names = (result.tools || []).map(tool => tool.name);
821
+ const discovered = names.filter(name => name.startsWith('voc_api_'));
822
+ if (!discovered.length) {
823
+ throw new Error('MCP started, but no voc_api_* tool was discovered');
824
+ }
825
+ console.log(`Verified MCP tool discovery (no data request): ${discovered.join(', ')}`);
826
+ return discovered;
827
+ } finally {
828
+ await client.close();
829
+ }
830
+ }
831
+
832
+ function printNextSteps(targetDir, opts) {
833
+ const workspaceMode = opts.workspaceRequested;
626
834
  console.log('');
627
835
  console.log('Install complete.');
628
836
  console.log('');
629
837
  if (workspaceMode) {
630
- console.log('VSCode workspace mode is ready.');
838
+ console.log(`${opts.channel} ${opts.scope} mode is ready.`);
631
839
  console.log('');
632
840
  console.log('Generated project files:');
633
- console.log(' .\\.mcp.json');
634
- console.log(' .\\.claude\\skills\\xiaohongshu-trend-intelligence\\SKILL.md');
635
- console.log(' .\\.claude\\skills\\douyin-trend-intelligence\\SKILL.md');
636
- console.log(' .\\.claude\\skills\\voc-issue-pool\\SKILL.md');
637
- console.log(' .\\.claude\\skills\\voc-problem-deep-dive\\SKILL.md');
638
- console.log(' .\\.claude\\skills\\voc-content-plan\\SKILL.md');
639
- console.log(' .\\.claude\\skills\\voc-speaking-script\\SKILL.md');
640
- console.log(' .\\.claude\\skills\\voc-competitor-map\\SKILL.md');
641
- console.log(' .\\.claude\\skills\\voc-business-workflow\\SKILL.md');
642
- console.log(' .\\.claude\\skills\\fmode-image-analysis\\SKILL.md');
643
- console.log(' .\\.claude\\plugins\\voc-intelligence');
841
+ console.log(` ${targetDir}`);
644
842
  console.log('');
645
843
  console.log('Restart the VSCode Claude Code session if it was already open.');
646
844
  } else {
@@ -664,7 +862,7 @@ function printNextSteps(targetDir) {
664
862
  console.log(`Install target: ${targetDir}`);
665
863
  }
666
864
 
667
- function install(opts) {
865
+ async function install(opts) {
668
866
  checkNodeVersion();
669
867
  if (path.resolve(SOURCE_ROOT) !== path.resolve(opts.target)) {
670
868
  if (fs.existsSync(opts.target)) {
@@ -680,7 +878,7 @@ function install(opts) {
680
878
 
681
879
  assertInstalled(opts.target);
682
880
  writeMcpConfig(opts.target);
683
- writeWorkspaceActivation(opts.target);
881
+ writeWorkspaceActivation(opts.target, opts);
684
882
 
685
883
  if (!opts.skipInstall) {
686
884
  console.log('Installing runtime dependencies...');
@@ -694,7 +892,11 @@ function install(opts) {
694
892
  run(process.execPath, ['install.js', '--check', '--no-next'], opts.target);
695
893
  }
696
894
 
697
- printNextSteps(opts.target);
895
+ if (opts.activate) {
896
+ persistNeutralCredential();
897
+ await verifyVocToolDiscovery(opts.target);
898
+ }
899
+ printNextSteps(opts.target, opts);
698
900
  }
699
901
 
700
902
  function check(opts) {
@@ -716,7 +918,7 @@ function printPath(opts) {
716
918
  }
717
919
  }
718
920
 
719
- function main() {
921
+ async function main() {
720
922
  const directCommand = process.argv[2];
721
923
  if (directCommand === 'image' || directCommand === 'fmode-image') {
722
924
  run(process.execPath, ['mcp/src/tools/fmode-image-analysis.js', ...process.argv.slice(3)], SOURCE_ROOT);
@@ -728,17 +930,33 @@ function main() {
728
930
  console.log(usage());
729
931
  return;
730
932
  }
731
- if (opts.command === 'install') return install(opts);
933
+ if (opts.command === 'install') {
934
+ await install(opts);
935
+ if (opts.activate) {
936
+ await ensureActivated({ channel: opts.channel });
937
+ }
938
+ return;
939
+ }
732
940
  if (opts.command === 'check') return check(opts);
733
941
  if (opts.command === 'smoke') return smoke(opts);
734
942
  if (opts.command === 'path') return printPath(opts);
735
943
  throw new Error(`Unknown command: ${opts.command}\n\n${usage()}`);
736
944
  }
737
945
 
738
- try {
739
- main();
740
- } catch (error) {
741
- console.error('');
742
- console.error(`claude-voc failed: ${error.message}`);
743
- process.exit(1);
946
+ if (require.main === module) {
947
+ main().catch((error) => {
948
+ console.error('');
949
+ console.error(`claude-voc failed: ${error.message}`);
950
+ process.exit(1);
951
+ });
744
952
  }
953
+
954
+ module.exports = {
955
+ parseArgs,
956
+ normalizeInstallChannel,
957
+ normalizeInstallScope,
958
+ defaultTargetFor,
959
+ upsertCodexProjectMcp,
960
+ verifyVocToolDiscovery,
961
+ persistNeutralCredential
962
+ };
@@ -0,0 +1,104 @@
1
+ 'use strict';
2
+
3
+ // 安装收尾激活检查(仅供外部 AI IDE 渠道的 `workspace --activate` 调用)。
4
+ //
5
+ // 设计要点(与 voc-api-catalog 计费链一致,复用同一套凭证解析,绝不另建路径):
6
+ // - 复用 credentials.js 的 readNewApiToken()(sk- 主链)/ readVocToken()(r: 会话回退)。
7
+ // - 按「是否已有可用 token」这一状态触发,而不是按命令触发:
8
+ // * 已检测到 token(飞马 UI 已写入,或 Claude Code 已配 sk-)→ 打印「✅ 已激活」,不弹浏览器。
9
+ // * 完全没有 token(现场新教练)→ 安装收尾立刻打开充值/登录页,避免进入取数流程后才弹、空耗 token。
10
+ // - 不主动发计费请求做探测(探测会真扣费);余额不足留给真实取数时由 catalog 的 402 分支处理。
11
+ // - VOC_OPEN_PAYMENT=0 可强制关闭弹窗(CI / 冒烟 / 无头环境)。
12
+ // - 始终把 URL 打印到终端兜底;并追加 &channel=&src=install 做渠道归因。
13
+ // - token 绝不回显到终端 / 日志。
14
+
15
+ const { spawn } = require('child_process');
16
+ const { readNewApiToken, readVocToken } = require('./credentials');
17
+ const { buildVocRechargeInfo } = require('./payment-links');
18
+
19
+ function appendAttribution(rawUrl, channel, src) {
20
+ try {
21
+ const url = new URL(rawUrl);
22
+ if (channel) url.searchParams.set('channel', String(channel));
23
+ if (src) url.searchParams.set('src', String(src));
24
+ return url.toString();
25
+ } catch {
26
+ return rawUrl;
27
+ }
28
+ }
29
+
30
+ function openBrowser(url) {
31
+ try {
32
+ let command;
33
+ let args;
34
+ if (process.platform === 'win32') {
35
+ command = 'cmd';
36
+ // 空字符串是 start 的窗口标题占位,避免带引号的 URL 被当成标题
37
+ args = ['/d', '/s', '/c', 'start', '', url];
38
+ } else if (process.platform === 'darwin') {
39
+ command = 'open';
40
+ args = [url];
41
+ } else {
42
+ command = 'xdg-open';
43
+ args = [url];
44
+ }
45
+ const child = spawn(command, args, { stdio: 'ignore', detached: true });
46
+ child.on('error', () => {});
47
+ child.unref();
48
+ return true;
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+
54
+ // channel: 渠道名(claude-code / codex / workbuddy),用于付费归因
55
+ // src: 来源标记,默认 install
56
+ async function ensureActivated({ channel = 'external', src = 'install' } = {}) {
57
+ if (process.env.VOC_OPEN_PAYMENT === '0') {
58
+ console.log('');
59
+ console.log('VOC_OPEN_PAYMENT=0:已跳过激活弹窗(CI/无头环境)。');
60
+ return { status: 'skipped' };
61
+ }
62
+
63
+ const newApiToken = readNewApiToken();
64
+ const vocToken = readVocToken();
65
+
66
+ if (newApiToken || vocToken) {
67
+ console.log('');
68
+ console.log('✅ 已激活:检测到 VOC 计费/会话 token,可直接开始取数,无需充值。');
69
+ console.log('(若取数时返回 402 余额不足,再按工具提示充值即可。)');
70
+ return { status: 'activated' };
71
+ }
72
+
73
+ // 完全没有 token:现场新用户。打开充值/登录页,引导先付费再进流程。
74
+ // 无 token 时 buildVocRechargeInfo 不会发起网络请求(resolveVocUserId 对空 token 直接返回)。
75
+ let recharge;
76
+ try {
77
+ recharge = await buildVocRechargeInfo({ token: '' });
78
+ } catch {
79
+ recharge = {};
80
+ }
81
+ const primaryUrl = appendAttribution(
82
+ recharge.recommendedPackageUrl || recharge.paymentUrl || 'https://app.fmode.cn/dev/apig-pay/',
83
+ channel,
84
+ src
85
+ );
86
+
87
+ console.log('');
88
+ console.log('⚠️ 尚未检测到 VOC token —— 你还没有激活。');
89
+ console.log('为避免进入取数流程后才发现需要付费、白白消耗 token,请先在浏览器完成登录并充值/开通:');
90
+ console.log(` ${primaryUrl}`);
91
+ if (recharge.paymentUrl) {
92
+ console.log(` (充值入口:${appendAttribution(recharge.paymentUrl, channel, src)})`);
93
+ }
94
+ console.log('完成付费后,再让 AI 进入取数/报告流程。');
95
+ console.log('如本机无法自动打开浏览器,请手动复制上面的链接到浏览器打开。');
96
+
97
+ const opened = openBrowser(primaryUrl);
98
+ if (!opened) {
99
+ console.log('(自动打开浏览器失败,请手动打开上面的链接。)');
100
+ }
101
+ return { status: 'needs_activation', url: primaryUrl };
102
+ }
103
+
104
+ module.exports = { ensureActivated, openBrowser, appendAttribution };
@@ -149,6 +149,7 @@ function readXiaohongshuToken(input = {}) {
149
149
  // → Claude Code env(进程注入 + ~/.claude/settings.json)的 ANTHROPIC_AUTH_TOKEN(sk-、且 base 指向 fmode)。
150
150
  function readNewApiToken(input = {}) {
151
151
  const workspaceEnv = readEnvLocalFiles();
152
+ const vocMarketCreds = readJsonMaybe(path.join(os.homedir(), '.vocmarket', 'credentials.json'));
152
153
  const claudeCreds = readJsonMaybe(path.join(os.homedir(), '.claude', 'voc-credentials.json'));
153
154
  const fmodeConfig = readFmodeConfig();
154
155
  const claudeEnv = readClaudeSettingsEnv();
@@ -165,6 +166,9 @@ function readNewApiToken(input = {}) {
165
166
  process.env.NEW_API_TOKEN,
166
167
  process.env.FMODE_API_KEY,
167
168
  process.env.FMODE_API_TOKEN,
169
+ vocMarketCreds.fmodeApiKey,
170
+ vocMarketCreds.fmodeApiToken,
171
+ vocMarketCreds.newApiToken,
168
172
  claudeCreds.fmodeApiKey,
169
173
  claudeCreds.fmodeApiToken,
170
174
  fmodeConfig.newapiToken,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vocmarket/voc-skill",
3
- "version": "0.3.29",
3
+ "version": "0.3.31",
4
4
  "description": "Claude Code VOC intelligence skills for Xiaohongshu and Douyin trend reports, live social VOC collection, Fmode image analysis, single-issue deep dives, boss/operator action plans, and multi-turn memory.",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -29,7 +29,8 @@
29
29
  "voc:business-workflow": "node mcp/src/tools/voc-business-workflow-run.js --brand 示例品牌A --industry 家居定制 --platform douyin --collection-mode sample --keywords \"家居定制怎么选,第一次买怎么对比,售后\" --assistant-message-only",
30
30
  "mcp:start": "node mcp/src/server.js",
31
31
  "mcp:smoke": "node scripts/smoke-mcp.js",
32
- "acceptance": "node scripts/acceptance.js"
32
+ "acceptance": "node scripts/acceptance.js",
33
+ "acceptance:installer": "node scripts/installer-acceptance.js"
33
34
  },
34
35
  "files": [
35
36
  ".claude-plugin/",
@@ -13,6 +13,7 @@ function main() {
13
13
  run('npm', ['run', 'fmode:image:smoke'], ROOT);
14
14
  run('npm', ['run', 'smoke:package'], ROOT);
15
15
  run('npm', ['run', 'mcp:smoke'], ROOT);
16
+ run('npm', ['run', 'acceptance:installer'], ROOT);
16
17
  run('npm', ['pack', '--dry-run'], ROOT);
17
18
 
18
19
  const workspace = fs.mkdtempSync(path.join(os.tmpdir(), 'voc-workspace-install-'));
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const assert = require('assert');
5
+ const fs = require('fs');
6
+ const os = require('os');
7
+ const path = require('path');
8
+ const {
9
+ parseArgs,
10
+ normalizeInstallChannel,
11
+ normalizeInstallScope,
12
+ upsertCodexProjectMcp,
13
+ verifyVocToolDiscovery,
14
+ persistNeutralCredential
15
+ } = require('../bin/claude-voc');
16
+
17
+ async function main() {
18
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'voc-installer-acceptance-'));
19
+ const targetDir = path.join(tempRoot, '.codex', 'plugins', 'voc-intelligence');
20
+ const configDir = path.join(tempRoot, '.codex');
21
+ const configPath = path.join(configDir, 'config.toml');
22
+ const credentialsPath = path.join(tempRoot, '.vocmarket', 'credentials.json');
23
+ const previousToken = process.env.FMODE_API_KEY;
24
+
25
+ try {
26
+ assert.strictEqual(normalizeInstallChannel('CODEX'), 'codex');
27
+ assert.strictEqual(normalizeInstallScope('codex', 'global'), 'global');
28
+ assert.strictEqual(normalizeInstallScope('workbuddy', 'global'), 'project');
29
+
30
+ const options = parseArgs(['workspace', '--activate', '--channel=codex', '--scope=global']);
31
+ assert.strictEqual(options.activate, true);
32
+ assert.strictEqual(options.smoke, false);
33
+ assert.strictEqual(options.channel, 'codex');
34
+ assert.strictEqual(options.scope, 'global');
35
+
36
+ fs.mkdirSync(configDir, { recursive: true });
37
+ fs.writeFileSync(configPath, [
38
+ 'model = "gpt-test"',
39
+ '',
40
+ '[mcp_servers.existing]',
41
+ 'command = "existing-command"',
42
+ '',
43
+ '[mcp_servers.voc]',
44
+ 'command = "old-command"',
45
+ ''
46
+ ].join('\n'));
47
+ upsertCodexProjectMcp(targetDir, tempRoot);
48
+ const config = fs.readFileSync(configPath, 'utf8');
49
+ assert.match(config, /model = "gpt-test"/);
50
+ assert.match(config, /\[mcp_servers\.existing]/);
51
+ assert.strictEqual((config.match(/\[mcp_servers\.voc]/g) || []).length, 1);
52
+ assert.ok(config.includes('mcp\\\\src\\\\server.js') || config.includes('mcp/src/server.js'));
53
+
54
+ process.env.FMODE_API_KEY = 'sk-installer_acceptance-123456789';
55
+ assert.strictEqual(persistNeutralCredential(credentialsPath), true);
56
+ const credentials = JSON.parse(fs.readFileSync(credentialsPath, 'utf8'));
57
+ assert.strictEqual(credentials.fmodeApiKey, process.env.FMODE_API_KEY);
58
+
59
+ const tools = await verifyVocToolDiscovery(path.resolve(__dirname, '..'));
60
+ assert.ok(tools.includes('voc_api_search'));
61
+ assert.ok(tools.includes('voc_api_doc'));
62
+ assert.ok(tools.includes('voc_api_call'));
63
+
64
+ console.log(JSON.stringify({
65
+ status: 'ok',
66
+ checks: ['channels', 'no-smoke activation', 'Codex TOML merge', 'neutral credentials', 'MCP ListTools'],
67
+ discoveredTools: tools
68
+ }, null, 2));
69
+ } finally {
70
+ if (previousToken === undefined) delete process.env.FMODE_API_KEY;
71
+ else process.env.FMODE_API_KEY = previousToken;
72
+ fs.rmSync(tempRoot, { recursive: true, force: true });
73
+ }
74
+ }
75
+
76
+ main().catch(error => {
77
+ console.error(error.message);
78
+ process.exit(1);
79
+ });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-voc-intelligence",
3
- "version": "0.3.29",
3
+ "version": "0.3.31",
4
4
  "capabilities": {
5
5
  "summary": "三类情报源同源接入:①社媒内容 ②国内电商+创作者情报 ③海外选品。统一 search→doc→call、鉴权计费一致。",
6
6
  "channels": [
@@ -123,4 +123,4 @@
123
123
  "workspaceInstallCommand": "npx @vocmarket/voc-skill workspace --smoke",
124
124
  "workspaceInstallTarget": ".claude/plugins/voc-intelligence",
125
125
  "installHint": "Supports three delivery modes: unzip and run node install.js, install globally into ~/.claude/plugins/voc-intelligence, or install into the current workspace. Workspace mode writes .mcp.json plus .claude/skills/* entries for trend intelligence, issue pool, deep dive, competitor map, content plan, speaking script, VOC business workflow, and Fmode image analysis so VSCode Claude Code can discover the workflows without relying only on --plugin-dir."
126
- }
126
+ }