@vocmarket/voc-skill 0.3.40 → 0.3.42

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/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.40",
4
+ "version": "0.3.42",
5
5
  "author": {
6
6
  "name": "nkkj-BrainHack"
7
7
  }
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "1.6.0",
3
- "updatedAt": "2026-06-28",
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",
@@ -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 ? 'get_note_info' : 'app/v3/fetch_video_comments';
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
- function classifyApiError({ response, json, message }) {
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
- async function requestJson(url, token, { method = 'GET', body, retries = 3, headers = {} } = {}) {
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
- lastError = networkError;
193
+ // 网络层失败基本都是瞬时故障:重试耗尽后给结论,不把 undici 的原文当结论抛出去。
119
194
  if (attempt < retries) {
120
195
  await delay(400 * (attempt + 1));
121
196
  continue;
122
197
  }
123
- const error = new Error(redactSecret(networkError && networkError.message ? networkError.message : 'fetch failed'));
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
- if (!response.ok || (json && !isSuccessCode(json))) {
135
- const message = json?.message || json?.msg || json?.data?.msg || json?.data?.message || json?.mess || text || `HTTP ${response.status}`;
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.httpStatus = pickBusinessStatus(json) || response.status || 500;
138
- error.kind = classifyApiError({ response, json, message });
139
- error.responseBody = redactSecret(text);
140
- const retryableCollectionFailure = error.kind === 'upstream' &&
141
- (error.httpStatus === 301 || /collect failed|send request again/i.test(message));
142
- if ((error.httpStatus >= 500 || retryableCollectionFailure) && attempt < retries) {
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
- const DETAIL_APP_FALLBACK_PATH = 'app/get_note_info';
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 sortType = mapSortType(sort);
103
- let firstError;
104
-
105
- try {
106
- const result = await this.request(SEARCH_APP_V2_PATH, {
107
- query: {
108
- keyword,
109
- page,
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
- } catch (error) {
124
- if (!isRetryableUpstream(error)) throw error;
125
- firstError = error;
126
- }
127
-
128
- try {
129
- const result = await this.request(SEARCH_WEB_V3_PATH, {
130
- query: {
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
- try {
174
- const result = await this.request(DETAIL_APP_FALLBACK_PATH, {
175
- query: {
176
- note_id: noteId,
177
- share_text: shareText
178
- }
179
- });
180
- return {
181
- ...result,
182
- endpoint: DETAIL_APP_FALLBACK_PATH,
183
- fallbackUsedForEndpoint: true
184
- };
185
- } catch (error) {
186
- throw lastError || error;
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
@@ -66,7 +66,7 @@ function normalizeToolResult(result = {}) {
66
66
  function createServer() {
67
67
  const server = new McpServer({
68
68
  name: 'voc-intelligence',
69
- version: '0.3.40'
69
+ version: '0.3.42'
70
70
  });
71
71
 
72
72
  server.registerTool(
@@ -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} 这次调用失败,是上游数据接口返回错误或暂时不稳定(如 5xx、连接超时、fetch failed)。`,
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.40",
3
+ "version": "0.3.42",
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": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-voc-intelligence",
3
- "version": "0.3.40",
3
+ "version": "0.3.42",
4
4
  "capabilities": {
5
5
  "summary": "三类情报源同源接入:①社媒内容(含抖音/小红书/微信视频号等)②国内电商+创作者情报 ③海外选品。统一 search→doc→call、鉴权计费一致。",
6
6
  "channels": [
@@ -48,7 +48,34 @@ VOC 后端是一个**通用转发网关**(`https://server.fmode.cn/api/voc-soc
48
48
 
49
49
  ## 小红书现用接口(V2)
50
50
 
51
- 小红书社媒采集只走下面这些健康的 V2 接口。`voc_api_search` 会把它们排在前面;标了 `deprecated` 的旧接口不要调用,也不要在给用户的说明里提到。
51
+ 客户 Agent 处理小红书时,先按任务选接口,再用下表的 `id` 调 `voc_api_call`。不要手写未登记路径,不要给路径自行追加版本号,也不要把标了 `deprecated` 的旧接口告诉用户。
52
+
53
+ | 用户任务 | 第一步 | 下一步 | 不能下的结论 |
54
+ |---|---|---|---|
55
+ | 找某话题/关键词下的笔记 | `xiaohongshu.app_v2.search_notes` | 按 `type` 取详情,再取评论 | 这是搜索样本,不是达人主页全量 |
56
+ | 看某条笔记完整内容和素材 | 视频用 `xiaohongshu.app_v2.get_video_note_detail`,图文用 `xiaohongshu.app_v2.get_image_note_detail` | 用 `note_id` 拉评论 | 类型不明时先试视频,空结果再试图文 |
57
+ | 看用户怎么评价 | `xiaohongshu.note_comments` | 有回复时用 `xiaohongshu.sub_comments` | 只拉一级评论时,不能称已看完全部讨论 |
58
+ | 找某个达人/账号 | `xiaohongshu.app_v2.search_users` | 取 24 位 hex `userid` 后拉资料和主页 | 昵称搜索结果不是该账号全部笔记 |
59
+ | 拉达人主页历史 | `xiaohongshu.app_v2.get_user_posted_notes` | 翻完后再补详情和评论 | 关键词搜索不能代替主页列表 |
60
+ | 看话题内容 | 从笔记详情取得话题 `id` 后调用 `xiaohongshu.app_v2.get_topic_feed` | 话题详情接口当前会返回参数错误,不要用它判断平台是否可用 | 没有话题 `id` 时不能猜话题接口 |
61
+ | 找站内商品 | `xiaohongshu.app_v2.search_products` | 用返回的商品字段继续读取 | 这是商品搜索,不是笔记口碑 |
62
+ | 看蒲公英创作者经营数据 | `xiaohongshu.pgy.creator_search` | 用返回的 `userId` 补画像、粉丝、笔记绩效 | 这是创作者生态数据,不是投放采买建议 |
63
+
64
+ ### 字段怎么接
65
+
66
+ - 笔记:搜索或主页结果里的 `note.id` / `id` 作为后续 `note_id`;`type` 含 `video` 走视频详情,否则走图文详情。
67
+ - 评论:一级评论返回的 `comments[].id` 作为楼中楼的 `comment_id`。`sub_comment_count > 0` 才需要继续拉楼中楼。
68
+ - 用户:笔记结果使用 24 位 hex 的 `user.userid`;用户搜索结果使用 24 位 hex 的 `users[].id`。数字 `red_id`、昵称、主页 URL 都不是 `user_id`。
69
+ - 话题:从笔记详情的 `topics` 或 `hash_tag` 读取 `id`,作为 `page_id`。搜索卡片里的展示标签不一定有这个 ID。
70
+ - 翻页字段必须原样透传。笔记搜索用 `page`、`search_id`、`search_session_id`;达人主页先看顶层 `cursor`,没有时再用本页最后一条笔记的 `cursor`,并用 `has_more` 判断停止;电商主页备用接口用 `lastCursor`;楼中楼用 `start`。
71
+
72
+ ### 参数边界
73
+
74
+ - 笔记搜索:`sort_type` 只取 `general`、`time_descending`、`popularity_descending`;`note_type` 只取 `不限`、`视频笔记`、`普通笔记`。
75
+ - 话题流:`sort` 只取 `trend`、`time`。
76
+ - 详情、用户资料、达人主页都是 `note_id`/`user_id` 与 `share_text` 二选一,不能两个都空着。
77
+ - 一次调用只取一页。需要多关键词、多账号或持续翻页时,先用 `voc_cost_estimate` 估算,结束后用 `voc_usage_report` 核对。
78
+ - 空列表要保留为「本次查询无结果」。只有缺少必填参数时才是 `needs_input`,不要改关键词来掩盖参数错误。
52
79
 
53
80
  | 用途 | 接口 id | proxyPath | 必填 |
54
81
  |---|---|---|---|
@@ -57,6 +84,7 @@ VOC 后端是一个**通用转发网关**(`https://server.fmode.cn/api/voc-soc
57
84
  | 视频详情 | `xiaohongshu.app_v2.get_video_note_detail` | `xiaohongshu/app_v2/get_video_note_detail` | `note_id` 或 `share_text` |
58
85
  | 一级评论 | `xiaohongshu.note_comments` | `xiaohongshu/app_v2/get_note_comments` | `note_id` |
59
86
  | 楼中楼 | `xiaohongshu.sub_comments` | `xiaohongshu/app_v2/get_note_sub_comments` | `note_id` + `comment_id` |
87
+ | 用户搜索 | `xiaohongshu.app_v2.search_users` | `xiaohongshu/app_v2/search_users` | `keyword` |
60
88
  | 用户资料 | `xiaohongshu.app_v2.get_user_info` | `xiaohongshu/app_v2/get_user_info` | `user_id` |
61
89
  | 用户笔记 | `xiaohongshu.app_v2.get_user_posted_notes` | `xiaohongshu/app_v2/get_user_posted_notes` | `user_id` |
62
90
  | 话题详情 | `xiaohongshu.app_v2.get_topic_info` | `xiaohongshu/app_v2/get_topic_info` | `page_id` |
@@ -105,7 +133,7 @@ VOC 后端是一个**通用转发网关**(`https://server.fmode.cn/api/voc-soc
105
133
  { "id": "xiaohongshu.app_v2.get_user_info", "params": { "user_id": "<24位hex user_id>" } }
106
134
  ```
107
135
 
108
- 话题笔记先从笔记详情的话题字段拿 `page_id`,再拉话题信息和话题流。`sort` 只用 `trend` 或 `time`。
136
+ 话题内容从笔记详情的 `topics[].id` 读取,传给话题流的 `page_id`。`sort` 只用 `trend` 或 `time`。`xiaohongshu.app_v2.get_topic_info` 目前用该 ID 会返回参数错误,不能作为话题能力是否可用的依据;先使用话题流结果。
109
137
 
110
138
  ```json
111
139
  { "id": "xiaohongshu.app_v2.get_topic_info", "params": { "page_id": "<page_id>" } }
@@ -146,6 +174,31 @@ VOC 后端是一个**通用转发网关**(`https://server.fmode.cn/api/voc-soc
146
174
 
147
175
  一次只拉一页。批量拉多个达人、多个关键词或持续翻页前,先用 `voc_cost_estimate` 估算次数,跑完用 `voc_usage_report` 核对实际消耗。
148
176
 
177
+ ### 单次失败时怎么处理
178
+
179
+ 一次 404、500 或空结果,只能说明「这一次请求或这一条路径失败」,不能据此说小红书能力不可用。先用同一凭证调用 `xiaohongshu.app_v2.search_notes` 做一次最小探测;它成功就说明采集通道仍可用,然后按失败类型处理。
180
+
181
+ 工具返回的 `summary.reason` 是网关给出的结构化失败原因,`summary.retryable` 表示是否值得重发——先看这两个字段,再决定怎么做:
182
+
183
+ | 看到的结果 | 实际含义 | 下一步 |
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` 字段重发,不要换路径碰运气 |
188
+ | 404,或路径是自己拼的 `get_user_note_list`、无版本号路径 | 路径不存在,不是平台没有此能力 | 回到本表选择现用 `id`。达人主页改用 `xiaohongshu.app_v2.get_user_posted_notes` |
189
+ | 500,且路径包含旧的 `xiaohongshu/app/` | 旧接口失效 | 改用同用途的 `app_v2` 接口,不要把旧路径报告给用户 |
190
+ | `needs_input`、参数错误 | 入参不完整或类型不对 | 用 `voc_api_doc` 核对。不要改关键词,也不要换一条路径碰运气 |
191
+ | 视频详情无正文 | 笔记类型可能判断错 | 再用 `xiaohongshu.app_v2.get_image_note_detail` 试一次 |
192
+ | 详情或主页没有 `page_id` | 当前结果没带话题 ID | 到笔记详情的 `topics`、`hash_tag` 中提取;仍没有就说明本次不能做话题分析 |
193
+ | 空列表且参数正确 | 本次查询没有匹配数据 | 如实说无结果。不要声称接口损坏或该行业不支持 |
194
+ | `upstream_unstable`、5xx、超时 | 上游波动 | 用原参数重试,不要改成 WebSearch 或其它抓取方式 |
195
+ | `needs_token`、`needs_valid_token` | 凭证没读到或无效 | 按错误码速查自救,不要说功能下线 |
196
+ | 只有 HTTP 402 | 当前 VOC 账户额度不足 | 转述充值入口。内层业务码或其它账户的额度问题不能混报成用户余额不足 |
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
+
200
+ 探测请求也算一次调用。通道确认可用后,不要重复探测;回到用户原始任务继续。若现用接口和清单里的替代接口都按文档重试后仍失败,才说明是本次采集失败,并保留工具返回的 `status`,不能扩大成「小红书不可用」。
201
+
149
202
  ## 创作者侧情报(channel=ecommerce)
150
203
 
151
204
  抖音星图 / 小红书蒲公英(PGY)的创作者数据是 VOC 在**创作者生态**维度的情报源——它服务的是 VOC 一贯的「内容战略、竞对理解、受众洞察、选题与脚本」闭环,而**不是**「替用户挑达人投广告」。同一套数据用来回答这些 VOC 问题:
@@ -215,10 +268,10 @@ VOC 后端是一个**通用转发网关**(`https://server.fmode.cn/api/voc-soc
215
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。
216
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` 链路。
217
270
  - `needs_permission`:**HTTP 403(账号禁用/无权限)**,**≠ 余额不足**,不引导充值,提示联系服务方确认权限。
218
- - `needs_input`:必填参数缺失或参数错误(不是类目/余额问题)。
271
+ - `needs_input`:必填参数缺失、参数错误,或 HTTP 405(方法不对,看 `summary.errorKind=method`)——不是类目/余额问题,也不要换路径碰运气。
219
272
  - `stale_data`:接口有返回,但 `recordTime` 已超过该接口的时效门槛;只能作为历史参考。
220
- - `not_supported`:业务码 202,当前商品或数据口径不受该接口支持。
221
- - `upstream_unstable`:上游接口报错或波动(5xx / fetch failed),稍后重试。
273
+ - `not_supported`:业务码 202(当前商品或数据口径不受支持),或命中了已下架的接口(`summary.errorKind=deprecated_endpoint`,会带 `replacedBy`)——本次**未发起调用、未产生费用**,按 `replacedBy` 改接口即可,不代表平台没有数据。
274
+ - `upstream_unstable`:**平台侧瞬时故障**(5xx / 超时 / `reason=upstream_html` 的 HTML 错误页)。已自动重试,稍后用原参数重发通常即可成功;不要据此断言「该平台采集通道故障」。
222
275
 
223
276
  > ⚠️ 不要把 **403/401** 一律说成「余额不足」。只有 **402** 才是真的没钱要充值;403 是权限、401 是 token 问题。
224
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
  ## 二、真实报文样例(实打抓取)
@@ -71,6 +71,7 @@ node "<插件目录>/mcp/src/tools/xiaohongshu-trend-run.js" --collection-mode l
71
71
  - `references/user-workflow.md`:用户交互流程——第一次使用要问清的三件事、澄清话术、样例 vs 真实采集的判断。
72
72
  - `references/output-format.md`:第一轮趋势情报的推荐输出结构与段落格式。
73
73
  - `references/live-mode.md`:live 真实采集模式的接入说明与执行细节(先 sample 跑通再 live)。
74
+ - 指定达人主页、单条笔记、话题或商品时,读取 voc-api-catalog 的「小红书现用接口」:先按任务选接口,再按 `note_id`、24 位 `userid`、`comment_id`、`page_id` 和对应游标衔接。
74
75
  - 错误码 / 充值计费的完整权威对照见 voc-api-catalog 技能的 `references/error-codes.md`。
75
76
 
76
77
  ## 推荐用户启动话术
@@ -16,12 +16,15 @@ live 模式默认保持小规模:
16
16
 
17
17
  ## 接口优先级
18
18
 
19
- live 采集只走健康的 V2:
19
+ live 采集只走健康的 V2。指定达人、话题或商品等精确任务,先按 voc-api-catalog 的「小红书现用接口」选择链路,不要猜路径:
20
20
 
21
- - 搜索:`app_v2/search_notes`,空结果再降级 `web_v3/fetch_search_notes`
22
- - 达人主页:`app_v2/get_user_posted_notes`;只有昵称时先用 `app_v2/search_users` 拿 `user_id`
23
- - 详情:视频笔记用 `app_v2/get_video_note_detail`,图文笔记用 `app_v2/get_image_note_detail`
24
- - 评论:`app_v2/get_note_comments`
21
+ - 关键词样本:`app_v2/search_notes`。空结果就是该词没有匹配笔记,如实记录,不要再降级到已下线的 `web_v3/fetch_search_notes`(该路径稳定 404)。这只是搜索结果,不是达人主页全量。
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`;两条都取不到且结果里带 `xsec_token` 时,用 `web_v3/fetch_note_detail` 兜底。已下线的 `app/get_note_info` 不再调用。
24
+ - 评论:一级评论用 `app_v2/get_note_comments`;`sub_comment_count > 0` 时,再用该评论的 `id` 调用 `app_v2/get_note_sub_comments`。
25
+ - 话题:从笔记详情的话题字段取得 `page_id` 后,依次调用 `app_v2/get_topic_info` 和 `app_v2/get_topic_feed`。
26
+
27
+ 一条路径 404、500 或空结果,只说明该次请求或旧路径失败。先按 voc-api-catalog 的失败处理表改用同用途的现用 V2 接口、核对参数或原参重试;`app_v2/search_notes` 仍能返回时,不得说小红书能力不可用。只有工具明确返回 `needs_token`、`needs_valid_token` 或 HTTP 402 时,才按对应的凭证或充值流程处理。
25
28
 
26
29
  ## 媒体持久化
27
30