@tencentcloud/sdk-mcp 1.7.7 → 1.7.9-beta.1

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 (224) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +7 -9
  3. package/index.js +8 -8
  4. package/knowledge/chunker.js +1 -0
  5. package/knowledge/doc-url-resolver.js +16 -9
  6. package/knowledge/embedding/build-vectors.js +92 -0
  7. package/knowledge/embedding/embed-model.js +41 -0
  8. package/knowledge/embedding/vector-augment.js +126 -0
  9. package/knowledge/embedding/vector-index.js +70 -0
  10. package/knowledge/frontmatter-meta.js +23 -0
  11. package/knowledge/path-meta.js +56 -0
  12. package/knowledge/search/exact-lookup.js +145 -16
  13. package/knowledge/search/filter-pool.js +77 -33
  14. package/knowledge/search/rank-profile.js +25 -0
  15. package/knowledge/search/requires-framework.js +4 -1
  16. package/knowledge/search/search-core.js +74 -13
  17. package/knowledge/search/search-knowledge.js +24 -34
  18. package/knowledge/search/search-pass.js +495 -169
  19. package/knowledge/search/url-row-scorer.js +24 -6
  20. package/package.json +3 -1
  21. package/resource/index/manifest.json +1 -1
  22. package/resource/index/vectors/index.json +4 -0
  23. package/resource/integration/callkit/preset/android/integration.md +0 -2
  24. package/resource/integration/callkit/preset/ios/integration.md +0 -2
  25. package/resource/integration/chat/preset/react/integration.md +9 -5
  26. package/resource/integration/chat/preset/uni-app/integration.md +4 -0
  27. package/resource/integration/chat/preset/vue/integration.md +10 -6
  28. package/resource/integration/livekit/preset/live/android/prepare-integration.md +0 -4
  29. package/resource/integration/livekit/preset/live/ios/prepare-integration.md +0 -4
  30. package/resource/integration/roomkit/preset/vue/integration.md +154 -208
  31. package/resource/knowledge/callkit/best-practice/android/video-chat-dating.md +0 -2
  32. package/resource/knowledge/callkit/core-sdk/android/answer-call.md +0 -2
  33. package/resource/knowledge/callkit/core-sdk/android/make-call.md +0 -2
  34. package/resource/knowledge/callkit/core-sdk/flutter/answer-call.md +0 -4
  35. package/resource/knowledge/callkit/core-sdk/flutter/make-call.md +0 -2
  36. package/resource/knowledge/callkit/core-sdk/flutter/offline-push.md +0 -2
  37. package/resource/knowledge/callkit/core-sdk/ios/answer-call.md +0 -2
  38. package/resource/knowledge/callkit/core-sdk/ios/make-call.md +0 -4
  39. package/resource/knowledge/callkit/core-sdk/ios/offline-push.md +0 -2
  40. package/resource/knowledge/callkit/core-sdk/uni-app/answer-call.md +0 -2
  41. package/resource/knowledge/callkit/core-sdk/uni-app/make-call.md +0 -2
  42. package/resource/knowledge/callkit/faq/ios.md +0 -2
  43. package/resource/knowledge/callkit/faq/uniapp-app.md +0 -2
  44. package/resource/knowledge/callkit/preset/miniprogram/integration.md +2 -3
  45. package/resource/knowledge/callkit/preset/miniprogram/uniapp-integration.md +0 -2
  46. package/resource/knowledge/chat/preset/ios-uikit/integration.md +0 -16
  47. package/resource/knowledge/chat/preset/react/h5/integration.md +16 -5
  48. package/resource/knowledge/chat/preset/vue/features/conversation-list.md +6 -3
  49. package/resource/knowledge/chat/preset/vue/h5/integration.md +7 -4
  50. package/resource/knowledge/iot/app/activate-service.md +95 -0
  51. package/resource/knowledge/iot/app/demo/android.md +93 -0
  52. package/resource/knowledge/iot/app/demo/ios.md +87 -0
  53. package/resource/knowledge/iot/app/demo/miniprogram.md +79 -0
  54. package/resource/knowledge/iot/app/device-binding.md +378 -0
  55. package/resource/knowledge/iot/app/device-sharing.md +414 -0
  56. package/resource/knowledge/iot/app/family-management.md +762 -0
  57. package/resource/knowledge/iot/app/integration/android.md +328 -0
  58. package/resource/knowledge/iot/app/integration/harmonyos.md +326 -0
  59. package/resource/knowledge/iot/app/integration/ios.md +351 -0
  60. package/resource/knowledge/iot/app/integration/miniprogram.md +268 -0
  61. package/resource/knowledge/iot/app/monitor-playback.md +557 -0
  62. package/resource/knowledge/iot/app/remote-control.md +139 -0
  63. package/resource/knowledge/iot/app/video-call.md +559 -0
  64. package/resource/knowledge/iot/app/wechat-call/device-call-miniprogram.md +204 -0
  65. package/resource/knowledge/iot/app/wechat-call/miniprogram-call-device.md +193 -0
  66. package/resource/knowledge/iot/client-api/app/android/txiotcallsession.md +189 -0
  67. package/resource/knowledge/iot/client-api/app/android/txiotcloudstorage.md +77 -0
  68. package/resource/knowledge/iot/client-api/app/android/txiotdevicemanager.md +175 -0
  69. package/resource/knowledge/iot/client-api/app/android/txiotengine.md +172 -0
  70. package/resource/knowledge/iot/client-api/app/android/txiotenginedef.md +163 -0
  71. package/resource/knowledge/iot/client-api/app/android/txiotfamilymanager.md +146 -0
  72. package/resource/knowledge/iot/client-api/app/android/txiotmonitorsession.md +246 -0
  73. package/resource/knowledge/iot/client-api/app/harmonyos/txiotcallsession.md +191 -0
  74. package/resource/knowledge/iot/client-api/app/harmonyos/txiotcloudstorage.md +77 -0
  75. package/resource/knowledge/iot/client-api/app/harmonyos/txiotdevicemanager.md +183 -0
  76. package/resource/knowledge/iot/client-api/app/harmonyos/txiotengine.md +186 -0
  77. package/resource/knowledge/iot/client-api/app/harmonyos/txiotenginedef.md +164 -0
  78. package/resource/knowledge/iot/client-api/app/harmonyos/txiotfamilymanager.md +150 -0
  79. package/resource/knowledge/iot/client-api/app/harmonyos/txiotmonitorsession.md +252 -0
  80. package/resource/knowledge/iot/client-api/app/ios/txiotcallsession.md +199 -0
  81. package/resource/knowledge/iot/client-api/app/ios/txiotcloudstorage.md +76 -0
  82. package/resource/knowledge/iot/client-api/app/ios/txiotdevicemanager.md +173 -0
  83. package/resource/knowledge/iot/client-api/app/ios/txiotengine.md +173 -0
  84. package/resource/knowledge/iot/client-api/app/ios/txiotenginedef.md +171 -0
  85. package/resource/knowledge/iot/client-api/app/ios/txiotfamilymanager.md +146 -0
  86. package/resource/knowledge/iot/client-api/app/ios/txiotmonitorsession.md +250 -0
  87. package/resource/knowledge/iot/client-api/app/miniprogram/txiotcallsession.md +180 -0
  88. package/resource/knowledge/iot/client-api/app/miniprogram/txiotcloudstorage.md +76 -0
  89. package/resource/knowledge/iot/client-api/app/miniprogram/txiotdevicemanager.md +281 -0
  90. package/resource/knowledge/iot/client-api/app/miniprogram/txiotengine.md +183 -0
  91. package/resource/knowledge/iot/client-api/app/miniprogram/txiotenginedef.md +152 -0
  92. package/resource/knowledge/iot/client-api/app/miniprogram/txiotfamilymanager.md +179 -0
  93. package/resource/knowledge/iot/client-api/app/miniprogram/txiotmonitorsession.md +226 -0
  94. package/resource/knowledge/iot/client-api/device/android-smart-device.md +813 -0
  95. package/resource/knowledge/iot/client-api/device/linux-rtos-device.md +2027 -0
  96. package/resource/knowledge/iot/device/activate-service.md +167 -0
  97. package/resource/knowledge/iot/device/ai-voice-chat/advanced-features/custom-agent.md +171 -0
  98. package/resource/knowledge/iot/device/ai-voice-chat/advanced-features/smart-device-control.md +721 -0
  99. package/resource/knowledge/iot/device/ai-voice-chat/integration-guide/android-smart-device.md +247 -0
  100. package/resource/knowledge/iot/device/ai-voice-chat/integration-guide/linux-rtos-device.md +368 -0
  101. package/resource/knowledge/iot/device/ai-voice-chat/overview.md +84 -0
  102. package/resource/knowledge/iot/device/cloud-storage.md +696 -0
  103. package/resource/knowledge/iot/device/data-model.md +972 -0
  104. package/resource/knowledge/iot/device/demo/android-smart-device.md +97 -0
  105. package/resource/knowledge/iot/device/demo/linux-rtos-device.md +106 -0
  106. package/resource/knowledge/iot/device/device-ota-upgrade.md +237 -0
  107. package/resource/knowledge/iot/device/dynamic-registration/android-smart-device.md +275 -0
  108. package/resource/knowledge/iot/device/dynamic-registration/linux-rtos-device.md +297 -0
  109. package/resource/knowledge/iot/device/login-and-register/android-smart-device.md +131 -0
  110. package/resource/knowledge/iot/device/login-and-register/linux-rtos-device.md +291 -0
  111. package/resource/knowledge/iot/device/monitor-pushing/android-smart-device.md +237 -0
  112. package/resource/knowledge/iot/device/monitor-pushing/linux-rtos-device.md +493 -0
  113. package/resource/knowledge/iot/device/video-call/android-smart-device.md +299 -0
  114. package/resource/knowledge/iot/device/video-call/linux-rtos-device.md +465 -0
  115. package/resource/knowledge/iot/device/wechat-call/android-smart-device.md +534 -0
  116. package/resource/knowledge/iot/device/wechat-call/linux-rtos-device.md +681 -0
  117. package/resource/knowledge/iot/kids-watch/overview.md +28 -0
  118. package/resource/knowledge/iot/security-camera/activate-service.md +74 -0
  119. package/resource/knowledge/iot/security-camera/monitor-playback/app-integration.md +302 -0
  120. package/resource/knowledge/iot/security-camera/monitor-playback/device-integration/android.md +237 -0
  121. package/resource/knowledge/iot/security-camera/monitor-playback/device-integration/linux-rtos.md +493 -0
  122. package/resource/knowledge/iot/security-camera/overview.md +31 -0
  123. package/resource/knowledge/iot/security-camera/ptz-control.md +153 -0
  124. package/resource/knowledge/iot/security-camera/recording-and-snapshot.md +182 -0
  125. package/resource/knowledge/iot/security-camera/voice-intercom.md +174 -0
  126. package/resource/knowledge/iot/smart-vision/serviceoverview.md +114 -0
  127. package/resource/knowledge/iot/voice-recorder/app-demo.md +99 -0
  128. package/resource/knowledge/iot/voice-recorder/device-demo.md +189 -0
  129. package/resource/knowledge/iot/voice-recorder/overview.md +64 -0
  130. package/resource/knowledge/livekit/core-sdk/features/android/base-beauty.md +0 -2
  131. package/resource/knowledge/livekit/core-sdk/features/android/gift.md +0 -4
  132. package/resource/knowledge/livekit/core-sdk/features/android/live-list.md +0 -2
  133. package/resource/knowledge/livekit/core-sdk/features/flutter/audio-effect.md +0 -2
  134. package/resource/knowledge/livekit/core-sdk/features/flutter/base-beauty.md +0 -2
  135. package/resource/knowledge/livekit/core-sdk/features/flutter/gift.md +0 -4
  136. package/resource/knowledge/livekit/core-sdk/features/ios/base-beauty.md +0 -2
  137. package/resource/knowledge/livekit/core-sdk/features/ios/gift.md +0 -4
  138. package/resource/knowledge/livekit/core-sdk/features/ios/live-list.md +0 -2
  139. package/resource/knowledge/livekit/core-sdk/features/uni-app/base-beauty.md +0 -2
  140. package/resource/knowledge/livekit/core-sdk/features/uni-app/gift.md +0 -4
  141. package/resource/knowledge/livekit/core-sdk/live/android/co-guest-interaction.md +0 -4
  142. package/resource/knowledge/livekit/core-sdk/live/android/host-interaction.md +0 -10
  143. package/resource/knowledge/livekit/core-sdk/live/android/integration.md +0 -2
  144. package/resource/knowledge/livekit/core-sdk/live/flutter/co-guest-interaction.md +0 -2
  145. package/resource/knowledge/livekit/core-sdk/live/flutter/host-interaction.md +0 -10
  146. package/resource/knowledge/livekit/core-sdk/live/flutter/integration.md +0 -2
  147. package/resource/knowledge/livekit/core-sdk/live/ios/co-guest-interaction.md +0 -4
  148. package/resource/knowledge/livekit/core-sdk/live/ios/host-interaction.md +0 -10
  149. package/resource/knowledge/livekit/core-sdk/live/ios/integration.md +0 -2
  150. package/resource/knowledge/livekit/core-sdk/live/uni-app/co-guest-interaction.md +0 -4
  151. package/resource/knowledge/livekit/core-sdk/live/uni-app/host-interaction.md +0 -4
  152. package/resource/knowledge/livekit/core-sdk/live/uni-app/integration.md +0 -2
  153. package/resource/knowledge/livekit/core-sdk/voice/android/co-guest-interaction.md +0 -2
  154. package/resource/knowledge/livekit/core-sdk/voice/android/host-interaction.md +0 -4
  155. package/resource/knowledge/livekit/core-sdk/voice/android/integration.md +0 -4
  156. package/resource/knowledge/livekit/core-sdk/voice/flutter/integration.md +0 -2
  157. package/resource/knowledge/livekit/core-sdk/voice/ios/co-guest-interaction.md +0 -2
  158. package/resource/knowledge/livekit/core-sdk/voice/ios/host-interaction.md +0 -4
  159. package/resource/knowledge/livekit/core-sdk/voice/ios/integration.md +0 -4
  160. package/resource/knowledge/livekit/core-sdk/voice/uni-app/co-guest-interaction.md +0 -2
  161. package/resource/knowledge/livekit/core-sdk/voice/uni-app/integration.md +0 -2
  162. package/resource/knowledge/livekit/faq/error-code.md +0 -2
  163. package/resource/knowledge/push/config/android/honor.md +16 -1
  164. package/resource/knowledge/push/config/android/huawei.md +20 -4
  165. package/resource/knowledge/push/config/android/meizu.md +19 -5
  166. package/resource/knowledge/push/config/android/vivo.md +20 -1
  167. package/resource/knowledge/push/config/donut.md +0 -2
  168. package/resource/knowledge/push/config/harmonyos.md +21 -5
  169. package/resource/knowledge/push/integration/android.md +0 -5
  170. package/resource/knowledge/push/integration/donut.md +0 -3
  171. package/resource/knowledge/push/integration/flutter.md +1 -4
  172. package/resource/knowledge/push/integration/harmonyos.md +0 -3
  173. package/resource/knowledge/push/integration/ios.md +0 -3
  174. package/resource/knowledge/push/integration/react-native.md +0 -5
  175. package/resource/knowledge/push/integration/uni-app.md +0 -3
  176. package/resource/knowledge/push/integration/unity.md +0 -3
  177. package/resource/knowledge/push/integration/unreal-engine.md +0 -3
  178. package/resource/knowledge/roomkit/core-sdk/android/call-user-to-room.md +0 -2
  179. package/resource/knowledge/roomkit/core-sdk/android/room-participant-state.md +0 -2
  180. package/resource/knowledge/roomkit/core-sdk/android/room-state.md +0 -2
  181. package/resource/knowledge/roomkit/core-sdk/android/schedule-room.md +0 -2
  182. package/resource/knowledge/roomkit/core-sdk/flutter/call-user-to-room.md +0 -2
  183. package/resource/knowledge/roomkit/core-sdk/flutter/room-participant-state.md +0 -2
  184. package/resource/knowledge/roomkit/core-sdk/flutter/room-state.md +0 -2
  185. package/resource/knowledge/roomkit/core-sdk/flutter/schedule-room.md +0 -2
  186. package/resource/knowledge/roomkit/core-sdk/ios/call-user-to-room.md +0 -2
  187. package/resource/knowledge/roomkit/core-sdk/ios/room-participant-state.md +0 -2
  188. package/resource/knowledge/roomkit/core-sdk/ios/room-state.md +0 -2
  189. package/resource/knowledge/roomkit/core-sdk/ios/schedule-room.md +0 -2
  190. package/resource/knowledge/roomkit/core-sdk/vue/room-view.md +0 -3
  191. package/resource/knowledge/roomkit/preset/uniapp-app/integration.md +18 -14
  192. package/resource/knowledge/rtcengine/web/how_to_run_web_sdk_demo.md +0 -2
  193. package/resource/knowledge/rtcengine/web/plugin/23-advanced-support-detection.md +30 -0
  194. package/resource/knowledge/rtcengine/web/plugin/24-advanced-network-quality.md +0 -2
  195. package/resource/knowledge/rtcengine/web/plugin/31-advanced-plugins-overview.md +1 -1
  196. package/resource/knowledge/rtcengine/web/plugin/35-advanced-ai-denoiser.md +1 -1
  197. package/resource/knowledge/rtcengine/web/plugin/36-advanced-virtual-background.md +4 -0
  198. package/resource/knowledge/trtc/android/TRTCCloud.md +94 -85
  199. package/resource/knowledge/trtc/android/TRTCCloudDef.md +48 -46
  200. package/resource/knowledge/trtc/android/TRTCCloudListener.md +48 -48
  201. package/resource/knowledge/trtc/android/TXAudioEffectManager.md +18 -18
  202. package/resource/knowledge/trtc/android/TXDeviceManager.md +4 -4
  203. package/resource/knowledge/trtc/android/TXLiteAVCode.md +1 -0
  204. package/resource/knowledge/trtc/c++/ITRTCCloud.md +90 -90
  205. package/resource/knowledge/trtc/c++/ITXAudioEffectManager.md +18 -18
  206. package/resource/knowledge/trtc/c++/ITXDeviceManager.md +9 -9
  207. package/resource/knowledge/trtc/c++/TRTCCloudCallback.md +47 -47
  208. package/resource/knowledge/trtc/c++/TRTCTypeDef.md +47 -45
  209. package/resource/knowledge/trtc/c++/TXLiteAVCode.md +1 -0
  210. package/resource/knowledge/trtc/ios/TRTCCloud.md +93 -93
  211. package/resource/knowledge/trtc/ios/TRTCCloudDelegate.md +50 -50
  212. package/resource/knowledge/trtc/ios/TXAudioEffectManager.md +8 -8
  213. package/resource/knowledge/trtc/ios/TXDeviceManager.md +9 -9
  214. package/resource/knowledge/trtc/ios/TXLiteAVCode.md +1 -0
  215. package/resource/knowledge/trtc/ios/TXLiveSDKTypeDef.md +48 -46
  216. package/server-config.js +8 -9
  217. package/server.js +1 -1
  218. package/tools/common/finalize_answer.js +1 -1
  219. package/tools/common/present_framework_choice.js +1 -1
  220. package/tools/common/search_trtc_knowledge.js +22 -3
  221. package/tools/definitions/search_trtc_knowledge_def.js +3 -2
  222. package/utils/preflight-route.js +19 -2
  223. package/utils/report-cls-client.js +1 -1
  224. package/utils/report-es-client.js +1 -1
