@tencentcloud/sdk-mcp 1.7.7 → 1.7.8

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 (63) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +7 -9
  3. package/index.js +8 -8
  4. package/knowledge/doc-url-resolver.js +16 -9
  5. package/knowledge/embedding/build-vectors.js +92 -0
  6. package/knowledge/embedding/embed-model.js +41 -0
  7. package/knowledge/embedding/vector-augment.js +126 -0
  8. package/knowledge/embedding/vector-index.js +70 -0
  9. package/knowledge/search/exact-lookup.js +145 -16
  10. package/knowledge/search/filter-pool.js +68 -32
  11. package/knowledge/search/requires-framework.js +1 -1
  12. package/knowledge/search/search-core.js +18 -12
  13. package/knowledge/search/search-knowledge.js +24 -34
  14. package/knowledge/search/search-pass.js +373 -168
  15. package/knowledge/search/url-row-scorer.js +24 -6
  16. package/package.json +3 -1
  17. package/resource/index/manifest.json +1 -1
  18. package/resource/index/vectors/index.json +4 -0
  19. package/resource/integration/chat/preset/react/integration.md +9 -5
  20. package/resource/integration/chat/preset/uni-app/integration.md +4 -0
  21. package/resource/integration/chat/preset/vue/integration.md +10 -6
  22. package/resource/integration/roomkit/preset/vue/integration.md +154 -208
  23. package/resource/knowledge/callkit/preset/miniprogram/integration.md +2 -1
  24. package/resource/knowledge/chat/preset/ios-uikit/integration.md +0 -16
  25. package/resource/knowledge/chat/preset/react/h5/integration.md +16 -5
  26. package/resource/knowledge/chat/preset/vue/features/conversation-list.md +6 -3
  27. package/resource/knowledge/chat/preset/vue/h5/integration.md +7 -4
  28. package/resource/knowledge/push/config/android/honor.md +16 -1
  29. package/resource/knowledge/push/config/android/huawei.md +20 -4
  30. package/resource/knowledge/push/config/android/meizu.md +19 -5
  31. package/resource/knowledge/push/config/android/vivo.md +20 -1
  32. package/resource/knowledge/push/config/harmonyos.md +21 -5
  33. package/resource/knowledge/push/integration/flutter.md +1 -1
  34. package/resource/knowledge/roomkit/preset/uniapp-app/integration.md +18 -14
  35. package/resource/knowledge/rtcengine/web/plugin/23-advanced-support-detection.md +30 -0
  36. package/resource/knowledge/rtcengine/web/plugin/31-advanced-plugins-overview.md +1 -1
  37. package/resource/knowledge/rtcengine/web/plugin/35-advanced-ai-denoiser.md +1 -1
  38. package/resource/knowledge/rtcengine/web/plugin/36-advanced-virtual-background.md +4 -0
  39. package/resource/knowledge/trtc/android/TRTCCloud.md +94 -85
  40. package/resource/knowledge/trtc/android/TRTCCloudDef.md +48 -46
  41. package/resource/knowledge/trtc/android/TRTCCloudListener.md +48 -48
  42. package/resource/knowledge/trtc/android/TXAudioEffectManager.md +18 -18
  43. package/resource/knowledge/trtc/android/TXDeviceManager.md +4 -4
  44. package/resource/knowledge/trtc/android/TXLiteAVCode.md +1 -0
  45. package/resource/knowledge/trtc/c++/ITRTCCloud.md +90 -90
  46. package/resource/knowledge/trtc/c++/ITXAudioEffectManager.md +18 -18
  47. package/resource/knowledge/trtc/c++/ITXDeviceManager.md +9 -9
  48. package/resource/knowledge/trtc/c++/TRTCCloudCallback.md +47 -47
  49. package/resource/knowledge/trtc/c++/TRTCTypeDef.md +47 -45
  50. package/resource/knowledge/trtc/c++/TXLiteAVCode.md +1 -0
  51. package/resource/knowledge/trtc/ios/TRTCCloud.md +93 -93
  52. package/resource/knowledge/trtc/ios/TRTCCloudDelegate.md +50 -50
  53. package/resource/knowledge/trtc/ios/TXAudioEffectManager.md +8 -8
  54. package/resource/knowledge/trtc/ios/TXDeviceManager.md +9 -9
  55. package/resource/knowledge/trtc/ios/TXLiteAVCode.md +1 -0
  56. package/resource/knowledge/trtc/ios/TXLiveSDKTypeDef.md +48 -46
  57. package/server-config.js +8 -9
  58. package/server.js +1 -1
  59. package/tools/common/present_framework_choice.js +1 -1
  60. package/tools/common/search_trtc_knowledge.js +20 -1
  61. package/utils/preflight-route.js +8 -0
  62. package/utils/report-cls-client.js +1 -1
  63. package/utils/report-es-client.js +1 -1
