@vocmarket/voc-skill 0.3.21 → 0.3.23
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 +96 -1
- package/mcp/src/core/payment-links.js +56 -1
- package/mcp/src/features/fmode-image-analysis/image-analysis.js +16 -12
- 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 +52 -35
- 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 +21 -7
- package/skills/voc-api-catalog/references/error-codes.md +73 -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": "返回用户资料对象,含粉丝数、获赞收藏、简介。"
|
|
@@ -64,6 +64,56 @@ function readEnvLocalFiles() {
|
|
|
64
64
|
}, {});
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
// 合并读取 Claude Code 的 settings env(用户级 + 项目级,含 .local 覆盖文件)。
|
|
68
|
+
// Claude Code 用户的 fmode newapi SK 默认就放在这里的 env.ANTHROPIC_AUTH_TOKEN。
|
|
69
|
+
function readClaudeSettingsEnv() {
|
|
70
|
+
const files = [
|
|
71
|
+
path.join(os.homedir(), '.claude', 'settings.json'),
|
|
72
|
+
path.join(os.homedir(), '.claude', 'settings.local.json'),
|
|
73
|
+
path.join(process.cwd(), '.claude', 'settings.json'),
|
|
74
|
+
path.join(process.cwd(), '.claude', 'settings.local.json'),
|
|
75
|
+
];
|
|
76
|
+
const merged = {};
|
|
77
|
+
for (const filePath of files) {
|
|
78
|
+
const json = readJsonMaybe(filePath);
|
|
79
|
+
const env = json && typeof json.env === 'object' && json.env ? json.env : null;
|
|
80
|
+
if (!env) continue;
|
|
81
|
+
for (const [key, value] of Object.entries(env)) {
|
|
82
|
+
if (merged[key] === undefined && typeof value === 'string' && value.trim()) {
|
|
83
|
+
merged[key] = value;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return merged;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 读取 ~/.fmode/config.json(及项目级 ./.fmode/config.json)里的 newapi token。
|
|
91
|
+
function readFmodeConfig() {
|
|
92
|
+
const files = [
|
|
93
|
+
path.join(os.homedir(), '.fmode', 'config.json'),
|
|
94
|
+
path.join(process.cwd(), '.fmode', 'config.json'),
|
|
95
|
+
];
|
|
96
|
+
const merged = {};
|
|
97
|
+
for (const filePath of files) {
|
|
98
|
+
const json = readJsonMaybe(filePath);
|
|
99
|
+
for (const [key, value] of Object.entries(json || {})) {
|
|
100
|
+
if (merged[key] === undefined) merged[key] = value;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return merged;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 仅当 ANTHROPIC_AUTH_TOKEN 看起来是 fmode 的 newapi SK 时才采纳:
|
|
107
|
+
// - 必须 sk- 开头,且排除真 Anthropic 官方 key(sk-ant- 开头);
|
|
108
|
+
// - 若 settings 里设了 ANTHROPIC_BASE_URL,必须指向 fmode(否则这把 token 是发往别处的,不能拿来当 fmode 计费 token)。
|
|
109
|
+
function pickFmodeAnthropicToken(env) {
|
|
110
|
+
const token = env && typeof env.ANTHROPIC_AUTH_TOKEN === 'string' ? env.ANTHROPIC_AUTH_TOKEN.trim() : '';
|
|
111
|
+
if (!token || !/^sk-/i.test(token) || /^sk-ant-/i.test(token)) return '';
|
|
112
|
+
const base = String((env && (env.ANTHROPIC_BASE_URL || env.ANTHROPIC_API_BASE)) || '').toLowerCase();
|
|
113
|
+
if (base && !base.includes('fmode')) return '';
|
|
114
|
+
return token;
|
|
115
|
+
}
|
|
116
|
+
|
|
67
117
|
function readVocToken(input = {}) {
|
|
68
118
|
const workspaceEnv = readEnvLocalFiles();
|
|
69
119
|
const claudeCreds = readJsonMaybe(path.join(os.homedir(), '.claude', 'voc-credentials.json'));
|
|
@@ -90,10 +140,55 @@ function readXiaohongshuToken(input = {}) {
|
|
|
90
140
|
return readVocToken(input);
|
|
91
141
|
}
|
|
92
142
|
|
|
143
|
+
// NewAPI(fmode-api)计费 token,以 sk- 开头。
|
|
144
|
+
// 计费已迁移到 NewAPI:优先用它发请求,鉴权失败再回退平台 sessionToken(apig,原路径保留)。
|
|
145
|
+
// 关键:这把 sk- 就是 Claude Code 的 env.ANTHROPIC_AUTH_TOKEN(在 ~/.claude/settings.json),
|
|
146
|
+
// 默认装完技能不必再手动配置——这里直接从 settings.json / 进程环境读取它,避免「明明配了 SK 却判缺 token、被甩进老付费框」的死循环。
|
|
147
|
+
// 取值优先级:显式入参 → .env.local / 环境变量 FMODE_API_KEY·NEWAPI_TOKEN
|
|
148
|
+
// → ~/.claude/voc-credentials.json 的 fmodeApiKey → ~/.fmode/config.json 的 newapiToken/fmodeApiToken
|
|
149
|
+
// → Claude Code env(进程注入 + ~/.claude/settings.json)的 ANTHROPIC_AUTH_TOKEN(sk-、且 base 指向 fmode)。
|
|
150
|
+
function readNewApiToken(input = {}) {
|
|
151
|
+
const workspaceEnv = readEnvLocalFiles();
|
|
152
|
+
const claudeCreds = readJsonMaybe(path.join(os.homedir(), '.claude', 'voc-credentials.json'));
|
|
153
|
+
const fmodeConfig = readFmodeConfig();
|
|
154
|
+
const claudeEnv = readClaudeSettingsEnv();
|
|
155
|
+
return firstNonEmpty([
|
|
156
|
+
input.newapiToken,
|
|
157
|
+
input.newApiToken,
|
|
158
|
+
input.fmodeApiKey,
|
|
159
|
+
input.fmodeApiToken,
|
|
160
|
+
workspaceEnv.NEWAPI_TOKEN,
|
|
161
|
+
workspaceEnv.NEW_API_TOKEN,
|
|
162
|
+
workspaceEnv.FMODE_API_KEY,
|
|
163
|
+
workspaceEnv.FMODE_API_TOKEN,
|
|
164
|
+
process.env.NEWAPI_TOKEN,
|
|
165
|
+
process.env.NEW_API_TOKEN,
|
|
166
|
+
process.env.FMODE_API_KEY,
|
|
167
|
+
process.env.FMODE_API_TOKEN,
|
|
168
|
+
claudeCreds.fmodeApiKey,
|
|
169
|
+
claudeCreds.fmodeApiToken,
|
|
170
|
+
fmodeConfig.newapiToken,
|
|
171
|
+
fmodeConfig.newApiToken,
|
|
172
|
+
fmodeConfig.fmodeApiKey,
|
|
173
|
+
fmodeConfig.fmodeApiToken,
|
|
174
|
+
// Claude Code 显式写在 settings.json env 里的 fmode key(少见,但优先于自动推断的 ANTHROPIC_AUTH_TOKEN)
|
|
175
|
+
claudeEnv.FMODE_API_KEY,
|
|
176
|
+
claudeEnv.NEWAPI_TOKEN,
|
|
177
|
+
// Claude Code 默认入口:进程注入的 ANTHROPIC_AUTH_TOKEN(Claude Code 会把 settings.env 注入子进程)
|
|
178
|
+
pickFmodeAnthropicToken(process.env),
|
|
179
|
+
// 兜底:直接读 ~/.claude/settings.json 里的 env.ANTHROPIC_AUTH_TOKEN(独立运行、未被注入时)
|
|
180
|
+
pickFmodeAnthropicToken(claudeEnv),
|
|
181
|
+
]);
|
|
182
|
+
}
|
|
183
|
+
|
|
93
184
|
module.exports = {
|
|
94
185
|
readVocToken,
|
|
95
186
|
readXiaohongshuToken,
|
|
187
|
+
readNewApiToken,
|
|
96
188
|
readEnvFileMaybe,
|
|
97
189
|
readWorkspaceEnvLocal,
|
|
98
|
-
envLocalCandidates
|
|
190
|
+
envLocalCandidates,
|
|
191
|
+
readClaudeSettingsEnv,
|
|
192
|
+
readFmodeConfig,
|
|
193
|
+
pickFmodeAnthropicToken
|
|
99
194
|
};
|
|
@@ -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,54 @@ function buildWrongTokenTypeMessage(paymentUrl, recharge = {}, options = {}) {
|
|
|
123
126
|
].join('\n');
|
|
124
127
|
}
|
|
125
128
|
|
|
129
|
+
// 没读到 NewAPI(fmode-api)的 sk- 计费 token 时的提示。
|
|
130
|
+
// 关键:这是「没读到 SK」这种**可恢复**状态,不是余额不足、也不是功能用不了——
|
|
131
|
+
// 先引导 agent 从 Claude Code 的 settings.json 取 SK 重试,充值只作为最后兜底(且仅真 402 才需要)。
|
|
132
|
+
function buildMissingNewApiTokenMessage(paymentUrl, recharge = {}, options = {}) {
|
|
133
|
+
const platformLabel = options.platformLabel || '小红书';
|
|
134
|
+
const lines = [
|
|
135
|
+
`还没读到 NewAPI(fmode-api)计费 token,暂时不能进行真实 ${platformLabel} 采集——但这是**配置/读取**问题,不是余额不足,也不代表这个功能用不了。`,
|
|
136
|
+
'',
|
|
137
|
+
'这把计费 token 就是 Claude Code 的 `ANTHROPIC_AUTH_TOKEN`(`sk-` 开头),通常配在用户级 `~/.claude/settings.json` 的 `env` 里。',
|
|
138
|
+
'',
|
|
139
|
+
'请按这个顺序自救(优先,不要直接让用户去点充值):',
|
|
140
|
+
'1. 读取 `~/.claude/settings.json` 的 `env.ANTHROPIC_AUTH_TOKEN`(确认是 `sk-` 开头、`ANTHROPIC_BASE_URL` 指向 `api.fmode.cn`)。',
|
|
141
|
+
'2. 用 `FMODE_API_KEY=sk-…`(或入参 `newapiToken`)把它传给本工具,重新调用一次。',
|
|
142
|
+
'3. 仍报缺 token / 鉴权失败时,才考虑回退 `r:` 开头的会话 token,或确认是否真的 `402 余额不足`。'
|
|
143
|
+
];
|
|
144
|
+
if (paymentUrl) {
|
|
145
|
+
lines.push('', `(仅当确认是余额不足时才充值:${paymentUrl})`);
|
|
146
|
+
}
|
|
147
|
+
lines.push('', '系统只会用 token 发起请求,绝不在报告、日志或聊天输出里回显 token。');
|
|
148
|
+
return lines.join('\n');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function buildFmodeApiRechargeUrl() {
|
|
152
|
+
return FMODE_API_RECHARGE_URL;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// NewAPI(fmode-api)余额不足(HTTP 402)时的充值提示。
|
|
156
|
+
function buildFmodeApiRechargeMessage(paymentUrl = FMODE_API_RECHARGE_URL, options = {}) {
|
|
157
|
+
const platformLabel = options.platformLabel || '小红书';
|
|
158
|
+
return [
|
|
159
|
+
`VOC-AI 数据服务余额不足(fmode-api 计费额度已用尽),暂时不能继续真实 ${platformLabel} 采集。`,
|
|
160
|
+
'',
|
|
161
|
+
`请打开充值链接补充余额:${paymentUrl}`,
|
|
162
|
+
'',
|
|
163
|
+
'链接会自动打开余额充值弹窗;充值后用相同参数重试即可。底层鉴权/上游响应不会展示给用户。'
|
|
164
|
+
].join('\n');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 用户被禁用或无该接口权限(HTTP 403,区别于余额不足)。
|
|
168
|
+
function buildPermissionDeniedMessage(options = {}) {
|
|
169
|
+
const platformLabel = options.platformLabel || '小红书';
|
|
170
|
+
return [
|
|
171
|
+
`当前账号被禁用或没有该 ${platformLabel} 接口的访问权限,暂时不能调用(这不是余额不足、也不是关键词/类目问题)。`,
|
|
172
|
+
'',
|
|
173
|
+
'请联系服务方确认账号状态或接口权限后重试。底层鉴权/上游响应不会展示给用户。'
|
|
174
|
+
].join('\n');
|
|
175
|
+
}
|
|
176
|
+
|
|
126
177
|
module.exports = {
|
|
127
178
|
VOC_SOCIAL_APIG_ID,
|
|
128
179
|
VOC_RECHARGE_FUN_ID,
|
|
@@ -131,6 +182,10 @@ module.exports = {
|
|
|
131
182
|
buildVocRechargeUrl,
|
|
132
183
|
buildVocWorkshopUrl,
|
|
133
184
|
buildMissingTokenMessage,
|
|
185
|
+
buildMissingNewApiTokenMessage,
|
|
134
186
|
buildRechargeRequiredMessage,
|
|
135
|
-
buildWrongTokenTypeMessage
|
|
187
|
+
buildWrongTokenTypeMessage,
|
|
188
|
+
buildFmodeApiRechargeUrl,
|
|
189
|
+
buildFmodeApiRechargeMessage,
|
|
190
|
+
buildPermissionDeniedMessage
|
|
136
191
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const { readVocToken, readEnvFileMaybe, envLocalCandidates } = require('../../core/credentials');
|
|
3
|
+
const { readVocToken, readNewApiToken, readEnvFileMaybe, envLocalCandidates } = require('../../core/credentials');
|
|
4
4
|
const { buildVocRechargeInfo } = require('../../core/payment-links');
|
|
5
5
|
const { okResult, errorResult } = require('../../core/result-envelope');
|
|
6
6
|
|
|
@@ -30,12 +30,12 @@ async function analyzeFmodeImage(input = {}) {
|
|
|
30
30
|
if (!token) {
|
|
31
31
|
const recharge = await buildVocRechargeInfo();
|
|
32
32
|
return friendlyResult('needs_token', buildMissingTokenMessage(recharge.paymentUrl, recharge), {
|
|
33
|
-
summary: { imageCount: images.length, model: provider.model },
|
|
33
|
+
summary: { imageCount: images.length, model: provider.model, errorKind: 'missing_token', recoverable: true },
|
|
34
34
|
data: { recharge, model: provider.model, imageCount: images.length },
|
|
35
35
|
nextActions: [
|
|
36
|
-
|
|
37
|
-
'
|
|
38
|
-
'
|
|
36
|
+
'从 ~/.claude/settings.json 取 env.ANTHROPIC_AUTH_TOKEN(sk-,base 指向 api.fmode.cn),用 FMODE_API_KEY=sk-… 或入参 newapiToken/fmodeToken 传入后重试',
|
|
37
|
+
'或在当前工作区 .env.local 配置 FMODE_API_KEY / VOC_TOKEN / FMODE_TOKEN / TIHAO_SESSION_TOKEN',
|
|
38
|
+
'仅当确认是 402 余额不足时才打开充值链接,缺 token 不是没钱、也不代表功能用不了'
|
|
39
39
|
]
|
|
40
40
|
});
|
|
41
41
|
}
|
|
@@ -61,8 +61,8 @@ async function analyzeFmodeImage(input = {}) {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
if (response.status === 401) {
|
|
64
|
-
return friendlyResult('needs_valid_token', '
|
|
65
|
-
summary: { imageCount: images.length, model: provider.model },
|
|
64
|
+
return friendlyResult('needs_valid_token', 'token 鉴权没过(无效/过期/类型不对),这是**可恢复**问题,不代表功能用不了、也不是没钱。优先从 `~/.claude/settings.json` 取 `env.ANTHROPIC_AUTH_TOKEN`(`sk-`)用 `FMODE_API_KEY=sk-…` 传入重试;仍不行再换有效 vocToken / sessionToken。', {
|
|
65
|
+
summary: { imageCount: images.length, model: provider.model, errorKind: 'auth', recoverable: true },
|
|
66
66
|
data: { model: provider.model, imageCount: images.length }
|
|
67
67
|
});
|
|
68
68
|
}
|
|
@@ -216,7 +216,11 @@ function resolveFmodeToken(input = {}) {
|
|
|
216
216
|
process.env.DOUBAO_VISION_TOKEN,
|
|
217
217
|
process.env.VIDEO_ANALYSIS_TOKEN,
|
|
218
218
|
process.env.TIHAO_SESSION_TOKEN,
|
|
219
|
-
readVocToken(input)
|
|
219
|
+
readVocToken(input),
|
|
220
|
+
// 兜底:NewAPI(fmode-api)的 sk- 计费 token——它就是 Claude Code 的
|
|
221
|
+
// ANTHROPIC_AUTH_TOKEN(~/.claude/settings.json 的 env),api.fmode.cn 直接认。
|
|
222
|
+
// 默认装完技能即用,避免「明明配了 SK 却判缺 token」的死循环。
|
|
223
|
+
readNewApiToken(input)
|
|
220
224
|
]);
|
|
221
225
|
}
|
|
222
226
|
|
|
@@ -371,12 +375,12 @@ function buildAssistantMessage({ analysis, provider, images, files }) {
|
|
|
371
375
|
function buildMissingTokenMessage(paymentUrl, recharge = {}) {
|
|
372
376
|
const packageUrl = recharge.recommendedPackageUrl || recharge.workshopUrl || paymentUrl;
|
|
373
377
|
return [
|
|
374
|
-
'
|
|
378
|
+
'还没读到可用于 Fmode 图片分析的计费 token,暂时不能调用 vision 模型——这是**配置/读取**问题,不是余额不足,也不代表这个功能用不了。',
|
|
375
379
|
'',
|
|
376
|
-
|
|
377
|
-
|
|
380
|
+
'这把计费 token 就是 Claude Code 的 `ANTHROPIC_AUTH_TOKEN`(`sk-` 开头),通常配在用户级 `~/.claude/settings.json` 的 `env` 里(`ANTHROPIC_BASE_URL` 指向 `api.fmode.cn`)。',
|
|
381
|
+
'请优先取出它,用 `FMODE_API_KEY=sk-…`(或入参 `newapiToken`/`fmodeToken`)传给本工具重试;也可写进当前工作区 `.env.local`。',
|
|
378
382
|
'',
|
|
379
|
-
|
|
383
|
+
`(仅当确认是 402 余额不足时才充值:${paymentUrl},套餐入口:${packageUrl})`,
|
|
380
384
|
'',
|
|
381
385
|
'系统只会把 token 用作接口鉴权,不会在聊天、报告、日志或输出文件里回显。'
|
|
382
386
|
].join('\n');
|
|
@@ -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,10 @@ const { callSocialGateway, redactSecret } = require('../providers/voc-gateway');
|
|
|
11
11
|
const {
|
|
12
12
|
buildVocRechargeInfo,
|
|
13
13
|
buildMissingTokenMessage,
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
buildMissingNewApiTokenMessage,
|
|
15
|
+
buildFmodeApiRechargeUrl,
|
|
16
|
+
buildFmodeApiRechargeMessage,
|
|
17
|
+
buildPermissionDeniedMessage
|
|
16
18
|
} = require('../core/payment-links');
|
|
17
19
|
const { okResult, errorResult } = require('../core/result-envelope');
|
|
18
20
|
|
|
@@ -95,7 +97,7 @@ async function getVocApiDoc(input = {}) {
|
|
|
95
97
|
JSON.stringify(template, null, 2),
|
|
96
98
|
'```',
|
|
97
99
|
'',
|
|
98
|
-
'token
|
|
100
|
+
'token 优先用 NewAPI(fmode-api)的 sk- token(计费已迁移);没有 sk- 时回退用充值/开通页复制的、以 r: 开头的会话 token。'
|
|
99
101
|
].filter(line => line !== '');
|
|
100
102
|
return okResult({
|
|
101
103
|
assistantMessage: lines.join('\n'),
|
|
@@ -156,34 +158,28 @@ async function callVocApi(input = {}) {
|
|
|
156
158
|
}
|
|
157
159
|
if (endpoint) endpoint = hydrateEndpoint(endpoint);
|
|
158
160
|
|
|
159
|
-
|
|
161
|
+
// 计费优先走 NewAPI(fmode-api)的 sk- token;该 token 鉴权失败(含「未入仓」类报错)时
|
|
162
|
+
// 自动回退平台 sessionToken 重试。注意:以前因为 sk- 直接请求会命中「入仓」错误,这里曾经
|
|
163
|
+
// 直接拒绝 sk- token;现在改为「优先 sk-、鉴权失败再回退 sessionToken」,并对外只给友好提示。
|
|
164
|
+
const explicitNewApi = readNewApiToken(input);
|
|
165
|
+
const vocToken = readVocToken(input);
|
|
166
|
+
// 兼容用户把 sk- 直接填进 VOC_TOKEN 的情况:该槽里若是 sk-,也当作 newapi token。
|
|
167
|
+
const skFromVocToken = isSkToken(vocToken) ? vocToken : '';
|
|
168
|
+
const newapiToken = explicitNewApi || skFromVocToken;
|
|
169
|
+
const sessionToken = isSkToken(vocToken) ? '' : vocToken;
|
|
170
|
+
const token = newapiToken || sessionToken;
|
|
171
|
+
const fallbackToken = (newapiToken && sessionToken && newapiToken !== sessionToken) ? sessionToken : '';
|
|
160
172
|
if (!token) {
|
|
161
173
|
const recharge = await buildVocRechargeInfo();
|
|
162
174
|
return {
|
|
163
175
|
status: 'needs_token',
|
|
164
|
-
assistantMessage:
|
|
165
|
-
summary: { endpoint: endpoint?.id || rawPath },
|
|
176
|
+
assistantMessage: buildMissingNewApiTokenMessage(recharge.paymentUrl, recharge, { platformLabel: '社交平台' }),
|
|
177
|
+
summary: { endpoint: endpoint?.id || rawPath, errorKind: 'missing_token', recoverable: true },
|
|
166
178
|
data: { recharge },
|
|
167
179
|
files: [],
|
|
168
180
|
nextActions: [
|
|
169
|
-
|
|
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
|
+
'从 ~/.claude/settings.json 取 env.ANTHROPIC_AUTH_TOKEN(sk-),用 FMODE_API_KEY=sk-… 或入参 newapiToken 传入后重新调用',
|
|
182
|
+
'仍不行再回退以 r: 开头的会话 token;仅当确认是 402 余额不足时才打开充值链接'
|
|
187
183
|
],
|
|
188
184
|
warnings: [],
|
|
189
185
|
errors: []
|
|
@@ -228,6 +224,7 @@ async function callVocApi(input = {}) {
|
|
|
228
224
|
query,
|
|
229
225
|
body,
|
|
230
226
|
token,
|
|
227
|
+
fallbackToken,
|
|
231
228
|
baseUrl: input.baseUrl,
|
|
232
229
|
retries: Number.isFinite(input.retries) ? input.retries : 3
|
|
233
230
|
});
|
|
@@ -248,33 +245,53 @@ async function callVocApi(input = {}) {
|
|
|
248
245
|
const safeMessage = redactSecret(error && error.message ? error.message : 'request failed');
|
|
249
246
|
|
|
250
247
|
if (kind === 'auth') {
|
|
251
|
-
|
|
248
|
+
// sk- 与 sessionToken(若有)都未通过校验。不把底层「未入仓」/鉴权报错甩给用户,只给友好提示。
|
|
249
|
+
const recharge = await buildVocRechargeInfo({ token: sessionToken || token });
|
|
252
250
|
return {
|
|
253
251
|
status: 'needs_valid_token',
|
|
254
252
|
assistantMessage: [
|
|
255
|
-
'当前
|
|
253
|
+
'当前 token 没有通过校验,暂时不能调用该接口——这是 token 缺失/无效/失效的**可恢复**问题,不是关键词/类目/参数问题,也不代表这个功能用不了、更不是余额不足。',
|
|
256
254
|
'',
|
|
257
|
-
'
|
|
255
|
+
'优先自救:NewAPI(fmode-api)计费用的 `sk-` 就是 Claude Code 的 `ANTHROPIC_AUTH_TOKEN`(在 `~/.claude/settings.json` 的 `env` 里)。请取出它(确认 `sk-` 开头、`ANTHROPIC_BASE_URL` 指向 `api.fmode.cn`),用 `FMODE_API_KEY=sk-…` 或入参 `newapiToken` 传入后重试。',
|
|
258
256
|
'',
|
|
259
|
-
|
|
257
|
+
'若 `sk-` 仍校验不过,再回退以 `r:` 开头的会话 token。',
|
|
258
|
+
'',
|
|
259
|
+
`(仅当确认是 402 余额不足时才需充值:${recharge.paymentUrl})`
|
|
260
260
|
].join('\n'),
|
|
261
|
-
summary: { endpoint: endpoint?.id || proxyPath, errorKind: kind, httpStatus },
|
|
261
|
+
summary: { endpoint: endpoint?.id || proxyPath, errorKind: kind, httpStatus, recoverable: true },
|
|
262
262
|
data: { recharge },
|
|
263
263
|
files: [],
|
|
264
|
-
nextActions: [
|
|
264
|
+
nextActions: [
|
|
265
|
+
'从 ~/.claude/settings.json 取 env.ANTHROPIC_AUTH_TOKEN(sk-),用 FMODE_API_KEY=sk-… 或入参 newapiToken 传入后重试',
|
|
266
|
+
'仍不行再回退以 r: 开头的 session token;仅当确认是 402 余额不足时才打开充值链接'
|
|
267
|
+
],
|
|
265
268
|
warnings: [],
|
|
266
269
|
errors: []
|
|
267
270
|
};
|
|
268
271
|
}
|
|
269
272
|
if (kind === 'billing') {
|
|
270
|
-
|
|
273
|
+
// 402:NewAPI(fmode-api)余额不足,跳转 fmode-api 余额充值弹窗。
|
|
274
|
+
const fmodeApiRechargeUrl = buildFmodeApiRechargeUrl();
|
|
271
275
|
return {
|
|
272
276
|
status: 'needs_recharge',
|
|
273
|
-
assistantMessage:
|
|
277
|
+
assistantMessage: buildFmodeApiRechargeMessage(fmodeApiRechargeUrl, { platformLabel: '社交平台' }),
|
|
274
278
|
summary: { endpoint: endpoint?.id || proxyPath, errorKind: kind, httpStatus },
|
|
275
|
-
data: { recharge },
|
|
279
|
+
data: { recharge: { paymentUrl: fmodeApiRechargeUrl, kind: 'fmode-api' } },
|
|
280
|
+
files: [],
|
|
281
|
+
nextActions: [`打开充值链接补充余额:${fmodeApiRechargeUrl}`],
|
|
282
|
+
warnings: [],
|
|
283
|
+
errors: []
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
if (kind === 'permission') {
|
|
287
|
+
// 403:用户被禁用/无权限,区别于余额不足,不引导充值。
|
|
288
|
+
return {
|
|
289
|
+
status: 'needs_permission',
|
|
290
|
+
assistantMessage: buildPermissionDeniedMessage({ platformLabel: '社交平台' }),
|
|
291
|
+
summary: { endpoint: endpoint?.id || proxyPath, errorKind: kind, httpStatus },
|
|
292
|
+
data: {},
|
|
276
293
|
files: [],
|
|
277
|
-
nextActions: [
|
|
294
|
+
nextActions: ['联系服务方确认账号状态或接口权限后重试'],
|
|
278
295
|
warnings: [],
|
|
279
296
|
errors: []
|
|
280
297
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vocmarket/voc-skill",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.23",
|
|
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,23 +33,37 @@ 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
|
+
> **这把 `sk-` 计费 token 是什么、从哪来**:它就是 **Claude Code 自己的 `ANTHROPIC_AUTH_TOKEN`**——配在用户级 **`~/.claude/settings.json` 的 `env.ANTHROPIC_AUTH_TOKEN`**(`sk-` 开头,且 `ANTHROPIC_BASE_URL` 指向 `api.fmode.cn`)。Claude Code 用户**装完技能即用,无需任何额外配置**:工具会自动从这里(以及进程环境)读到它。所以 newapi(fmode-api)≠ 某个要单独开通的东西,它用的就是你正在用的这把 fmode key。
|
|
41
|
+
|
|
42
|
+
- 取值优先级:入参 `newapiToken`/`fmodeApiKey` → `.env.local` / 环境变量 `FMODE_API_KEY`·`NEWAPI_TOKEN` → `~/.claude/voc-credentials.json` 的 `fmodeApiKey` → `~/.fmode/config.json` 的 `newapiToken`/`fmodeApiToken` → **`~/.claude/settings.json` 的 `env.ANTHROPIC_AUTH_TOKEN`(Claude Code 默认入口,`sk-` 开头)**。
|
|
43
|
+
- `sk-` 鉴权失败(401,含「未入仓」类报错)且存在 `r:` 开头的会话 token 时,工具会**自动回退**用 `r:` sessionToken 走原 apig 接口重试一次(过渡期:服务端未迁完 / 该号未入仓)。此回退是**正常**的原始付费路径,不是出故障。
|
|
44
|
+
- 兼容:若把 `sk-` 直接填进 `VOC_TOKEN`,也会被当作 NewAPI token 使用。
|
|
45
|
+
- token **绝不回显**;底层鉴权/入仓原始报错不甩给用户,只给友好提示。
|
|
46
|
+
|
|
47
|
+
> **缺 token / token 无效时该怎么自救(重要,别进死循环)**:这是**可恢复**状态,不是「技能用不了」。先确认 `~/.claude/settings.json` 的 `env.ANTHROPIC_AUTH_TOKEN` 是否为 `sk-` 开头的 fmode key;若工具仍报缺 token,把它取出来用 `FMODE_API_KEY=sk-…`(或入参 `newapiToken`)传给工具再调一次。**不要**因为提示里出现充值/付费链接就让用户去点充值——**只有真正 `402 余额不足` 才需要充值**,缺 token 只是没读到这把 SK 而已。
|
|
48
|
+
|
|
49
|
+
> 错误码(401/402/403 等)的完整对照、真实报文样例与处理纪律见 **`references/error-codes.md`**。
|
|
40
50
|
|
|
41
51
|
## 禁止事项(护栏)
|
|
42
52
|
|
|
43
53
|
- 禁止把**入参/参数错误**(`needs_input`)误报成「采集不到数据 / 该关键词没结果 / 类目不支持」。参数错就如实说参数错,并用 `voc_api_doc` 核对后重试。
|
|
44
54
|
- 禁止把**上游不稳定**(`upstream_unstable`,如 5xx / 连接超时 / fetch failed)误报成「关键词问题 / 类目不支持 / 余额不足」。如实说明是上游接口波动,稍后重试。
|
|
45
55
|
- 禁止把 **token 无效/类型不对**(`needs_valid_token`)误报成「余额不足」或「类目不支持」。
|
|
56
|
+
- 禁止把**缺 token / token 无效**(`needs_token` / `needs_valid_token`)误报成「这个技能用不了 / 功能不可用」——它是**可恢复**的:SK 就在 `~/.claude/settings.json` 的 `env.ANTHROPIC_AUTH_TOKEN`,取出来用 `FMODE_API_KEY=sk-…` 传入重试即可,别因此放弃或去点充值。
|
|
46
57
|
- 禁止输出「某行业不可用 / 系统只支持某类目」这类结论——**任何行业、任何关键词都能采集**,前提只是 token 类型正确且有额度。
|
|
47
58
|
- 失败时禁止退回 web search / yt-dlp / ffmpeg / 让用户手动传媒体文件来「绕过」采集。
|
|
48
59
|
|
|
49
|
-
##
|
|
60
|
+
## 状态码含义(详表见 `references/error-codes.md`)
|
|
50
61
|
|
|
51
62
|
- `ok`:调用成功,`data.result` 是上游返回数据。
|
|
52
|
-
- `needs_token
|
|
53
|
-
- `needs_recharge
|
|
63
|
+
- `needs_token`:没读到任何 token(**可恢复,不是没钱、不是功能用不了**)——优先去 `~/.claude/settings.json` 取 `env.ANTHROPIC_AUTH_TOKEN`(`sk-`)用 `FMODE_API_KEY=sk-…` 传入重试。`needs_valid_token`:token 缺失/失效/无效(HTTP 401)——**不是没钱**,提示配置有效 `sk-`(或回退 `r:`)token。
|
|
64
|
+
- `needs_recharge`:**仅 HTTP 402(余额不足)**,提示充值,跳 `https://app.fmode.cn/dev/studio/?balance=fmodeapi`。
|
|
65
|
+
- `needs_permission`:**HTTP 403(账号禁用/无权限)**,**≠ 余额不足**,不引导充值,提示联系服务方确认权限。
|
|
54
66
|
- `needs_input`:必填参数缺失或参数错误(不是类目/余额问题)。
|
|
55
|
-
- `upstream_unstable
|
|
67
|
+
- `upstream_unstable`:上游接口报错或波动(5xx / fetch failed),稍后重试。
|
|
68
|
+
|
|
69
|
+
> ⚠️ 不要把 **403/401** 一律说成「余额不足」。只有 **402** 才是真的没钱要充值;403 是权限、401 是 token 问题。
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# VOC 社交数据采集 · 错误码速查
|
|
2
|
+
|
|
3
|
+
> 给 Agent 按需查阅。**核心纪律:不是所有报错都是「余额不足」,也不是所有报错都代表「功能用不了」。** 只有 **HTTP 402** 才提示充值;
|
|
4
|
+
> **401 = token 问题**(缺失/失效/类型不对),**403 = 账号禁用/无权限**,都**不是没钱**,不要引导充值。
|
|
5
|
+
> **缺 token / 401 是可恢复的**:NewAPI 计费用的 `sk-` 就是 Claude Code 的 `ANTHROPIC_AUTH_TOKEN`(在 `~/.claude/settings.json` 的 `env` 里),取出来用 `FMODE_API_KEY=sk-…` 传入重试即可,**别当成「这个技能不能用」而放弃或去点充值**。
|
|
6
|
+
|
|
7
|
+
适用范围:`voc_api_call` / `voc_api_doc`(voc-social 通用转发网关 `https://server.fmode.cn/api/voc-social/<proxyPath>`)。
|
|
8
|
+
|
|
9
|
+
## 一、总览表(HTTP 状态 → 含义 → 工具返回 → 给客户的话术 → Agent 动作)
|
|
10
|
+
|
|
11
|
+
| HTTP | 后端 `mess` 样例 | 判定 kind | 工具 status | 给客户 Agent 的话术 | Agent 该做什么 |
|
|
12
|
+
|------|------------------|-----------|-------------|----------------------|----------------|
|
|
13
|
+
| 200 | —(`{"code":200,"data":{...}}`) | — | `ok` | 正常返回 | 直接用 `data.result` |
|
|
14
|
+
| 401 | `Invalid NewAPI token` / `Missing NewAPI token` / `请输入API_KEY或用户sessionToken` / `当前用户不存在,无使用权限` | `auth` | `needs_valid_token`(无任何 token 时 `needs_token`) | 「token 缺失/失效/类型不对,**不是没钱、也不是关键词/类目问题,更不是功能用不了**」 | **先**从 `~/.claude/settings.json` 取 `env.ANTHROPIC_AUTH_TOKEN`(`sk-`),用 `FMODE_API_KEY=sk-…` 传入重试;不行再回退 `r:` sessionToken |
|
|
15
|
+
| 402 | `余额不足` / `额度不足` | `billing` | `needs_recharge` | 「余额不足,请充值」 | 给充值链接 `https://app.fmode.cn/dev/studio/?balance=fmodeapi`(自动打开余额弹窗) |
|
|
16
|
+
| 403 | `用户禁用` / `无权限` / `permission`(**不含**余额/开通关键词) | `permission` | `needs_permission` | 「账号被禁用或没有该接口权限,**≠ 余额不足**」 | 联系服务方确认账号状态/接口权限,**不要**引导充值 |
|
|
17
|
+
| 403 | 含 `未开通` / `没开通` / `余额` / `额度` / `insufficient` / `balance` | `billing` | `needs_recharge` | 「需开通/充值后使用」 | 给充值/开通链接 |
|
|
18
|
+
| 400 / 422 | `参数` / `入参` / `keyword` / `bad request` | `request` | `needs_input` | 「参数有误,**不是没数据/不是类目不支持**」 | 用 `voc_api_doc` 核对参数后重试 |
|
|
19
|
+
| 5xx / 连接失败 | `fetch failed` / 超时 / `500` | `upstream` | `upstream_unstable` | 「上游接口波动」 | 稍后重试,不要误报成关键词/余额问题 |
|
|
20
|
+
|
|
21
|
+
## 二、真实报文样例(实打抓取)
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
# 有效 sk- token(NewAPI / fmode-api)
|
|
25
|
+
HTTP 200 {"code":200,"data":{"code":0,"data":{...}}}
|
|
26
|
+
|
|
27
|
+
# sk- token 无效/写错
|
|
28
|
+
HTTP 401 {"code":401,"mess":"Invalid NewAPI token"}
|
|
29
|
+
|
|
30
|
+
# 发的是旧版 Parse sessionToken(r:),后端已迁 NewAPI 计费
|
|
31
|
+
HTTP 401 {"code":401,"mess":"Missing NewAPI token"}
|
|
32
|
+
|
|
33
|
+
# 完全没带 Authorization
|
|
34
|
+
HTTP 401 {"code":401,"mess":"请输入API_KEY或用户sessionToken"}
|
|
35
|
+
|
|
36
|
+
# token 对应的用户不存在
|
|
37
|
+
HTTP 401 {"code":401,"mess":"当前用户不存在,无使用权限"}
|
|
38
|
+
|
|
39
|
+
# 余额不足(NewAPI 计费额度用尽)—— 仅此情形才引导充值
|
|
40
|
+
HTTP 402 {"code":402,"mess":"余额不足"}
|
|
41
|
+
|
|
42
|
+
# 用户被禁用 / 无该接口权限(区别于余额不足)
|
|
43
|
+
HTTP 403 {"code":403,"mess":"用户禁用或无权限"}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## 三、Token 优先级与回退(避开「入仓」错误)
|
|
47
|
+
|
|
48
|
+
计费已迁移到 **NewAPI(fmode-api)**,请求头统一 `Authorization: Bearer ${token}`:
|
|
49
|
+
|
|
50
|
+
> **这把 `sk-` 是什么**:它就是 **Claude Code 自己的 `ANTHROPIC_AUTH_TOKEN`**(配在 `~/.claude/settings.json` 的 `env`,`sk-` 开头,`ANTHROPIC_BASE_URL` 指向 `api.fmode.cn`)。所以 Claude Code 用户**装完即用、零额外配置**,工具会自动从 settings.json / 进程环境读到它。newapi(fmode-api)不是要单独开通的东西,就是你正在用的这把 fmode key。
|
|
51
|
+
|
|
52
|
+
1. **优先**用 NewAPI 的 `sk-` token,取值优先级:
|
|
53
|
+
入参 `newapiToken/fmodeApiKey` → `.env.local` / 环境变量 `FMODE_API_KEY`/`NEWAPI_TOKEN` → `~/.claude/voc-credentials.json` 的 `fmodeApiKey` → `~/.fmode/config.json` 的 `newapiToken/fmodeApiToken` → **`~/.claude/settings.json` 的 `env.ANTHROPIC_AUTH_TOKEN`(Claude Code 默认入口)**。
|
|
54
|
+
- 兼容历史:若 `VOC_TOKEN` 槽里填的就是 `sk-`,也当作 NewAPI token 使用。
|
|
55
|
+
- 若工具报 `needs_token`/`needs_valid_token`:先去 `~/.claude/settings.json` 取 `env.ANTHROPIC_AUTH_TOKEN`,用 `FMODE_API_KEY=sk-…` 传入重试——**这是缺 token 的标准自救,不是去点充值**。
|
|
56
|
+
2. 若 `sk-` token **鉴权失败(kind=auth / 401,含「未入仓」类报错)** 且存在平台 `r:` sessionToken,**自动回退** sessionToken 重试一次。这正是规避「用户拿 token 请求时遇到入仓错误」的方式——不再因为 token 是 `sk-` 就直接拒绝。
|
|
57
|
+
3. **402(余额不足)/ 403(无权限)不回退**——它们是终态,按上表处理。
|
|
58
|
+
4. 底层「未入仓」/鉴权原始报错**不甩给用户**,对外只给上表的友好提示。
|
|
59
|
+
|
|
60
|
+
## 四、判定规则摘要(`classifyApiError`)
|
|
61
|
+
|
|
62
|
+
判定顺序(命中即返回):
|
|
63
|
+
|
|
64
|
+
1. 文案含「用户/账号信息不存在、未登录、登录失效、请输入…token、sessiontoken」→ `auth`
|
|
65
|
+
2. `status===401` 或文案含「invalid token / 无效token / 未授权 / unauthorized」→ `auth`
|
|
66
|
+
3. `status===402` 或文案含「余额不足 / 额度不足 / 未开通 / 开通…权限 / insufficient / balance / quota / payment / 充值」→ `billing`
|
|
67
|
+
4. `status===403` **且**含余额/开通类关键词 → `billing`
|
|
68
|
+
5. 文案含「参数 / 入参 / keyword / 关键词 / bad request」或 `status∈{400,422}` → `request`
|
|
69
|
+
6. `status>=500` → `upstream`
|
|
70
|
+
7. `status===403` 或文案含「permission / 权限 / 无权限」→ `permission`
|
|
71
|
+
8. 其它 → `upstream`
|
|
72
|
+
|
|
73
|
+
> 注意第 4 与第 7 条的区别:**带「余额/开通」关键词的 403 才当充值**;**纯权限 403 一律 `permission`,不充值**。
|