@@ -1,103 +1,41 @@
1
- import { attachPoolContent } from '../content-loader.js';
1
+ import { attachChunkContent, attachPoolContent } from '../content-loader.js';
2
2
  import { buildReferences } from '../doc-url-resolver.js';
3
3
  import { rankSubsetWithIndex, tokenize } from '../bm25.js';
4
4
  import { getProductsBM25Index } from '../bm25-cache.js';
5
- import { filterPool } from './filter-pool.js';
5
+ import { filterPool, filterPoolHard } from './filter-pool.js';
6
6
  import { runExactLookup } from './exact-lookup.js';
7
7
  import { computeGenericAnchorBoosts } from './generic-rerank.js';
8
+ import { embedQuery, withQueryInstruction } from '../embedding/embed-model.js';
9
+ import { queryVectors } from '../embedding/vector-index.js';
8
10
  import { applyRankMultipliers, computeApiNameCoverage, computeMissingApiTokens, isUrlMappingSource, postProcess, } from './search-core.js';
11
+ import { resolveRankProfile } from './rank-profile.js';
9
12
  function isBestPracticeSource(source) {
10
13
  return /\/best-practice\//i.test(source);
11
14
  }
12
- function normalizeTopicText(text) {
13
- return text
14
- .toLowerCase()
15
- .replace(/[(())【】\[\]{}《》"""'`]/g, '')
16
- .replace(/[\s/\\,,。::;;|·\-—_]+/g, '');
17
- }
18
- const TOPIC_EN_NOISE_WORDS = new Set([
19
- 'a', 'an', 'the', 'and', 'or', 'to', 'of', 'in', 'on', 'for', 'with',
20
- 'is', 'are', 'was', 'were', 'be', 'how', 'what', 'why', 'when', 'where',
21
- 'chat', 'message', 'messages', 'api', 'sdk', 'doc', 'docs',
22
- ]);
23
- function extractTopicTokens(text) {
24
- const tokens = new Set();
25
- for (const match of text.matchAll(/\b\d{2,}\b/g)) {
26
- tokens.add(match[0]);
27
- }
28
- for (const match of text.matchAll(/\b[a-z][a-z0-9_]{1,}\b/gi)) {
29
- const token = match[0].toLowerCase();
30
- if (TOPIC_EN_NOISE_WORDS.has(token))
31
- continue;
32
- tokens.add(token);
33
- }
34
- const cjkSegments = text.match(/[\u4e00-\u9fff]+/g) ?? [];
35
- for (const seg of cjkSegments) {
36
- if (seg.length >= 2)
37
- tokens.add(seg);
38
- for (let i = 0; i < seg.length - 1; i += 1) {
39
- tokens.add(seg.slice(i, i + 2));
40
- }
41
- }
42
- return [...tokens].filter((token) => token.length >= 2);
43
- }
44
- function countTopicTokenHits(tokens, normalizedTarget) {
45
- if (!tokens.length || !normalizedTarget)
46
- return 0;
47
- let hits = 0;
48
- for (const token of tokens) {
49
- const normalizedToken = normalizeTopicText(token);
50
- if (normalizedToken.length < 2)
51
- continue;
52
- if (normalizedTarget.includes(normalizedToken))
53
- hits += 1;
54
- }
55
- return hits;
56
- }
57
- const TOPIC_GENERIC_GUARD_WORDS = new Set([
58
- ...TOPIC_EN_NOISE_WORDS,
59
- 'web', 'ios', 'android', 'flutter', 'react', 'vue',
60
- '直播', '消息', '文档', '教程', '指南', '重要', '保证', '丢失', '聊天',
61
- ]);
62
- /**
63
- * 提取更强区分度的话题锚点(通用策略,不针对任何单 case):
64
- * - 英文:长度 >= 4 或包含数字(如 AVChatRoom / trtc123)
65
- * - 中文:连续 3 字以上片段(避免 2-gram 过宽)
66
- */
67
- function extractStrongTopicAnchors(text) {
68
- const anchors = new Set();
69
- for (const match of text.matchAll(/\b[a-z][a-z0-9_]{3,}\b/gi)) {
70
- const token = match[0].toLowerCase();
71
- if (TOPIC_GENERIC_GUARD_WORDS.has(token))
72
- continue;
73
- anchors.add(token);
74
- }
75
- for (const segment of text.match(/[\u4e00-\u9fff]{3,}/g) ?? []) {
76
- if (TOPIC_GENERIC_GUARD_WORDS.has(segment))
77
- continue;
78
- anchors.add(segment);
79
- }
80
- return [...anchors];
81
- }
82
15
  function extractPrimaryHeading(content) {
83
16
  const heading = content.split('\n').find((line) => /^#\s+/.test(line.trim()));
84
17
  return heading ? heading.replace(/^#\s+/, '').trim() : '';
85
18
  }
86
- function buildBestPracticeTopicText(chunk) {
87
- if (!chunk)
88
- return '';
89
- return normalizeTopicText([
90
- chunk.title,
91
- ...(chunk.key_phrases ?? []),
92
- extractPrimaryHeading(chunk.content),
93
- ].filter(Boolean).join(' '));
94
- }
95
19
  const DIAGNOSIS_INTENT_RE = /丢消息|消息丢失|message\s*loss|消息可靠性|message\s*reliability|超时|timeout|失败|fail(?:ed|ure)?|排查|诊断|troubleshoot(?:ing)?/i;
96
20
  const DIAGNOSIS_DOC_RE = /diagnosis|troubleshoot|排查|诊断|丢失|message\s*loss|reliability|可靠性|timeout|fail(?:ed|ure)?/i;
97
21
  const TUTORIAL_DOC_RE = /audio[\s-]*message|voice[\s-]*message|语音消息|录音|recorder|create[a-z]+message|sendmessage|standard\s*(approach|usage)|标准用法|如何发送|how\s*to\s*send/i;
98
22
  function isDiagnosisIntent(prompt) {
99
23
  return DIAGNOSIS_INTENT_RE.test(prompt);
100
24
  }
25
+ function decidePromptLockPolicy(prompt) {
26
+ const integrationIntent = /集成|接入|指南|步骤|教程|快速开始|初始化|安装|跑通|demo|sample|example/i.test(prompt);
27
+ const productIntent = /配置|限制|上限|规则|权限|计费|套餐|价格|资费|多端登录|错误码|命名|长度|开通|购买/i.test(prompt);
28
+ if (integrationIntent && !productIntent) {
29
+ return {
30
+ enableContentPriority: true,
31
+ enableFullDocumentLock: true,
32
+ };
33
+ }
34
+ return {
35
+ enableContentPriority: false,
36
+ enableFullDocumentLock: false,
37
+ };
38
+ }
101
39
  function buildChunkIntentText(chunk) {
102
40
  return [
103
41
  chunk.title,
@@ -131,16 +69,21 @@ function applyDiagnosisIntentGuard(ranked, chunksById, prompt) {
131
69
  adjusted = true;
132
70
  return { ...item, score: item.score - penalty };
133
71
  });
134
- return adjusted ? next.sort((a, b) => b.score - a.score) : ranked;
72
+ return adjusted ? stableSortRanked(next) : ranked;
135
73
  }
136
74
  /**
137
- * A3: 同 prompt 下 best-practice 优先(仅在分数接近时触发,避免召回退化)
138
- * 触发条件:
75
+ * A3: best-practice 后处理轻校准(不再做“主通道抢首位”)
76
+ *
77
+ * 目标:
78
+ * - 保留 BP 的可见性优势(在接近相关时可轻微上浮)
79
+ * - 避免强行顶到 Top1 导致“策略叠策略”的排序失真
80
+ *
81
+ * 触发条件(更保守):
139
82
  * - 当前 Top1 不是 best-practice
140
- * - 存在 best-practice 候选
141
- * - 该候选分数与 Top1 差距不超过 18%
83
+ * - Top20 内存在 BP 候选
84
+ * - 候选与 Top1 分差不大(gap <= 20%)且具备基础强度
142
85
  */
143
- function prioritizeBestPracticeWhenCompetitive(ranked, chunksById, prompt) {
86
+ function prioritizeBestPracticeWhenCompetitive(ranked, chunksById, _prompt) {
144
87
  if (ranked.length < 2)
145
88
  return ranked;
146
89
  const top = ranked[0];
@@ -148,14 +91,12 @@ function prioritizeBestPracticeWhenCompetitive(ranked, chunksById, prompt) {
148
91
  if (!topChunk || isBestPracticeSource(topChunk.source) || top.score <= 0) {
149
92
  return ranked;
150
93
  }
151
- // 收集所有 best-practice 候选,取分数最高者,避免"首个 best-practice 不一定最相关"的偏置。
94
+ const searchWindow = Math.min(ranked.length, 20);
152
95
  let bestPracticeIdx = -1;
153
96
  let bestPracticeScore = -Infinity;
154
- for (let i = 1; i < ranked.length; i += 1) {
97
+ for (let i = 1; i < searchWindow; i += 1) {
155
98
  const chunk = chunksById.get(ranked[i].id);
156
- if (!chunk)
157
- continue;
158
- if (!isBestPracticeSource(chunk.source))
99
+ if (!chunk || !isBestPracticeSource(chunk.source))
159
100
  continue;
160
101
  if (ranked[i].score > bestPracticeScore) {
161
102
  bestPracticeScore = ranked[i].score;
@@ -166,71 +107,25 @@ function prioritizeBestPracticeWhenCompetitive(ranked, chunksById, prompt) {
166
107
  return ranked;
167
108
  const candidate = ranked[bestPracticeIdx];
168
109
  const gapRatio = (top.score - candidate.score) / top.score;
169
- // 常规场景:仅在分数接近时提升(避免召回退化)
170
- const normalCompetitive = gapRatio <= 0.35;
171
- // URL 映射/FAQ 抢首位场景:只要 best-practice 达到基础相关阈值,也应优先返回可执行内容
172
- // 这样可避免"只有 URL 映射行或 FAQ 简答,没有本地内容"的体验退化。
173
- // chat/urls 的 URL 行:需额外校验 BP 和 URL 行是否同一话题,
174
- // 避免"问地理位置消息,URL 行是地理位置,但 BP 是语音消息"的跨话题 shadow。
175
- const topIsUrlMapping = Boolean(isUrlMappingSource(topChunk.source));
176
- const topIsChatUrlRow = topIsUrlMapping && topChunk.source.includes('knowledge/chat/urls/');
177
- const topIsFaq = topChunk.source.includes('/faq/');
178
- // chat/urls 话题一致性:收紧为“高精度 + 多信号”
179
- // 1) prompt 中数字/错误码(高区分信号)
180
- // 2) prompt 切词与 BP 主题 token 命中
181
- // 3) URL 行标题切词与 BP 主题 token 命中(跨语言兜底)
182
- // 4) 通用强锚点(长 token / CJK>=3)需与 BP 主题有交集(若 prompt 存在强锚点)
183
- let chatUrlRowTopicMatch = true;
184
- let chatUrlTopicSignalCount = 0;
185
- if (topIsChatUrlRow) {
186
- const bpChunk = chunksById.get(candidate.id);
187
- const bpTopicText = buildBestPracticeTopicText(bpChunk);
188
- const promptDigits = prompt.match(/\b\d{2,}\b/g) ?? [];
189
- const digitMatch = promptDigits.some((d) => bpTopicText.includes(d));
190
- const promptTokens = extractTopicTokens(prompt);
191
- const promptTokenHitCount = countTopicTokenHits(promptTokens, bpTopicText);
192
- const promptTokenMatch = promptTokenHitCount >= 1;
193
- const urlTopicRaw = (topChunk.title ?? '').replace(/[((].*$/, '').trim();
194
- const urlTopicTokens = extractTopicTokens(urlTopicRaw);
195
- const urlTitleHitCount = countTopicTokenHits(urlTopicTokens, bpTopicText);
196
- const urlTitleMatch = urlTitleHitCount >= 1;
197
- const strongAnchors = extractStrongTopicAnchors(prompt);
198
- const strongAnchorHitCount = countTopicTokenHits(strongAnchors, bpTopicText);
199
- const strongAnchorMatch = strongAnchors.length === 0 || strongAnchorHitCount >= 1;
200
- chatUrlTopicSignalCount = Number(digitMatch) + Number(promptTokenMatch) + Number(urlTitleMatch);
201
- const hasHighPrecisionSignal = digitMatch || urlTitleMatch;
202
- chatUrlRowTopicMatch = hasHighPrecisionSignal
203
- && chatUrlTopicSignalCount >= 2
204
- && strongAnchorMatch;
205
- }
206
- const bestPracticeStrongEnough = candidate.score >= Math.max(12, top.score * 0.05);
207
- const urlRatioThreshold = topIsChatUrlRow ? 0.045 : 0.05;
208
- const urlAbsoluteThreshold = topIsChatUrlRow ? 20 : 16;
209
- const urlStrongEnough = candidate.score >= Math.max(urlAbsoluteThreshold, top.score * urlRatioThreshold);
210
- const urlMappingShadowing = topIsUrlMapping
211
- && urlStrongEnough
212
- && chatUrlRowTopicMatch;
213
- const faqShadowing = topIsFaq && bestPracticeStrongEnough;
214
- // Feature/API 文档抢首位场景:top1 是功能文档(非 integration、非 BP),
215
- // BP 的精准实践应优先于功能文档的泛匹配。
216
- // 条件更严格:BP 分数需达到 top 的 40%(避免完全不相关的 BP 被误提)。
217
- const topIsFeatureDoc = !topIsUrlMapping && !topIsFaq
218
- && !isBestPracticeSource(topChunk.source)
219
- && !isIntegrationSource(topChunk.source);
220
- const featureDocShadowing = topIsFeatureDoc && candidate.score >= top.score * 0.4;
221
- if (!normalCompetitive && !urlMappingShadowing && !faqShadowing && !featureDocShadowing)
110
+ const competitiveEnough = gapRatio <= 0.2;
111
+ const strengthEnough = candidate.score >= Math.max(14, top.score * 0.3);
112
+ if (!competitiveEnough || !strengthEnough)
113
+ return ranked;
114
+ // 轻量校准:最多只允许轻微越过 top,避免“强提权抢首位”。
115
+ const lift = Math.max(top.score * 0.03, 1.2);
116
+ const boostedScore = Math.min(top.score * 1.01, candidate.score + lift);
117
+ if (boostedScore <= candidate.score)
222
118
  return ranked;
223
119
  const next = [...ranked];
224
- next[bestPracticeIdx] = { ...candidate, score: top.score + 2 };
225
- return next.sort((a, b) => b.score - a.score);
120
+ next[bestPracticeIdx] = { ...candidate, score: boostedScore };
121
+ return stableSortRanked(next);
226
122
  }
227
123
  /**
228
- * P0-R1/P0-R2:best-practice 单 chunk 直通。
229
- * 在高置信且无强竞争时,直接锁定 preferredSource + fullDocumentMode。
230
- * 当 promote 已发生(faqShadowing 触发后 best-practice 排到首位),
231
- * 即使分数未达 80 也信任 promote 结果执行收敛。
124
+ * P0-R1/P0-R2:best-practice 单 chunk 收敛(高置信门控版)。
125
+ * 仅当 BP 在跨来源竞争中具备明显优势时,才锁定 preferredSource + fullDocumentMode。
126
+ * 不再因为轻量校准后的微小领先而触发整文锁定。
232
127
  */
233
- function pickDominantBestPracticeSource(ranked, chunksById) {
128
+ function pickDominantBestPracticeSource(ranked, chunksById, fullDocumentMaxChars) {
234
129
  const top = ranked[0];
235
130
  if (!top || top.score <= 0)
236
131
  return { fullDocumentMode: false };
@@ -259,11 +154,12 @@ function pickDominantBestPracticeSource(ranked, chunksById) {
259
154
  .length;
260
155
  const sourceSections = [...chunksById.values()].filter((chunk) => chunk.source === source);
261
156
  const sourceTotalChars = sourceSections.reduce((sum, chunk) => sum + chunk.content.length, 0);
262
- const isPromotedResult = top.score >= 20 && ranked.length >= 2 && (top.score - ranked[1].score) >= 0.05;
263
- const scoreStrongEnough = top.score >= 80 || isPromotedResult;
264
- const dominanceStrongEnough = secondCrossScore === 0 || (top.score / secondCrossScore) >= 1.2 || isPromotedResult;
265
- const crossSourceNotCompeting = strongCrossSourceCount === 0 || isPromotedResult;
266
- const withinLengthBudget = sourceTotalChars <= 7000;
157
+ const scoreStrongEnough = top.score >= 85;
158
+ const dominanceStrongEnough = secondCrossScore === 0
159
+ ? top.score >= 65
160
+ : (top.score / secondCrossScore) >= 1.25;
161
+ const crossSourceNotCompeting = strongCrossSourceCount === 0;
162
+ const withinLengthBudget = sourceTotalChars <= fullDocumentMaxChars;
267
163
  if (scoreStrongEnough && dominanceStrongEnough && crossSourceNotCompeting && withinLengthBudget) {
268
164
  return {
269
165
  preferredSource: source,
@@ -276,6 +172,51 @@ function isIntegrationSource(source) {
276
172
  return /\/integration\.md$/i.test(source)
277
173
  || /\/preset\/.+integration/i.test(source);
278
174
  }
175
+ function isNavigationIntent(prompt) {
176
+ return /文档|链接|地址|url|网址|入口|在哪看|在哪里看|官网|文档地址|sdk文档/i.test(prompt);
177
+ }
178
+ /**
179
+ * 通用语义仲裁(非 integration 特化):
180
+ * 当 top1 落在 urls 映射行(导航型来源),且向量 top1 指向内容型文档时,
181
+ * 对“非导航意图”的 query 优先选择内容文档来源。
182
+ *
183
+ * 目的:避免 BM25 在 urls 索引行上的词面优势压制真实内容页。
184
+ */
185
+ function resolveSemanticSourcePriorityByVector(params) {
186
+ const { ranked, chunksById, vectorTopId, prompt, fullDocumentMaxChars, } = params;
187
+ if (!vectorTopId || ranked.length === 0)
188
+ return null;
189
+ if (isNavigationIntent(prompt))
190
+ return null;
191
+ const top = ranked[0];
192
+ const topChunk = chunksById.get(top.id);
193
+ const vectorTopChunk = chunksById.get(vectorTopId);
194
+ if (!topChunk || !vectorTopChunk)
195
+ return null;
196
+ // 仅在“导航来源(top1) vs 内容来源(vectorTop)”冲突时触发
197
+ if (!isUrlMappingSource(topChunk.source))
198
+ return null;
199
+ if (isUrlMappingSource(vectorTopChunk.source))
200
+ return null;
201
+ if (topChunk.source === vectorTopChunk.source)
202
+ return null;
203
+ const vectorTopRanked = ranked.find((item) => item.id === vectorTopId);
204
+ if (!vectorTopRanked)
205
+ return null;
206
+ // 置信门槛:vector top 不能离 top1 太远,避免误切
207
+ const scoreGapOk = vectorTopRanked.score >= top.score * 0.45;
208
+ if (!scoreGapOk)
209
+ return null;
210
+ const sourceTotalChars = [...chunksById.values()]
211
+ .filter((chunk) => chunk.source === vectorTopChunk.source)
212
+ .reduce((sum, chunk) => sum + chunk.content.length, 0);
213
+ if (sourceTotalChars > fullDocumentMaxChars)
214
+ return null;
215
+ return {
216
+ preferredSource: vectorTopChunk.source,
217
+ fullDocumentMode: true,
218
+ };
219
+ }
279
220
  /**
280
221
  * 覆盖度竞争:在 A3 之前执行。
281
222
  * 当 integration 的多个 section 被查询广泛命中时,选择 integration 完整文档;
@@ -287,16 +228,16 @@ function isIntegrationSource(source) {
287
228
  * - BP 分数远高于 integration(>1.2×)→ 直接返回 null
288
229
  * - 否则 → 返回 null
289
230
  */
290
- function resolveContentPriority(ranked, chunksById) {
231
+ function resolveContentPriority(ranked, chunksById, fullDocumentMaxChars) {
291
232
  if (ranked.length < 2)
292
233
  return null;
293
234
  // 找 BP 最高分(在 top-30 范围内)
294
235
  let bpTopScore = -1;
295
236
  for (let i = 0; i < Math.min(ranked.length, 30); i++) {
296
237
  const chunk = chunksById.get(ranked[i].id);
297
- if (!chunk || !isBestPracticeSource(chunk.source))
238
+ if (!chunk)
298
239
  continue;
299
- if (ranked[i].score > bpTopScore) {
240
+ if (isBestPracticeSource(chunk.source) && ranked[i].score > bpTopScore) {
300
241
  bpTopScore = ranked[i].score;
301
242
  }
302
243
  }
@@ -334,9 +275,13 @@ function resolveContentPriority(ranked, chunksById) {
334
275
  bestIntTopScore = stats.topScore;
335
276
  }
336
277
  }
278
+ const integrationSourceTotalChars = [...chunksById.values()]
279
+ .filter((chunk) => chunk.source === bestIntSource)
280
+ .reduce((sum, chunk) => sum + chunk.content.length, 0);
281
+ const withinLengthBudget = integrationSourceTotalChars <= fullDocumentMaxChars;
337
282
  // 无 BP 候选 → 选 integration(如果有足够 section 命中)
338
283
  if (bpTopScore < 0) {
339
- return bestHitCount >= 2
284
+ return bestHitCount >= 2 && withinLengthBudget
340
285
  ? { preferredSource: bestIntSource, fullDocumentMode: true }
341
286
  : null;
342
287
  }
@@ -345,21 +290,248 @@ function resolveContentPriority(ranked, chunksById) {
345
290
  return null;
346
291
  // 覆盖度判定
347
292
  const BREADTH_THRESHOLD = 3;
348
- if (bestHitCount >= BREADTH_THRESHOLD && bestIntTopScore >= bpTopScore * 0.6) {
293
+ if (bestHitCount >= BREADTH_THRESHOLD && bestIntTopScore >= bpTopScore * 0.6 && withinLengthBudget) {
349
294
  return { preferredSource: bestIntSource, fullDocumentMode: true };
350
295
  }
351
296
  return null;
352
297
  }
353
- export function runSearchPass(params) {
298
+ const DEFAULT_VECTOR_TOP_K = 30;
299
+ const DEFAULT_RRF_K = 60;
300
+ const DEFAULT_RERANK_TOP_K = 20;
301
+ function readEnvNumber(name, fallback) {
302
+ const raw = process.env[name];
303
+ if (raw === undefined || raw === '')
304
+ return fallback;
305
+ const val = Number(raw);
306
+ return Number.isFinite(val) ? val : fallback;
307
+ }
308
+ function stableSortRanked(items) {
309
+ return [...items].sort((a, b) => {
310
+ if (b.score !== a.score)
311
+ return b.score - a.score;
312
+ return a.id.localeCompare(b.id);
313
+ });
314
+ }
315
+ function inferIotFeatureDomainFromPrompt(prompt) {
316
+ if (!prompt)
317
+ return undefined;
318
+ if (/安防摄像头|云台|\bPTZ\b|语音对讲|监控|回放|录像|录制|截图/i.test(prompt)) {
319
+ return 'security-camera';
320
+ }
321
+ if (/儿童手表/i.test(prompt))
322
+ return 'kids-watch';
323
+ if (/录音卡|语音卡/i.test(prompt))
324
+ return 'voice-recorder';
325
+ if (/智能视觉|视觉服务/i.test(prompt))
326
+ return 'smart-vision';
327
+ if (/设备端|linux|rtos|模组|芯片/i.test(prompt))
328
+ return 'device';
329
+ if (/客户端\s*api|client\s*api|\bTXIoT[A-Za-z0-9_]+\b/i.test(prompt))
330
+ return 'client_api';
331
+ if (/app\s*demo|示例工程|demo/i.test(prompt))
332
+ return 'app_demo';
333
+ if (/应用端接入|app接入|app\s*集成/i.test(prompt))
334
+ return 'app_integration';
335
+ return undefined;
336
+ }
337
+ /**
338
+ * IoT 强 feature 意图 → source 路径关键词映射。
339
+ * 中文意图词(云台/语音对讲)与英文文件名(ptz-control/voice-intercom)无直接词面关系,
340
+ * 需显式映射才能在排序层精准置顶对应 feature 文档,避免被短文本泛文档的 BM25 优势压制。
341
+ *
342
+ * 注意:仅收录「一 feature 一精准文件」的映射。像 monitor-playback 这类分布在
343
+ * 多端侧(app/device/security-camera)的 feature 不在此列,避免无差别 source boost
344
+ * 把 device 侧文档抬高、压制 app 侧(见 regression case 22)。
345
+ */
346
+ const IOT_FEATURE_INTENT_MAP = [
347
+ { re: /云台|\bPTZ\b/i, keyword: 'ptz-control' },
348
+ { re: /语音对讲|对讲|intercom/i, keyword: 'voice-intercom' },
349
+ ];
350
+ function inferIotFeatureSourceKeyword(prompt) {
351
+ if (!prompt)
352
+ return undefined;
353
+ for (const { re, keyword } of IOT_FEATURE_INTENT_MAP) {
354
+ if (re.test(prompt))
355
+ return keyword;
356
+ }
357
+ return undefined;
358
+ }
359
+ function inferPreferredIotFeatureDomain(ranked, chunksById, prompt) {
360
+ const byPrompt = inferIotFeatureDomainFromPrompt(prompt ?? '');
361
+ if (byPrompt)
362
+ return byPrompt;
363
+ const scoreByDomain = new Map();
364
+ const sampleWindow = Math.min(ranked.length, 80);
365
+ for (let i = 0; i < sampleWindow; i += 1) {
366
+ const item = ranked[i];
367
+ const chunk = chunksById.get(item.id);
368
+ if (!chunk || chunk.product !== 'iot' || !chunk.feature_domain)
369
+ continue;
370
+ const domain = chunk.feature_domain;
371
+ scoreByDomain.set(domain, (scoreByDomain.get(domain) ?? 0) + Math.max(item.score, 0));
372
+ }
373
+ let preferred;
374
+ let best = -1;
375
+ for (const [domain, score] of scoreByDomain.entries()) {
376
+ if (score > best) {
377
+ best = score;
378
+ preferred = domain;
379
+ }
380
+ }
381
+ return preferred;
382
+ }
383
+ function fuseByRRF(params) {
384
+ const { bm25Ranked, vectorHits, rrfK, baseBeta, alpha, profile, } = params;
385
+ const bmWeight = profile?.bmWeight ?? 1;
386
+ const vecWeight = profile?.vecWeight ?? 1;
387
+ const alphaScale = profile?.alphaScale ?? 1;
388
+ if (!bm25Ranked.length && !vectorHits.length) {
389
+ return { ranked: [], bm25AdjustedCount: 0, vectorOnlyAddedCount: 0 };
390
+ }
391
+ const topBM25 = bm25Ranked[0]?.score ?? baseBeta;
392
+ const bm25ById = new Map(bm25Ranked.map((item) => [item.id, item]));
393
+ const bm25Rank = new Map();
394
+ bm25Ranked.forEach((item, idx) => bm25Rank.set(item.id, idx + 1));
395
+ const vecRank = new Map();
396
+ vectorHits.forEach((hit, idx) => vecRank.set(hit.id, idx + 1));
397
+ const vecMin = vectorHits.length ? Math.min(...vectorHits.map((h) => h.score)) : 0;
398
+ const vecMax = vectorHits.length ? Math.max(...vectorHits.map((h) => h.score)) : 1;
399
+ const vecRange = vecMax - vecMin || 1;
400
+ const vecNorm = new Map();
401
+ for (const hit of vectorHits) {
402
+ vecNorm.set(hit.id, (hit.score - vecMin) / vecRange);
403
+ }
404
+ const unionIds = new Set([
405
+ ...bm25Ranked.map((item) => item.id),
406
+ ...vectorHits.map((item) => item.id),
407
+ ]);
408
+ const rrfRaw = new Map();
409
+ let maxRRF = 0;
410
+ for (const id of unionIds) {
411
+ const bRank = bm25Rank.get(id);
412
+ const vRank = vecRank.get(id);
413
+ const score = (bRank ? bmWeight / (rrfK + bRank) : 0)
414
+ + (vRank ? vecWeight / (rrfK + vRank) : 0);
415
+ rrfRaw.set(id, score);
416
+ if (score > maxRRF)
417
+ maxRRF = score;
418
+ }
419
+ let bm25AdjustedCount = 0;
420
+ let vectorOnlyAddedCount = 0;
421
+ // 类别权重也影响 hybrid 融合权重,避免 rrf 权重与 sim 权重口径分裂。
422
+ const totalWeight = bmWeight + vecWeight || 1;
423
+ const simWeight = vecWeight / totalWeight; // 0..1
424
+ const rrfWeight = 1 - simWeight;
425
+ const fused = [...unionIds].map((id) => {
426
+ const original = bm25ById.get(id);
427
+ const rrfNorm = maxRRF > 0 ? (rrfRaw.get(id) ?? 0) / maxRRF : 0;
428
+ const simNorm = vecNorm.get(id) ?? 0;
429
+ const hybridNorm = rrfWeight * rrfNorm + simWeight * simNorm;
430
+ const effectiveAlpha = alpha * alphaScale;
431
+ if (original) {
432
+ const hasVectorSignal = vecRank.has(id);
433
+ if (hasVectorSignal)
434
+ bm25AdjustedCount += 1;
435
+ const boost = hasVectorSignal ? original.score * effectiveAlpha * 0.25 * hybridNorm : 0;
436
+ return { id, score: original.score + boost };
437
+ }
438
+ vectorOnlyAddedCount += 1;
439
+ return { id, score: topBM25 * baseBeta * (1 + effectiveAlpha * 0.9 * hybridNorm) };
440
+ });
441
+ return {
442
+ ranked: stableSortRanked(fused),
443
+ bm25AdjustedCount,
444
+ vectorOnlyAddedCount,
445
+ };
446
+ }
447
+ function resolveFusionProfile(prompt, queryApiTokens) {
448
+ const trimmed = prompt.trim();
449
+ if (queryApiTokens.length > 0) {
450
+ return { name: 'api_token', bmWeight: 1.3, vecWeight: 0.8, alphaScale: 0.7 };
451
+ }
452
+ // 长自然语言:字符数或分词粒度足够
453
+ const tokenCount = tokenize(trimmed).filter((t) => t.length >= 2).length;
454
+ const isLongNatural = trimmed.length >= 14 || tokenCount >= 6;
455
+ if (isLongNatural) {
456
+ return { name: 'natural_language', bmWeight: 0.85, vecWeight: 1.2, alphaScale: 1.15 };
457
+ }
458
+ const isShortKeyword = trimmed.length <= 6 && tokenCount <= 2;
459
+ if (isShortKeyword) {
460
+ return { name: 'short_keyword', bmWeight: 1.0, vecWeight: 1.0, alphaScale: 0.9 };
461
+ }
462
+ return { name: 'default', bmWeight: 1.0, vecWeight: 1.0, alphaScale: 1.0 };
463
+ }
464
+ function normalizeCompactText(input) {
465
+ return input.toLowerCase().replace(/[\s\-_]+/g, '');
466
+ }
467
+ function hasTitleExactMatch(prompt, title) {
468
+ if (!prompt || !title)
469
+ return false;
470
+ const p = normalizeCompactText(prompt);
471
+ const t = normalizeCompactText(title);
472
+ if (!p || !t)
473
+ return false;
474
+ if (t.length >= 3 && p.includes(t))
475
+ return true;
476
+ if (p.length >= 3 && t.includes(p))
477
+ return true;
478
+ return false;
479
+ }
480
+ function shouldTriggerLowConfidenceRerank(params) {
481
+ const { ranked, rankProfile } = params;
482
+ if (rankProfile.lowConfidenceRerankTopK <= 0 || ranked.length < 2)
483
+ return false;
484
+ const top = ranked[0]?.score ?? 0;
485
+ const second = ranked[1]?.score ?? 0;
486
+ if (top <= 0)
487
+ return false;
488
+ const ratio = second > 0 ? top / second : Number.POSITIVE_INFINITY;
489
+ return top <= rankProfile.lowConfidenceTopScoreThreshold || ratio <= rankProfile.lowConfidenceGapRatioThreshold;
490
+ }
491
+ function rerankTopK(params) {
492
+ const { prompt, ranked, chunksById, topK, rankProfile, } = params;
493
+ if (ranked.length < 2 || topK <= 0)
494
+ return { ranked, applied: false };
495
+ const head = ranked.slice(0, topK);
496
+ const tail = ranked.slice(topK);
497
+ const promptTokens = [...new Set(tokenize(prompt).filter((t) => t.length >= 2))];
498
+ if (!promptTokens.length)
499
+ return { ranked, applied: false };
500
+ const topScore = head[0]?.score ?? 0;
501
+ const bumped = head.map((item) => {
502
+ const chunk = chunksById.get(item.id);
503
+ if (!chunk)
504
+ return item;
505
+ const text = [chunk.title, ...(chunk.key_phrases ?? [])].join(' ').toLowerCase();
506
+ const overlap = promptTokens.reduce((acc, token) => (text.includes(token.toLowerCase()) ? acc + 1 : acc), 0);
507
+ if (overlap <= 0 && !hasTitleExactMatch(prompt, chunk.title))
508
+ return item;
509
+ let boost = topScore * 0.002 * overlap;
510
+ if (hasTitleExactMatch(prompt, chunk.title)) {
511
+ boost += Math.max(topScore * 0.015, 1.5) * (rankProfile.titleExactMultiplier - 1);
512
+ }
513
+ boost += rankProfile.titleTokenBoost * overlap;
514
+ return { ...item, score: item.score + boost };
515
+ });
516
+ const rerankedHead = stableSortRanked(bumped);
517
+ const applied = rerankedHead.some((item, idx) => item.id !== head[idx]?.id);
518
+ return { ranked: [...rerankedHead, ...tail], applied };
519
+ }
520
+ export async function runSearchPass(params) {
354
521
  const { allChunks, passNorm, retrievalMode, queryApiTokens, opts, } = params;
355
522
  const effectivePassNorm = retrievalMode === 'discovery'
356
523
  ? { ...passNorm, types: undefined }
357
524
  : passNorm;
525
+ const rankProfile = resolveRankProfile({ product: effectivePassNorm.product });
358
526
  let pool = filterPool(allChunks, effectivePassNorm);
359
527
  // types 误传自愈:首轮保留,二次扩检也同样保留该能力
360
528
  if (pool.length === 0 && effectivePassNorm.types?.length) {
361
529
  pool = filterPool(allChunks, { ...effectivePassNorm, types: undefined });
362
530
  }
531
+ // 分层过滤:向量语义召回仅受 Hard 层(product + retrievable_scope)约束,
532
+ // framework/types/scenario 转到 applyRankMultipliers 做软降权,保留向量的跨平台/类型自愈能力。
533
+ const hardPool = filterPoolHard(allChunks, effectivePassNorm);
534
+ const hardChunksById = new Map(hardPool.map((chunk) => [chunk.id, chunk]));
363
535
  const { boosts, exactHits, urlRowHitChunkIds } = runExactLookup(effectivePassNorm.prompt, pool, effectivePassNorm);
364
536
  // A2 补充正向路由:UserSig 问法下允许 share 命中且优先于无关 urls 噪声
365
537
  if (/usersig|sdkappid|secretkey|鉴权|签名|gentestusersig/i.test(passNorm.prompt)) {
@@ -387,36 +559,189 @@ export function runSearchPass(params) {
387
559
  boosts[id] = (boosts[id] ?? 0) + amount;
388
560
  }
389
561
  const ranked = rankSubsetWithIndex(effectivePassNorm.prompt, pool.map((chunk) => chunk.id), productIndex, boosts);
390
- const rankedWithMultipliers = applyRankMultipliers(ranked, chunksById, {
562
+ // 固定链路:Hybrid(BM25 + Vector)→ RRF 融合 → 轻量重排。
563
+ // 兼容开关:DISABLE_VECTOR_AUGMENT=1 时只关闭向量相关步骤,BM25 主链路保持不变。
564
+ const vectorAugmentDisabled = process.env.DISABLE_VECTOR_AUGMENT === '1';
565
+ const alphaOverride = process.env.VECTOR_AUGMENT_ALPHA
566
+ ? Number(process.env.VECTOR_AUGMENT_ALPHA)
567
+ : undefined;
568
+ const alpha = alphaOverride !== undefined && !Number.isNaN(alphaOverride) ? alphaOverride : 0.15;
569
+ const baseBeta = readEnvNumber('VECTOR_AUGMENT_BASE_BETA', 0.3);
570
+ const topK = Math.max(1, Math.floor(readEnvNumber('VECTOR_TOP_K', DEFAULT_VECTOR_TOP_K)));
571
+ const rrfK = Math.max(1, Math.floor(readEnvNumber('VECTOR_RRF_K', DEFAULT_RRF_K)));
572
+ const rerankTopKLimit = Math.max(0, Math.floor(readEnvNumber('VECTOR_RERANK_TOP_K', rankProfile.rerankTopK || DEFAULT_RERANK_TOP_K)));
573
+ const fullDocumentMaxChars = Math.max(1000, Math.floor(readEnvNumber('SEARCH_FULL_DOCUMENT_MAX_CHARS', 7000)));
574
+ let rankedFused = ranked;
575
+ // 向量归因埋点采集(终态归因在函数末尾统一构造):
576
+ // - bm25TopIdSnapshot:向量介入前的 BM25 单路头名(即 ranked[0],融合前的原始头名)
577
+ // - vectorTopIdSnapshot:向量单路头名(vecHits[0],经 hardChunksById 过滤后)
578
+ // - vecScoreById:向量单路每个候选的 sim 分,供 final_top1_id 查表得 vector_score_of_final_top1
579
+ // - bm25RankById:向量介入前 BM25 单路的 1-based 名次索引,供 rank_shift 与 promoted_by_vector_count 计算
580
+ const bm25TopIdSnapshot = ranked[0]?.id ?? null;
581
+ const bm25RankById = new Map();
582
+ ranked.forEach((item, idx) => bm25RankById.set(item.id, idx + 1));
583
+ let vectorTopIdSnapshot = null;
584
+ let vecScoreById = new Map();
585
+ if (!vectorAugmentDisabled) {
586
+ try {
587
+ const [qvecInst, qvecRaw] = await Promise.all([
588
+ embedQuery(withQueryInstruction(effectivePassNorm.prompt)),
589
+ embedQuery(effectivePassNorm.prompt),
590
+ ]);
591
+ const [hitsInst, hitsRaw] = await Promise.all([
592
+ queryVectors(passNorm.product, qvecInst, topK),
593
+ queryVectors(passNorm.product, qvecRaw, topK),
594
+ ]);
595
+ const merged = new Map();
596
+ for (const h of hitsInst) {
597
+ merged.set(h.id, Math.max(merged.get(h.id) ?? Number.NEGATIVE_INFINITY, h.score));
598
+ }
599
+ for (const h of hitsRaw) {
600
+ const weighted = h.score * 0.97;
601
+ merged.set(h.id, Math.max(merged.get(h.id) ?? Number.NEGATIVE_INFINITY, weighted));
602
+ }
603
+ const hits = [...merged.entries()]
604
+ .map(([id, score]) => ({ id, score }))
605
+ .sort((a, b) => {
606
+ if (b.score !== a.score)
607
+ return b.score - a.score;
608
+ return a.id.localeCompare(b.id);
609
+ })
610
+ .slice(0, topK);
611
+ // 分层过滤:向量仅受 Hard 层(product + retrievable_scope)约束,
612
+ // framework/types/scenario 交给 applyRankMultipliers 做软降权。
613
+ // 向量补召回但不在 BM25 pool 内的 chunk 需要挂到 chunksById,供下游 rerank / multiplier / postProcess 读取。
614
+ const vecHits = hits.filter((h) => hardChunksById.has(h.id));
615
+ for (const hit of vecHits) {
616
+ if (chunksById.has(hit.id))
617
+ continue;
618
+ const chunk = hardChunksById.get(hit.id);
619
+ if (!chunk)
620
+ continue;
621
+ chunksById.set(hit.id, attachChunkContent(chunk));
622
+ }
623
+ vectorTopIdSnapshot = vecHits[0]?.id ?? null;
624
+ vecScoreById = new Map(vecHits.map((h) => [h.id, h.score]));
625
+ if (vecHits.length > 0) {
626
+ const fusionProfile = resolveFusionProfile(effectivePassNorm.prompt, queryApiTokens);
627
+ const fused = fuseByRRF({
628
+ bm25Ranked: ranked,
629
+ vectorHits: vecHits,
630
+ rrfK,
631
+ baseBeta,
632
+ alpha,
633
+ profile: fusionProfile,
634
+ });
635
+ const reranked = rerankTopK({
636
+ prompt: effectivePassNorm.prompt,
637
+ ranked: fused.ranked,
638
+ chunksById,
639
+ topK: rerankTopKLimit,
640
+ rankProfile,
641
+ });
642
+ rankedFused = reranked.ranked;
643
+ }
644
+ }
645
+ catch {
646
+ // 向量链路失败(embed/query 异常):保持 rankedFused = ranked,
647
+ // 快照变量维持初始值即可。归因阶段 vector_top1_id 为 null 天然反映降级。
648
+ }
649
+ }
650
+ const preferredIotFeatureDomain = inferPreferredIotFeatureDomain(rankedFused, chunksById, passNorm.prompt);
651
+ const preferredIotFeatureSource = inferIotFeatureSourceKeyword(passNorm.prompt);
652
+ const rankedWithMultipliers = applyRankMultipliers(rankedFused, chunksById, {
391
653
  product: passNorm.product,
392
654
  framework: passNorm.framework,
393
655
  frameworks: passNorm.frameworks,
656
+ preferred_iot_feature_domain: preferredIotFeatureDomain,
657
+ preferred_iot_feature_source: preferredIotFeatureSource,
658
+ prompt: passNorm.prompt,
659
+ rank_profile: rankProfile.name,
394
660
  });
395
- const rankedAfterIntentGuard = applyDiagnosisIntentGuard(rankedWithMultipliers, chunksById, passNorm.prompt);
396
- // 覆盖度竞争先于 A3 执行
397
- const integrationPriority = resolveContentPriority(rankedAfterIntentGuard, chunksById);
661
+ let rankedAfterIntentGuard = applyDiagnosisIntentGuard(rankedWithMultipliers, chunksById, passNorm.prompt);
662
+ if (shouldTriggerLowConfidenceRerank({ ranked: rankedAfterIntentGuard, rankProfile })) {
663
+ const secondPass = rerankTopK({
664
+ prompt: passNorm.prompt,
665
+ ranked: rankedAfterIntentGuard,
666
+ chunksById,
667
+ topK: rankProfile.lowConfidenceRerankTopK,
668
+ rankProfile,
669
+ });
670
+ if (secondPass.applied) {
671
+ rankedAfterIntentGuard = secondPass.ranked;
672
+ }
673
+ }
674
+ const promptLockPolicy = decidePromptLockPolicy(passNorm.prompt);
675
+ const allowContentPriority = promptLockPolicy.enableContentPriority;
676
+ const allowFullDocumentLock = promptLockPolicy.enableFullDocumentLock;
677
+ // 覆盖度竞争先于 A3 执行(仅在前置策略允许时开启)
678
+ // 若覆盖度竞争未触发,再尝试“导航来源 vs 内容来源”的通用语义仲裁。
679
+ const integrationPriority = allowContentPriority
680
+ ? (resolveContentPriority(rankedAfterIntentGuard, chunksById, fullDocumentMaxChars)
681
+ ?? resolveSemanticSourcePriorityByVector({
682
+ ranked: rankedAfterIntentGuard,
683
+ chunksById,
684
+ vectorTopId: vectorTopIdSnapshot,
685
+ prompt: passNorm.prompt,
686
+ fullDocumentMaxChars,
687
+ }))
688
+ : null;
398
689
  let dominantBestPractice;
399
690
  let rankedAfterBestPractice;
400
691
  if (integrationPriority) {
401
692
  // integration 胜出 → 跳过 A3,直接走完整文档路径
402
693
  rankedAfterBestPractice = rankedAfterIntentGuard;
403
- dominantBestPractice = integrationPriority;
694
+ dominantBestPractice = allowFullDocumentLock
695
+ ? integrationPriority
696
+ : { fullDocumentMode: false };
404
697
  }
405
698
  else {
406
699
  // integration 未胜出 → A3 提升 BP → BP 收敛判定
407
700
  rankedAfterBestPractice = prioritizeBestPracticeWhenCompetitive(rankedAfterIntentGuard, chunksById, passNorm.prompt);
408
- dominantBestPractice = pickDominantBestPracticeSource(rankedAfterBestPractice, chunksById);
701
+ dominantBestPractice = allowFullDocumentLock
702
+ ? pickDominantBestPracticeSource(rankedAfterBestPractice, chunksById, fullDocumentMaxChars)
703
+ : { fullDocumentMode: false };
409
704
  }
410
705
  const pinnedUrlRowChunkIds = new Set(urlRowHitChunkIds);
706
+ // 独立信号:存在精确 URL 命中 → 启用 35% 断崖截断。
707
+ // 与 pin 语义解耦:即便 pin 集合发生变化或 BP 校准影响 fragments 组成,
708
+ // 截断策略仍由“是否存在精确 URL 命中”这一确定性信号决定。
709
+ const strictCutoffOnExactUrlHit = pinnedUrlRowChunkIds.size > 0;
411
710
  const fragments = postProcess(rankedAfterBestPractice, chunksById, passNorm.limit, {
412
711
  pinnedUrlRowChunkIds,
712
+ strictCutoffOnExactUrlHit,
413
713
  preferredSource: dominantBestPractice.preferredSource,
414
714
  fullDocumentMode: dominantBestPractice.fullDocumentMode,
715
+ fullDocumentMaxChars,
716
+ maxPerSource: rankProfile.maxPerSource,
415
717
  });
416
718
  const urlMappingSources = [...new Set(fragments.map((f) => f.source).filter((source) => isUrlMappingSource(source)))];
417
719
  const references = buildReferences(fragments);
418
720
  const apiNameCoverage = computeApiNameCoverage(queryApiTokens, poolWithContent);
419
721
  const missingApiTokens = computeMissingApiTokens(queryApiTokens, poolWithContent);
722
+ // === 向量归因(终态构造)===
723
+ // final_top1_id:postProcess 之后 fragments[0] 的 chunk_id(enrichL1Fragments 后续 in-place enrich
724
+ // content 但不改动 chunk_id / 顺序,所以此处快照即最终外部感知的 top1)。
725
+ const finalTop1Id = fragments[0]?.chunk_id ?? null;
726
+ const finalTop1BmRank = finalTop1Id ? (bm25RankById.get(finalTop1Id) ?? null) : null;
727
+ const finalTop1VecScore = finalTop1Id ? (vecScoreById.get(finalTop1Id) ?? null) : null;
728
+ // promoted_by_vector_count:BM25 原始名次 > 3 且经融合/后处理进入 final top3 的 chunk 数量。
729
+ // 只需交集 |{chunk : bm25Rank(chunk) > 3 AND chunk ∈ finalTop3}|;ranked 为空自然记 0。
730
+ const finalTop3Ids = fragments.slice(0, 3).map((f) => f.chunk_id);
731
+ let promotedByVectorCount = 0;
732
+ for (const id of finalTop3Ids) {
733
+ const bmRank = bm25RankById.get(id);
734
+ if (bmRank !== undefined && bmRank > 3)
735
+ promotedByVectorCount += 1;
736
+ }
737
+ const vectorDiagnostics = {
738
+ bm25_top1_id: bm25TopIdSnapshot,
739
+ vector_top1_id: vectorTopIdSnapshot,
740
+ final_top1_id: finalTop1Id,
741
+ rank_shift_from_bm25: finalTop1BmRank,
742
+ vector_score_of_final_top1: finalTop1VecScore,
743
+ promoted_by_vector_count: promotedByVectorCount,
744
+ };
420
745
  return {
421
746
  pool,
422
747
  poolWithContent,
@@ -429,6 +754,7 @@ export function runSearchPass(params) {
429
754
  missingApiTokens,
430
755
  urlMapRelaxed: pinnedUrlRowChunkIds.size > 0,
431
756
  urlMapExactHitCount: pinnedUrlRowChunkIds.size,
757
+ vectorDiagnostics,
432
758
  preferredSource: dominantBestPractice.preferredSource,
433
759
  fullDocumentMode: dominantBestPractice.fullDocumentMode,
434
760
  };