@tencent-rtc/mcp 1.6.8 → 1.7.0

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.
Files changed (75) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +53 -39
  3. package/knowledge/build-index.js +30 -6
  4. package/knowledge/chunker.js +82 -14
  5. package/knowledge/chunking-policy.js +100 -0
  6. package/knowledge/content-loader.js +3 -3
  7. package/knowledge/frontmatter-meta.js +167 -0
  8. package/knowledge/path-meta.js +28 -6
  9. package/knowledge/search/exact-lookup.js +9 -227
  10. package/knowledge/search/filter-pool.js +12 -29
  11. package/knowledge/search/framework-fallback.js +22 -0
  12. package/knowledge/search/intent-router.js +13 -0
  13. package/knowledge/search/query-normalizer.js +56 -3
  14. package/knowledge/search/search-core.js +391 -0
  15. package/knowledge/search/search-knowledge.js +292 -363
  16. package/knowledge/search/search-message.js +87 -0
  17. package/knowledge/search/search-pass.js +428 -0
  18. package/knowledge/search/search-types.js +1 -0
  19. package/knowledge/search/url-row-scorer.js +258 -0
  20. package/package.json +1 -1
  21. package/resource/_schema/feature-aliases.yaml +1 -1
  22. package/resource/index/manifest.json +1 -1
  23. package/resource/knowledge/callkit/best-practice/callkit-miniprogram/uniapp-shrink.md +60 -0
  24. package/resource/knowledge/callkit/best-practice/common/call-autoplay-blocked.md +77 -0
  25. package/resource/knowledge/callkit/best-practice/common/call-message-in-chat.md +78 -0
  26. package/resource/knowledge/callkit/best-practice/common/call-vs-calls.md +110 -0
  27. package/resource/knowledge/callkit/best-practice/common/callengine-call-flow.md +123 -0
  28. package/resource/knowledge/callkit/best-practice/common/callengine-log-level.md +67 -0
  29. package/resource/knowledge/callkit/best-practice/common/callkit-get-engine-instance.md +81 -0
  30. package/resource/knowledge/callkit/best-practice/common/callkit-get-roomid.md +78 -0
  31. package/resource/knowledge/callkit/best-practice/common/callkit-limitations.md +37 -0
  32. package/resource/knowledge/callkit/best-practice/common/callkit-user-register-and-kicked.md +98 -0
  33. package/resource/knowledge/chat/best-practice/common/avchatroom-message-loss-diagnosis.md +65 -0
  34. package/resource/knowledge/chat/best-practice/common/group-conversation-cleanup-on-system-notice.md +62 -0
  35. package/resource/knowledge/chat/best-practice/common/image-message-url-lifecycle.md +59 -0
  36. package/resource/knowledge/chat/best-practice/common/login-repeat-2025-diagnosis.md +61 -0
  37. package/resource/knowledge/chat/best-practice/common/message-read-report-and-receipt.md +64 -0
  38. package/resource/knowledge/chat/best-practice/common/message-receive-event.md +65 -0
  39. package/resource/knowledge/chat/best-practice/common/message-risk-error-and-render.md +65 -0
  40. package/resource/knowledge/chat/best-practice/common/message-unread-count.md +71 -0
  41. package/resource/knowledge/chat/best-practice/miniprogram/audio-message-send.md +113 -0
  42. package/resource/knowledge/chat/best-practice/miniprogram/login-timeout-2801-diagnosis.md +63 -0
  43. package/resource/knowledge/chat/best-practice/miniprogram/media-upload-fail-diagnosis.md +64 -0
  44. package/resource/knowledge/chat/best-practice/web/audio-message-send.md +118 -0
  45. package/resource/knowledge/chat/urls/sdk/web.md +1 -1
  46. package/resource/knowledge/livekit/best-practice/android/voice-room-anchor-start.md +120 -0
  47. package/resource/knowledge/livekit/best-practice/common/admin-take-seat.md +73 -0
  48. package/resource/knowledge/livekit/best-practice/common/audience-leave-live.md +81 -0
  49. package/resource/knowledge/livekit/best-practice/common/voice-room-no-audio-troubleshoot.md +100 -0
  50. package/resource/knowledge/livekit/best-practice/common/voice-room-seat-list-change.md +65 -0
  51. package/resource/knowledge/livekit/best-practice/flutter/voice-room-anchor-start.md +122 -0
  52. package/resource/knowledge/livekit/best-practice/ios/voice-room-anchor-start.md +126 -0
  53. package/resource/knowledge/roomkit/best-practice/common/room-ghost-member.md +70 -0
  54. package/resource/knowledge/roomkit/best-practice/common/room-limit-and-multi.md +83 -0
  55. package/resource/knowledge/roomkit/best-practice/common/room-screen-share-preview.md +83 -0
  56. package/resource/knowledge/roomkit/best-practice/common/room-status-after-all-leave.md +69 -0
  57. package/resource/knowledge/roomkit/best-practice/vue/room-uikit-vs-atomicx.md +111 -0
  58. package/resource/knowledge/rtcengine/web/plugin/30-advanced-cross-room-link.md +2 -0
  59. package/resource/knowledge/rtcengine/web/plugin/39-advanced-video-decoder.md +2 -0
  60. package/server.js +27 -22
  61. package/tools/common/finalize_answer.js +9 -1
  62. package/tools/common/search_trtc_knowledge.js +1 -1
  63. package/tools/definitions/finalize_answer_def.js +2 -2
  64. package/tools/definitions/integration-boundary.js +2 -1
  65. package/tools/definitions/search_trtc_knowledge_def.js +1 -1
  66. package/utils/report-cls-client.js +1 -1
  67. package/utils/report-es-client.js +1 -1
  68. package/cursor-setting.png +0 -0
  69. package/mcp-server-prompt.png +0 -0
  70. package/resource/knowledge/callkit/best-practice/web/callengine-invitation.md +0 -80
  71. package/resource/knowledge/callkit/best-practice/web/callengine-log-control.md +0 -24
  72. package/resource/knowledge/callkit/best-practice/web/callkit-call-vs-calls.md +0 -116
  73. package/resource/knowledge/callkit/preset/react/uikit-api-reference.md +0 -444
  74. package/resource/knowledge/callkit/preset/vue/uikit-api-reference.md +0 -464
  75. package/resource/knowledge/chat/best-practice/web/image-message-url-handling.md +0 -16
