@vocmarket/voc-skill 0.3.28 → 0.3.30

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.28",
4
+ "version": "0.3.30",
5
5
  "author": {
6
6
  "name": "nkkj-BrainHack"
7
7
  }
package/bin/claude-voc.js CHANGED
@@ -3,6 +3,7 @@ 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');
6
7
 
7
8
  const SOURCE_ROOT = path.resolve(__dirname, '..');
8
9
  const WORKSPACE_ROOT = process.cwd();
@@ -34,6 +35,7 @@ function usage() {
34
35
  'Usage:',
35
36
  ' claude-voc install',
36
37
  ' claude-voc workspace',
38
+ ' claude-voc workspace --activate',
37
39
  ' claude-voc install --workspace --smoke',
38
40
  ' claude-voc check',
39
41
  ' claude-voc smoke',
@@ -43,6 +45,7 @@ function usage() {
43
45
  'npx:',
44
46
  ' npx @vocmarket/voc-skill install',
45
47
  ' npx @vocmarket/voc-skill workspace --smoke',
48
+ ' npx @vocmarket/voc-skill workspace --activate (其他 AI 编程器渠道专用:安装并按状态弹付费)',
46
49
  '',
47
50
  'Options:',
48
51
  ' --workspace Install into ./.claude/plugins/voc-intelligence',
@@ -50,6 +53,8 @@ function usage() {
50
53
  ' --force Allow overwriting an existing custom target',
51
54
  ' --skip-install Skip npm install after copying files',
52
55
  ' --smoke Run sample/preference/MCP smoke checks after install',
56
+ ' --activate 安装后按 token 状态激活:无 token 弹付费/登录页,有 token 直接放行(外部 AI IDE 专用,不影响 --smoke)',
57
+ ' --channel <name> 付费归因渠道名(claude-code/codex/workbuddy)',
53
58
  ' --help, -h Show help'
54
59
  ].join('\n');
55
60
  }
@@ -65,6 +70,8 @@ function parseArgs(argv) {
65
70
  target: DEFAULT_TARGET,
66
71
  skipInstall: false,
67
72
  smoke: false,
73
+ activate: false,
74
+ channel: 'external',
68
75
  force: false,
69
76
  help: false
70
77
  };
@@ -88,6 +95,12 @@ function parseArgs(argv) {
88
95
  opts.skipInstall = true;
89
96
  } else if (token === '--smoke') {
90
97
  opts.smoke = true;
98
+ } else if (token === '--activate') {
99
+ opts.activate = true;
100
+ } else if (token === '--channel') {
101
+ opts.channel = argv[++i];
102
+ } else if (token.startsWith('--channel=')) {
103
+ opts.channel = token.slice('--channel='.length);
91
104
  } else if (token === '--help' || token === '-h') {
92
105
  opts.help = true;
93
106
  }
@@ -716,7 +729,7 @@ function printPath(opts) {
716
729
  }
717
730
  }
718
731
 
