@vocmarket/voc-skill 0.3.21 → 0.3.22
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/mcp/catalog/voc-social-endpoints.json +13 -13
- package/mcp/src/core/credentials.js +25 -0
- package/mcp/src/core/payment-links.js +33 -1
- package/mcp/src/providers/voc-gateway.js +13 -3
- package/mcp/src/server.js +4 -1
- package/mcp/src/tools/voc-api-catalog-run.js +41 -30
- package/package.json +1 -1
- package/scripts/smoke-mcp.js +3 -3
- package/skill-package-manifest.json +1 -1
- package/skills/voc-api-catalog/SKILL.md +16 -7
- package/skills/voc-api-catalog/references/error-codes.md +68 -0
|
@@ -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.22",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "nkkj-BrainHack"
|
|
7
7
|
}
|
|
@@ -723,8 +723,8 @@
|
|
|
723
723
|
"platform": "xiaohongshu",
|
|
724
724
|
"channel": "social",
|
|
725
725
|
"title": "小红书笔记评论",
|
|
726
|
-
"summary": "按 note_id
|
|
727
|
-
"proxyPath": "xiaohongshu/
|
|
726
|
+
"summary": "按 note_id 拉取小红书笔记的一级评论(App V2,VOC 口碑主力接口)。",
|
|
727
|
+
"proxyPath": "xiaohongshu/app_v2/get_note_comments",
|
|
728
728
|
"method": "GET",
|
|
729
729
|
"paramsIn": "query",
|
|
730
730
|
"tags": [
|
|
@@ -742,34 +742,34 @@
|
|
|
742
742
|
"in": "query",
|
|
743
743
|
"type": "string",
|
|
744
744
|
"required": true,
|
|
745
|
-
"desc": "笔记 id(note_id
|
|
745
|
+
"desc": "笔记 id(note_id),来自 search_notes 返回的 note.id。"
|
|
746
746
|
},
|
|
747
747
|
{
|
|
748
|
-
"name": "
|
|
748
|
+
"name": "share_text",
|
|
749
749
|
"in": "query",
|
|
750
750
|
"type": "string",
|
|
751
751
|
"required": false,
|
|
752
752
|
"default": "",
|
|
753
|
-
"desc": "
|
|
753
|
+
"desc": "可选:小红书分享文案/链接,与 note_id 二选一。"
|
|
754
754
|
},
|
|
755
755
|
{
|
|
756
|
-
"name": "
|
|
756
|
+
"name": "xsec_token",
|
|
757
757
|
"in": "query",
|
|
758
|
-
"type": "
|
|
758
|
+
"type": "string",
|
|
759
759
|
"required": false,
|
|
760
|
-
"default":
|
|
761
|
-
"desc": "
|
|
760
|
+
"default": "",
|
|
761
|
+
"desc": "可选安全令牌,来自 search_notes 返回的 note.xsec_token;部分笔记风控需要。"
|
|
762
762
|
}
|
|
763
763
|
],
|
|
764
|
-
"responseHint": "返回评论列表,含 comment_id
|
|
764
|
+
"responseHint": "返回评论列表,含 comment_id、文本、点赞、子评论数;翻页用返回的游标。"
|
|
765
765
|
},
|
|
766
766
|
{
|
|
767
767
|
"id": "xiaohongshu.sub_comments",
|
|
768
768
|
"platform": "xiaohongshu",
|
|
769
769
|
"channel": "social",
|
|
770
770
|
"title": "小红书评论回复(楼中楼)",
|
|
771
|
-
"summary": "按 note_id + comment_id
|
|
772
|
-
"proxyPath": "xiaohongshu/
|
|
771
|
+
"summary": "按 note_id + comment_id 拉取某条评论下的回复(App V2),用于深挖讨论。",
|
|
772
|
+
"proxyPath": "xiaohongshu/app_v2/get_note_sub_comments",
|
|
773
773
|
"method": "GET",
|
|
774
774
|
"paramsIn": "query",
|
|
775
775
|
"tags": [
|
|
@@ -835,7 +835,7 @@
|
|
|
835
835
|
"in": "query",
|
|
836
836
|
"type": "string",
|
|
837
837
|
"required": true,
|
|
838
|
-
"desc": "用户 id(
|
|
838
|
+
"desc": "用户 id(24 位 hex),来自 search_notes 返回的 note.user.userid;注意不是数字 red_id。"
|
|
839
839
|
}
|
|
840
840
|
],
|
|
841
841
|
"responseHint": "返回用户资料对象,含粉丝数、获赞收藏、简介。"
|
|
@@ -90,9 +90,34 @@ function readXiaohongshuToken(input = {}) {
|
|
|
90
90
|
return readVocToken(input);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
// NewAPI(fmode-api)计费 token,以 sk- 开头。
|
|
94
|
+
// 计费已迁移到 NewAPI:优先用它发请求,鉴权失败再回退平台 sessionToken。
|
|
95
|
+
// 取值优先级:显式入参 → .env.local / 环境变量 → ~/.claude/voc-credentials.json 的 fmodeApiKey。
|
|
96
|
+
function readNewApiToken(input = {}) {
|
|
97
|
+
const workspaceEnv = readEnvLocalFiles();
|
|
98
|
+
const claudeCreds = readJsonMaybe(path.join(os.homedir(), '.claude', 'voc-credentials.json'));
|
|
99
|
+
return firstNonEmpty([
|
|
100
|
+
input.newapiToken,
|
|
101
|
+
input.newApiToken,
|
|
102
|
+
input.fmodeApiKey,
|
|
103
|
+
input.fmodeApiToken,
|
|
104
|
+
workspaceEnv.NEWAPI_TOKEN,
|
|
105
|
+
workspaceEnv.NEW_API_TOKEN,
|
|
106
|
+
workspaceEnv.FMODE_API_KEY,
|
|
107
|
+
workspaceEnv.FMODE_API_TOKEN,
|
|
108
|
+
process.env.NEWAPI_TOKEN,
|
|
109
|
+
process.env.NEW_API_TOKEN,
|
|
110
|
+
process.env.FMODE_API_KEY,
|
|
111
|
+
process.env.FMODE_API_TOKEN,
|
|
112
|
+
claudeCreds.fmodeApiKey,
|
|
113
|
+
claudeCreds.fmodeApiToken,
|
|
114
|
+
]);
|
|
115
|
+
}
|
|
116
|
+
|
|
93
117
|
module.exports = {
|
|
94
118
|
readVocToken,
|
|
95
119
|
readXiaohongshuToken,
|
|
120
|
+
readNewApiToken,
|
|
96
121
|
readEnvFileMaybe,
|
|
97
122
|
readWorkspaceEnvLocal,
|
|
98
123
|
envLocalCandidates
|
|
@@ -2,6 +2,9 @@ const VOC_SOCIAL_APIG_ID = 'Vo3ROWEvDy';
|
|
|
2
2
|
const VOC_RECHARGE_FUN_ID = 'HOkkX72PMF';
|
|
3
3
|
const VOC_RECHARGE_BASE_URL = 'https://app.fmode.cn/dev/apig-pay/';
|
|
4
4
|
const PARSE_USERS_ME_URL = 'https://server.fmode.cn/parse/users/me';
|
|
5
|
+
|
|
6
|
+
// NewAPI(fmode-api)余额充值入口。携带 ?balance=fmodeapi 会自动打开余额充值弹窗。
|
|
7
|
+
const FMODE_API_RECHARGE_URL = 'https://app.fmode.cn/dev/studio/?balance=fmodeapi';
|
|
5
8
|
const DEFAULT_WORKSHOP_PACKAGE_SLUG = '99';
|
|
6
9
|
const VOC_WORKSHOP_PACKAGE_SLUGS = {
|
|
7
10
|
trial199: '19-9',
|
|
@@ -123,6 +126,32 @@ function buildWrongTokenTypeMessage(paymentUrl, recharge = {}, options = {}) {
|
|
|
123
126
|
].join('\n');
|
|
124
127
|
}
|
|
125
128
|
|
|
129
|
+
function buildFmodeApiRechargeUrl() {
|
|
130
|
+
return FMODE_API_RECHARGE_URL;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// NewAPI(fmode-api)余额不足(HTTP 402)时的充值提示。
|
|
134
|
+
function buildFmodeApiRechargeMessage(paymentUrl = FMODE_API_RECHARGE_URL, options = {}) {
|
|
135
|
+
const platformLabel = options.platformLabel || '小红书';
|
|
136
|
+
return [
|
|
137
|
+
`VOC-AI 数据服务余额不足(fmode-api 计费额度已用尽),暂时不能继续真实 ${platformLabel} 采集。`,
|
|
138
|
+
'',
|
|
139
|
+
`请打开充值链接补充余额:${paymentUrl}`,
|
|
140
|
+
'',
|
|
141
|
+
'链接会自动打开余额充值弹窗;充值后用相同参数重试即可。底层鉴权/上游响应不会展示给用户。'
|
|
142
|
+
].join('\n');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 用户被禁用或无该接口权限(HTTP 403,区别于余额不足)。
|
|
146
|
+
function buildPermissionDeniedMessage(options = {}) {
|
|
147
|
+
const platformLabel = options.platformLabel || '小红书';
|
|
148
|
+
return [
|
|
149
|
+
`当前账号被禁用或没有该 ${platformLabel} 接口的访问权限,暂时不能调用(这不是余额不足、也不是关键词/类目问题)。`,
|
|
150
|
+
'',
|
|
151
|
+
'请联系服务方确认账号状态或接口权限后重试。底层鉴权/上游响应不会展示给用户。'
|
|
152
|
+
].join('\n');
|
|
153
|
+
}
|
|
154
|
+
|
|
126
155
|
module.exports = {
|
|
127
156
|
VOC_SOCIAL_APIG_ID,
|
|
128
157
|
VOC_RECHARGE_FUN_ID,
|
|
@@ -132,5 +161,8 @@ module.exports = {
|
|
|
132
161
|
buildVocWorkshopUrl,
|
|
133
162
|
buildMissingTokenMessage,
|
|
134
163
|
buildRechargeRequiredMessage,
|
|
135
|
-
buildWrongTokenTypeMessage
|
|
164
|
+
buildWrongTokenTypeMessage,
|
|
165
|
+
buildFmodeApiRechargeUrl,
|
|
166
|
+
buildFmodeApiRechargeMessage,
|
|
167
|
+
buildPermissionDeniedMessage
|
|
136
168
|
};
|
|
@@ -139,15 +139,25 @@ async function requestJson(url, token, { method = 'GET', body, retries = 3 } = {
|
|
|
139
139
|
throw lastError;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
|
|
142
|
+
// 优先用 newapi 的 sk- token 发请求;若该 token 鉴权失败(kind === 'auth',例如服务端
|
|
143
|
+
// 尚未迁移 / 该号未入仓 / token 未被接受)且提供了回退 token(平台 sessionToken),自动用
|
|
144
|
+
// 回退 token 重试一次。402(余额不足)/403(无权限)不回退。
|
|
145
|
+
async function callSocialGateway({ proxyPath, method = 'GET', query = {}, body, token, fallbackToken, baseUrl, retries = 3 } = {}) {
|
|
143
146
|
const root = resolveSocialRoot(baseUrl);
|
|
144
147
|
const url = buildGatewayUrl(root, proxyPath, query);
|
|
145
148
|
const upper = String(method || 'GET').toUpperCase();
|
|
146
|
-
|
|
149
|
+
const requestOptions = {
|
|
147
150
|
method: upper,
|
|
148
151
|
body: upper === 'GET' ? undefined : body,
|
|
149
152
|
retries
|
|
150
|
-
}
|
|
153
|
+
};
|
|
154
|
+
try {
|
|
155
|
+
return await requestJson(url, token, requestOptions);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
const canFallback = fallbackToken && fallbackToken !== token && error && error.kind === 'auth';
|
|
158
|
+
if (!canFallback) throw error;
|
|
159
|
+
return requestJson(url, fallbackToken, requestOptions);
|
|
160
|
+
}
|
|
151
161
|
}
|
|
152
162
|
|
|
153
163
|
module.exports = {
|
package/mcp/src/server.js
CHANGED
|
@@ -735,7 +735,7 @@ function createServer() {
|
|
|
735
735
|
description: [
|
|
736
736
|
'Invoke any VOC social forwarding interface and return the upstream data.',
|
|
737
737
|
'Provide a catalog id (preferred) or a rawPath + method for unlisted interfaces, plus a params object assembled from the interface doc.',
|
|
738
|
-
'
|
|
738
|
+
'Prefer the NewAPI (fmode-api) sk- billing token; if it is missing or unauthorized, automatically falls back to an r: session token. Input/parameter errors, upstream instability, auth, and billing are reported distinctly and are never disguised as \u201cno data / category unsupported\u201d.'
|
|
739
739
|
].join(' '),
|
|
740
740
|
inputSchema: {
|
|
741
741
|
id: z.string().optional().describe('Catalog interface id, e.g. douyin.search_general.'),
|
|
@@ -747,6 +747,9 @@ function createServer() {
|
|
|
747
747
|
body: z.object({}).passthrough().optional().describe('Optional explicit request body.'),
|
|
748
748
|
retries: z.number().int().min(0).max(8).optional(),
|
|
749
749
|
baseUrl: z.string().optional(),
|
|
750
|
+
newapiToken: z.string().optional().describe('Preferred NewAPI (fmode-api) sk- billing token. Used only for this run and never echoed back.'),
|
|
751
|
+
newApiToken: z.string().optional().describe('Alias of newapiToken.'),
|
|
752
|
+
fmodeApiKey: z.string().optional().describe('Alias of newapiToken.'),
|
|
750
753
|
vocToken: z.string().optional().describe('Optional VOC social r: session token. Used only for this run and never echoed back.'),
|
|
751
754
|
token: z.string().optional().describe('Alias of vocToken. Never echoed back.'),
|
|
752
755
|
douyinToken: z.string().optional(),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { readVocToken } = require('../core/credentials');
|
|
1
|
+
const { readVocToken, readNewApiToken } = require('../core/credentials');
|
|
2
2
|
const {
|
|
3
3
|
listEndpoints,
|
|
4
4
|
findEndpoint,
|
|
@@ -11,8 +11,9 @@ const { callSocialGateway, redactSecret } = require('../providers/voc-gateway');
|
|
|
11
11
|
const {
|
|
12
12
|
buildVocRechargeInfo,
|
|
13
13
|
buildMissingTokenMessage,
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
buildFmodeApiRechargeUrl,
|
|
15
|
+
buildFmodeApiRechargeMessage,
|
|
16
|
+
buildPermissionDeniedMessage
|
|
16
17
|
} = require('../core/payment-links');
|
|
17
18
|
const { okResult, errorResult } = require('../core/result-envelope');
|
|
18
19
|
|
|
@@ -95,7 +96,7 @@ async function getVocApiDoc(input = {}) {
|
|
|
95
96
|
JSON.stringify(template, null, 2),
|
|
96
97
|
'```',
|
|
97
98
|
'',
|
|
98
|
-
'token
|
|
99
|
+
'token 优先用 NewAPI(fmode-api)的 sk- token(计费已迁移);没有 sk- 时回退用充值/开通页复制的、以 r: 开头的会话 token。'
|
|
99
100
|
].filter(line => line !== '');
|
|
100
101
|
return okResult({
|
|
101
102
|
assistantMessage: lines.join('\n'),
|
|
@@ -156,7 +157,17 @@ async function callVocApi(input = {}) {
|
|
|
156
157
|
}
|
|
157
158
|
if (endpoint) endpoint = hydrateEndpoint(endpoint);
|
|
158
159
|
|
|
159
|
-
|
|
160
|
+
// 计费优先走 NewAPI(fmode-api)的 sk- token;该 token 鉴权失败(含「未入仓」类报错)时
|
|
161
|
+
// 自动回退平台 sessionToken 重试。注意:以前因为 sk- 直接请求会命中「入仓」错误,这里曾经
|
|
162
|
+
// 直接拒绝 sk- token;现在改为「优先 sk-、鉴权失败再回退 sessionToken」,并对外只给友好提示。
|
|
163
|
+
const explicitNewApi = readNewApiToken(input);
|
|
164
|
+
const vocToken = readVocToken(input);
|
|
165
|
+
// 兼容用户把 sk- 直接填进 VOC_TOKEN 的情况:该槽里若是 sk-,也当作 newapi token。
|
|
166
|
+
const skFromVocToken = isSkToken(vocToken) ? vocToken : '';
|
|
167
|
+
const newapiToken = explicitNewApi || skFromVocToken;
|
|
168
|
+
const sessionToken = isSkToken(vocToken) ? '' : vocToken;
|
|
169
|
+
const token = newapiToken || sessionToken;
|
|
170
|
+
const fallbackToken = (newapiToken && sessionToken && newapiToken !== sessionToken) ? sessionToken : '';
|
|
160
171
|
if (!token) {
|
|
161
172
|
const recharge = await buildVocRechargeInfo();
|
|
162
173
|
return {
|
|
@@ -167,23 +178,7 @@ async function callVocApi(input = {}) {
|
|
|
167
178
|
files: [],
|
|
168
179
|
nextActions: [
|
|
169
180
|
`打开充值/开通链接:${recharge.paymentUrl}`,
|
|
170
|
-
'
|
|
171
|
-
],
|
|
172
|
-
warnings: [],
|
|
173
|
-
errors: []
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
if (isSkToken(token)) {
|
|
177
|
-
const recharge = await buildVocRechargeInfo();
|
|
178
|
-
return {
|
|
179
|
-
status: 'needs_valid_token',
|
|
180
|
-
assistantMessage: buildWrongTokenTypeMessage(recharge.paymentUrl, recharge, { platformLabel: '社交平台' }),
|
|
181
|
-
summary: { endpoint: endpoint?.id || rawPath, tokenType: 'sk-' },
|
|
182
|
-
data: { recharge },
|
|
183
|
-
files: [],
|
|
184
|
-
nextActions: [
|
|
185
|
-
'从充值/开通页复制以 r: 开头的会话 token(不是 sk- 开头的 AIGate API Key)',
|
|
186
|
-
`打开充值/开通链接:${recharge.paymentUrl}`
|
|
181
|
+
'配置 NewAPI 的 fmode-api token(sk-)或以 r: 开头的会话 token 后重新调用'
|
|
187
182
|
],
|
|
188
183
|
warnings: [],
|
|
189
184
|
errors: []
|
|
@@ -228,6 +223,7 @@ async function callVocApi(input = {}) {
|
|
|
228
223
|
query,
|
|
229
224
|
body,
|
|
230
225
|
token,
|
|
226
|
+
fallbackToken,
|
|
231
227
|
baseUrl: input.baseUrl,
|
|
232
228
|
retries: Number.isFinite(input.retries) ? input.retries : 3
|
|
233
229
|
});
|
|
@@ -248,33 +244,48 @@ async function callVocApi(input = {}) {
|
|
|
248
244
|
const safeMessage = redactSecret(error && error.message ? error.message : 'request failed');
|
|
249
245
|
|
|
250
246
|
if (kind === 'auth') {
|
|
251
|
-
|
|
247
|
+
// sk- 与 sessionToken(若有)都未通过校验。不把底层「未入仓」/鉴权报错甩给用户,只给友好提示。
|
|
248
|
+
const recharge = await buildVocRechargeInfo({ token: sessionToken || token });
|
|
252
249
|
return {
|
|
253
250
|
status: 'needs_valid_token',
|
|
254
251
|
assistantMessage: [
|
|
255
252
|
'当前 VOC-AI 数据服务 token 没有通过校验,暂时不能调用该接口。',
|
|
256
253
|
'',
|
|
257
|
-
'这是 token
|
|
254
|
+
'这是 token 缺失/无效/失效,不是关键词/类目/参数问题:请配置有效的 NewAPI(fmode-api)token(sk-)或以 r: 开头的会话 token。',
|
|
258
255
|
'',
|
|
259
|
-
|
|
256
|
+
`如需开通/充值,请打开:${recharge.paymentUrl}`
|
|
260
257
|
].join('\n'),
|
|
261
258
|
summary: { endpoint: endpoint?.id || proxyPath, errorKind: kind, httpStatus },
|
|
262
259
|
data: { recharge },
|
|
263
260
|
files: [],
|
|
264
|
-
nextActions: [`打开充值/开通链接:${recharge.paymentUrl}`, '
|
|
261
|
+
nextActions: [`打开充值/开通链接:${recharge.paymentUrl}`, '配置 NewAPI token(sk-)或以 r: 开头的 session token 后重试'],
|
|
265
262
|
warnings: [],
|
|
266
263
|
errors: []
|
|
267
264
|
};
|
|
268
265
|
}
|
|
269
266
|
if (kind === 'billing') {
|
|
270
|
-
|
|
267
|
+
// 402:NewAPI(fmode-api)余额不足,跳转 fmode-api 余额充值弹窗。
|
|
268
|
+
const fmodeApiRechargeUrl = buildFmodeApiRechargeUrl();
|
|
271
269
|
return {
|
|
272
270
|
status: 'needs_recharge',
|
|
273
|
-
assistantMessage:
|
|
271
|
+
assistantMessage: buildFmodeApiRechargeMessage(fmodeApiRechargeUrl, { platformLabel: '社交平台' }),
|
|
274
272
|
summary: { endpoint: endpoint?.id || proxyPath, errorKind: kind, httpStatus },
|
|
275
|
-
data: { recharge },
|
|
273
|
+
data: { recharge: { paymentUrl: fmodeApiRechargeUrl, kind: 'fmode-api' } },
|
|
274
|
+
files: [],
|
|
275
|
+
nextActions: [`打开充值链接补充余额:${fmodeApiRechargeUrl}`],
|
|
276
|
+
warnings: [],
|
|
277
|
+
errors: []
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
if (kind === 'permission') {
|
|
281
|
+
// 403:用户被禁用/无权限,区别于余额不足,不引导充值。
|
|
282
|
+
return {
|
|
283
|
+
status: 'needs_permission',
|
|
284
|
+
assistantMessage: buildPermissionDeniedMessage({ platformLabel: '社交平台' }),
|
|
285
|
+
summary: { endpoint: endpoint?.id || proxyPath, errorKind: kind, httpStatus },
|
|
286
|
+
data: {},
|
|
276
287
|
files: [],
|
|
277
|
-
nextActions: [
|
|
288
|
+
nextActions: ['联系服务方确认账号状态或接口权限后重试'],
|
|
278
289
|
warnings: [],
|
|
279
290
|
errors: []
|
|
280
291
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vocmarket/voc-skill",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.22",
|
|
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": {
|
package/scripts/smoke-mcp.js
CHANGED
|
@@ -304,13 +304,13 @@ async function main() {
|
|
|
304
304
|
const apiCallSentinel = 'sk-SMOKE_WRONG_TOKEN_TYPE';
|
|
305
305
|
const apiCallResult = await client.callTool({
|
|
306
306
|
name: 'voc_api_call',
|
|
307
|
-
arguments: { id: 'douyin.search_general', params: { keyword: '律师' },
|
|
307
|
+
arguments: { id: 'douyin.search_general', params: { keyword: '律师' }, newapiToken: apiCallSentinel, vocToken: 'r:SMOKE_INVALID_SESSION_TOKEN' }
|
|
308
308
|
});
|
|
309
309
|
const apiCallStructured = apiCallResult.structuredContent || {};
|
|
310
310
|
if (apiCallStructured.status !== 'needs_valid_token') {
|
|
311
|
-
throw new Error(`Expected voc_api_call to
|
|
311
|
+
throw new Error(`Expected voc_api_call to map invalid sk-/r: tokens to needs_valid_token (token problem, not 余额不足), got: ${JSON.stringify(apiCallStructured)}`);
|
|
312
312
|
}
|
|
313
|
-
if (JSON.stringify(apiCallStructured).includes(apiCallSentinel)) {
|
|
313
|
+
if (JSON.stringify(apiCallStructured).includes(apiCallSentinel) || JSON.stringify(apiCallStructured).includes('r:SMOKE_INVALID_SESSION_TOKEN')) {
|
|
314
314
|
throw new Error('voc_api_call leaked the request-scoped token');
|
|
315
315
|
}
|
|
316
316
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-voc-intelligence",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.22",
|
|
4
4
|
"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.",
|
|
5
5
|
"plugin": "voc-intelligence",
|
|
6
6
|
"skills": [
|
|
@@ -33,10 +33,16 @@ VOC 后端是一个**通用转发网关**(`https://server.fmode.cn/api/voc-soc
|
|
|
33
33
|
4. `voc_api_call` 用真实关键词调用;翻页/拉评论时把上一次返回里的 cursor / id 透传进下一次调用。
|
|
34
34
|
5. 用返回数据继续后续分析(可交给 voc-issue-pool、voc-content-plan 等技能)。
|
|
35
35
|
|
|
36
|
-
## Token
|
|
36
|
+
## Token(计费已迁移 NewAPI)
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
请求头统一 `Authorization: Bearer ${token}`,**优先用 NewAPI(fmode-api)的 `sk-` token**:
|
|
39
|
+
|
|
40
|
+
- 取值优先级:入参 `newapiToken`/`fmodeApiKey` → `.env.local` / 环境变量 `FMODE_API_KEY`·`NEWAPI_TOKEN` → `~/.claude/voc-credentials.json` 的 `fmodeApiKey`(`sk-` 开头)。
|
|
41
|
+
- `sk-` 鉴权失败(401,含「未入仓」类报错)且存在 `r:` 开头的会话 token 时,工具会**自动回退**用 `r:` sessionToken 重试一次(过渡期:服务端未迁完 / 该号未入仓)。
|
|
42
|
+
- 兼容:若把 `sk-` 直接填进 `VOC_TOKEN`,也会被当作 NewAPI token 使用。
|
|
43
|
+
- token **绝不回显**;底层鉴权/入仓原始报错不甩给用户,只给友好提示。
|
|
44
|
+
|
|
45
|
+
> 错误码(401/402/403 等)的完整对照、真实报文样例与处理纪律见 **`references/error-codes.md`**。
|
|
40
46
|
|
|
41
47
|
## 禁止事项(护栏)
|
|
42
48
|
|
|
@@ -46,10 +52,13 @@ VOC 后端是一个**通用转发网关**(`https://server.fmode.cn/api/voc-soc
|
|
|
46
52
|
- 禁止输出「某行业不可用 / 系统只支持某类目」这类结论——**任何行业、任何关键词都能采集**,前提只是 token 类型正确且有额度。
|
|
47
53
|
- 失败时禁止退回 web search / yt-dlp / ffmpeg / 让用户手动传媒体文件来「绕过」采集。
|
|
48
54
|
|
|
49
|
-
##
|
|
55
|
+
## 状态码含义(详表见 `references/error-codes.md`)
|
|
50
56
|
|
|
51
57
|
- `ok`:调用成功,`data.result` 是上游返回数据。
|
|
52
|
-
- `needs_token
|
|
53
|
-
- `needs_recharge
|
|
58
|
+
- `needs_token`:没配置任何 token。`needs_valid_token`:token 缺失/失效/无效(HTTP 401)——**不是没钱**,提示配置有效 `sk-`(或回退 `r:`)token。
|
|
59
|
+
- `needs_recharge`:**仅 HTTP 402(余额不足)**,提示充值,跳 `https://app.fmode.cn/dev/studio/?balance=fmodeapi`。
|
|
60
|
+
- `needs_permission`:**HTTP 403(账号禁用/无权限)**,**≠ 余额不足**,不引导充值,提示联系服务方确认权限。
|
|
54
61
|
- `needs_input`:必填参数缺失或参数错误(不是类目/余额问题)。
|
|
55
|
-
- `upstream_unstable
|
|
62
|
+
- `upstream_unstable`:上游接口报错或波动(5xx / fetch failed),稍后重试。
|
|
63
|
+
|
|
64
|
+
> ⚠️ 不要把 **403/401** 一律说成「余额不足」。只有 **402** 才是真的没钱要充值;403 是权限、401 是 token 问题。
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# VOC 社交数据采集 · 错误码速查
|
|
2
|
+
|
|
3
|
+
> 给 Agent 按需查阅。**核心纪律:不是所有报错都是「余额不足」。** 只有 **HTTP 402** 才提示充值;
|
|
4
|
+
> **401 = token 问题**(缺失/失效/类型不对),**403 = 账号禁用/无权限**,都**不是没钱**,不要引导充值。
|
|
5
|
+
|
|
6
|
+
适用范围:`voc_api_call` / `voc_api_doc`(voc-social 通用转发网关 `https://server.fmode.cn/api/voc-social/<proxyPath>`)。
|
|
7
|
+
|
|
8
|
+
## 一、总览表(HTTP 状态 → 含义 → 工具返回 → 给客户的话术 → Agent 动作)
|
|
9
|
+
|
|
10
|
+
| HTTP | 后端 `mess` 样例 | 判定 kind | 工具 status | 给客户 Agent 的话术 | Agent 该做什么 |
|
|
11
|
+
|------|------------------|-----------|-------------|----------------------|----------------|
|
|
12
|
+
| 200 | —(`{"code":200,"data":{...}}`) | — | `ok` | 正常返回 | 直接用 `data.result` |
|
|
13
|
+
| 401 | `Invalid NewAPI token` / `Missing NewAPI token` / `请输入API_KEY或用户sessionToken` / `当前用户不存在,无使用权限` | `auth` | `needs_valid_token`(无任何 token 时 `needs_token`) | 「token 缺失/失效/类型不对,**不是没钱、也不是关键词/类目问题**」 | 配置有效的 NewAPI `sk-` token(或回退 `r:` sessionToken)后重试 |
|
|
14
|
+
| 402 | `余额不足` / `额度不足` | `billing` | `needs_recharge` | 「余额不足,请充值」 | 给充值链接 `https://app.fmode.cn/dev/studio/?balance=fmodeapi`(自动打开余额弹窗) |
|
|
15
|
+
| 403 | `用户禁用` / `无权限` / `permission`(**不含**余额/开通关键词) | `permission` | `needs_permission` | 「账号被禁用或没有该接口权限,**≠ 余额不足**」 | 联系服务方确认账号状态/接口权限,**不要**引导充值 |
|
|
16
|
+
| 403 | 含 `未开通` / `没开通` / `余额` / `额度` / `insufficient` / `balance` | `billing` | `needs_recharge` | 「需开通/充值后使用」 | 给充值/开通链接 |
|
|
17
|
+
| 400 / 422 | `参数` / `入参` / `keyword` / `bad request` | `request` | `needs_input` | 「参数有误,**不是没数据/不是类目不支持**」 | 用 `voc_api_doc` 核对参数后重试 |
|
|
18
|
+
| 5xx / 连接失败 | `fetch failed` / 超时 / `500` | `upstream` | `upstream_unstable` | 「上游接口波动」 | 稍后重试,不要误报成关键词/余额问题 |
|
|
19
|
+
|
|
20
|
+
## 二、真实报文样例(实打抓取)
|
|
21
|
+
|
|
22
|
+
```text
|
|
23
|
+
# 有效 sk- token(NewAPI / fmode-api)
|
|
24
|
+
HTTP 200 {"code":200,"data":{"code":0,"data":{...}}}
|
|
25
|
+
|
|
26
|
+
# sk- token 无效/写错
|
|
27
|
+
HTTP 401 {"code":401,"mess":"Invalid NewAPI token"}
|
|
28
|
+
|
|
29
|
+
# 发的是旧版 Parse sessionToken(r:),后端已迁 NewAPI 计费
|
|
30
|
+
HTTP 401 {"code":401,"mess":"Missing NewAPI token"}
|
|
31
|
+
|
|
32
|
+
# 完全没带 Authorization
|
|
33
|
+
HTTP 401 {"code":401,"mess":"请输入API_KEY或用户sessionToken"}
|
|
34
|
+
|
|
35
|
+
# token 对应的用户不存在
|
|
36
|
+
HTTP 401 {"code":401,"mess":"当前用户不存在,无使用权限"}
|
|
37
|
+
|
|
38
|
+
# 余额不足(NewAPI 计费额度用尽)—— 仅此情形才引导充值
|
|
39
|
+
HTTP 402 {"code":402,"mess":"余额不足"}
|
|
40
|
+
|
|
41
|
+
# 用户被禁用 / 无该接口权限(区别于余额不足)
|
|
42
|
+
HTTP 403 {"code":403,"mess":"用户禁用或无权限"}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## 三、Token 优先级与回退(避开「入仓」错误)
|
|
46
|
+
|
|
47
|
+
计费已迁移到 **NewAPI(fmode-api)**,请求头统一 `Authorization: Bearer ${token}`:
|
|
48
|
+
|
|
49
|
+
1. **优先**用 NewAPI 的 `sk-` token(取值:入参 `newapiToken/fmodeApiKey` → `.env.local` / 环境变量 `FMODE_API_KEY`/`NEWAPI_TOKEN` → `~/.claude/voc-credentials.json` 的 `fmodeApiKey`)。
|
|
50
|
+
- 兼容历史:若 `VOC_TOKEN` 槽里填的就是 `sk-`,也当作 NewAPI token 使用。
|
|
51
|
+
2. 若 `sk-` token **鉴权失败(kind=auth / 401,含「未入仓」类报错)** 且存在平台 `r:` sessionToken,**自动回退** sessionToken 重试一次。这正是规避「用户拿 token 请求时遇到入仓错误」的方式——不再因为 token 是 `sk-` 就直接拒绝。
|
|
52
|
+
3. **402(余额不足)/ 403(无权限)不回退**——它们是终态,按上表处理。
|
|
53
|
+
4. 底层「未入仓」/鉴权原始报错**不甩给用户**,对外只给上表的友好提示。
|
|
54
|
+
|
|
55
|
+
## 四、判定规则摘要(`classifyApiError`)
|
|
56
|
+
|
|
57
|
+
判定顺序(命中即返回):
|
|
58
|
+
|
|
59
|
+
1. 文案含「用户/账号信息不存在、未登录、登录失效、请输入…token、sessiontoken」→ `auth`
|
|
60
|
+
2. `status===401` 或文案含「invalid token / 无效token / 未授权 / unauthorized」→ `auth`
|
|
61
|
+
3. `status===402` 或文案含「余额不足 / 额度不足 / 未开通 / 开通…权限 / insufficient / balance / quota / payment / 充值」→ `billing`
|
|
62
|
+
4. `status===403` **且**含余额/开通类关键词 → `billing`
|
|
63
|
+
5. 文案含「参数 / 入参 / keyword / 关键词 / bad request」或 `status∈{400,422}` → `request`
|
|
64
|
+
6. `status>=500` → `upstream`
|
|
65
|
+
7. `status===403` 或文案含「permission / 权限 / 无权限」→ `permission`
|
|
66
|
+
8. 其它 → `upstream`
|
|
67
|
+
|
|
68
|
+
> 注意第 4 与第 7 条的区别:**带「余额/开通」关键词的 403 才当充值**;**纯权限 403 一律 `permission`,不充值**。
|