@@ -110,7 +110,9 @@ export function inferPathMeta(relativeSource) {
110
110
  // 两者都统一标记为 types=['faq'],便于通过 types=faq 命中“经验类问答”。
111
111
  if (parts[1] === 'chat' && parts[2] === 'best-practice' && parts.length >= 5) {
112
112
  const fwSlug = normalizeFrameworkDir(parts[3]);
113
- framework = [fwSlug];
113
+ // chat best-practice 的 common 目录表示平台通用(Web + 小程序),
114
+ // 不能按 framework=common 打标,否则会被 framework 硬过滤误排除。
115
+ framework = fwSlug === 'common' ? ['web', 'miniprogram'] : [fwSlug];
114
116
  types.push('faq');
115
117
  }
116
118
  if (parts[1] === 'chat' && parts[2] === 'faq' && parts.length >= 4) {
@@ -137,13 +139,13 @@ export function inferPathMeta(relativeSource) {
137
139
  }
138
140
  // callkit 最佳实践目录:knowledge/callkit/best-practice/{platform}/{topic}.md
139
141
  // 统一按 FAQ/经验类知识处理,支持 types=faq + frameworks={platform} 的精确检索。
142
+ // 与 chat best-practice 对齐:common 目录表示多平台通用,按最全集覆盖。
140
143
  if (parts[1] === 'callkit' && parts[2] === 'best-practice' && parts.length >= 5) {
141
- framework = [normalizeFrameworkDir(parts[3])];
144
+ const fwSlug = normalizeFrameworkDir(parts[3]);
145
+ framework = fwSlug === 'common'
146
+ ? ['react', 'vue', 'miniprogram', 'web', 'android', 'ios', 'flutter', 'uni-app']
147
+ : [fwSlug];
142
148
  types.push('faq');
143
- // calls 相关最佳实践既是 FAQ 也是 API 语义,避免 types=api 时被误过滤。
144
- if (/(callengine-invitation|callkit-call-vs-calls)\.md$/i.test(fileName)) {
145
- types.push('api');
146
- }
147
149
  }
148
150
  if (parts.includes('faq'))
149
151
  types.push('faq', 'error_code');
@@ -226,6 +228,15 @@ export function inferPathMeta(relativeSource) {
226
228
  types.push('component');
227
229
  if (fileName === 'faq.md')
228
230
  types.push('faq');
231
+ // livekit 最佳实践目录:knowledge/livekit/best-practice/{platform}/{topic}.md
232
+ // 与 chat/callkit 对齐:common 目录表示多平台通用,按最全集覆盖。
233
+ if (parts[1] === 'livekit' && parts[2] === 'best-practice' && parts.length >= 5) {
234
+ const fwSlug = normalizeFrameworkDir(parts[3]);
235
+ framework = fwSlug === 'common'
236
+ ? ['react', 'vue', 'web', 'android', 'ios', 'flutter']
237
+ : [fwSlug];
238
+ types.push('faq');
239
+ }
229
240
  // 与 roomkit 对齐:core-sdk 下 capability-matrix.md / unsupported-features.md
230
241
  // 视为能力边界文档(doc_kind + capability_status),方便检索层按 doc_kind 过滤
231
242
  const docMeta = inferDocMeta(fileName);
@@ -255,6 +266,15 @@ export function inferPathMeta(relativeSource) {
255
266
  types.push('feature');
256
267
  if (parts.includes('faq'))
257
268
  types.push('faq', 'error_code');
269
+ // roomkit 最佳实践目录:knowledge/roomkit/best-practice/{platform}/{topic}.md
270
+ // 与 chat/callkit 对齐:common 目录表示多平台通用,按最全集覆盖。
271
+ if (parts[1] === 'roomkit' && parts[2] === 'best-practice' && parts.length >= 5) {
272
+ const fwSlug = normalizeFrameworkDir(parts[3]);
273
+ framework = fwSlug === 'common'
274
+ ? ['react', 'vue', 'web', 'android', 'ios', 'flutter']
275
+ : [fwSlug];
276
+ types.push('faq');
277
+ }
258
278
  const docMeta = inferDocMeta(fileName);
259
279
  doc_kind = docMeta.doc_kind;
260
280
  capability_status = docMeta.capability_status ?? capability_status;
@@ -333,5 +353,7 @@ export function inferPathMeta(relativeSource) {
333
353
  doc_kind,
334
354
  capability_status,
335
355
  retrievable_scope,
356
+ meta_source: 'path',
357
+ meta_override_fields: [],
336
358
  };
337
359
  }
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { fileURLToPath } from 'url';
4
+ import { normalizeUrlRowQuery, scoreUrlRow } from './url-row-scorer.js';
4
5
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
5
6
  const SCHEMA_ROOT = path.resolve(__dirname, '../../resource/_schema');
6
7
  export const DOC_PATH_MAP = {
@@ -69,184 +70,6 @@ function loadJson(fileName, fallback) {
69
70
  return fallback;
70
71
  return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
71
72
  }
72
- const E7_EN_STOPWORDS = new Set([
73
- 'the', 'and', 'for', 'with', 'from', 'into', 'about', 'using', 'via',
74
- 'how', 'what', 'where', 'when', 'which', 'please', 'docs', 'documentation',
75
- 'tencent', 'im', 'rest', 'server', 'webhook', 'webhooks',
76
- ]);
77
- /** Generic terms — excluded from API-token hits to avoid false positives */
78
- const E7_GENERIC_TERMS = new Set([
79
- 'message', 'messages', 'group', 'groups', 'user', 'users', 'account', 'accounts',
80
- 'send', 'get', 'set', 'delete', 'create', 'update', 'query', 'before', 'after',
81
- 'webhook', 'friend', 'single', 'multiple', 'history', 'chat', 'sdk', 'api',
82
- 'profile', 'conversation', 'import', 'recall', 'modify', 'pull', 'mark', 'data',
83
- 'list', 'online', 'status', 'custom', 'attributes', 'counter', 'member', 'members',
84
- 'management', 'overview', 'integration', 'initialization', 'billing', 'topic',
85
- 'topics', 'community', 'robot', 'chatbot', 'streaming', 'broadcast', 'official',
86
- 'extension', 'receipt', 'receipts', 'global', 'mute', 'ban', 'unban', 'forbid',
87
- 'ordinary', 'specified', 'historical', 'unread', 'read', 'configure', 'configuration', 'invalidate',
88
- 'invalidating', 'disband', 'change', 'search', 'cloud', 'local', 'robot', 'bots',
89
- ]);
90
- const E7_QUERY_NOISE = /\b(rest\s*ful?\s*api|rest\s*api|server\s*api|webhook|webhooks|how\s+to|how\s+do\s+i|what\s+is|where\s+is|please|documentation|docs?)\b/gi;
91
- const E7_QUERY_NOISE_CN = /怎么|如何|什么|用哪个|查询|服务端/g;
92
- const E7_PHRASES = [
93
- 'before send one-to-one message',
94
- 'after send one-to-one message',
95
- 'one-to-one message',
96
- 'group message',
97
- 'recall group message',
98
- 'recall one-to-one message',
99
- 'send one-to-one message',
100
- 'send single chat message',
101
- 'getmessagelist',
102
- 'message history',
103
- 'add friend',
104
- 'import single account',
105
- 'import a single account',
106
- 'multi-device login',
107
- 'multi device login',
108
- 'login policy',
109
- 'login strategy',
110
- 'message storage duration',
111
- 'roaming message storage',
112
- ];
113
- function extractRestEndpoints(text) {
114
- return [...text.matchAll(/v4\/[\w/]+/gi)].map((m) => m[0].toLowerCase());
115
- }
116
- function extractE7ApiTokens(text) {
117
- const tokens = new Set();
118
- for (const match of text.matchAll(/\b([a-z][a-z0-9_]{2,})\b/gi)) {
119
- const token = match[1].toLowerCase();
120
- if (!E7_GENERIC_TERMS.has(token) && !E7_EN_STOPWORDS.has(token))
121
- tokens.add(token);
122
- }
123
- for (const match of text.matchAll(/\b([A-Z][A-Za-z0-9]*(?:\.[A-Za-z]\w*)?)\b/g)) {
124
- const token = match[1].toLowerCase();
125
- if (!E7_GENERIC_TERMS.has(token))
126
- tokens.add(token);
127
- }
128
- for (const match of text.matchAll(/([a-z]+[A-Z][A-Za-z0-9]*)/g)) {
129
- tokens.add(match[1].toLowerCase());
130
- }
131
- return [...tokens];
132
- }
133
- function normalizeE7Text(text) {
134
- return text
135
- .toLowerCase()
136
- .replace(E7_QUERY_NOISE, ' ')
137
- .replace(E7_QUERY_NOISE_CN, '')
138
- .replace(/[^\w\u4e00-\u9fff/]+/g, ' ')
139
- .replace(/\s+/g, ' ')
140
- .trim();
141
- }
142
- function phraseBonus(query, title) {
143
- const q = query.toLowerCase().replace(/\s+/g, ' ');
144
- const t = title.toLowerCase();
145
- let bonus = 0;
146
- for (const phrase of E7_PHRASES) {
147
- if (q.includes(phrase) && t.includes(phrase))
148
- bonus += 180;
149
- }
150
- if (/send single chat message/.test(q) && /send one-to-one message/.test(t))
151
- bonus += 200;
152
- if (/recall group message/.test(q) && /recall group message/.test(t))
153
- bonus += 200;
154
- if (/before send one-to-one message/.test(q) && /before send one-to-one message/.test(t))
155
- bonus += 220;
156
- if (/\bbefore\b/.test(q) && t.startsWith('before '))
157
- bonus += 120;
158
- if (/\bafter\b/.test(q) && t.startsWith('after '))
159
- bonus += 120;
160
- if (/多端登录/.test(query) && /多端登录|multi[\s-]?device/i.test(title))
161
- bonus += 220;
162
- if (/登录策略|login\s*(policy|strategy)/i.test(query) && /登录策略|login\s*(policy|strategy)/i.test(title))
163
- bonus += 200;
164
- return bonus;
165
- }
166
- function scoreE7UrlRow(query, title) {
167
- const queryEndpoints = extractRestEndpoints(query);
168
- const titleEndpoints = extractRestEndpoints(title);
169
- for (const qe of queryEndpoints) {
170
- if (titleEndpoints.some((te) => te === qe || te.endsWith(qe) || qe.endsWith(te))) {
171
- return 1000;
172
- }
173
- }
174
- let score = phraseBonus(query, title);
175
- const titleLower = title.toLowerCase();
176
- const queryApis = extractE7ApiTokens(query);
177
- let apiHits = 0;
178
- let longApiHit = false;
179
- for (const api of queryApis) {
180
- if (!titleLower.includes(api))
181
- continue;
182
- apiHits += 1;
183
- if (api.length >= 8)
184
- longApiHit = true;
185
- }
186
- if (longApiHit)
187
- score = Math.max(score, 940);
188
- else if (apiHits >= 2)
189
- score = Math.max(score, 900);
190
- else if (apiHits === 1)
191
- score = Math.max(score, 860);
192
- const qNorm = normalizeE7Text(query);
193
- const tNorm = normalizeE7Text(title);
194
- const qTokens = qNorm.split(' ').filter((t) => t.length >= 3 && !E7_GENERIC_TERMS.has(t));
195
- const tTokens = tNorm.split(' ').filter((t) => t.length >= 3 && !E7_GENERIC_TERMS.has(t));
196
- let overlap = 0;
197
- for (const qt of qTokens) {
198
- if (tTokens.some((tt) => tt.includes(qt) || qt.includes(tt)))
199
- overlap += 1;
200
- }
201
- if (overlap >= 3)
202
- score = Math.max(score, 720 + overlap * 15);
203
- else if (overlap >= 2)
204
- score = Math.max(score, 650);
205
- else if (overlap >= 1)
206
- score = Math.max(score, 520);
207
- const qCompact = qNorm.replace(/\s+/g, '');
208
- const tCompact = tNorm.replace(/\s+/g, '');
209
- if (qCompact.length >= 5 && tCompact.includes(qCompact)) {
210
- score = Math.max(score, 560 - Math.min(tCompact.length - qCompact.length, 80));
211
- }
212
- else if (tCompact.length >= 5 && qCompact.includes(tCompact)) {
213
- score = Math.max(score, 500 + Math.min(tCompact.length, 30));
214
- }
215
- const qCjk = query.replace(/[^\u4e00-\u9fff]/g, '');
216
- const tCjk = title.replace(/[^\u4e00-\u9fff]/g, '');
217
- if (qCjk.length >= 2 && tCjk.includes(qCjk)) {
218
- let cjkScore = 500;
219
- if (/批量|群组|群流|广播|导入|公众号|已读|回执/.test(tCjk) && !/批量|群组|群|已读|回执/.test(qCjk)) {
220
- cjkScore -= 250;
221
- }
222
- if (/^单发/.test(tCjk) && /发送|单聊/.test(qCjk))
223
- cjkScore += 120;
224
- score = Math.max(score, cjkScore);
225
- }
226
- if (score <= 0)
227
- return 0;
228
- const queryL = query.toLowerCase();
229
- const titleL = title.toLowerCase();
230
- if (/group/.test(queryL) && /one-to-one|c2c/.test(titleL) && !/group/.test(titleL))
231
- score -= 400;
232
- if (/one-to-one|c2c|single chat/.test(queryL) && /import.*account|add friend/.test(titleL))
233
- score -= 400;
234
- if (/send.*message|one-to-one message/.test(queryL) && /add friend|import.*account/.test(titleL))
235
- score -= 400;
236
- if (/getmessagelist|message history/.test(qCompact) && /integration|initialization/.test(titleL) && !/getmessagelist|history/.test(tCompact))
237
- score -= 400;
238
- if (/extension/.test(titleL) && !/extension/.test(queryL))
239
- score -= 350;
240
- if (/\bbefore\b/.test(queryL) && !/^before\b/.test(titleL.trim()))
241
- score -= 250;
242
- if (/\bafter\b/.test(queryL) && !/^after\b/.test(titleL.trim()))
243
- score -= 250;
244
- if (/batch|multiple/.test(titleL) && !/batch|multiple/.test(queryL))
245
- score -= 200;
246
- if (/group/.test(titleL) && !/group|channel/.test(queryL) && /one-to-one|c2c|single chat/.test(queryL))
247
- score -= 200;
248
- return Math.max(score, 0);
249
- }
250
73
  function loadFeatureAliases() {
251
74
  const filePath = path.join(SCHEMA_ROOT, 'feature-aliases.yaml');
252
75
  if (!fs.existsSync(filePath))
@@ -314,8 +137,6 @@ const ROOM_FEATURE_SPECS = [
314
137
  function detectRoomFeatureHits(prompt) {
315
138
  return ROOM_FEATURE_SPECS.filter((spec) => spec.promptPattern.test(prompt));
316
139
  }
317
- const LIVE_LIFECYCLE_PROMPT = /create\s*live|end\s*live|join\s*live|leave\s*live|start\s*live|stop\s*live|watch\s*live|host_streaming|audience_streaming|开播|创建直播|结束直播|停播|观看直播|退出直播|进房|离房|createLive|endLive|joinLive|leaveLive/i;
318
- const LIVE_LIFECYCLE_APIS = ['createLive', 'endLive', 'joinLive', 'leaveLive'];
319
140
  export function runExactLookup(prompt, pool, norm) {
320
141
  const boosts = {};
321
142
  const exactHits = [];
@@ -486,52 +307,35 @@ export function runExactLookup(prompt, pool, norm) {
486
307
  }
487
308
  }
488
309
  // E7: URL map row match (chunk.title = Query column); boost multiple strong hits with pinned relaxation
489
- if (normalizeE7Text(prompt).replace(/\s+/g, '').length >= 2) {
310
+ if (normalizeUrlRowQuery(prompt).replace(/\s+/g, '').length >= 2) {
490
311
  const candidates = [];
491
312
  for (const chunk of pool) {
492
313
  if (!chunk.source.includes('knowledge/chat/urls/'))
493
314
  continue;
494
315
  if (!chunk.title.trim())
495
316
  continue;
496
- const score = scoreE7UrlRow(prompt, chunk.title);
497
- if (score > 0) {
498
- candidates.push({ chunk, score });
317
+ const scoreResult = scoreUrlRow(prompt, chunk.title);
318
+ if (scoreResult.score > 0) {
319
+ candidates.push({ chunk, score: scoreResult.score });
499
320
  }
500
321
  }
501
322
  candidates.sort((a, b) => b.score - a.score);
502
323
  const strongCandidates = candidates
503
- .filter(({ score }) => score >= 150)
324
+ .filter(({ score }) => score >= 500)
504
325
  .slice(0, 12);
505
326
  strongCandidates.forEach(({ chunk, score }, idx) => {
327
+ // 递减 boost:前两条更高,其余保持可召回但不过度压制 BM25
328
+ // token 匹配(600 分)至少给 150,确保子句拆分的行不被 BM25 原始分压掉
506
329
  const amount = idx === 0
507
330
  ? 200 + Math.min(score, 100)
508
331
  : idx === 1
509
332
  ? 170 + Math.min(score, 90)
510
- : Math.max(score >= 300 ? 100 : 70, 100 - idx * 5);
333
+ : Math.max(score >= 600 ? 150 : 90, 140 - idx * 8);
511
334
  addBoost(chunk, amount, `E7:url-row:${chunk.title}`);
512
335
  urlRowHitChunkIds.add(chunk.id);
513
336
  });
514
337
  }
515
338
  }
516
- // E8: 集成查阅意图 boost——prompt 含集成/接入/快速开始/接入文档等意图词时,
517
- // 把 {integration|knowledge}/{product}/preset/{fw}/integration.md 系列文件顶到前列。
518
- // 解决问题:集成文档内容长、词密度低,BM25 原始分常被短小的 features/ 文档压过。
519
- // 注:android-view / ios-uikit 已迁到 knowledge corpus,但同样是集成指引,需要一并 boost。
520
- const isIntegrationIntent = /集成|接入|快速开始|快速接入|集成文档|接入文档|集成指南|接入指南|接入流程|集成步骤|如何接入|如何集成|怎么接入|怎么集成/i.test(prompt)
521
- || /\b(integrat(e|ion|ing)|getting\s+started|quick\s+start|setup\s+guide|how\s+to\s+(integrate|set\s+up)|install(?:ation)?\s+guide)\b/i.test(prompt);
522
- if (isIntegrationIntent && norm.product?.length && norm.frameworks?.length) {
523
- for (const chunk of pool) {
524
- if (!norm.product.includes(chunk.product))
525
- continue;
526
- const fwMatch = norm.frameworks.some((fw) => chunk.framework.includes(fw));
527
- if (!fwMatch)
528
- continue;
529
- // 命中 preset/{fw}/integration.md 系列(含 integration/ 和 knowledge/ 两种 corpus)
530
- if (!chunk.source.includes('/preset/') || (!chunk.source.endsWith('integration.md') && !chunk.source.endsWith('-integration.md')))
531
- continue;
532
- addBoost(chunk, 120, `E8:integration-doc:${chunk.source}`);
533
- }
534
- }
535
339
  // E9: chat faq — types=faq 时优先 knowledge/ 语料,避免 integration 文档内 FAQ 章节抢首位
536
340
  if (norm.searchIntent?.includes('faq') && norm.product?.includes('chat')) {
537
341
  const authPrompt = /auth|login|signature|usersig|鉴权|登录/i.test(prompt);
@@ -545,28 +349,6 @@ export function runExactLookup(prompt, pool, norm) {
545
349
  }
546
350
  }
547
351
  }
548
- // E11: LiveKit lifecycle strong recall
549
- if (norm.product?.includes('live') && LIVE_LIFECYCLE_PROMPT.test(prompt)) {
550
- const queryApiHits = LIVE_LIFECYCLE_APIS.filter((api) => new RegExp(api, 'i').test(prompt));
551
- for (const chunk of pool) {
552
- const isLiveCoreIntegration = chunk.source.startsWith('knowledge/livekit/core-sdk/live/')
553
- && chunk.source.endsWith('/integration.md');
554
- const isVoiceCoreIntegration = chunk.source.startsWith('knowledge/livekit/core-sdk/voice/')
555
- && chunk.source.endsWith('/integration.md');
556
- if (!isLiveCoreIntegration && !isVoiceCoreIntegration)
557
- continue;
558
- if (norm.scenario === 'live' && !isLiveCoreIntegration)
559
- continue;
560
- if (norm.scenario === 'voice' && !isVoiceCoreIntegration)
561
- continue;
562
- addBoost(chunk, 140, 'E11:live-lifecycle-core-integration');
563
- for (const api of queryApiHits) {
564
- if (chunk.api_names?.some((name) => name.toLowerCase() === api.toLowerCase())) {
565
- addBoost(chunk, 160, `E11:live-lifecycle-api:${api}`);
566
- }
567
- }
568
- }
569
- }
570
352
  // E10: RoomKit Core/Preset disambiguation + unsupported feature routing
571
353
  if (norm.product?.includes('room') && norm.frameworks?.length) {
572
354
  const platform = norm.frameworks.find((fw) => ['android', 'ios', 'flutter', 'vue'].includes(fw));
@@ -1,5 +1,6 @@
1
1
  import { downgradeToProductType } from './product-scope-intent.js';
2
2
  import { normalizeQueryPrompt } from './query-normalizer.js';
3
+ import { hasExplicitFrameworkMatch, hasFrameworkFallbackMatch } from './framework-fallback.js';
3
4
  /**
4
5
  * MCP framework enum → index 中对应的 framework 值列表。
5
6
  * path-meta.ts 已通过 normalizeFrameworkDir 把目录名归一到标准 framework,
@@ -42,14 +43,6 @@ function resolveTypes(products, types, prompt, frameworks) {
42
43
  return undefined;
43
44
  const downgraded = downgradeToProductType(products, types, prompt, frameworks);
44
45
  const expanded = expandTypeAliases(downgraded);
45
- // Live 生命周期兼容:用户常传 types=['api'],但核心生命周期方法来自 core-sdk/live|voice/*/integration.md。
46
- // 这里补充 core/integration,避免被类型硬过滤提前排除。
47
- if (products?.includes('live') && expanded.includes('api') && LIVE_LIFECYCLE_PROMPT.test(prompt)) {
48
- if (!expanded.includes('core'))
49
- expanded.push('core');
50
- if (!expanded.includes('integration'))
51
- expanded.push('integration');
52
- }
53
46
  return expanded;
54
47
  }
55
48
  /** 单平台产品的隐式 framework 推断:避免对显然唯一的平台再发起询问 */
@@ -133,33 +126,23 @@ export function filterPool(chunks, norm) {
133
126
  // 收紧:framework=['*'] 仅在该 chunk 明确是跨平台 type(product/server_api/webhook)时放行;
134
127
  // 否则要求 chunk.framework 与请求 framework 有交集。
135
128
  const wildcardAllowed = isWildcardChunk && chunkIsCrossPlatform(chunk.types);
136
- const explicitMatch = norm.frameworks.some((fw) => chunk.framework.includes(fw));
137
- // CallKit 特例:core-sdk 仍维持 web 单套(纯 JS),允许 react/vue 查询命中 core-sdk/web。
138
- const callCoreWebFallback = Boolean(norm.product?.includes('call')
139
- && (norm.framework === 'react' || norm.framework === 'vue')
140
- && chunk.variant === 'core-sdk'
141
- && chunk.framework.includes('web'));
142
- // Live 特例:写写侧把 'Web' 平台归到 livekit/core-sdk/api-reference/{vue,web} 目录(写写节点名是 'Web'),
143
- // 本地目录重命名为 'vue' 后 framework 标签变为 'vue'。'web' 查询仍应能命中这些 chunk。
144
- const liveWebVueFallback = Boolean(norm.product?.includes('live')
145
- && norm.framework === 'web'
146
- && chunk.variant === 'core-sdk'
147
- && chunk.framework.includes('vue')
148
- && chunk.source.includes('/api-reference/'));
149
- // Room special-case: Conference Web docs are under roomkit/core-sdk/vue.
150
- const roomWebVueFallback = Boolean(norm.product?.includes('room')
151
- && norm.framework === 'web'
152
- && chunk.variant === 'core-sdk'
153
- && chunk.framework.includes('vue')
154
- && chunk.source.includes('knowledge/roomkit/core-sdk/vue/'));
155
- if (!explicitMatch && !wildcardAllowed && !callCoreWebFallback && !liveWebVueFallback && !roomWebVueFallback)
129
+ const explicitMatch = hasExplicitFrameworkMatch(chunk.framework, norm.frameworks);
130
+ const fallbackMatch = hasFrameworkFallbackMatch(chunk, {
131
+ product: norm.product,
132
+ framework: norm.framework,
133
+ frameworks: norm.frameworks,
134
+ });
135
+ if (!explicitMatch && !wildcardAllowed && !fallbackMatch)
156
136
  return false;
157
137
  }
158
138
  if (norm.scenario && chunk.scenario && chunk.scenario !== norm.scenario) {
159
139
  return false;
160
140
  }
141
+ // best-practice 文档对 types 过滤免疫:内容不依赖 types 分类,应始终进 pool 靠 BM25 自然排名。
142
+ // 典型场景:Agent 传 types=['api'] 时,types=['faq'] 的 best-practice 不应被过滤掉。
161
143
  if (norm.types?.length && !norm.types.some((t) => chunk.types.includes(t))) {
162
- return false;
144
+ if (!chunk.source.includes('/best-practice/'))
145
+ return false;
163
146
  }
164
147
  return true;
165
148
  });
@@ -0,0 +1,22 @@
1
+ export function hasExplicitFrameworkMatch(chunkFrameworks, requestFrameworks) {
2
+ if (!requestFrameworks?.length)
3
+ return false;
4
+ return requestFrameworks.some((fw) => chunkFrameworks.includes(fw));
5
+ }
6
+ export function hasFrameworkFallbackMatch(chunk, opts) {
7
+ const callCoreWebFallback = Boolean(opts.product?.includes('call')
8
+ && (opts.framework === 'react' || opts.framework === 'vue')
9
+ && chunk.variant === 'core-sdk'
10
+ && chunk.framework.includes('web'));
11
+ const liveWebVueFallback = Boolean(opts.product?.includes('live')
12
+ && opts.framework === 'web'
13
+ && chunk.variant === 'core-sdk'
14
+ && chunk.framework.includes('vue')
15
+ && chunk.source.includes('/api-reference/'));
16
+ const roomWebVueFallback = Boolean(opts.product?.includes('room')
17
+ && opts.framework === 'web'
18
+ && chunk.variant === 'core-sdk'
19
+ && chunk.framework.includes('vue')
20
+ && chunk.source.includes('knowledge/roomkit/core-sdk/vue/'));
21
+ return callCoreWebFallback || liveWebVueFallback || roomWebVueFallback;
22
+ }
@@ -0,0 +1,13 @@
1
+ /** 枚举/目录浏览意图 */
2
+ const DISCOVERY_INTENT_RE = /有哪些|支持哪些|列一下|清单|目录|可用|全部|全量|对比|怎么选|选哪个|which\s+one|compare|comparison|what\s+(is|are)\s+available|what\s+.*\b(components?|features?|apis?)\b|available\s+(components?|features?|apis?)|supported\s+(components?|features?|apis?)|list\s+(all|available)|catalog/i;
3
+ /** 显式完整文档意图(用户明确说"完整/从零/step by step") */
4
+ const NARRATIVE_INTENT_RE = /从头到尾|一步一步|完整指南|完整教程|完整流程|完整接入|完整集成|完整的\s*(接入|集成|教程|指南|流程)|从零|全流程|接入指南|集成指南|step\s*by(?:\s*step)?|end[-\s]?to[-\s]?end|full\s+(guide|tutorial|flow|integration)|complete\s+(guide|tutorial|integration)|quick\s*start|getting\s*started/i;
5
+ /** 强 point 信号:提到具体 API/错误码/参数 → 即使同时命中 narrative 关键词也优先走 point */
6
+ const POINT_STRONG_SIGNAL_RE = /\b\d{5,7}\b|\bERR_[A-Z0-9_]+\b|\buse[A-Z]\w*\b|\b[A-Z][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*\b|\b(TRTC|TX)[A-Za-z0-9_]+\b|\bprops?\b|\b参数\b|\b事件\b|\b回调\b|\berror\s*code\b|错误码|怎么用|如何用|参数是什么/i;
7
+ export function classifyRetrievalMode(prompt) {
8
+ if (NARRATIVE_INTENT_RE.test(prompt) && !POINT_STRONG_SIGNAL_RE.test(prompt))
9
+ return 'narrative';
10
+ if (DISCOVERY_INTENT_RE.test(prompt))
11
+ return 'discovery';
12
+ return 'point';
13
+ }
@@ -5,6 +5,46 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
5
5
  const SCHEMA_ROOT = path.resolve(__dirname, '../../resource/_schema');
6
6
  const QUERY_NORMALIZATION_FILE = path.join(SCHEMA_ROOT, 'query-normalization.yaml');
7
7
  const MAX_APPEND_TOKENS = 6;
8
+ const BASE_QUERY_NORMALIZATION = {
9
+ literal: {
10
+ 登录: ['login', 'signin', 'authentication', 'auth'],
11
+ 鉴权: ['authentication', 'auth', 'usersig'],
12
+ 错误码: ['error', 'error code', 'code'],
13
+ 重复登录: ['repeat login', 'multi-device login', 'login policy'],
14
+ 单聊: ['one-to-one', 'c2c', 'single chat'],
15
+ 群聊: ['group chat', 'group message'],
16
+ 直播群: ['avchatroom'],
17
+ 直播间: ['avchatroom', 'live room'],
18
+ 丢消息: ['message loss', 'reliability', 'priority'],
19
+ 消息丢失: ['message loss', 'reliability', 'priority'],
20
+ 重要消息: ['priority', 'high priority', 'critical message'],
21
+ 用户: ['user', 'account'],
22
+ 消息: ['message', 'messages'],
23
+ userSig: ['usersig', 'auth', 'authentication'],
24
+ AVChatRoom: ['直播群'],
25
+ 'error code': ['错误码', '错误', 'code'],
26
+ 'repeat login': ['重复登录', '多端登录', 'login policy'],
27
+ 'one-to-one': ['单聊', 'c2c', 'single chat'],
28
+ },
29
+ word: {
30
+ login: ['登录', '鉴权', 'authentication', 'auth'],
31
+ auth: ['鉴权', '登录', 'authentication'],
32
+ authentication: ['鉴权', '登录', 'auth'],
33
+ usersig: ['userSig', '鉴权', '登录'],
34
+ error: ['错误', '错误码', 'error code'],
35
+ code: ['错误码', 'error code'],
36
+ message: ['消息', 'chat', 'conversation'],
37
+ messages: ['消息', 'chat', 'conversation'],
38
+ group: ['群组', '群聊', 'group chat'],
39
+ c2c: ['单聊', 'one-to-one', 'single chat'],
40
+ webhook: ['回调', 'webhook', 'callbacks'],
41
+ avchatroom: ['直播群'],
42
+ priority: ['重要消息', '优先级'],
43
+ reliability: ['消息可靠性'],
44
+ 'message loss': ['丢消息', '消息丢失'],
45
+ 'rate limit': ['限频', '刷屏'],
46
+ },
47
+ };
8
48
  let cachedConfig = null;
9
49
  function unquote(input) {
10
50
  return input.trim().replace(/^['"]|['"]$/g, '');
@@ -12,7 +52,10 @@ function unquote(input) {
12
52
  function loadQueryNormalizationConfig() {
13
53
  if (cachedConfig)
14
54
  return cachedConfig;
15
- const config = { literal: {}, word: {} };
55
+ const config = {
56
+ literal: { ...BASE_QUERY_NORMALIZATION.literal },
57
+ word: { ...BASE_QUERY_NORMALIZATION.word },
58
+ };
16
59
  if (!fs.existsSync(QUERY_NORMALIZATION_FILE)) {
17
60
  cachedConfig = config;
18
61
  return config;
@@ -46,6 +89,17 @@ function loadQueryNormalizationConfig() {
46
89
  function escapeRegExp(value) {
47
90
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
48
91
  }
92
+ function hasCjk(text) {
93
+ return /[\u4e00-\u9fff]/.test(text);
94
+ }
95
+ function hasWordMatch(prompt, promptLower, needle) {
96
+ const normalizedNeedle = needle.toLowerCase();
97
+ if (hasCjk(normalizedNeedle)) {
98
+ return prompt.includes(needle);
99
+ }
100
+ const pattern = new RegExp(`\\b${escapeRegExp(normalizedNeedle)}\\b`, 'i');
101
+ return pattern.test(promptLower);
102
+ }
49
103
  export function normalizeQueryPrompt(prompt) {
50
104
  const config = loadQueryNormalizationConfig();
51
105
  const terms = new Set(prompt.split(/\s+/).filter(Boolean));
@@ -67,8 +121,7 @@ export function normalizeQueryPrompt(prompt) {
67
121
  tryAppend(expansion);
68
122
  }
69
123
  for (const [needle, expansion] of Object.entries(config.word)) {
70
- const pattern = new RegExp(`\\b${escapeRegExp(needle.toLowerCase())}\\b`, 'i');
71
- if (!pattern.test(promptLower))
124
+ if (!hasWordMatch(prompt, promptLower, needle))
72
125
  continue;
73
126
  tryAppend(expansion);
74
127
  }