@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.
- package/.claude-plugin/plugin.json +1 -1
- package/bin/claude-voc.js +251 -33
- package/mcp/src/core/activation.js +104 -0
- package/mcp/src/core/credentials.js +4 -0
- package/package.json +3 -2
- package/scripts/acceptance.js +1 -0
- package/scripts/installer-acceptance.js +79 -0
- package/skill-package-manifest.json +2 -2
|
@@ -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.
|
|
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
|
|
253
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
|
625
|
-
|
|
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(
|
|
838
|
+
console.log(`${opts.channel} ${opts.scope} mode is ready.`);
|
|
631
839
|
console.log('');
|
|
632
840
|
console.log('Generated project files:');
|
|
633
|
-
console.log(
|
|
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
|
-
|
|
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')
|
|
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
|
-
|
|
739
|
-
main()
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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.
|
|
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/",
|
package/scripts/acceptance.js
CHANGED
|
@@ -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.
|
|
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
|
+
}
|