@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
@@ -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 类型分层召回)
@@ -6,7 +6,12 @@ import { hasExplicitFrameworkMatch, hasFrameworkFallbackMatch } from './framewor
6
6
  * path-meta.ts 已通过 normalizeFrameworkDir 把目录名归一到标准 framework,
7
7
  * 所有产品的 framework 均已对齐枚举值,无需额外别名扩展。
8
8
  */
9
- const FRAMEWORK_SEARCH_ALIASES = {};
9
+ const FRAMEWORK_SEARCH_ALIASES = {
10
+ miniprogram: ['miniprogram', 'minprogram'],
11
+ minprogram: ['minprogram', 'miniprogram'],
12
+ 'react-native': ['react-native', 'rn'],
13
+ rn: ['rn', 'react-native'],
14
+ };
10
15
  function expandFrameworks(frameworks, _products) {
11
16
  if (!frameworks?.length)
12
17
  return undefined;
@@ -112,38 +117,77 @@ function promptMatchesRetrievableScope(prompt, scope) {
112
117
  const normalizedPrompt = normalizeScopeToken(prompt);
113
118
  return scope.some((token) => normalizedPrompt.includes(normalizeScopeToken(token)));
114
119
  }
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)) {
120
+ /**
121
+ * 硬过滤层(Hard filter):语义无关的强约束,任何检索通道都必须遵守。
122
+ *
123
+ * 语义:
124
+ * - product:产品维度物理隔离(不同产品的语料本就不应互相召回)。
125
+ * - A1 retrievable_scope:通配语料(share 等)必须命中作用域,防止 share 语义误召到具体产品下。
126
+ *
127
+ * 使用者:
128
+ * - BM25 通道:入池前调用
129
+ * - 向量通道:召回后二次校验,作为索引层 product 分片的兜底
130
+ */
131
+ export function filterPoolHard(chunks, norm) {
132
+ return chunks.filter((chunk) => isHardAllowed(chunk, norm));
133
+ }
134
+ export function isHardAllowed(chunk, norm) {
135
+ if (norm.product && !norm.product.includes(chunk.product) && chunk.product !== '*') {
136
+ return false;
137
+ }
138
+ // A1: 通配语料(share 等)必须命中 retrievable_scope 才能进入检索池
139
+ if (chunk.product === '*' && !promptMatchesRetrievableScope(norm.prompt, chunk.retrievable_scope)) {
140
+ return false;
141
+ }
142
+ return true;
143
+ }
144
+ /**
145
+ * 软过滤层(Soft filter):语义相关的偏好约束,向量语义召回不应被此层直接砍掉。
146
+ *
147
+ * 语义:
148
+ * - framework:平台维度偏好(向量语义可跨平台复用,应转为软降权)
149
+ * - scenario:场景维度偏好(live/voice)
150
+ * - types:文档类型偏好(Agent 可能传错,向量语义能自愈到正确类型)
151
+ *
152
+ * 使用者:
153
+ * - BM25 通道:与 Hard 合用(保持现网行为)
154
+ * - 向量通道:不使用;由 applyRankMultipliers 在融合后做软降权
155
+ */
156
+ export function isSoftAllowed(chunk, norm) {
157
+ if (norm.frameworks?.length) {
158
+ const isWildcardChunk = chunk.framework.includes('*');
159
+ // 收紧:framework=['*'] 仅在该 chunk 明确是跨平台 type(product/server_api/webhook)时放行;
160
+ // 否则要求 chunk.framework 与请求 framework 有交集。
161
+ const wildcardAllowed = isWildcardChunk && chunkIsCrossPlatform(chunk.types);
162
+ const explicitMatch = hasExplicitFrameworkMatch(chunk.framework, norm.frameworks);
163
+ const fallbackMatch = hasFrameworkFallbackMatch(chunk, {
164
+ product: norm.product,
165
+ framework: norm.framework,
166
+ frameworks: norm.frameworks,
167
+ });
168
+ if (!explicitMatch && !wildcardAllowed && !fallbackMatch)
122
169
  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) {
170
+ }
171
+ if (norm.scenario && chunk.scenario && chunk.scenario !== norm.scenario) {
172
+ return false;
173
+ }
174
+ // IoT 端别过滤改为“召回保留 + 排序分层”:
175
+ // Phase2 中 exact/common/conflict 的优先级由 applyRankMultipliers 处理,
176
+ // 这里不做硬过滤,避免因标注偏差误杀有效候选。
177
+ // best-practice 文档对 types 过滤免疫:内容不依赖 types 分类,应始终进 pool 靠 BM25 自然排名。
178
+ // 典型场景:Agent 传 types=['api'] 时,types=['faq'] 的 best-practice 不应被过滤掉。
179
+ if (norm.types?.length && !norm.types.some((t) => chunk.types.includes(t))) {
180
+ if (!chunk.source.includes('/best-practice/'))
139
181
  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
- });
182
+ }
183
+ return true;
184
+ }
185
+ /**
186
+ * 完整过滤(Hard + Soft):BM25 主通道使用,保持现网行为不变。
187
+ *
188
+ * 向量通道**不应**使用此函数——它会把 framework/types/scenario 硬砍掉,
189
+ * 抵消向量语义的跨平台/类型自愈能力。向量通道请使用 filterPoolHard。
190
+ */
191
+ export function filterPool(chunks, norm) {
192
+ return chunks.filter((chunk) => isHardAllowed(chunk, norm) && isSoftAllowed(chunk, norm));
149
193
  }