719
- function main() {
732
+ async function main() {
720
733
  const directCommand = process.argv[2];
721
734
  if (directCommand === 'image' || directCommand === 'fmode-image') {
722
735
  run(process.execPath, ['mcp/src/tools/fmode-image-analysis.js', ...process.argv.slice(3)], SOURCE_ROOT);
@@ -728,17 +741,23 @@ function main() {
728
741
  console.log(usage());
729
742
  return;
730
743
  }
731
- if (opts.command === 'install') return install(opts);
744
+ if (opts.command === 'install') {
745
+ install(opts);
746
+ if (opts.activate) {
747
+ await ensureActivated({ channel: opts.channel });
748
+ }
749
+ return;
750
+ }
732
751
  if (opts.command === 'check') return check(opts);
733
752
  if (opts.command === 'smoke') return smoke(opts);
734
753
  if (opts.command === 'path') return printPath(opts);
735
754
  throw new Error(`Unknown command: ${opts.command}\n\n${usage()}`);
736
755
  }
737
756
 
738
- try {
739
- main();
740
- } catch (error) {
757
+ main().catch((error) => {
741
758
  console.error('');
742
759
  console.error(`claude-voc failed: ${error.message}`);
743
760
  process.exit(1);
744
- }
761
+ });
762
+
763
+
@@ -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 };
@@ -0,0 +1,211 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Self-update detection for the locally-installed VOC skill package.
5
+ *
6
+ * Surfaces a one-line "new version available" hint inside tool results so that
7
+ * a user running the skill in Claude Code is told, while they work, that an
8
+ * update exists. Network access is best-effort: it is throttled (24h cache),
9
+ * fail-silent (never throws, never blocks a tool call), and can be disabled.
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const os = require('os');
15
+ const https = require('https');
16
+
17
+ const PKG = '@vocmarket/voc-skill';
18
+ const REGISTRY_URL = `https://registry.npmjs.org/${PKG.replace('/', '%2F')}/latest`;
19
+ const TTL_MS = 24 * 60 * 60 * 1000; // check npm at most once per day
20
+ const FETCH_TIMEOUT_MS = 1500;
21
+ const CACHE_FILE = path.join(os.homedir(), '.claude', 'voc-update-check.json');
22
+
23
+ let sessionNotified = false; // notify at most once per server process (session)
24
+ let latestInMemory = null; // populated by primeUpdateCheck()
25
+
26
+ function isDisabled() {
27
+ return process.env.VOC_SKILL_NO_UPDATE_CHECK === '1';
28
+ }
29
+
30
+ function getInstalledVersion() {
31
+ // version-check.js lives at <pkg>/mcp/src/core/, package.json at <pkg>/.
32
+ const candidates = [
33
+ path.join(__dirname, '..', '..', '..', 'package.json'),
34
+ path.join(__dirname, '..', '..', '..', 'skill-package-manifest.json')
35
+ ];
36
+ for (const file of candidates) {
37
+ try {
38
+ const json = JSON.parse(fs.readFileSync(file, 'utf8'));
39
+ if (json && typeof json.version === 'string') return json.version;
40
+ } catch (_) {
41
+ /* ignore and try next */
42
+ }
43
+ }
44
+ return null;
45
+ }
46
+
47
+ // Compare two semver strings. Returns 1 if a>b, -1 if a<b, 0 if equal.
48
+ // Handles `major.minor.patch[-prerelease]`; a release outranks its prerelease.
49
+ function compareSemver(a, b) {
50
+ const parse = v => {
51
+ const [core, pre] = String(v).trim().replace(/^v/, '').split('-');
52
+ const nums = core.split('.').map(n => parseInt(n, 10) || 0);
53
+ return { nums: [nums[0] || 0, nums[1] || 0, nums[2] || 0], pre: pre || '' };
54
+ };
55
+ const pa = parse(a);
56
+ const pb = parse(b);
57
+ for (let i = 0; i < 3; i += 1) {
58
+ if (pa.nums[i] > pb.nums[i]) return 1;
59
+ if (pa.nums[i] < pb.nums[i]) return -1;
60
+ }
61
+ if (pa.pre === pb.pre) return 0;
62
+ if (!pa.pre) return 1; // release > prerelease
63
+ if (!pb.pre) return -1;
64
+ return pa.pre > pb.pre ? 1 : -1;
65
+ }
66
+
67
+ function readCache() {
68
+ try {
69
+ const cache = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8'));
70
+ if (cache && typeof cache === 'object') return cache;
71
+ } catch (_) {
72
+ /* no cache yet / unreadable */
73
+ }
74
+ return null;
75
+ }
76
+
77
+ function writeCache(next) {
78
+ try {
79
+ fs.mkdirSync(path.dirname(CACHE_FILE), { recursive: true });
80
+ const tmp = `${CACHE_FILE}.${process.pid}.tmp`;
81
+ fs.writeFileSync(tmp, JSON.stringify(next));
82
+ fs.renameSync(tmp, CACHE_FILE); // atomic swap, survives concurrent writers
83
+ } catch (_) {
84
+ /* caching is best-effort */
85
+ }
86
+ }
87
+
88
+ function fetchLatest() {
89
+ return new Promise(resolve => {
90
+ let done = false;
91
+ const finish = value => {
92
+ if (done) return;
93
+ done = true;
94
+ resolve(value);
95
+ };
96
+ try {
97
+ const req = https.get(REGISTRY_URL, { timeout: FETCH_TIMEOUT_MS }, res => {
98
+ if (res.statusCode !== 200) {
99
+ res.resume();
100
+ return finish(null);
101
+ }
102
+ let body = '';
103
+ res.on('data', chunk => {
104
+ body += chunk;
105
+ if (body.length > 1e6) req.destroy(); // guard against pathological size
106
+ });
107
+ res.on('end', () => {
108
+ try {
109
+ const v = JSON.parse(body).version;
110
+ finish(typeof v === 'string' ? v : null);
111
+ } catch (_) {
112
+ finish(null);
113
+ }
114
+ });
115
+ });
116
+ req.on('timeout', () => {
117
+ req.destroy();
118
+ finish(null);
119
+ });
120
+ req.on('error', () => finish(null));
121
+ } catch (_) {
122
+ finish(null);
123
+ }
124
+ });
125
+ }
126
+
127
+ /**
128
+ * Fire-and-forget at server startup. Refreshes `latestInMemory` from the 24h
129
+ * cache, or from npm when the cache is stale. Always resolves, never throws.
130
+ */
131
+ async function primeUpdateCheck() {
132
+ if (isDisabled()) return;
133
+ try {
134
+ const cache = readCache();
135
+ if (cache && cache.latest && typeof cache.fetchedAt === 'number' && Date.now() - cache.fetchedAt < TTL_MS) {
136
+ latestInMemory = cache.latest;
137
+ return;
138
+ }
139
+ const latest = await fetchLatest();
140
+ if (latest) {
141
+ latestInMemory = latest;
142
+ writeCache({ ...(cache || {}), latest, fetchedAt: Date.now() });
143
+ } else if (cache && cache.latest) {
144
+ latestInMemory = cache.latest; // fall back to a stale-but-known value
145
+ }
146
+ } catch (_) {
147
+ /* never let update detection affect the server */
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Synchronous. Returns a one-line update notice the first time it is called in
153
+ * a session where a newer version is known, otherwise null. Safe to call from
154
+ * the hot path of every tool result.
155
+ */
156
+ function consumeUpdateNotice() {
157
+ try {
158
+ if (isDisabled() || sessionNotified) return null;
159
+
160
+ let latest = latestInMemory;
161
+ if (!latest) {
162
+ const cache = readCache(); // prime may not have finished yet
163
+ if (cache && cache.latest) latest = cache.latest;
164
+ }
165
+ if (!latest) return null;
166
+
167
+ const installed = getInstalledVersion();
168
+ if (!installed) return null;
169
+ if (compareSemver(latest, installed) <= 0) return null;
170
+
171
+ // Don't re-nag across sessions for a version we already announced.
172
+ const cache = readCache() || {};
173
+ if (cache.notifiedVersion === latest) {
174
+ sessionNotified = true;
175
+ return null;
176
+ }
177
+
178
+ sessionNotified = true;
179
+ writeCache({ ...cache, notifiedVersion: latest });
180
+ return (
181
+ `ℹ️ VOC 技能包有新版本 ${latest}(当前 ${installed})。新版多平台电商/海外选品等接口能力更全。\n` +
182
+ '更新命令:npx @vocmarket/voc-skill@latest install(或 workspace 模式同理)。'
183
+ );
184
+ } catch (_) {
185
+ return null;
186
+ }
187
+ }
188
+
189
+ /** Prepend the update notice (once) to a normalized tool result in place. */
190
+ function applyUpdateNotice(result) {
191
+ try {
192
+ if (!result || typeof result.assistantMessage !== 'string') return result;
193
+ const notice = consumeUpdateNotice();
194
+ if (notice) {
195
+ result.assistantMessage = result.assistantMessage
196
+ ? `${notice}\n\n${result.assistantMessage}`
197
+ : notice;
198
+ }
199
+ } catch (_) {
200
+ /* never block a tool result */
201
+ }
202
+ return result;
203
+ }
204
+
205
+ module.exports = {
206
+ primeUpdateCheck,
207
+ consumeUpdateNotice,
208
+ applyUpdateNotice,
209
+ getInstalledVersion,
210
+ compareSemver
211
+ };
package/mcp/src/server.js CHANGED
@@ -16,6 +16,7 @@ const { runBusinessWorkflow } = require('./tools/voc-business-workflow-run');
16
16
  const { analyzeFmodeImage } = require('./tools/fmode-image-analysis');
17
17
  const { searchVocApis, getVocApiDoc, callVocApi } = require('./tools/voc-api-catalog-run');
18
18
  const { buildVocRechargeInfo, buildMissingTokenMessage, buildMissingNewApiTokenMessage } = require('./core/payment-links');
19
+ const { primeUpdateCheck, applyUpdateNotice } = require('./core/version-check');
19
20
 
20
21
  function asToolResult(result) {
21
22
  const normalized = normalizeToolResult(result);
@@ -55,6 +56,7 @@ function normalizeToolResult(result = {}) {
55
56
  if (Object.prototype.hasOwnProperty.call(result, 'errors')) {
56
57
  normalized.errors = Array.isArray(result.errors) ? result.errors : [];
57
58
  }
59
+ applyUpdateNotice(normalized);
58
60
  return normalized;
59
61
  }
60
62
 
@@ -774,6 +776,7 @@ function createServer() {
774
776
 
775
777
  async function main() {
776
778
  const server = createServer();
779
+ primeUpdateCheck(); // fire-and-forget: refresh latest-version knowledge for update hints
777
780
  const transport = new StdioServerTransport();
778
781
  await server.connect(transport);
779
782
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vocmarket/voc-skill",
3
- "version": "0.3.28",
3
+ "version": "0.3.30",
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": {
@@ -1,7 +1,63 @@
1
1
  {
2
2
  "name": "claude-code-voc-intelligence",
3
- "version": "0.3.28",
3
+ "version": "0.3.30",
4
+ "capabilities": {
5
+ "summary": "三类情报源同源接入:①社媒内容 ②国内电商+创作者情报 ③海外选品。统一 search→doc→call、鉴权计费一致。",
6
+ "channels": [
7
+ {
8
+ "key": "social",
9
+ "label": "社媒内容",
10
+ "platforms": 18,
11
+ "apis": 1018,
12
+ "examples": [
13
+ "抖音",
14
+ "小红书",
15
+ "TikTok",
16
+ "YouTube",
17
+ "微博",
18
+ "哔哩哔哩",
19
+ "快手",
20
+ "知乎",
21
+ "Instagram",
22
+ "Reddit",
23
+ "Twitter/X",
24
+ "微信/视频号"
25
+ ]
26
+ },
27
+ {
28
+ "key": "ecommerce",
29
+ "label": "国内电商 + 创作者情报",
30
+ "platforms": 32,
31
+ "apis": 285,
32
+ "examples": [
33
+ "京东",
34
+ "淘宝天猫",
35
+ "1688",
36
+ "抖音电商",
37
+ "TikTok Shop",
38
+ "闲鱼",
39
+ "得物",
40
+ "抖音星图",
41
+ "小红书蒲公英"
42
+ ]
43
+ },
44
+ {
45
+ "key": "overseas",
46
+ "label": "海外选品",
47
+ "platforms": 1,
48
+ "apis": 37,
49
+ "examples": [
50
+ "亚马逊"
51
+ ]
52
+ }
53
+ ],
54
+ "totals": {
55
+ "platforms": 51,
56
+ "apis": 1340
57
+ }
58
+ },
4
59
  "description": "Claude Code native VOC intelligence package for cross-industry social VOC workflows. Includes Xiaohongshu and Douyin trend intelligence, live token checks, evidence-card reports, single-issue VOC deep dives, boss/operator action plans, and scoped multi-turn memory.",
60
+ "changelogUrl": "https://www.npmjs.com/package/@vocmarket/voc-skill?activeTab=versions",
5
61
  "plugin": "voc-intelligence",
6
62
  "skills": [
7
63
  "xiaohongshu-trend-intelligence",
@@ -67,4 +123,4 @@
67
123
  "workspaceInstallCommand": "npx @vocmarket/voc-skill workspace --smoke",
68
124
  "workspaceInstallTarget": ".claude/plugins/voc-intelligence",
69
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."
70
- }
126
+ }