@vocmarket/voc-skill 0.3.41 → 0.3.43
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 +61 -19
- package/mcp/src/features/voc-cost/workflow-planner.js +3 -3
- package/mcp/src/features/xiaohongshu-trend/live-collector.js +3 -1
- package/mcp/src/providers/ecommerce-gateway.js +2 -1
- package/mcp/src/providers/overseas-gateway.js +1 -1
- package/mcp/src/providers/voc-gateway.js +143 -13
- package/mcp/src/providers/xiaohongshu-api.js +40 -69
- package/mcp/src/server.js +1 -1
- package/mcp/src/tools/voc-api-catalog-run.js +69 -6
- package/package.json +1 -1
- package/skill-package-manifest.json +1 -1
- package/skills/voc-api-catalog/SKILL.md +11 -4
- package/skills/voc-api-catalog/references/error-codes.md +3 -0
- package/skills/xiaohongshu-trend-intelligence/references/live-mode.md +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "voc-intelligence",
|
|
3
3
|
"description": "VOC intelligence skills for Claude Code: end-to-end VOC business workflows, Xiaohongshu/Douyin/WeChat Channels collection and 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.43",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "nkkj-BrainHack"
|
|
7
7
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.
|
|
3
|
-
"updatedAt": "2026-
|
|
2
|
+
"version": "1.7.0",
|
|
3
|
+
"updatedAt": "2026-11-09",
|
|
4
4
|
"gateway": {
|
|
5
5
|
"social": {
|
|
6
6
|
"baseUrl": "https://server.fmode.cn/api/voc-social",
|
|
@@ -10215,7 +10215,10 @@
|
|
|
10215
10215
|
"requiredParams": [
|
|
10216
10216
|
"note_id",
|
|
10217
10217
|
"xsec_token"
|
|
10218
|
-
]
|
|
10218
|
+
],
|
|
10219
|
+
"deprecated": true,
|
|
10220
|
+
"replacedBy": "xiaohongshu.app_v2.get_note_comments",
|
|
10221
|
+
"recommendationPriority": -1000
|
|
10219
10222
|
},
|
|
10220
10223
|
{
|
|
10221
10224
|
"id": "xiaohongshu.web_v3.fetch_sub_comments",
|
|
@@ -10237,7 +10240,10 @@
|
|
|
10237
10240
|
"note_id",
|
|
10238
10241
|
"root_comment_id",
|
|
10239
10242
|
"xsec_token"
|
|
10240
|
-
]
|
|
10243
|
+
],
|
|
10244
|
+
"deprecated": true,
|
|
10245
|
+
"replacedBy": "xiaohongshu.app_v2.get_note_sub_comments",
|
|
10246
|
+
"recommendationPriority": -1000
|
|
10241
10247
|
},
|
|
10242
10248
|
{
|
|
10243
10249
|
"id": "xiaohongshu.web_v3.fetch_search_notes",
|
|
@@ -10257,7 +10263,10 @@
|
|
|
10257
10263
|
],
|
|
10258
10264
|
"requiredParams": [
|
|
10259
10265
|
"keyword"
|
|
10260
|
-
]
|
|
10266
|
+
],
|
|
10267
|
+
"deprecated": true,
|
|
10268
|
+
"replacedBy": "xiaohongshu.app_v2.search_notes",
|
|
10269
|
+
"recommendationPriority": -1000
|
|
10261
10270
|
},
|
|
10262
10271
|
{
|
|
10263
10272
|
"id": "xiaohongshu.web_v3.fetch_search_users",
|
|
@@ -10277,7 +10286,10 @@
|
|
|
10277
10286
|
],
|
|
10278
10287
|
"requiredParams": [
|
|
10279
10288
|
"keyword"
|
|
10280
|
-
]
|
|
10289
|
+
],
|
|
10290
|
+
"deprecated": true,
|
|
10291
|
+
"replacedBy": "xiaohongshu.app_v2.search_users",
|
|
10292
|
+
"recommendationPriority": -1000
|
|
10281
10293
|
},
|
|
10282
10294
|
{
|
|
10283
10295
|
"id": "xiaohongshu.web_v3.fetch_trending",
|
|
@@ -10295,7 +10307,10 @@
|
|
|
10295
10307
|
"web",
|
|
10296
10308
|
"v3"
|
|
10297
10309
|
],
|
|
10298
|
-
"requiredParams": []
|
|
10310
|
+
"requiredParams": [],
|
|
10311
|
+
"deprecated": true,
|
|
10312
|
+
"replacedBy": "xiaohongshu.hot_search_v1",
|
|
10313
|
+
"recommendationPriority": -1000
|
|
10299
10314
|
},
|
|
10300
10315
|
{
|
|
10301
10316
|
"id": "xiaohongshu.web_v3.fetch_search_suggest",
|
|
@@ -10389,7 +10404,10 @@
|
|
|
10389
10404
|
],
|
|
10390
10405
|
"requiredParams": [
|
|
10391
10406
|
"user_id"
|
|
10392
|
-
]
|
|
10407
|
+
],
|
|
10408
|
+
"deprecated": true,
|
|
10409
|
+
"replacedBy": "xiaohongshu.app_v2.get_user_posted_notes",
|
|
10410
|
+
"recommendationPriority": -1000
|
|
10393
10411
|
},
|
|
10394
10412
|
{
|
|
10395
10413
|
"id": "xiaohongshu.web_v3.fetch_hot_list",
|
|
@@ -10407,7 +10425,10 @@
|
|
|
10407
10425
|
"web",
|
|
10408
10426
|
"v3"
|
|
10409
10427
|
],
|
|
10410
|
-
"requiredParams": []
|
|
10428
|
+
"requiredParams": [],
|
|
10429
|
+
"deprecated": true,
|
|
10430
|
+
"replacedBy": "xiaohongshu.hot_search_v1",
|
|
10431
|
+
"recommendationPriority": -1000
|
|
10411
10432
|
},
|
|
10412
10433
|
{
|
|
10413
10434
|
"id": "xiaohongshu.app_v2.get_image_note_detail",
|
|
@@ -10827,7 +10848,10 @@
|
|
|
10827
10848
|
],
|
|
10828
10849
|
"requiredParams": [
|
|
10829
10850
|
"note_id"
|
|
10830
|
-
]
|
|
10851
|
+
],
|
|
10852
|
+
"deprecated": true,
|
|
10853
|
+
"replacedBy": "xiaohongshu.web_v3.fetch_homefeed",
|
|
10854
|
+
"recommendationPriority": -1000
|
|
10831
10855
|
},
|
|
10832
10856
|
{
|
|
10833
10857
|
"id": "xiaohongshu.web_v2.fetch_feed_notes_v2",
|
|
@@ -10847,7 +10871,10 @@
|
|
|
10847
10871
|
],
|
|
10848
10872
|
"requiredParams": [
|
|
10849
10873
|
"note_id"
|
|
10850
|
-
]
|
|
10874
|
+
],
|
|
10875
|
+
"deprecated": true,
|
|
10876
|
+
"replacedBy": "xiaohongshu.web_v3.fetch_homefeed",
|
|
10877
|
+
"recommendationPriority": -1000
|
|
10851
10878
|
},
|
|
10852
10879
|
{
|
|
10853
10880
|
"id": "xiaohongshu.web_v2.fetch_home_notes_app",
|
|
@@ -10867,7 +10894,10 @@
|
|
|
10867
10894
|
],
|
|
10868
10895
|
"requiredParams": [
|
|
10869
10896
|
"user_id"
|
|
10870
|
-
]
|
|
10897
|
+
],
|
|
10898
|
+
"deprecated": true,
|
|
10899
|
+
"replacedBy": "xiaohongshu.web_v3.fetch_homefeed",
|
|
10900
|
+
"recommendationPriority": -1000
|
|
10871
10901
|
},
|
|
10872
10902
|
{
|
|
10873
10903
|
"id": "xiaohongshu.web_v2.fetch_note_comments",
|
|
@@ -10887,7 +10917,10 @@
|
|
|
10887
10917
|
],
|
|
10888
10918
|
"requiredParams": [
|
|
10889
10919
|
"note_id"
|
|
10890
|
-
]
|
|
10920
|
+
],
|
|
10921
|
+
"deprecated": true,
|
|
10922
|
+
"replacedBy": "xiaohongshu.app_v2.get_note_comments",
|
|
10923
|
+
"recommendationPriority": -1000
|
|
10891
10924
|
},
|
|
10892
10925
|
{
|
|
10893
10926
|
"id": "xiaohongshu.web_v2.fetch_sub_comments",
|
|
@@ -10908,7 +10941,10 @@
|
|
|
10908
10941
|
"requiredParams": [
|
|
10909
10942
|
"note_id",
|
|
10910
10943
|
"comment_id"
|
|
10911
|
-
]
|
|
10944
|
+
],
|
|
10945
|
+
"deprecated": true,
|
|
10946
|
+
"replacedBy": "xiaohongshu.app_v2.get_note_sub_comments",
|
|
10947
|
+
"recommendationPriority": -1000
|
|
10912
10948
|
},
|
|
10913
10949
|
{
|
|
10914
10950
|
"id": "xiaohongshu.web_v2.fetch_user_info_app",
|
|
@@ -10928,7 +10964,10 @@
|
|
|
10928
10964
|
],
|
|
10929
10965
|
"requiredParams": [
|
|
10930
10966
|
"user_id"
|
|
10931
|
-
]
|
|
10967
|
+
],
|
|
10968
|
+
"deprecated": true,
|
|
10969
|
+
"replacedBy": "xiaohongshu.app_v2.get_user_info",
|
|
10970
|
+
"recommendationPriority": -1000
|
|
10932
10971
|
},
|
|
10933
10972
|
{
|
|
10934
10973
|
"id": "xiaohongshu.web_v2.fetch_hot_list",
|
|
@@ -10946,7 +10985,10 @@
|
|
|
10946
10985
|
"web",
|
|
10947
10986
|
"v2"
|
|
10948
10987
|
],
|
|
10949
|
-
"requiredParams": []
|
|
10988
|
+
"requiredParams": [],
|
|
10989
|
+
"deprecated": true,
|
|
10990
|
+
"replacedBy": "xiaohongshu.hot_search_v1",
|
|
10991
|
+
"recommendationPriority": -1000
|
|
10950
10992
|
},
|
|
10951
10993
|
{
|
|
10952
10994
|
"id": "lemon8.app.fetch_user_profile",
|
|
@@ -27389,7 +27431,7 @@
|
|
|
27389
27431
|
"platform": "youtube",
|
|
27390
27432
|
"channel": "ecommerce",
|
|
27391
27433
|
"title": "YouTube·通用搜索",
|
|
27392
|
-
"summary": "
|
|
27434
|
+
"summary": "通过通用搜索接口检索 YouTube,返回混合的视频、频道、播放列表和电影结果,包含续传令牌以及上传日期、类型、时长、功能和排序顺序的筛选条件,用于发现和监控。",
|
|
27393
27435
|
"method": "GET",
|
|
27394
27436
|
"paramsIn": "query",
|
|
27395
27437
|
"proxyPath": "youtube/search/v1",
|
|
@@ -27453,7 +27495,7 @@
|
|
|
27453
27495
|
"platform": "youtube",
|
|
27454
27496
|
"channel": "ecommerce",
|
|
27455
27497
|
"title": "YouTube·视频字幕",
|
|
27456
|
-
"summary": "
|
|
27498
|
+
"summary": "获取 YouTube 视频字幕数据,包括可用的字幕轨道或格式化内容(SRT、XML、JSON3 或纯文本),用于转录、本地化、可访问性审查和内容分析。",
|
|
27457
27499
|
"method": "GET",
|
|
27458
27500
|
"paramsIn": "query",
|
|
27459
27501
|
"proxyPath": "youtube/get-video-captions/v1",
|
|
@@ -27949,7 +27991,7 @@
|
|
|
27949
27991
|
"platform": "twitter",
|
|
27950
27992
|
"channel": "ecommerce",
|
|
27951
27993
|
"title": "Twitter/X·搜索时间线",
|
|
27952
|
-
"summary": "
|
|
27994
|
+
"summary": "检索 Twitter 时间线数据,包括热门、最新、媒体、人物和列表结果,支持游标分页,用于社交聆听、话题追踪和账号发现。",
|
|
27953
27995
|
"method": "GET",
|
|
27954
27996
|
"paramsIn": "query",
|
|
27955
27997
|
"proxyPath": "twitter/search/v1",
|
|
@@ -81,9 +81,9 @@ function planSocialTrend(input, mode, platform) {
|
|
|
81
81
|
const commentPages = positive(input.commentPagesPerKeyword ?? input.maxCommentPages ?? input.commentPagesPerNote, mode === 'complete' ? 1 : 0);
|
|
82
82
|
const commentCount = keywordCount * itemCount * commentPages;
|
|
83
83
|
const isXhs = platform === 'xiaohongshu';
|
|
84
|
-
const searchPath = isXhs ? 'search_notes' : 'search/fetch_general_search_v2';
|
|
85
|
-
const itemPath = isXhs ? '
|
|
86
|
-
const commentPath = isXhs ? 'get_note_comments' : 'app/v3/fetch_video_comments';
|
|
84
|
+
const searchPath = isXhs ? 'app_v2/search_notes' : 'douyin/search/fetch_general_search_v2';
|
|
85
|
+
const itemPath = isXhs ? 'app_v2/get_image_note_detail' : 'douyin/app/v3/fetch_one_video';
|
|
86
|
+
const commentPath = isXhs ? 'app_v2/get_note_comments' : 'douyin/app/v3/fetch_video_comments';
|
|
87
87
|
const operations = [
|
|
88
88
|
operation('voc-social', searchPath, keywordCount, isXhs ? '小红书关键词搜索' : '抖音关键词搜索', { valueScore: 5, required: true }),
|
|
89
89
|
isXhs
|
|
@@ -335,7 +335,9 @@ async function collectLiveDataset({ profile, token, fallbackToken, options = {}
|
|
|
335
335
|
try {
|
|
336
336
|
const detail = await api.getNoteDetail({
|
|
337
337
|
noteId: rawNote.id,
|
|
338
|
-
noteType: rawNote.type
|
|
338
|
+
noteType: rawNote.type,
|
|
339
|
+
// 搜索结果里带的 xsec_token 是 web_v3 详情回退的必要入参。
|
|
340
|
+
xsecToken: rawNote.xsec_token || ''
|
|
339
341
|
});
|
|
340
342
|
note = normalizeNote(rawNote, keyword, extractDetailNote(detail.data));
|
|
341
343
|
} catch (error) {
|
|
@@ -32,7 +32,8 @@ async function callEcommerceGateway({ proxyPath, method = 'GET', query = {}, bod
|
|
|
32
32
|
method: upper,
|
|
33
33
|
body: upper === 'GET' ? undefined : body,
|
|
34
34
|
retries,
|
|
35
|
-
headers
|
|
35
|
+
headers,
|
|
36
|
+
endpoint: proxyPath
|
|
36
37
|
};
|
|
37
38
|
try {
|
|
38
39
|
const result = await requestJson(url, token, requestOptions);
|
|
@@ -36,7 +36,7 @@ async function callOverseasGateway({ proxyPath, method = 'POST', query = {}, bod
|
|
|
36
36
|
query: query || {},
|
|
37
37
|
body: body || {}
|
|
38
38
|
};
|
|
39
|
-
const requestOptions = { method: 'POST', body: envelope, retries, headers };
|
|
39
|
+
const requestOptions = { method: 'POST', body: envelope, retries, headers, endpoint: rawPath };
|
|
40
40
|
try {
|
|
41
41
|
const result = await requestJson(url, token, requestOptions);
|
|
42
42
|
return { ...result, authMode: authModeForToken(token), fallbackUsed: false };
|
|
@@ -57,11 +57,73 @@ function isSuccessCode(json) {
|
|
|
57
57
|
return true;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
/**
|
|
61
|
+
* 网关(voc-social-api)新契约里显式回传的失败原因。
|
|
62
|
+
* 有了它就不必再靠 HTTP 状态码猜「这是用法错了还是平台抖了」。
|
|
63
|
+
*/
|
|
64
|
+
const TRANSIENT_REASONS = new Set([
|
|
65
|
+
'upstream_html',
|
|
66
|
+
'upstream_invalid_body',
|
|
67
|
+
'upstream_unreachable',
|
|
68
|
+
'upstream_server_error'
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
const USAGE_REASON_KINDS = {
|
|
72
|
+
method_not_allowed: 'method',
|
|
73
|
+
not_found: 'not_found',
|
|
74
|
+
request_invalid: 'request'
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** 上游返回 HTML 错误页 / 空响应 / 无法解析的响应体时,只给结论,绝不透传原文。 */
|
|
78
|
+
function describeUnparsableBody(text, httpStatus) {
|
|
79
|
+
const status = Number(httpStatus) || 502;
|
|
80
|
+
if (/^\s*(<!doctype|<html|<head|<pre|<\?xml)/i.test(String(text || ''))) {
|
|
81
|
+
return `本次请求命中了平台侧的异常节点,返回的是 HTML 错误页而不是数据(HTTP ${status})。该接口本身可用,属于瞬时故障,已自动重试。`;
|
|
82
|
+
}
|
|
83
|
+
if (!String(text || '').trim()) {
|
|
84
|
+
return `平台侧返回了空响应(HTTP ${status}),属于瞬时故障,已自动重试。`;
|
|
85
|
+
}
|
|
86
|
+
return `平台侧返回了无法解析的响应体(HTTP ${status}),属于瞬时故障,已自动重试。`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 用法类错误(方法/路径/入参)的文案,措辞刻意不含「上游」,避免被误判成平台抖动。 */
|
|
90
|
+
function describeUsageError({ reason, endpoint, httpStatus, serverMessage }) {
|
|
91
|
+
const detail = String(serverMessage || '').trim();
|
|
92
|
+
const hasCjk = /[一-龥]/.test(detail);
|
|
93
|
+
if (reason === 'method_not_allowed') {
|
|
94
|
+
return hasCjk
|
|
95
|
+
? `${detail} 请按目录里的 method 字段重发。`
|
|
96
|
+
: `接口 ${endpoint} 不接受该 HTTP 方法(HTTP 405)。请按目录里的 method 字段重发。`;
|
|
97
|
+
}
|
|
98
|
+
if (reason === 'not_found') {
|
|
99
|
+
return `接口 ${endpoint} 在数据服务中不存在(HTTP 404),该路径可能已下线,请改用同平台的其他在售接口。`;
|
|
100
|
+
}
|
|
101
|
+
if (reason === 'request_invalid') {
|
|
102
|
+
return hasCjk
|
|
103
|
+
? `${detail} 请按目录里的 requiredParams 补齐后重发。`
|
|
104
|
+
: `接口 ${endpoint} 入参校验未通过(HTTP ${httpStatus}):${detail} 请按目录里的 requiredParams 补齐后重发。`;
|
|
105
|
+
}
|
|
106
|
+
return detail;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function classifyApiError({ response, json, message, reason }) {
|
|
61
110
|
const httpStatus = response?.status || 0;
|
|
62
111
|
const businessStatus = pickBusinessStatus(json);
|
|
63
112
|
const status = businessStatus || httpStatus;
|
|
64
113
|
const text = String(message || '');
|
|
114
|
+
if (reason && USAGE_REASON_KINDS[reason]) {
|
|
115
|
+
return USAGE_REASON_KINDS[reason];
|
|
116
|
+
}
|
|
117
|
+
if (reason === 'upstream_business_error') {
|
|
118
|
+
if (status === 401) return 'auth';
|
|
119
|
+
if (status === 402) return 'billing';
|
|
120
|
+
if (status === 403) return 'permission';
|
|
121
|
+
if (status === 202) return 'unsupported';
|
|
122
|
+
return 'upstream';
|
|
123
|
+
}
|
|
124
|
+
if (reason && TRANSIENT_REASONS.has(reason)) {
|
|
125
|
+
return 'upstream';
|
|
126
|
+
}
|
|
65
127
|
if (/company或用户信息不存在|用户信息不存在|用户不存在|账[号户]信息不存在|未登录|登录失效|请输入.*token|sessiontoken/i.test(text)) {
|
|
66
128
|
return 'auth';
|
|
67
129
|
}
|
|
@@ -97,7 +159,20 @@ function authModeForToken(token) {
|
|
|
97
159
|
return 'unknown';
|
|
98
160
|
}
|
|
99
161
|
|
|
100
|
-
|
|
162
|
+
function isMarkupText(text) {
|
|
163
|
+
return /^\s*(<!doctype|<html|<head|<pre|<\?xml)/i.test(String(text || ''));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function endpointFromUrl(url) {
|
|
167
|
+
try {
|
|
168
|
+
return new URL(url).pathname.replace(/^\/api\/voc-[a-z-]+\//, '');
|
|
169
|
+
} catch {
|
|
170
|
+
return String(url || '');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function requestJson(url, token, { method = 'GET', body, retries = 3, headers = {}, endpoint } = {}) {
|
|
175
|
+
const endpointLabel = endpoint || endpointFromUrl(url);
|
|
101
176
|
let lastError;
|
|
102
177
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
103
178
|
let response;
|
|
@@ -115,31 +190,85 @@ async function requestJson(url, token, { method = 'GET', body, retries = 3, head
|
|
|
115
190
|
});
|
|
116
191
|
text = await response.text();
|
|
117
192
|
} catch (networkError) {
|
|
118
|
-
|
|
193
|
+
// 网络层失败基本都是瞬时故障:重试耗尽后给结论,不把 undici 的原文当结论抛出去。
|
|
119
194
|
if (attempt < retries) {
|
|
120
195
|
await delay(400 * (attempt + 1));
|
|
121
196
|
continue;
|
|
122
197
|
}
|
|
123
|
-
const error = new Error(
|
|
198
|
+
const error = new Error(
|
|
199
|
+
`连接平台数据服务失败(已重试 ${retries} 次):${redactSecret(networkError && networkError.message ? networkError.message : 'fetch failed')}`
|
|
200
|
+
);
|
|
124
201
|
error.kind = 'upstream';
|
|
202
|
+
error.reason = 'upstream_unreachable';
|
|
203
|
+
error.retryable = true;
|
|
125
204
|
error.httpStatus = 0;
|
|
205
|
+
error.endpoint = endpointLabel;
|
|
206
|
+
error.attempts = retries + 1;
|
|
126
207
|
throw error;
|
|
127
208
|
}
|
|
209
|
+
|
|
128
210
|
let json;
|
|
129
211
|
try {
|
|
130
212
|
json = JSON.parse(text);
|
|
131
213
|
} catch {
|
|
132
214
|
json = undefined;
|
|
133
215
|
}
|
|
134
|
-
|
|
135
|
-
|
|
216
|
+
|
|
217
|
+
// —— 非 JSON 响应体 ——
|
|
218
|
+
// 典型场景:网关的上游负载均衡把请求打到了缺路由的节点,返回 HTML 错误页。
|
|
219
|
+
// 这是**瞬时**故障(同一接口重发即可成功),所以既重试,也绝不把 HTML 原文
|
|
220
|
+
// 或 `Unexpected token '<'` 这类解析器文本当成失败结论抛给调用方——否则技能
|
|
221
|
+
// 会把它读成「该平台能力不可用」。
|
|
222
|
+
if (!json) {
|
|
223
|
+
const error = new Error(describeUnparsableBody(text, response.status));
|
|
224
|
+
error.kind = 'upstream';
|
|
225
|
+
error.reason = isMarkupText(text) ? 'upstream_html' : 'upstream_invalid_body';
|
|
226
|
+
error.retryable = true;
|
|
227
|
+
// 非 JSON 响应说明请求根本没到达业务接口(典型是节点缺路由),此时 HTTP 状态
|
|
228
|
+
// 描述的是故障节点而非资源,统一按 503 归类,避免把「节点异常 404」误读成
|
|
229
|
+
// 「该路径已下线」。真实响应码保留在 message 里供排查。
|
|
230
|
+
error.httpStatus = 503;
|
|
231
|
+
error.endpoint = endpointLabel;
|
|
232
|
+
error.attempts = attempt + 1;
|
|
233
|
+
if (attempt < retries) {
|
|
234
|
+
lastError = error;
|
|
235
|
+
await delay(400 * (attempt + 1));
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (!response.ok || !isSuccessCode(json)) {
|
|
242
|
+
const reason = typeof json.reason === 'string' ? json.reason : '';
|
|
243
|
+
const businessStatus = pickBusinessStatus(json);
|
|
244
|
+
const serverMessage = redactSecret(
|
|
245
|
+
json.message || json.msg || json.data?.msg || json.data?.message || json.mess || `HTTP ${response.status}`
|
|
246
|
+
);
|
|
247
|
+
// 网关显式回传 reason 时,HTTP 状态就是真实状态,业务码不再冒充 HTTP 状态;
|
|
248
|
+
// 没有 reason(老契约)时保持历史语义(业务码优先),避免下游摘要字段漂移。
|
|
249
|
+
const httpStatus = reason
|
|
250
|
+
? Number(response.status) || businessStatus || 500
|
|
251
|
+
: businessStatus || Number(response.status) || 500;
|
|
252
|
+
const message = USAGE_REASON_KINDS[reason]
|
|
253
|
+
? describeUsageError({ reason, endpoint: endpointLabel, httpStatus, serverMessage })
|
|
254
|
+
: serverMessage;
|
|
136
255
|
const error = new Error(redactSecret(message));
|
|
137
|
-
error.
|
|
138
|
-
error.
|
|
139
|
-
error.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
256
|
+
error.kind = classifyApiError({ response, json, message, reason });
|
|
257
|
+
error.httpStatus = httpStatus;
|
|
258
|
+
if (businessStatus) error.businessCode = businessStatus;
|
|
259
|
+
if (reason) error.reason = reason;
|
|
260
|
+
error.endpoint = endpointLabel;
|
|
261
|
+
error.attempts = attempt + 1;
|
|
262
|
+
// 诊断信息只留结构化 JSON 片段,HTML 错误页不入库。
|
|
263
|
+
error.responseBody = isMarkupText(text) ? undefined : redactSecret(String(text || '').slice(0, 2000));
|
|
264
|
+
error.retryable = error.kind === 'upstream' && (
|
|
265
|
+
TRANSIENT_REASONS.has(reason) ||
|
|
266
|
+
json.retryable === true ||
|
|
267
|
+
Number(response.status) >= 500 ||
|
|
268
|
+
businessStatus === 301 ||
|
|
269
|
+
/collect failed|send request again|this operation was aborted|timeout|超时/i.test(serverMessage)
|
|
270
|
+
);
|
|
271
|
+
if (error.retryable && attempt < retries) {
|
|
143
272
|
lastError = error;
|
|
144
273
|
await delay(400 * (attempt + 1));
|
|
145
274
|
continue;
|
|
@@ -162,7 +291,8 @@ async function callSocialGateway({ proxyPath, method = 'GET', query = {}, body,
|
|
|
162
291
|
method: upper,
|
|
163
292
|
body: upper === 'GET' ? undefined : body,
|
|
164
293
|
retries,
|
|
165
|
-
headers
|
|
294
|
+
headers,
|
|
295
|
+
endpoint: proxyPath
|
|
166
296
|
};
|
|
167
297
|
try {
|
|
168
298
|
const result = await requestJson(url, token, requestOptions);
|
|
@@ -2,13 +2,15 @@ const { callSocialGateway } = require('./voc-gateway');
|
|
|
2
2
|
|
|
3
3
|
const VOC_SOCIAL_API_ROOT = 'https://server.fmode.cn/api/voc-social';
|
|
4
4
|
const XIAOHONGSHU_API_ROOT = `${VOC_SOCIAL_API_ROOT}/xiaohongshu`;
|
|
5
|
-
const XIAOHONGSHU_APP_BASE_URL = `${XIAOHONGSHU_API_ROOT}/app`;
|
|
6
5
|
|
|
6
|
+
// 只保留实测可用的路径。历史回退路径 web_v3/fetch_search_notes 与 app/get_note_info
|
|
7
|
+
// 已于数据服务侧下线(实测稳定返回 404 Not Found),继续留着只会把「路径不存在」
|
|
8
|
+
// 伪装成「平台不可用」,故整体移除。
|
|
7
9
|
const SEARCH_APP_V2_PATH = 'app_v2/search_notes';
|
|
8
|
-
const SEARCH_WEB_V3_PATH = 'web_v3/fetch_search_notes';
|
|
9
10
|
const DETAIL_IMAGE_PATH = 'app_v2/get_image_note_detail';
|
|
10
11
|
const DETAIL_VIDEO_PATH = 'app_v2/get_video_note_detail';
|
|
11
|
-
|
|
12
|
+
// 详情回退:web_v3/fetch_note_detail 实测可用,但需要 xsec_token(笔记分享链里带)。
|
|
13
|
+
const DETAIL_WEB_V3_PATH = 'web_v3/fetch_note_detail';
|
|
12
14
|
const COMMENTS_PATH = 'app_v2/get_note_comments';
|
|
13
15
|
|
|
14
16
|
function normalizeXiaohongshuBaseUrl(baseUrl) {
|
|
@@ -40,13 +42,6 @@ function mapAppV2NoteType(noteType) {
|
|
|
40
42
|
return value;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
|
-
function mapWebV3NoteType(noteType) {
|
|
44
|
-
const value = String(noteType == null ? '_0' : noteType).trim();
|
|
45
|
-
if (value === '_1' || value === '1' || value === '视频' || value === '视频笔记' || value === 'video') return 1;
|
|
46
|
-
if (value === '_2' || value === '2' || value === '图文' || value === '普通笔记' || value === 'normal') return 2;
|
|
47
|
-
return 0;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
45
|
function extractSearchItems(data) {
|
|
51
46
|
if (!data || typeof data !== 'object') return [];
|
|
52
47
|
const candidates = [data.items, data.note_list, data.notes, data.data && data.data.items];
|
|
@@ -99,52 +94,26 @@ class XiaohongshuApi {
|
|
|
99
94
|
searchId = '',
|
|
100
95
|
sessionId = ''
|
|
101
96
|
}) {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
sort_type: sortType,
|
|
111
|
-
note_type: mapAppV2NoteType(noteType),
|
|
112
|
-
search_id: searchId,
|
|
113
|
-
search_session_id: sessionId
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
if (extractSearchItems(result.data).length > 0) {
|
|
117
|
-
return {
|
|
118
|
-
...result,
|
|
119
|
-
endpoint: SEARCH_APP_V2_PATH,
|
|
120
|
-
fallbackUsedForEndpoint: false
|
|
121
|
-
};
|
|
97
|
+
const result = await this.request(SEARCH_APP_V2_PATH, {
|
|
98
|
+
query: {
|
|
99
|
+
keyword,
|
|
100
|
+
page,
|
|
101
|
+
sort_type: mapSortType(sort),
|
|
102
|
+
note_type: mapAppV2NoteType(noteType),
|
|
103
|
+
search_id: searchId,
|
|
104
|
+
search_session_id: sessionId
|
|
122
105
|
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
keyword,
|
|
132
|
-
page,
|
|
133
|
-
sort: sortType,
|
|
134
|
-
note_type: mapWebV3NoteType(noteType)
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
return {
|
|
138
|
-
...result,
|
|
139
|
-
endpoint: SEARCH_WEB_V3_PATH,
|
|
140
|
-
fallbackUsedForEndpoint: true
|
|
141
|
-
};
|
|
142
|
-
} catch (error) {
|
|
143
|
-
throw firstError || error;
|
|
144
|
-
}
|
|
106
|
+
});
|
|
107
|
+
// 空结果就是空结果:不再降级到已下线的 web_v3 搜索路径,否则一次空结果会
|
|
108
|
+
// 触发一个必然 404 的请求,把「这个词没笔记」误报成「搜索能力故障」。
|
|
109
|
+
return {
|
|
110
|
+
...result,
|
|
111
|
+
endpoint: SEARCH_APP_V2_PATH,
|
|
112
|
+
fallbackUsedForEndpoint: false
|
|
113
|
+
};
|
|
145
114
|
}
|
|
146
115
|
|
|
147
|
-
async getNoteDetail({ noteId, noteType = '', shareText = '' }) {
|
|
116
|
+
async getNoteDetail({ noteId, noteType = '', shareText = '', xsecToken = '' }) {
|
|
148
117
|
const query = {
|
|
149
118
|
note_id: noteId,
|
|
150
119
|
share_text: shareText
|
|
@@ -170,21 +139,24 @@ class XiaohongshuApi {
|
|
|
170
139
|
}
|
|
171
140
|
}
|
|
172
141
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
142
|
+
// 详情回退只在拿到 xsec_token(分享链里带)时才走 web_v3,该接口实测可用;
|
|
143
|
+
// 没有 token 就如实抛出首个错误,不做注定失败的尝试。
|
|
144
|
+
if (xsecToken) {
|
|
145
|
+
try {
|
|
146
|
+
const result = await this.request(DETAIL_WEB_V3_PATH, {
|
|
147
|
+
query: { note_id: noteId, xsec_token: xsecToken }
|
|
148
|
+
});
|
|
149
|
+
return {
|
|
150
|
+
...result,
|
|
151
|
+
endpoint: DETAIL_WEB_V3_PATH,
|
|
152
|
+
fallbackUsedForEndpoint: true
|
|
153
|
+
};
|
|
154
|
+
} catch (error) {
|
|
155
|
+
throw lastError || error;
|
|
156
|
+
}
|
|
187
157
|
}
|
|
158
|
+
|
|
159
|
+
throw lastError || new Error(`未能获取笔记 ${noteId} 的详情`);
|
|
188
160
|
}
|
|
189
161
|
|
|
190
162
|
getNoteComments({ noteId, cursor = '', shareText = '', xsecToken = '' }) {
|
|
@@ -202,11 +174,10 @@ class XiaohongshuApi {
|
|
|
202
174
|
module.exports = {
|
|
203
175
|
VOC_SOCIAL_API_ROOT,
|
|
204
176
|
XIAOHONGSHU_API_ROOT,
|
|
205
|
-
XIAOHONGSHU_APP_BASE_URL,
|
|
206
177
|
SEARCH_APP_V2_PATH,
|
|
207
|
-
SEARCH_WEB_V3_PATH,
|
|
208
178
|
DETAIL_IMAGE_PATH,
|
|
209
179
|
DETAIL_VIDEO_PATH,
|
|
180
|
+
DETAIL_WEB_V3_PATH,
|
|
210
181
|
COMMENTS_PATH,
|
|
211
182
|
normalizeXiaohongshuBaseUrl,
|
|
212
183
|
extractSearchItems,
|
package/mcp/src/server.js
CHANGED
|
@@ -200,6 +200,35 @@ async function callVocApi(input = {}) {
|
|
|
200
200
|
}
|
|
201
201
|
if (endpoint) endpoint = hydrateEndpoint(endpoint);
|
|
202
202
|
|
|
203
|
+
// 已下架接口直接拦在调用前:这些路径在数据服务侧已返回 404,发出去只会得到
|
|
204
|
+
// 一次必然失败的请求,还会被读成「平台能力不可用」。直接给出替代接口。
|
|
205
|
+
if (endpoint && endpoint.deprecated) {
|
|
206
|
+
const replacement = endpoint.replacedBy
|
|
207
|
+
? `请改用 ${endpoint.replacedBy}(参数主要用于 keyword/page;可用 voc_api_doc 查看)。`
|
|
208
|
+
: '请用 voc_api_search 改选现用接口。';
|
|
209
|
+
return {
|
|
210
|
+
status: 'not_supported',
|
|
211
|
+
assistantMessage: [
|
|
212
|
+
`接口 ${endpoint.id} 已下架,本次未发起调用,也未产生任何费用。`,
|
|
213
|
+
'',
|
|
214
|
+
replacement,
|
|
215
|
+
'这不是余额、token 或关键词问题,也不影响同平台其他接口的可用性。'
|
|
216
|
+
].join('\n'),
|
|
217
|
+
summary: {
|
|
218
|
+
endpoint: endpoint.id,
|
|
219
|
+
proxyPath: endpoint.proxyPath,
|
|
220
|
+
errorKind: 'deprecated_endpoint',
|
|
221
|
+
replacedBy: endpoint.replacedBy || null,
|
|
222
|
+
httpStatus: 0
|
|
223
|
+
},
|
|
224
|
+
data: { endpoint: summarizeEndpoint(endpoint), replacedBy: endpoint.replacedBy || null },
|
|
225
|
+
files: [],
|
|
226
|
+
nextActions: endpoint.replacedBy ? [`改用 ${endpoint.replacedBy} 重试`] : ['用 voc_api_search 改选现用接口'],
|
|
227
|
+
warnings: [],
|
|
228
|
+
errors: []
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
203
232
|
// 计费优先走 NewAPI(fmode-api)的 sk- token;该 token 鉴权失败(含「未入仓」类报错)时
|
|
204
233
|
// 自动回退平台 sessionToken 重试。注意:以前因为 sk- 直接请求会命中「入仓」错误,这里曾经
|
|
205
234
|
// 直接拒绝 sk- token;现在改为「优先 sk-、鉴权失败再回退 sessionToken」,并对外只给友好提示。
|
|
@@ -421,6 +450,38 @@ async function callVocApi(input = {}) {
|
|
|
421
450
|
errors: [{ message: safeMessage, kind, httpStatus }]
|
|
422
451
|
};
|
|
423
452
|
}
|
|
453
|
+
if (kind === 'method') {
|
|
454
|
+
return {
|
|
455
|
+
status: 'needs_input',
|
|
456
|
+
assistantMessage: [
|
|
457
|
+
`接口 ${proxyPath} 不接受当前的 HTTP 方法(${method},HTTP 405)。`,
|
|
458
|
+
'',
|
|
459
|
+
'这是调用方式问题,不是平台能力问题,也不是余额或 token 问题。请按接口元数据里的 method 字段重发;如果目录里 method 缺失,说明该条元数据尚未补全。'
|
|
460
|
+
].join('\n'),
|
|
461
|
+
summary: { endpoint: endpoint?.id || proxyPath, channel, errorKind: kind, httpStatus },
|
|
462
|
+
data: { message: safeMessage, method },
|
|
463
|
+
files: [],
|
|
464
|
+
nextActions: [`按目录中的 method 字段重新调用(当前用了 ${method})`],
|
|
465
|
+
warnings: [],
|
|
466
|
+
errors: [{ message: safeMessage, kind, httpStatus }]
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
if (kind === 'not_found') {
|
|
470
|
+
return {
|
|
471
|
+
status: 'not_supported',
|
|
472
|
+
assistantMessage: [
|
|
473
|
+
`接口 ${proxyPath} 的路径在数据服务中不存在(HTTP 404),该路径很可能已经下线。`,
|
|
474
|
+
'',
|
|
475
|
+
'这是目录元数据过期,不代表该平台没有可用数据。请用 voc_api_search 重新查同平台的在售接口,不要据此判断「采集通道故障」。'
|
|
476
|
+
].join('\n'),
|
|
477
|
+
summary: { endpoint: endpoint?.id || proxyPath, channel, errorKind: kind, httpStatus },
|
|
478
|
+
data: { message: safeMessage },
|
|
479
|
+
files: [],
|
|
480
|
+
nextActions: ['用 voc_api_search 改选同平台在售接口', '确认目录版本是否为最新(voc_api_search 返回的版本号)'],
|
|
481
|
+
warnings: [],
|
|
482
|
+
errors: [{ message: safeMessage, kind, httpStatus }]
|
|
483
|
+
};
|
|
484
|
+
}
|
|
424
485
|
if (kind === 'unsupported') {
|
|
425
486
|
return {
|
|
426
487
|
status: 'not_supported',
|
|
@@ -436,16 +497,18 @@ async function callVocApi(input = {}) {
|
|
|
436
497
|
return {
|
|
437
498
|
status: 'upstream_unstable',
|
|
438
499
|
assistantMessage: [
|
|
439
|
-
`接口 ${proxyPath}
|
|
500
|
+
`接口 ${proxyPath} 本次调用没有成功:${safeMessage}`,
|
|
501
|
+
'',
|
|
502
|
+
'这是**平台侧瞬时故障**(例如请求落到了缺少该路由的节点、返回 HTML 错误页,或上游 5xx / 超时),工具已自动重试若干次。它不代表这个接口或这类数据不可用——同一接口重发通常就能成功。',
|
|
440
503
|
'',
|
|
441
|
-
'
|
|
504
|
+
'因此本轮结论不要写成「某平台采集通道故障」「平台不可用」这类判断。需要交付时,请先重试 1–2 次;仍失败再如实记录为「本次调用失败,能力本身可用」。'
|
|
442
505
|
].join('\n'),
|
|
443
|
-
summary: { endpoint: endpoint?.id || proxyPath, errorKind: kind, httpStatus },
|
|
444
|
-
data: { message: safeMessage },
|
|
506
|
+
summary: { endpoint: endpoint?.id || proxyPath, errorKind: kind, reason: error?.reason || null, retryable: error?.retryable !== false, httpStatus },
|
|
507
|
+
data: { message: safeMessage, reason: error?.reason || null },
|
|
445
508
|
files: [],
|
|
446
|
-
nextActions: ['
|
|
509
|
+
nextActions: ['稍后重试该接口调用(瞬时故障,重发通常即可成功)', '不要据此判断该平台无数据或能力下线'],
|
|
447
510
|
warnings: [],
|
|
448
|
-
errors: [{ message: safeMessage, kind, httpStatus }]
|
|
511
|
+
errors: [{ message: safeMessage, kind, httpStatus, reason: error?.reason || null }]
|
|
449
512
|
};
|
|
450
513
|
}
|
|
451
514
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vocmarket/voc-skill",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.43",
|
|
4
4
|
"description": "Claude Code VOC intelligence skills for Xiaohongshu, Douyin and WeChat Channels trend/content collection, live social VOC, Fmode image analysis, single-issue deep dives, boss/operator action plans, and multi-turn memory.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
|
@@ -176,10 +176,15 @@ VOC 后端是一个**通用转发网关**(`https://server.fmode.cn/api/voc-soc
|
|
|
176
176
|
|
|
177
177
|
### 单次失败时怎么处理
|
|
178
178
|
|
|
179
|
-
一次 404、500 或空结果,只能说明「这一次请求或这一条路径失败」,不能据此说小红书能力不可用。先用同一凭证调用 `xiaohongshu.app_v2.search_notes`
|
|
179
|
+
一次 404、500 或空结果,只能说明「这一次请求或这一条路径失败」,不能据此说小红书能力不可用。先用同一凭证调用 `xiaohongshu.app_v2.search_notes` 做一次最小探测;它成功就说明采集通道仍可用,然后按失败类型处理。
|
|
180
|
+
|
|
181
|
+
工具返回的 `summary.reason` 是网关给出的结构化失败原因,`summary.retryable` 表示是否值得重发——先看这两个字段,再决定怎么做:
|
|
180
182
|
|
|
181
183
|
| 看到的结果 | 实际含义 | 下一步 |
|
|
182
184
|
|---|---|---|
|
|
185
|
+
| `reason = upstream_html` / `upstream_invalid_body`,文案提到「HTML 错误页」 | 网关把请求打到了缺少该路由的节点,属于**瞬时**故障 | 原参数重发 1–2 次。绝不要写成「某采集通道故障」或「平台不可用」 |
|
|
186
|
+
| `kind = not_found`(404),或工具直接返回 `not_supported` + `replacedBy` | 该路径已下线 / 目录元数据过期 | 按 `replacedBy` 改用现用接口。不要据此说平台没有此能力 |
|
|
187
|
+
| `kind = method`(405) | 该路径不接受当前 HTTP 方法 | 按目录里的 `method` 字段重发,不要换路径碰运气 |
|
|
183
188
|
| 404,或路径是自己拼的 `get_user_note_list`、无版本号路径 | 路径不存在,不是平台没有此能力 | 回到本表选择现用 `id`。达人主页改用 `xiaohongshu.app_v2.get_user_posted_notes` |
|
|
184
189
|
| 500,且路径包含旧的 `xiaohongshu/app/` | 旧接口失效 | 改用同用途的 `app_v2` 接口,不要把旧路径报告给用户 |
|
|
185
190
|
| `needs_input`、参数错误 | 入参不完整或类型不对 | 用 `voc_api_doc` 核对。不要改关键词,也不要换一条路径碰运气 |
|
|
@@ -190,6 +195,8 @@ VOC 后端是一个**通用转发网关**(`https://server.fmode.cn/api/voc-soc
|
|
|
190
195
|
| `needs_token`、`needs_valid_token` | 凭证没读到或无效 | 按错误码速查自救,不要说功能下线 |
|
|
191
196
|
| 只有 HTTP 402 | 当前 VOC 账户额度不足 | 转述充值入口。内层业务码或其它账户的额度问题不能混报成用户余额不足 |
|
|
192
197
|
|
|
198
|
+
已确认下线的旧路径(不要再调用):`web_v3/fetch_search_notes`、`web_v3/fetch_search_users`、`web_v3/fetch_note_comments`、`web_v3/fetch_sub_comments`、`web_v3/fetch_user_notes`、`web_v3/fetch_trending`、`web_v3/fetch_hot_list`、整个 `web_v2/*` 家族,以及 `app/get_note_info`。它们的现用替代是 `app_v2/*` 与 `web_v3/fetch_homefeed`、`hot_search_v1`;目录里已标记 `deprecated` 并带 `replacedBy`。
|
|
199
|
+
|
|
193
200
|
探测请求也算一次调用。通道确认可用后,不要重复探测;回到用户原始任务继续。若现用接口和清单里的替代接口都按文档重试后仍失败,才说明是本次采集失败,并保留工具返回的 `status`,不能扩大成「小红书不可用」。
|
|
194
201
|
|
|
195
202
|
## 创作者侧情报(channel=ecommerce)
|
|
@@ -261,10 +268,10 @@ VOC 后端是一个**通用转发网关**(`https://server.fmode.cn/api/voc-soc
|
|
|
261
268
|
- `needs_token`:没读到任何 token(**可恢复,不是没钱、不是功能用不了**)——优先去 `~/.claude/settings.json` 取 `env.ANTHROPIC_AUTH_TOKEN`(`sk-`)用 `FMODE_API_KEY=sk-…` 传入重试。`needs_valid_token`:token 缺失/失效/无效(HTTP 401)——**不是没钱**,提示配置有效 `sk-`(或回退 `r:`)token。
|
|
262
269
|
- `needs_recharge`:**仅 HTTP 402(余额不足)**,透传工具生成的 Tokenized Balance 链接或二维码支付结果;Balance 链接格式为 `https://app.fmode.cn/dev/studio/balance/?token=USER_SESSION_TOKEN`。需要直接出二维码时,调用 `voc_billing_query` 时传 `createPaymentQr=true`,由技能包复用现有 `pay_code2` 链路。
|
|
263
270
|
- `needs_permission`:**HTTP 403(账号禁用/无权限)**,**≠ 余额不足**,不引导充值,提示联系服务方确认权限。
|
|
264
|
-
- `needs_input
|
|
271
|
+
- `needs_input`:必填参数缺失、参数错误,或 HTTP 405(方法不对,看 `summary.errorKind=method`)——不是类目/余额问题,也不要换路径碰运气。
|
|
265
272
|
- `stale_data`:接口有返回,但 `recordTime` 已超过该接口的时效门槛;只能作为历史参考。
|
|
266
|
-
- `not_supported`:业务码 202
|
|
267
|
-
- `upstream_unstable
|
|
273
|
+
- `not_supported`:业务码 202(当前商品或数据口径不受支持),或命中了已下架的接口(`summary.errorKind=deprecated_endpoint`,会带 `replacedBy`)——本次**未发起调用、未产生费用**,按 `replacedBy` 改接口即可,不代表平台没有数据。
|
|
274
|
+
- `upstream_unstable`:**平台侧瞬时故障**(5xx / 超时 / `reason=upstream_html` 的 HTML 错误页)。已自动重试,稍后用原参数重发通常即可成功;不要据此断言「该平台采集通道故障」。
|
|
268
275
|
|
|
269
276
|
> ⚠️ 不要把 **403/401** 一律说成「余额不足」。只有 **402** 才是真的没钱要充值;403 是权限、401 是 token 问题。
|
|
270
277
|
|
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
| 402 | `余额不足` / `额度不足` | `billing` | `needs_recharge` | 「余额不足,请充值」 | 透传工具生成的 Tokenized Balance 链接或二维码支付结果 |
|
|
18
18
|
| 403 | `用户禁用` / `无权限` / `permission` / `未开通` / `没开通` / `余额` / `额度` / `insufficient` / `balance` | `permission` | `needs_permission` | 「账号被禁用或没有该接口权限,**≠ 余额不足**」 | 联系服务方确认账号状态/接口权限,**不要**引导充值;只有 HTTP 402 才走充值 |
|
|
19
19
|
| 400 / 422 | `参数` / `入参` / `keyword` / `bad request` | `request` | `needs_input` | 「参数有误,**不是没数据/不是类目不支持**」 | 用 `voc_api_doc` 核对参数后重试 |
|
|
20
|
+
| 405 | `不接受 POST 请求` / `Method Not Allowed` | `method` | `needs_input` | 「调用方式不对,**不是平台没有这个能力**」 | 按目录里的 `method` 字段重发,不要换路径碰运气 |
|
|
21
|
+
| 404 | `在上游不存在` / `Not Found` | `not_found` | `not_supported`(带 `replacedBy`) | 「该路径已下线,**不是该平台没有数据**」 | 按 `replacedBy` 改用现用接口;目录里 `deprecated` 的条目不要调用 |
|
|
22
|
+
| 503 / 返回 HTML 错误页 | `返回了 HTML 错误页` / 响应体以 `<!DOCTYPE` 开头 | `upstream`(`reason=upstream_html`) | `upstream_unstable` | 「平台侧瞬时故障,接口本身可用」 | 原参数重发 1–2 次。**禁止**写成「采集通道故障 / 平台不可用」 |
|
|
20
23
|
| 5xx / 连接失败 | `fetch failed` / 超时 / `500` | `upstream` | `upstream_unstable` | 「上游接口波动」 | 稍后重试,不要误报成关键词/余额问题 |
|
|
21
24
|
|
|
22
25
|
## 二、真实报文样例(实打抓取)
|
|
@@ -18,9 +18,9 @@ live 模式默认保持小规模:
|
|
|
18
18
|
|
|
19
19
|
live 采集只走健康的 V2。指定达人、话题或商品等精确任务,先按 voc-api-catalog 的「小红书现用接口」选择链路,不要猜路径:
|
|
20
20
|
|
|
21
|
-
- 关键词样本:`app_v2/search_notes
|
|
21
|
+
- 关键词样本:`app_v2/search_notes`。空结果就是该词没有匹配笔记,如实记录,不要再降级到已下线的 `web_v3/fetch_search_notes`(该路径稳定 404)。这只是搜索结果,不是达人主页全量。
|
|
22
22
|
- 达人主页:只有昵称时先用 `app_v2/search_users` 获取 24 位 hex `userid`,再调用 `app_v2/get_user_posted_notes` 并按 `cursor` 翻页。
|
|
23
|
-
- 详情:`type` 含 `video` 时用 `app_v2/get_video_note_detail`,否则用 `app_v2/get_image_note_detail
|
|
23
|
+
- 详情:`type` 含 `video` 时用 `app_v2/get_video_note_detail`,否则用 `app_v2/get_image_note_detail`;两条都取不到且结果里带 `xsec_token` 时,用 `web_v3/fetch_note_detail` 兜底。已下线的 `app/get_note_info` 不再调用。
|
|
24
24
|
- 评论:一级评论用 `app_v2/get_note_comments`;`sub_comment_count > 0` 时,再用该评论的 `id` 调用 `app_v2/get_note_sub_comments`。
|
|
25
25
|
- 话题:从笔记详情的话题字段取得 `page_id` 后,依次调用 `app_v2/get_topic_info` 和 `app_v2/get_topic_feed`。
|
|
26
26
|
|