@@ -0,0 +1,25 @@
1
+ const DEFAULT_PROFILE = {
2
+ name: 'default',
3
+ maxPerSource: 3,
4
+ rerankTopK: 20,
5
+ lowConfidenceRerankTopK: 0,
6
+ lowConfidenceTopScoreThreshold: 0,
7
+ lowConfidenceGapRatioThreshold: 0,
8
+ titleExactMultiplier: 1.03,
9
+ titleTokenBoost: 0.05,
10
+ };
11
+ const IOT_PROFILE = {
12
+ name: 'iot',
13
+ maxPerSource: 2,
14
+ rerankTopK: 24,
15
+ lowConfidenceRerankTopK: 10,
16
+ lowConfidenceTopScoreThreshold: 28,
17
+ lowConfidenceGapRatioThreshold: 1.08,
18
+ titleExactMultiplier: 1.15,
19
+ titleTokenBoost: 0.15,
20
+ };
21
+ export function resolveRankProfile(params) {
22
+ if (params.product?.includes('iot'))
23
+ return IOT_PROFILE;
24
+ return DEFAULT_PROFILE;
25
+ }
@@ -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/...)时才拦询问。
@@ -60,6 +60,9 @@ export function requiresFramework(norm) {
60
60
  return false;
61
61
  return false;
62
62
  }
63
+ // iot:产品级/能力线/API 问法第一阶段允许跨平台检索,不做 framework 前置拦截
64
+ if (hasProduct(norm, 'iot'))
65
+ return false;
63
66
  // native_trtc_sdk:所有平台相关咨询都依赖具体客户端(android/ios/c++),必须先问
64
67
  if (hasProduct(norm, 'native_trtc_sdk'))
65
68
  return true;
@@ -36,11 +36,22 @@ 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 = {};
43
53
  const urlMappingSourceCount = {};
54
+ const maxPerSource = Math.max(1, opts?.maxPerSource ?? 3);
44
55
  /** 已入选的 push config/android/{vendor} 文件,用于抑制其他厂商噪声 */
45
56
  let selectedVendorFile = null;
46
57
  const fragments = [];
@@ -60,7 +71,7 @@ export function postProcess(ranked, chunksById, limit, opts) {
60
71
  }
61
72
  else {
62
73
  const count = sourceCount[chunk.source] ?? 0;
63
- if (count >= 3)
74
+ if (count >= maxPerSource)
64
75
  continue;
65
76
  sourceCount[chunk.source] = count + 1;
66
77
  }
@@ -88,22 +99,18 @@ export function postProcess(ranked, chunksById, limit, opts) {
88
99
  }
89
100
  else if (!isPinnedUrlRow) {
90
101
  const topScore = fragments[0].score;
91
- // 方案A:分数断崖截断。
92
- // 当已有 E7 精确命中行(pinned url-row)时,后续非 pinned 行若跌破 Top1 的 20%,
93
- // 视为「Top1 已精确命中、后续多为泛匹配噪声」,提前截断,避免凑满 limit 返回冗余。
94
- // 无 E7 命中时不触发(此时答案依赖 best-practice/本地文档,需完整保留)。
102
+ // 方案A:分数断崖截断(拆分为两个独立信号):
103
+ // 1) 通用分数衰减截断(不依赖 pin/exact-url 状态)
104
+ // 2) 精确 URL 命中断崖:由 caller 通过 strictCutoffOnExactUrlHit 显式启用,
105
+ // 避免"fragments 里恰好含 pinned row"这类间接信号驱动策略。
95
106
  if (topScore > 0) {
96
- const pinnedChunkIds = opts?.pinnedUrlRowChunkIds;
97
- const hasPinnedRow = pinnedChunkIds
98
- ? fragments.some((f) => pinnedChunkIds.has(f.chunk_id))
99
- : false;
100
107
  // 原有:分数衰减到 top 的 5% 以下时停止
101
108
  if (item.score < topScore * 0.05)
102
109
  break;
103
- // 新增:已有 E7 精确命中 + 后续跌破 35% → 断崖截断
110
+ // 新增:精确 URL 命中场景 → 后续跌破 35% → 断崖截断
104
111
  // 阈值取 35%(非 20%):best-practice 泛匹配分数常在 Top1 的 20%~35% 区间,
105
- // 用 35% 才能拦住「Top1 精确命中后仍返回一堆无关 best-practice」的冗余。
106
- if (hasPinnedRow && item.score < topScore * 0.35)
112
+ // 用 35% 才能拦住"Top1 精确命中后仍返回一堆无关 best-practice"的冗余。
113
+ if (opts?.strictCutoffOnExactUrlHit && item.score < topScore * 0.35)
107
114
  break;
108
115
  }
109
116
  }
