@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.
@@ -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.17",
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 拉取小红书笔记的一级评论。VOC 主力接口。",
727
- "proxyPath": "xiaohongshu/app/get_note_comments",
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": "start",
748
+ "name": "share_text",
749
749
  "in": "query",
750
750
  "type": "string",
751
751
  "required": false,
752
752
  "default": "",
753
- "desc": "分页游标,首页留空,翻页用上一次返回的 start。"
753
+ "desc": "可选:小红书分享文案/链接,与 note_id 二选一。"
754
754
  },
755
755
  {
756
- "name": "sort_strategy",
756
+ "name": "xsec_token",
757
757
  "in": "query",
758
- "type": "integer",
758
+ "type": "string",
759
759
  "required": false,
760
- "default": 1,
761
- "desc": "排序:1 默认。"
760
+ "default": "",
761
+ "desc": "可选安全令牌,来自 search_notes 返回的 note.xsec_token;部分笔记风控需要。"
762
762
  }
763
763
  ],
764
- "responseHint": "返回评论列表,含 comment_id、文本、点赞、子评论数;翻页用返回的 start。"
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/app/get_sub_comments",
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(user_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
- `打开开通/充值链接:${recharge.paymentUrl}`,
37
- '在当前工作区 .env.local 配置 VOC_TOKEN / FMODE_TOKEN / TIHAO_SESSION_TOKEN。',
38
- '或在 MCP 工具参数中传入 vocToken / sessionToken / fmodeToken。'
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', '当前平台 token 无效或已过期,请换成有效的 vocToken / sessionToken / fmodeToken 后重试。', {
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
- '还没有检测到可用于 Fmode 图片分析的平台 token,暂时不能调用 Doubao vision 模型。',
378
+ '还没读到可用于 Fmode 图片分析的计费 token,暂时不能调用 vision 模型——这是**配置/读取**问题,不是余额不足,也不代表这个功能用不了。',
375
379
  '',
376
- `开通/充值链接:${paymentUrl}`,
377
- `套餐入口:${packageUrl}`,
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
- '开通后,可把平台 token 配置到当前工作区 `.env.local`,字段可用 `VOC_TOKEN`、`FMODE_TOKEN` 或 `TIHAO_SESSION_TOKEN`;也可以在工具参数中传入 `vocToken` / `sessionToken` / `fmodeToken`。',
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
- async function callSocialGateway({ proxyPath, method = 'GET', query = {}, body, token, baseUrl, retries = 3 } = {}) {
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
- return requestJson(url, token, {
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
- 'Requires an r: session token (not an sk- AIGate key). Input/parameter errors, upstream instability, auth, and billing are reported distinctly and are never disguised as \u201cno data / category unsupported\u201d.'
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
- buildRechargeRequiredMessage,
15
- buildWrongTokenTypeMessage
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 用从充值/开通页复制的、以 r: 开头的会话 token(不要用 sk- 开头的 AIGate Key)。'
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
- const token = readVocToken(input);
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: buildMissingTokenMessage(recharge.paymentUrl, recharge, { platformLabel: '社交平台' }),
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
- `打开充值/开通链接:${recharge.paymentUrl}`,
170
- '配置以 r: 开头的会话 token 后重新调用'
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
- const recharge = await buildVocRechargeInfo({ token });
248
+ // sk- 与 sessionToken(若有)都未通过校验。不把底层「未入仓」/鉴权报错甩给用户,只给友好提示。
249
+ const recharge = await buildVocRechargeInfo({ token: sessionToken || token });
252
250
  return {
253
251
  status: 'needs_valid_token',
254
252
  assistantMessage: [
255
- '当前 VOC-AI 数据服务 token 没有通过校验,暂时不能调用该接口。',
253
+ '当前 token 没有通过校验,暂时不能调用该接口——这是 token 缺失/无效/失效的**可恢复**问题,不是关键词/类目/参数问题,也不代表这个功能用不了、更不是余额不足。',
256
254
  '',
257
- '这是 token 无效或类型不对,不是关键词/类目/参数问题:需要从充值/开通页复制的、以 r: 开头的会话 token(不要用 sk- 开头的 AIGate API Key)。',
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
- `请打开充值/开通链接:${recharge.paymentUrl}`
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: [`打开充值/开通链接:${recharge.paymentUrl}`, '配置以 r: 开头的 session token 后重试'],
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
- const recharge = await buildVocRechargeInfo({ token });
273
+ // 402:NewAPI(fmode-api)余额不足,跳转 fmode-api 余额充值弹窗。
274
+ const fmodeApiRechargeUrl = buildFmodeApiRechargeUrl();
271
275
  return {
272
276
  status: 'needs_recharge',
273
- assistantMessage: buildRechargeRequiredMessage(recharge.paymentUrl, recharge, { platformLabel: '社交平台' }),
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: [`打开充值链接补充额度:${recharge.paymentUrl}`],
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.21",
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": {
@@ -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: '律师' }, vocToken: apiCallSentinel }
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 reject sk- token as needs_valid_token, got: ${JSON.stringify(apiCallStructured)}`);
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.17",
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
- 采集需要从充值/开通页复制的、以 `r:` 开头的会话 token(session token)。
39
- **不要**用 `~/.claude/voc-credentials.json` 里 `sk-` 开头的 AIGate API Key —— 后端不认它,会被工具直接判为 `needs_valid_token`。
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` / `needs_valid_token`:没配置 token,或 token 类型不对(sk-)。提示配置 `r:` 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`,不充值**。