@@ -6,11 +6,17 @@ import { computeCommonSectionTokens, isUrlMappingSource, normalizeUrlRowQuery, q
6
6
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
7
  const SCHEMA_ROOT = path.resolve(__dirname, '../../resource/_schema');
8
8
  /**
9
- * 分组语义命中的基础分:当 scoreUrlRow=0 但该 url 行「分组被 query 命中 + types 匹配」时给予。
10
- * 取值需满足 E7 strongCandidates 门槛(typeMatch ×1.2 后 ≥500 → base ≥417)。
11
- * 只在「分组命中」时给(区别于旧的「所有 types 匹配行给 500」),避免无关行涌入。
9
+ * 分组语义命中兜底相关常量。
10
+ * 结构性约束:兜底分永远 < 同批次真实命中的最低分,避免「未命中兜底反超弱命中真分」倒挂。
11
+ *
12
+ * - FALLBACK_SAFETY_MARGIN:兜底分与最低真实命中分之间的安全间距。
13
+ * - FALLBACK_MIN_SCORE:兜底分硬下限(无真实命中时的默认底),仍能进入候选池;
14
+ * 后续 filter 门槛会决定它是否上榜。
15
+ * - FALLBACK_DEFAULT_BASELINE:无任何真实命中时用于估算兜底的默认真分基线。
12
16
  */
13
- const SECTION_MATCH_BASE_SCORE = 450;
17
+ const FALLBACK_SAFETY_MARGIN = 50;
18
+ const FALLBACK_MIN_SCORE = 100;
19
+ const FALLBACK_DEFAULT_BASELINE = 500;
14
20
  export const DOC_PATH_MAP = {
15
21
  android: {
16
22
  core_api: 'TRTCCloud',
@@ -316,8 +322,14 @@ export function runExactLookup(prompt, pool, norm) {
316
322
  // 支持 types 领域限定:上游推断出 types 时,优先 types 匹配的子文件行,
317
323
  // 不匹配的行降权(避免跨域误命中,如 SDK 问题命中 restapi)。
318
324
  if (normalizeUrlRowQuery(prompt).replace(/\s+/g, '').length >= 2) {
319
- const candidates = [];
320
- const hasTypeFilter = Boolean(norm.searchIntent?.length);
325
+ // 结构性修复:分两遍收集,避免「未命中兜底分」反超「弱命中真实分」的倒挂 bug。
326
+ // 第一遍收集 realHits(scoreUrlRow>0)和 fallbackChunks(分组语义命中兜底);
327
+ // 第二遍以 realHits 的最低分为基准,动态计算兜底分(min - MARGIN),保证:
328
+ // 任意 fallback score < 任意 realHit score
329
+ // 这样无论 query 怎么变化,命中的真分永远优先于未命中的兜底分。
330
+ const realHits = [];
331
+ const fallbackChunks = [];
332
+ const hasTypeFilter = Boolean((norm.types ?? []).length);
321
333
  // 预收集 urls 行的分组结构:按 source 读整个 md 源文件,解析 `##` 分组与每行归属。
322
334
  // 分组粗筛对所有 urls 资源生效(产品无关),不针对 webhook.md 打补丁。
323
335
  const urlChunks = pool.filter((c) => isUrlMappingSource(c.source) && c.title.trim());
@@ -359,11 +371,12 @@ export function runExactLookup(prompt, pool, norm) {
359
371
  for (const [source, sections] of sectionsBySource) {
360
372
  commonTokensBySource.set(source, computeCommonSectionTokens([...sections]));
361
373
  }
374
+ // 第一遍:分流真实命中与兜底候选
362
375
  for (const chunk of urlChunks) {
363
376
  const scoreResult = scoreUrlRow(prompt, chunk.title);
364
377
  // types 领域限定:query types 与 chunk types 是否有交集
365
378
  const typeMatch = hasTypeFilter
366
- ? (norm.searchIntent ?? []).some((t) => chunk.types.includes(t))
379
+ ? (norm.types ?? []).some((t) => chunk.types.includes(t))
367
380
  : true;
368
381
  // 分组粗筛:query 是否命中该 chunk 所在的分组。
369
382
  // 命中分组 → 正常;未命中分组 → 降权(不排除,避免 query 与分组名匹配不准时误杀)。
@@ -377,27 +390,40 @@ export function runExactLookup(prompt, pool, norm) {
377
390
  // 跟 section 标题「群组系统回调」共享字符少(仅「群」),sectionMatch 失败,
378
391
  // 但 row title 本身完美匹配 user 意图,强行降权会让单聊 row 顶上。
379
392
  const highConfidence = scoreResult.score >= 700;
380
- // 分组语义命中基础分:scoreUrlRow 可能因「同义词/长句」对精确行打 0 分
381
- // (如「群消息发送前回调」vs「群内发言之前回调」),但分组已语义命中,
382
- // 给予基础分进入候选,避免该领域行被 score<=0 排除。
393
+ // scoreUrlRow<=0 且分组语义命中 → 归入 fallbackChunks(第二遍统一定分)
383
394
  // 区别于旧的「所有 types 匹配行给 500」——只在分组命中时才给,避免无关行涌入。
384
395
  if (scoreResult.score <= 0) {
385
396
  if (sectionMatch && typeMatch) {
386
- candidates.push({ chunk, score: SECTION_MATCH_BASE_SCORE, typeMatch, sectionMatch });
397
+ fallbackChunks.push({ chunk, typeMatch, sectionMatch });
387
398
  }
388
399
  continue;
389
400
  }
390
401
  // finalScore 排序用:sectionMatch 失败且非高置信时,×0.7 软降权。
391
402
  // 高置信(scoreUrlRow ≥ 700)跳过降权,避免短 query + 长 section 标题时的误杀。
392
403
  const finalScore = (sectionMatch || highConfidence) ? scoreResult.score : scoreResult.score * 0.7;
393
- candidates.push({ chunk, score: finalScore, typeMatch, sectionMatch });
404
+ realHits.push({ chunk, score: finalScore, typeMatch, sectionMatch });
394
405
  }
395
- candidates.sort((a, b) => b.score - a.score);
406
+ // 第二遍:以最低真分为基准动态计算兜底分,保证兜底 < 所有真命中
407
+ const minRealScore = realHits.length > 0
408
+ ? Math.min(...realHits.map((h) => h.score))
409
+ : FALLBACK_DEFAULT_BASELINE;
410
+ const fallbackScore = Math.max(FALLBACK_MIN_SCORE, minRealScore - FALLBACK_SAFETY_MARGIN);
411
+ const candidates = [
412
+ ...realHits,
413
+ ...fallbackChunks.map((f) => ({ ...f, score: fallbackScore })),
414
+ ];
415
+ candidates.sort((a, b) => {
416
+ if (b.score !== a.score)
417
+ return b.score - a.score;
418
+ return a.chunk.id.localeCompare(b.chunk.id);
419
+ });
396
420
  // types 匹配的行加分(更优先);不匹配的行轻度降权但仍可兜底召回。
397
421
  // 避免上游 types 推断不准确时误杀正确文档(如「多端登录配置」被推断为 feature 而非 product)。
422
+ // 门槛从 >=500 降至 >=400:配套动态兜底逻辑——真实弱命中(如 scoreUrlRow=400 × 0.7 = 280
423
+ // → 280×1.2=336 仍会被 >=500 淘汰),需要将 filter 门槛与新的兜底基线保持在同一整数域。
398
424
  const typeScored = candidates
399
425
  .map((c) => (c.typeMatch ? { ...c, score: c.score * 1.2 } : { ...c, score: c.score * 0.6 }))
400
- .filter(({ score }) => score >= 500);
426
+ .filter(({ score }) => score >= 400);
401
427
  // 同分 row(典型:query「群消息撤回回调」时「撤回群消息」跟「单聊消息撤回」都 scoreUrlRow=660),
402
428
  // 按「query 字符集被 row title 覆盖比例」再排,覆盖更高的 row 排前。
403
429
  // 修 E7 boost idx=0 多 40 分的副作用:让字符集更精准的 row 拿 idx=0 → 300 分而不是 260 分。
@@ -417,7 +443,10 @@ export function runExactLookup(prompt, pool, norm) {
417
443
  // 优先按 typeScored score 降序;同分时按字符集覆盖率降序
418
444
  if (b.score !== a.score)
419
445
  return b.score - a.score;
420
- return charCoverage(b.chunk.title) - charCoverage(a.chunk.title);
446
+ const coverageDiff = charCoverage(b.chunk.title) - charCoverage(a.chunk.title);
447
+ if (coverageDiff !== 0)
448
+ return coverageDiff;
449
+ return a.chunk.id.localeCompare(b.chunk.id);
421
450
  })
422
451
  .slice(0, 12);
423
452
  strongCandidates.forEach(({ chunk, score }, idx) => {
@@ -433,7 +462,7 @@ export function runExactLookup(prompt, pool, norm) {
433
462
  }
434
463
  }
435
464
  // E9: chat faq——types=faq 时优先 knowledge 语料,避免 integration 文档内 FAQ 章节抢首位
436
- if (norm.searchIntent?.includes('faq') && norm.product?.includes('chat')) {
465
+ if ((norm.types ?? []).includes('faq') && norm.product?.includes('chat')) {
437
466
  const authPrompt = /auth|login|signature|usersig|鉴权|登录/i.test(prompt);
438
467
  for (const chunk of pool) {
439
468
  if (chunk.source.startsWith('knowledge/chat/')) {
@@ -487,6 +516,106 @@ export function runExactLookup(prompt, pool, norm) {
487
516
  }
488
517
  }
489
518
  }
519
+ // E10.5: room best-practice 召回补偿
520
+ // 背景:E10 只对 `scope === coreScope` 的 chunk 广谱 +40,
521
+ // 而 `knowledge/roomkit/best-practice/` 下的跨平台 FAQ(scope=platform_common)
522
+ // 一分未拿,同 query 下会被同一份 core-sdk 文件的多 chunk +40 累加压掉。
523
+ // 三层加分(可叠加):
524
+ // 1) 广谱 +80:所有 room best-practice chunk 统一 +80,跟 core-sdk 广谱 +40 拉开档次;
525
+ // 2) 平台匹配 +40:chunk.framework 与当前 platform 有交集(含 web/common 兼容项);
526
+ // 3) §8 同义问法命中 +200:读源文件解析 §8「同义问法覆盖」条目,query 命中任一问法 → +200。
527
+ // 参考 callkit E13A/E13B 的 +260~+300 精准 boost,room 这里用略低的 +200 尊重现有排序。
528
+ const bpChunks = pool.filter((c) => c.product === 'room' && c.source.includes('/best-practice/'));
529
+ // Source → §8 问法条目缓存(同一 source 下多 chunk 共用文件级 §8)
530
+ const bpSynonymsBySource = new Map();
531
+ const extractSynonyms = (source) => {
532
+ if (bpSynonymsBySource.has(source))
533
+ return bpSynonymsBySource.get(source);
534
+ let full = '';
535
+ try {
536
+ full = getSourceFileContent(source);
537
+ }
538
+ catch {
539
+ full = '';
540
+ }
541
+ const lines = full.split('\n');
542
+ const list = [];
543
+ let inSection = false;
544
+ for (const line of lines) {
545
+ // §8 允许 "## 8. xxx" 或 "## 同义问法..." 两种标题格式
546
+ if (/^##\s+(8\.\s*)?同义问法/.test(line)) {
547
+ inSection = true;
548
+ continue;
549
+ }
550
+ if (inSection && /^##\s+/.test(line))
551
+ break;
552
+ if (inSection) {
553
+ const item = line.match(/^-\s+(.+?)[??]?$/);
554
+ if (item)
555
+ list.push(item[1].trim());
556
+ }
557
+ }
558
+ bpSynonymsBySource.set(source, list);
559
+ return list;
560
+ };
561
+ // 平台兼容集:query platform 命中 chunk.framework 里的这些标签均视为匹配
562
+ // vue → [vue, web, common, react](跨平台 web 类问法通常写作 react/vue/web)
563
+ // react → [react, web, common, vue]
564
+ // android/ios/flutter → [self, common]
565
+ const platformCompat = {
566
+ vue: ['vue', 'web', 'common', 'react'],
567
+ react: ['react', 'web', 'common', 'vue'],
568
+ android: ['android', 'common'],
569
+ ios: ['ios', 'common'],
570
+ flutter: ['flutter', 'common'],
571
+ };
572
+ const compatSet = new Set(platformCompat[platform] ?? [platform, 'common']);
573
+ // query 归一化:小写 + 去空格,用于宽松包含匹配(问法通常是短句)
574
+ const promptForMatch = prompt.toLowerCase().replace(/\s+/g, '');
575
+ for (const chunk of bpChunks) {
576
+ // 1) 广谱 +80
577
+ addBoost(chunk, 80, 'E10.5:room-bp');
578
+ // 2) 平台匹配 +40
579
+ const frameworkMatch = (chunk.framework ?? []).some((fw) => compatSet.has(fw));
580
+ if (frameworkMatch) {
581
+ addBoost(chunk, 40, `E10.5:room-bp-platform:${platform}`);
582
+ }
583
+ // 3) §8 同义问法命中 +200(同 source 只算一次,避免同文件多 chunk 重复加)
584
+ const synonyms = extractSynonyms(chunk.source);
585
+ if (synonyms.length === 0)
586
+ continue;
587
+ const hit = synonyms.some((q) => {
588
+ const normQ = q.toLowerCase().replace(/\s+/g, '');
589
+ if (normQ.length < 4)
590
+ return false;
591
+ // 双向宽松匹配:query 包含问法(短问法命中长 query),或问法包含 query(罕见,防守)
592
+ return promptForMatch.includes(normQ) || normQ.includes(promptForMatch);
593
+ });
594
+ if (hit) {
595
+ addBoost(chunk, 200, `E10.5:room-bp-synonym`);
596
+ }
597
+ else {
598
+ // 退化匹配:若整句问法未匹配,用问法内 4+ 长中文/英文子串扫描
599
+ // 覆盖 "整屏共享怎么避免无限镜像" vs "分享屏幕出现无限套娃" 这类只共享部分词汇的情况
600
+ const keywords = new Set();
601
+ for (const q of synonyms) {
602
+ const zhTokens = q.match(/[\u4e00-\u9fff]{2,}/g) ?? [];
603
+ for (const t of zhTokens)
604
+ if (t.length >= 2)
605
+ keywords.add(t);
606
+ }
607
+ let matchedKeywordCount = 0;
608
+ for (const kw of keywords) {
609
+ if (promptForMatch.includes(kw.toLowerCase()))
610
+ matchedKeywordCount += 1;
611
+ if (matchedKeywordCount >= 2)
612
+ break;
613
+ }
614
+ if (matchedKeywordCount >= 2) {
615
+ addBoost(chunk, 120, `E10.5:room-bp-keywords`);
616
+ }
617
+ }
618
+ }
490
619
  }
491
620
  }
492
621
  // E12: CallKit Vue 混合类型问题消歧(状态机 STATUS 与 Core Store 类型分层召回)
@@ -112,38 +112,74 @@ function promptMatchesRetrievableScope(prompt, scope) {
112
112
  const normalizedPrompt = normalizeScopeToken(prompt);
113
113
  return scope.some((token) => normalizedPrompt.includes(normalizeScopeToken(token)));
114
114
  }
115
- export function filterPool(chunks, norm) {
116
- return chunks.filter((chunk) => {
117
- if (norm.product && !norm.product.includes(chunk.product) && chunk.product !== '*') {
118
- return false;
119
- }
120
- // A1: 通配语料(share 等)必须命中 retrievable_scope 才能进入检索池
121
- if (chunk.product === '*' && !promptMatchesRetrievableScope(norm.prompt, chunk.retrievable_scope)) {
115
+ /**
116
+ * 硬过滤层(Hard filter):语义无关的强约束,任何检索通道都必须遵守。
117
+ *
118
+ * 语义:
119
+ * - product:产品维度物理隔离(不同产品的语料本就不应互相召回)。
120
+ * - A1 retrievable_scope:通配语料(share 等)必须命中作用域,防止 share 语义误召到具体产品下。
121
+ *
122
+ * 使用者:
123
+ * - BM25 通道:入池前调用
124
+ * - 向量通道:召回后二次校验,作为索引层 product 分片的兜底
125
+ */
126
+ export function filterPoolHard(chunks, norm) {
127
+ return chunks.filter((chunk) => isHardAllowed(chunk, norm));
128
+ }
129
+ export function isHardAllowed(chunk, norm) {
130
+ if (norm.product && !norm.product.includes(chunk.product) && chunk.product !== '*') {
131
+ return false;
132
+ }
133
+ // A1: 通配语料(share 等)必须命中 retrievable_scope 才能进入检索池
134
+ if (chunk.product === '*' && !promptMatchesRetrievableScope(norm.prompt, chunk.retrievable_scope)) {
135
+ return false;
136
+ }
137
+ return true;
138
+ }
139
+ /**
140
+ * 软过滤层(Soft filter):语义相关的偏好约束,向量语义召回不应被此层直接砍掉。
141
+ *
142
+ * 语义:
143
+ * - framework:平台维度偏好(向量语义可跨平台复用,应转为软降权)
144
+ * - scenario:场景维度偏好(live/voice)
145
+ * - types:文档类型偏好(Agent 可能传错,向量语义能自愈到正确类型)
146
+ *
147
+ * 使用者:
148
+ * - BM25 通道:与 Hard 合用(保持现网行为)
149
+ * - 向量通道:不使用;由 applyRankMultipliers 在融合后做软降权
150
+ */
151
+ export function isSoftAllowed(chunk, norm) {
152
+ if (norm.frameworks?.length) {
153
+ const isWildcardChunk = chunk.framework.includes('*');
154
+ // 收紧:framework=['*'] 仅在该 chunk 明确是跨平台 type(product/server_api/webhook)时放行;
155
+ // 否则要求 chunk.framework 与请求 framework 有交集。
156
+ const wildcardAllowed = isWildcardChunk && chunkIsCrossPlatform(chunk.types);
157
+ const explicitMatch = hasExplicitFrameworkMatch(chunk.framework, norm.frameworks);
158
+ const fallbackMatch = hasFrameworkFallbackMatch(chunk, {
159
+ product: norm.product,
160
+ framework: norm.framework,
161
+ frameworks: norm.frameworks,
162
+ });
163
+ if (!explicitMatch && !wildcardAllowed && !fallbackMatch)
122
164
  return false;
123
- }
124
- if (norm.frameworks?.length) {
125
- const isWildcardChunk = chunk.framework.includes('*');
126
- // 收紧:framework=['*'] 仅在该 chunk 明确是跨平台 type(product/server_api/webhook)时放行;
127
- // 否则要求 chunk.framework 与请求 framework 有交集。
128
- const wildcardAllowed = isWildcardChunk && chunkIsCrossPlatform(chunk.types);
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)
136
- return false;
137
- }
138
- if (norm.scenario && chunk.scenario && chunk.scenario !== norm.scenario) {
165
+ }
166
+ if (norm.scenario && chunk.scenario && chunk.scenario !== norm.scenario) {
167
+ return false;
168
+ }
169
+ // best-practice 文档对 types 过滤免疫:内容不依赖 types 分类,应始终进 pool 靠 BM25 自然排名。
170
+ // 典型场景:Agent 传 types=['api'] 时,types=['faq'] 的 best-practice 不应被过滤掉。
171
+ if (norm.types?.length && !norm.types.some((t) => chunk.types.includes(t))) {
172
+ if (!chunk.source.includes('/best-practice/'))
139
173
  return false;
140
- }
141
- // best-practice 文档对 types 过滤免疫:内容不依赖 types 分类,应始终进 pool 靠 BM25 自然排名。
142
- // 典型场景:Agent 传 types=['api'] 时,types=['faq'] 的 best-practice 不应被过滤掉。
143
- if (norm.types?.length && !norm.types.some((t) => chunk.types.includes(t))) {
144
- if (!chunk.source.includes('/best-practice/'))
145
- return false;
146
- }
147
- return true;
148
- });
174
+ }
175
+ return true;
176
+ }
177
+ /**
178
+ * 完整过滤(Hard + Soft):BM25 主通道使用,保持现网行为不变。
179
+ *
180
+ * 向量通道**不应**使用此函数——它会把 framework/types/scenario 硬砍掉,
181
+ * 抵消向量语义的跨平台/类型自愈能力。向量通道请使用 filterPoolHard。
182
+ */
183
+ export function filterPool(chunks, norm) {
184
+ return chunks.filter((chunk) => isHardAllowed(chunk, norm) && isSoftAllowed(chunk, norm));
149
185
  }
@@ -8,7 +8,7 @@ const PLATFORM_TYPES = new Set([
8
8
  'integration',
9
9
  ]);
10
10
  const CROSS_PLATFORM_TYPES = new Set(['server_api', 'webhook', 'product']);
11
- const CROSS_PLATFORM_PROMPT = /计费|套餐|价格|UserSig\s*原理|签发流程|控制台|服务端\s*REST|云\s*API|Webhook|回调/i;
11
+ const CROSS_PLATFORM_PROMPT = /计费|套餐|价格|UserSig\s*原理|签发流程|控制台|服务端\s*REST|云\s*API|Webhook|回调|callback|server\s*callback/i;
12
12
  const PLATFORM_PROMPT = /API|组件|怎么用|怎么开|怎么设置|怎么配置|虚拟背景|屏幕分享|enterRoom|SDK|插件|集成步骤|客户端|Feature|主题定制|连麦|上麦/i;
13
13
  /**
14
14
  * push 必须问平台的强信号:仅当 prompt 明确提到具体「平台」(android/ios/flutter/...)时才拦询问。
@@ -36,7 +36,17 @@ export function postProcess(ranked, chunksById, limit, opts) {
36
36
  .filter((chunk) => chunk.source === opts.preferredSource)
37
37
  .sort((a, b) => a.char_start - b.char_start);
38
38
  if (sections.length > 0) {
39
- return sections.map((section, idx) => buildFragment(section, scoreMap.get(section.id) ?? 0, idx + 1));
39
+ const fullDocumentMaxChars = Math.max(1000, opts.fullDocumentMaxChars ?? 7000);
40
+ const selectedSections = [];
41
+ let usedChars = 0;
42
+ for (const section of sections) {
43
+ const sectionChars = section.content.length;
44
+ if (selectedSections.length > 0 && usedChars + sectionChars > fullDocumentMaxChars)
45
+ break;
46
+ selectedSections.push(section);
47
+ usedChars += sectionChars;
48
+ }
49
+ return selectedSections.map((section, idx) => buildFragment(section, scoreMap.get(section.id) ?? 0, idx + 1));
40
50
  }
41
51
  }
42
52
  const sourceCount = {};
@@ -88,22 +98,18 @@ export function postProcess(ranked, chunksById, limit, opts) {
88
98
  }
89
99
  else if (!isPinnedUrlRow) {
90
100
  const topScore = fragments[0].score;
91
- // 方案A:分数断崖截断。
92
- // 当已有 E7 精确命中行(pinned url-row)时,后续非 pinned 行若跌破 Top1 的 20%,
93
- // 视为「Top1 已精确命中、后续多为泛匹配噪声」,提前截断,避免凑满 limit 返回冗余。
94
- // 无 E7 命中时不触发(此时答案依赖 best-practice/本地文档,需完整保留)。
101
+ // 方案A:分数断崖截断(拆分为两个独立信号):
102
+ // 1) 通用分数衰减截断(不依赖 pin/exact-url 状态)
103
+ // 2) 精确 URL 命中断崖:由 caller 通过 strictCutoffOnExactUrlHit 显式启用,
104
+ // 避免"fragments 里恰好含 pinned row"这类间接信号驱动策略。
95
105
  if (topScore > 0) {
96
- const pinnedChunkIds = opts?.pinnedUrlRowChunkIds;
97
- const hasPinnedRow = pinnedChunkIds
98
- ? fragments.some((f) => pinnedChunkIds.has(f.chunk_id))
99
- : false;
100
106
  // 原有:分数衰减到 top 的 5% 以下时停止
101
107
  if (item.score < topScore * 0.05)
102
108
  break;
103
- // 新增:已有 E7 精确命中 + 后续跌破 35% → 断崖截断
109
+ // 新增:精确 URL 命中场景 → 后续跌破 35% → 断崖截断
104
110
  // 阈值取 35%(非 20%):best-practice 泛匹配分数常在 Top1 的 20%~35% 区间,
105
- // 用 35% 才能拦住「Top1 精确命中后仍返回一堆无关 best-practice」的冗余。
106
- if (hasPinnedRow && item.score < topScore * 0.35)
111
+ // 用 35% 才能拦住"Top1 精确命中后仍返回一堆无关 best-practice"的冗余。
112
+ if (opts?.strictCutoffOnExactUrlHit && item.score < topScore * 0.35)
107
113
  break;
108
114
  }
109
115
  }
@@ -7,7 +7,7 @@ import { classifyRetrievalMode } from './intent-router.js';
7
7
  import { rankSubsetWithIndex } from '../bm25.js';
8
8
  import { getProductsBM25Index } from '../bm25-cache.js';
9
9
  import { hasExplicitFrameworkMatch } from './framework-fallback.js';
10
- import { assessConfidence, assessNarrativeCompleteness, buildNarrativeDocument, hasNativeClientFramework, hasNativeTrtcApiSignal, hasProductScopeIntent, inferQueryApiTokens, pickNarrativeSource, } from './search-core.js';
10
+ import { assessConfidence, assessNarrativeCompleteness, buildDiscoveryCatalog, buildNarrativeDocument, hasNativeClientFramework, hasNativeTrtcApiSignal, hasProductScopeIntent, inferQueryApiTokens, pickNarrativeSource, } from './search-core.js';
11
11
  import { scoreUrlRow } from './url-row-scorer.js';
12
12
  import { buildAnswerContract, buildAgentMessage } from './search-message.js';
13
13
  import { runSearchPass } from './search-pass.js';
@@ -174,7 +174,7 @@ function computePrimaryCompleteness(allChunks, fragments, preferredSource) {
174
174
  primary_completeness: fileToken > 0 ? Number((hitToken / fileToken).toFixed(2)) : 0,
175
175
  };
176
176
  }
177
- export function searchKnowledge(params) {
177
+ export async function searchKnowledge(params) {
178
178
  const norm = normalizeParams(params);
179
179
  const retrievalMode = classifyRetrievalMode(norm.prompt);
180
180
  if (requiresFramework(norm)) {
@@ -196,36 +196,21 @@ export function searchKnowledge(params) {
196
196
  const manifest = loadManifest();
197
197
  const allChunks = manifest.chunks;
198
198
  const queryApiTokens = inferQueryApiTokens(norm.prompt);
199
- let pass = runSearchPass({
199
+ let pass = await runSearchPass({
200
200
  allChunks,
201
201
  passNorm: norm,
202
202
  retrievalMode,
203
203
  queryApiTokens,
204
204
  });
205
- // best-practice 懒补充:仅在主 pass 无本地片段(仅 URL 映射行或完全为空)时触发。
206
- // 典型场景:Agent 传 types=['sdk'],filterPool 把 best-practice(type=faq)硬过滤掉,
207
- // 只剩 URL 映射行(有 E7 boost 但内容只是一个 URL 链接)。
208
- // 不侵入 runSearchPass 层,避免每次调用都执行重复检索。
209
- // 修复 B1 副作用:之前 `types: undefined` 会同时把 chat/preset/{flutter,android,...}/integration.md
210
- // (types=['integration'])也放进 pool,integration 文档多 section 命中 generic 词会把 URL row 顶掉。
211
- // 改为 `types: ['faq']` 后:
212
- // - best-practice(types=['faq'])走 filterPool 的 bp-bypass 保留
213
- // - chat/preset/.../integration.md(types=['integration'])被硬过滤
214
- // - URL row(types 与原请求一致)继续保留
215
- // 合并策略:append BP fragments 到原 pass(不去重 URL row),保留 URL row 在 top。
205
+ // best-practice 懒补充(收敛版):仅在主 pass 完全无结果时触发。
206
+ //
207
+ // 设计意图:
208
+ // - BP 从“主通道召回增强”退到“兜底校准器”
209
+ // - 对“仅 URL 行”的场景不再补 BP,避免策略叠加导致的 BP 过召回
210
+ // - 保留 hard-fail 场景的可恢复性(空结果时补一次 faq)
216
211
  const passRetryReasons = [];
217
- const hasLocalContent = pass.fragments.some((f) => !f.source.includes('/urls/'));
218
- // 当主 pass 命中一个明确的 URL row(scoreUrlRow ≥ 300 强信号)时,**跳过 B1 补充**,
219
- // 避免 BP 兜底把无关 BP 文档(message-unread-count、avchatroom-message-loss-diagnosis 等)塞满 fragments。
220
- // 典型反例:query「im 内网代理」types=['product'],pass 命中「内网代理实现方案」(scoreUrlRow=460),
221
- // 旧 B1 会触发 → 拉 8 个无关 BP → fragments 膨胀到 9 个。
222
- // 进一步保护:当 top1 finalScore < 20(零命中场景,如「im 怎么发火箭」finalScore=16.8),
223
- // 也不触发 B1——无意义 query 不该被 BP 兜底撑满。
224
- const top1UrlScore = !hasLocalContent && pass.fragments[0]?.title
225
- ? scoreUrlRow(norm.prompt, pass.fragments[0].title).score
226
- : 0;
227
- const top1FinalScore = pass.fragments[0]?.score ?? 0;
228
- if (!hasLocalContent && top1UrlScore < 300 && top1FinalScore >= 20 && norm.product?.length && !norm.types?.includes('faq')) {
212
+ const noFragments = pass.fragments.length === 0;
213
+ if (noFragments && norm.product?.length && !norm.types?.includes('faq')) {
229
214
  const primaryProduct = norm.product[0];
230
215
  const bpCandidates = allChunks.filter((chunk) => {
231
216
  if (!/\/best-practice\//i.test(chunk.source))
@@ -244,7 +229,7 @@ export function searchKnowledge(params) {
244
229
  const bpIndex = getProductsBM25Index(allChunks, [norm.product[0]]);
245
230
  const bpRanked = rankSubsetWithIndex(norm.prompt, bpCandidates.map((c) => c.id), bpIndex, {});
246
231
  if ((bpRanked[0]?.score ?? 0) >= 6) {
247
- const bpPass = runSearchPass({
232
+ const bpPass = await runSearchPass({
248
233
  allChunks,
249
234
  passNorm: { ...norm, types: ['faq'] },
250
235
  retrievalMode,
@@ -276,7 +261,7 @@ export function searchKnowledge(params) {
276
261
  ? Number((pass.missingApiTokens.length / queryApiTokens.length).toFixed(2))
277
262
  : 0;
278
263
  if (queryApiTokens.length > 0 && (pass.apiNameCoverage === 0 || missingRatio >= 0.3)) {
279
- const retryPass = runSearchPass({
264
+ const retryPass = await runSearchPass({
280
265
  allChunks,
281
266
  passNorm: { ...norm, types: undefined },
282
267
  retrievalMode,
@@ -296,7 +281,7 @@ export function searchKnowledge(params) {
296
281
  // B3: 混合“集成 + 产品开通/计费/控制台”问法时,补充 product type,避免仅搜 integration/feature。
297
282
  if (norm.types?.length && !norm.types.includes('product') && hasProductScopeIntent(norm.prompt)) {
298
283
  const widenedTypes = [...new Set([...norm.types, 'product'])];
299
- const retryPass = runSearchPass({
284
+ const retryPass = await runSearchPass({
300
285
  allChunks,
301
286
  passNorm: { ...norm, types: widenedTypes },
302
287
  retrievalMode,
@@ -320,7 +305,7 @@ export function searchKnowledge(params) {
320
305
  && pass.apiNameCoverage < 0.5;
321
306
  if (shouldRetryNativeProduct) {
322
307
  const expandedProducts = [...new Set([...(norm.product ?? []), 'native_trtc_sdk'])];
323
- const retryPass = runSearchPass({
308
+ const retryPass = await runSearchPass({
324
309
  allChunks,
325
310
  passNorm: { ...norm, product: expandedProducts, types: undefined },
326
311
  retrievalMode,
@@ -356,7 +341,7 @@ export function searchKnowledge(params) {
356
341
  types: undefined,
357
342
  limit: Math.max(norm.limit ?? 8, 12),
358
343
  };
359
- const retryPass = runSearchPass({
344
+ const retryPass = await runSearchPass({
360
345
  allChunks,
361
346
  passNorm: narrativeRetryNorm,
362
347
  retrievalMode,
@@ -379,7 +364,7 @@ export function searchKnowledge(params) {
379
364
  if (shouldDeepCoverageCheck) {
380
365
  coverageReport = buildCoverageReport(allChunks, pass.fragments, pass.preferredSource);
381
366
  if (coverageReport.retry_suggested) {
382
- const coverageRetryPass = runSearchPass({
367
+ const coverageRetryPass = await runSearchPass({
383
368
  allChunks,
384
369
  passNorm: { ...norm, types: undefined, limit: Math.max(norm.limit ?? 8, 12) },
385
370
  retrievalMode,
@@ -441,6 +426,9 @@ export function searchKnowledge(params) {
441
426
  const documents = retrievalMode === 'narrative'
442
427
  ? (narrativeState.document ? [narrativeState.document] : [])
443
428
  : [];
429
+ const catalog = retrievalMode === 'discovery'
430
+ ? buildDiscoveryCatalog(pass.rankedWithMultipliers, pass.poolWithContent, Math.max(norm.limit ?? 8, 6))
431
+ : [];
444
432
  let finalStatus = status;
445
433
  let finalConfidence = confidence;
446
434
  if (retrievalMode === 'narrative' && documents.length > 0) {
@@ -453,7 +441,7 @@ export function searchKnowledge(params) {
453
441
  finalConfidence = 'low';
454
442
  }
455
443
  }
456
- if (retrievalMode === 'discovery' && pass.fragments.length > 0) {
444
+ if (retrievalMode === 'discovery' && catalog.length > 0) {
457
445
  finalStatus = 'success';
458
446
  finalConfidence = finalConfidence === 'none' ? 'medium' : finalConfidence;
459
447
  }
@@ -486,7 +474,7 @@ export function searchKnowledge(params) {
486
474
  const narrativeConverged = retrievalMode !== 'narrative'
487
475
  || (documents.length > 0 && narrativeState.quality.isComplete);
488
476
  const convergenceReached = (retrievalMode === 'narrative' && narrativeConverged)
489
- || (retrievalMode === 'discovery' && pass.fragments.length > 0)
477
+ || (retrievalMode === 'discovery' && catalog.length > 0)
490
478
  || (finalStatus === 'success'
491
479
  && (finalConfidence === 'high' || finalConfidence === 'medium')
492
480
  && (!hasApiQueryTokens || pass.apiNameCoverage >= 0.5)
@@ -529,6 +517,7 @@ export function searchKnowledge(params) {
529
517
  query: params,
530
518
  retrieval_mode: retrievalMode,
531
519
  documents,
520
+ catalog,
532
521
  meta: {
533
522
  // 参考文档(AI 输出「参考文档」小节依据):
534
523
  ...(pass.references.length ? { references: pass.references } : {}),
@@ -549,6 +538,7 @@ export function searchKnowledge(params) {
549
538
  },
550
539
  }
551
540
  : {}),
541
+ vector_retrieval: pass.vectorDiagnostics,
552
542
  },
553
543
  fragments: pass.fragments,
554
544
  };