@@ -378,7 +385,29 @@ export function hasNativeClientFramework(frameworks) {
378
385
  return false;
379
386
  return frameworks.some((fw) => ['android', 'ios', 'flutter', 'c++'].includes(fw));
380
387
  }
388
+ function normalizeCompactText(input) {
389
+ return input.toLowerCase().replace(/[\s\-_]+/g, '');
390
+ }
391
+ function hasExactTitleMatch(prompt, title) {
392
+ if (!prompt || !title)
393
+ return false;
394
+ const p = normalizeCompactText(prompt);
395
+ const t = normalizeCompactText(title);
396
+ if (!p || !t)
397
+ return false;
398
+ return (t.length >= 3 && p.includes(t)) || (p.length >= 3 && t.includes(p));
399
+ }
400
+ function tokenizePromptForTitle(prompt) {
401
+ return [...new Set(prompt
402
+ .toLowerCase()
403
+ .split(/[^a-z0-9\u4e00-\u9fff]+/)
404
+ .map((s) => s.trim())
405
+ .filter((s) => s.length >= 2))];
406
+ }
381
407
  export function applyRankMultipliers(ranked, chunksById, opts) {
408
+ const prompt = opts.prompt ?? '';
409
+ const promptTokens = tokenizePromptForTitle(prompt);
410
+ const isIotProfile = opts.rank_profile === 'iot';
382
411
  const adjusted = ranked.map((item) => {
383
412
  const chunk = chunksById.get(item.id);
384
413
  if (!chunk)
@@ -396,6 +425,38 @@ export function applyRankMultipliers(ranked, chunksById, opts) {
396
425
  multiplier *= 0.2;
397
426
  }
398
427
  }
428
+ // Phase1: title/h2 与 query 精确短语命中增强
429
+ if (prompt && hasExactTitleMatch(prompt, chunk.title)) {
430
+ multiplier *= isIotProfile ? 1.15 : 1.03;
431
+ }
432
+ if (promptTokens.length > 0) {
433
+ const titleText = `${chunk.title} ${(chunk.key_phrases ?? []).join(' ')}`.toLowerCase();
434
+ const overlap = promptTokens.reduce((acc, token) => (titleText.includes(token) ? acc + 1 : acc), 0);
435
+ if (overlap > 0) {
436
+ multiplier *= 1 + Math.min(0.06, overlap * (isIotProfile ? 0.015 : 0.008));
437
+ }
438
+ }
439
+ // Phase2: IoT feature_domain 软回退(IoT 唯一排序信号)
440
+ if (chunk.product === 'iot') {
441
+ if (chunk.feature_domain && opts.preferred_iot_feature_domain) {
442
+ if (chunk.feature_domain === opts.preferred_iot_feature_domain)
443
+ multiplier *= 1.15;
444
+ else
445
+ multiplier *= 0.9;
446
+ }
447
+ // Phase2b: 强 feature 意图 → source 精准置顶
448
+ // 中文意图词(云台/语音对讲)与英文文件名(ptz-control/voice-intercom)无词面关系,
449
+ // 用 source 路径关键词精准命中 feature 文档,越过短文本泛文档的 BM25 优势。
450
+ if (opts.preferred_iot_feature_source && chunk.source.includes(opts.preferred_iot_feature_source)) {
451
+ multiplier *= 1.7;
452
+ }
453
+ // 非 demo 意图下,弱化 app_demo/device_demo 对功能问法的抢位
454
+ if (chunk.feature_domain && /demo$/i.test(chunk.feature_domain)) {
455
+ if (!/\bdemo\b|示例|sample|example|演示/i.test(prompt)) {
456
+ multiplier *= 0.82;
457
+ }
458
+ }
459
+ }
399
460
  return { ...item, score: item.score * multiplier };
400
461
  });
401
462
  return adjusted.sort((a, b) => b.score - a.score);