@tencentcloud/sdk-mcp 1.6.8 → 1.7.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 (158) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +59 -35
  3. package/knowledge/build-index.js +28 -4
  4. package/knowledge/chunker.js +82 -14
  5. package/knowledge/chunking-policy.js +100 -0
  6. package/knowledge/doc-url-resolver.js +70 -19
  7. package/knowledge/frontmatter-meta.js +167 -0
  8. package/knowledge/path-meta.js +28 -6
  9. package/knowledge/search/exact-lookup.js +126 -125
  10. package/knowledge/search/filter-pool.js +12 -29
  11. package/knowledge/search/framework-fallback.js +22 -0
  12. package/knowledge/search/intent-router.js +29 -0
  13. package/knowledge/search/l1-enrich.js +159 -0
  14. package/knowledge/search/query-normalizer.js +63 -3
  15. package/knowledge/search/search-core.js +410 -0
  16. package/knowledge/search/search-knowledge.js +419 -377
  17. package/knowledge/search/search-message.js +88 -0
  18. package/knowledge/search/search-pass.js +428 -0
  19. package/knowledge/search/search-types.js +1 -0
  20. package/knowledge/search/url-row-idf.js +120 -0
  21. package/knowledge/search/url-row-scorer.js +337 -0
  22. package/package.json +3 -1
  23. package/resource/index/manifest.json +1 -1
  24. package/resource/integration/callkit/preset/flutter/integration.md +7 -3
  25. package/resource/integration/callkit/preset/ios/integration.md +1 -0
  26. package/resource/integration/chat/preset/android/integration.md +1 -0
  27. package/resource/integration/chat/preset/flutter/integration.md +111 -10
  28. package/resource/integration/chat/preset/ios/integration.md +1 -0
  29. package/resource/integration/chat/preset/react/integration.md +1 -0
  30. package/resource/integration/chat/preset/uni-app/integration.md +1 -0
  31. package/resource/integration/chat/preset/vue/integration.md +1 -0
  32. package/resource/integration/livekit/preset/live/android/prepare-integration.md +1 -0
  33. package/resource/integration/livekit/preset/live/vue/live-player-desk-integration.md +12 -10
  34. package/resource/integration/livekit/preset/live/vue/live-pusher-integration.md +14 -14
  35. package/resource/integration/roomkit/preset/vue/integration.md +1 -0
  36. package/resource/knowledge/callkit/best-practice/callkit-miniprogram/uniapp-shrink.md +64 -0
  37. package/resource/knowledge/callkit/best-practice/common/call-autoplay-blocked.md +81 -0
  38. package/resource/knowledge/callkit/best-practice/common/call-message-in-chat.md +84 -0
  39. package/resource/knowledge/callkit/best-practice/common/call-vs-calls.md +114 -0
  40. package/resource/knowledge/callkit/best-practice/common/callengine-call-flow.md +128 -0
  41. package/resource/knowledge/callkit/best-practice/common/callengine-log-level.md +72 -0
  42. package/resource/knowledge/callkit/best-practice/common/callkit-get-engine-instance.md +87 -0
  43. package/resource/knowledge/callkit/best-practice/common/callkit-get-roomid.md +83 -0
  44. package/resource/knowledge/callkit/best-practice/common/callkit-limitations.md +37 -0
  45. package/resource/knowledge/callkit/best-practice/common/callkit-user-register-and-kicked.md +103 -0
  46. package/resource/knowledge/callkit/core-sdk/android/answer-call.md +1 -0
  47. package/resource/knowledge/callkit/core-sdk/android/make-call.md +1 -0
  48. package/resource/knowledge/callkit/core-sdk/flutter/answer-call.md +1 -0
  49. package/resource/knowledge/callkit/core-sdk/flutter/make-call.md +1 -0
  50. package/resource/knowledge/callkit/core-sdk/ios/answer-call.md +1 -0
  51. package/resource/knowledge/callkit/core-sdk/ios/make-call.md +1 -0
  52. package/resource/knowledge/callkit/core-sdk/ios/offline-push.md +1 -1
  53. package/resource/knowledge/callkit/core-sdk/uni-app/answer-call.md +1 -0
  54. package/resource/knowledge/callkit/core-sdk/uni-app/make-call.md +1 -0
  55. package/resource/knowledge/callkit/faq/flutter.md +1 -0
  56. package/resource/knowledge/callkit/faq/uniapp-app.md +1 -0
  57. package/resource/knowledge/callkit/preset/react/api.md +1 -150
  58. package/resource/knowledge/callkit/preset/vue/api.md +1 -38
  59. package/resource/knowledge/chat/best-practice/common/avchatroom-message-loss-diagnosis.md +71 -0
  60. package/resource/knowledge/chat/best-practice/common/group-conversation-cleanup-on-system-notice.md +67 -0
  61. package/resource/knowledge/chat/best-practice/common/image-message-url-lifecycle.md +63 -0
  62. package/resource/knowledge/chat/best-practice/common/login-repeat-2025-diagnosis.md +65 -0
  63. package/resource/knowledge/chat/best-practice/common/message-read-report-and-receipt.md +72 -0
  64. package/resource/knowledge/chat/best-practice/common/message-receive-event.md +70 -0
  65. package/resource/knowledge/chat/best-practice/common/message-risk-error-and-render.md +69 -0
  66. package/resource/knowledge/chat/best-practice/common/message-unread-count.md +71 -0
  67. package/resource/knowledge/chat/best-practice/miniprogram/audio-message-send.md +118 -0
  68. package/resource/knowledge/chat/best-practice/miniprogram/login-timeout-2801-diagnosis.md +67 -0
  69. package/resource/knowledge/chat/best-practice/miniprogram/media-upload-fail-diagnosis.md +68 -0
  70. package/resource/knowledge/chat/best-practice/web/audio-message-send.md +123 -0
  71. package/resource/knowledge/chat/preset/android/features/conversation-list.md +1 -0
  72. package/resource/knowledge/chat/preset/android/features/language.md +1 -0
  73. package/resource/knowledge/chat/preset/android/features/message-input.md +1 -0
  74. package/resource/knowledge/chat/preset/android/features/message-list.md +1 -0
  75. package/resource/knowledge/chat/preset/android-view/integration.md +1 -0
  76. package/resource/knowledge/chat/preset/flutter/features/conversation-list.md +1 -0
  77. package/resource/knowledge/chat/preset/flutter/features/message-input.md +1 -0
  78. package/resource/knowledge/chat/preset/flutter/features/message-list.md +1 -0
  79. package/resource/knowledge/chat/preset/ios/features/conversation-list.md +1 -0
  80. package/resource/knowledge/chat/preset/ios/features/message-input.md +1 -0
  81. package/resource/knowledge/chat/preset/ios/features/message-list.md +1 -0
  82. package/resource/knowledge/chat/preset/ios-uikit/integration.md +1 -0
  83. package/resource/knowledge/chat/preset/react/features/conversation-preview.md +1 -0
  84. package/resource/knowledge/chat/preset/react/features/message-input.md +61 -3
  85. package/resource/knowledge/chat/preset/react/features/message-list.md +7 -0
  86. package/resource/knowledge/chat/preset/vue/features/message-input.md +58 -5
  87. package/resource/knowledge/chat/preset/vue/features/message-list.md +7 -0
  88. package/resource/knowledge/chat/urls/product.md +8 -8
  89. package/resource/knowledge/chat/urls/restapi.md +3 -3
  90. package/resource/knowledge/chat/urls/sdk/android.md +5 -5
  91. package/resource/knowledge/chat/urls/sdk/flutter.md +5 -5
  92. package/resource/knowledge/chat/urls/sdk/ios.md +5 -5
  93. package/resource/knowledge/chat/urls/sdk/web.md +8 -8
  94. package/resource/knowledge/chat/urls/webhook.md +11 -17
  95. package/resource/knowledge/livekit/best-practice/android/voice-room-anchor-start.md +124 -0
  96. package/resource/knowledge/livekit/best-practice/common/admin-take-seat.md +73 -0
  97. package/resource/knowledge/livekit/best-practice/common/audience-leave-live.md +81 -0
  98. package/resource/knowledge/livekit/best-practice/common/voice-room-no-audio-troubleshoot.md +100 -0
  99. package/resource/knowledge/livekit/best-practice/common/voice-room-seat-list-change.md +65 -0
  100. package/resource/knowledge/livekit/best-practice/flutter/voice-room-anchor-start.md +126 -0
  101. package/resource/knowledge/livekit/best-practice/ios/voice-room-anchor-start.md +130 -0
  102. package/resource/knowledge/push/config/harmonyos.md +1 -1
  103. package/resource/knowledge/push/config/ios.md +2 -2
  104. package/resource/knowledge/push/config/react-native.md +1 -0
  105. package/resource/knowledge/push/config/unity.md +1 -0
  106. package/resource/knowledge/push/config/unreal-engine.md +1 -0
  107. package/resource/knowledge/push/integration/android.md +17 -16
  108. package/resource/knowledge/push/integration/donut.md +2 -2
  109. package/resource/knowledge/push/integration/flutter.md +4 -3
  110. package/resource/knowledge/push/integration/harmonyos.md +1 -1
  111. package/resource/knowledge/push/integration/ios.md +4 -3
  112. package/resource/knowledge/push/integration/react-native.md +1 -1
  113. package/resource/knowledge/push/integration/uni-app.md +1 -0
  114. package/resource/knowledge/push/integration/unity.md +4 -4
  115. package/resource/knowledge/push/integration/unreal-engine.md +37 -35
  116. package/resource/knowledge/roomkit/best-practice/common/room-ghost-member.md +75 -0
  117. package/resource/knowledge/roomkit/best-practice/common/room-limit-and-multi.md +88 -0
  118. package/resource/knowledge/roomkit/best-practice/common/room-screen-share-preview.md +87 -0
  119. package/resource/knowledge/roomkit/best-practice/common/room-status-after-all-leave.md +73 -0
  120. package/resource/knowledge/roomkit/best-practice/vue/room-uikit-vs-atomicx.md +115 -0
  121. package/resource/knowledge/roomkit/core-sdk/android/call-user-to-room.md +1 -0
  122. package/resource/knowledge/roomkit/core-sdk/android/room-participant-state.md +1 -0
  123. package/resource/knowledge/roomkit/core-sdk/android/room-state.md +1 -0
  124. package/resource/knowledge/roomkit/core-sdk/android/schedule-room.md +1 -0
  125. package/resource/knowledge/roomkit/core-sdk/flutter/call-user-to-room.md +1 -0
  126. package/resource/knowledge/roomkit/core-sdk/flutter/room-participant-state.md +1 -0
  127. package/resource/knowledge/roomkit/core-sdk/flutter/room-state.md +1 -0
  128. package/resource/knowledge/roomkit/core-sdk/flutter/schedule-room.md +1 -0
  129. package/resource/knowledge/roomkit/core-sdk/ios/call-user-to-room.md +1 -0
  130. package/resource/knowledge/roomkit/core-sdk/ios/room-participant-state.md +1 -0
  131. package/resource/knowledge/roomkit/core-sdk/ios/room-state.md +1 -0
  132. package/resource/knowledge/roomkit/core-sdk/ios/schedule-room.md +1 -0
  133. package/resource/knowledge/roomkit/core-sdk/vue/room-view.md +1 -0
  134. package/resource/knowledge/roomkit/preset/react/integration.md +1 -0
  135. package/resource/knowledge/rtcengine/web/plugin/30-advanced-cross-room-link.md +2 -0
  136. package/resource/knowledge/rtcengine/web/plugin/39-advanced-video-decoder.md +2 -0
  137. package/resource/knowledge/trtc/android/TRTCCloud.md +1 -0
  138. package/resource/knowledge/trtc/c++/ITRTCCloud.md +1 -0
  139. package/resource/knowledge/trtc/ios/TRTCCloud.md +1 -0
  140. package/server.js +9 -4
  141. package/streamable-http.js +19 -54
  142. package/tools/common/search_trtc_knowledge.js +24 -12
  143. package/tools/definitions/finalize_answer_def.js +2 -2
  144. package/tools/definitions/integration-boundary.js +2 -1
  145. package/tools/definitions/search_trtc_knowledge_def.js +5 -3
  146. package/utils/fetch-doc-content.js +198 -0
  147. package/utils/preflight-route.js +197 -0
  148. package/utils/report-cls-client.js +1 -1
  149. package/utils/report-es-client.js +1 -1
  150. package/cursor-setting.png +0 -0
  151. package/mcp-server-prompt.png +0 -0
  152. package/resource/knowledge/callkit/best-practice/web/callengine-invitation.md +0 -80
  153. package/resource/knowledge/callkit/best-practice/web/callengine-log-control.md +0 -20
  154. package/resource/knowledge/callkit/best-practice/web/callkit-call-vs-calls.md +0 -116
  155. package/resource/knowledge/callkit/preset/react/uikit-api-reference.md +0 -469
  156. package/resource/knowledge/callkit/preset/vue/uikit-api-reference.md +0 -581
  157. package/resource/knowledge/callkit/preset/web/api.md +0 -79
  158. package/resource/knowledge/chat/best-practice/web/image-message-url-handling.md +0 -16
package/CHANGELOG.md ADDED
@@ -0,0 +1,36 @@
1
+ # CHANGELOG
2
+
3
+ ## Version 1.7.1 @2026.08.11
4
+
5
+ ### 新增
6
+ - 新增检索前置归一化:基于 prompt 真值对 `product` / `frameworks` 做归一化,解决 agent 推理错误导致的检索偏差。
7
+
8
+ ### 变更
9
+ - 优化 `search_trtc_knowledge` 输出契约:移除 "1-2 sentence per subsection" 等压缩性措辞,改为输出完整合成答案。
10
+
11
+ ### 修复
12
+ - 修复 "安卓 Web TRTC" 等 prompt 下 agent 误传双 product / 双 frameworks 时未按 prompt 真值校正的问题。
13
+
14
+ ## Version 1.7.0 @2026.08.02
15
+
16
+ ### 新增
17
+ - 补充并同步 Chat / Call / Live / Room 等产品域最佳实践文档,增强知识覆盖范围。
18
+
19
+ ### 变更
20
+ - 持续重构双语检索链路,优化 query 归一化、意图路由、候选过滤与精确命中策略。
21
+ - 优化最佳实践与 URL 类文档的召回与排序,提升问答结果可用性与稳定性。
22
+ - 优化 `search_trtc_knowledge` 相关定义与答复约束,提升回答可读性与可执行性。
23
+
24
+ ### 修复
25
+ - 修复复杂问法下部分检索结果偏题的问题,降低高分误召回概率。
26
+
27
+ ## Version 1.6.6 @2026.07.18
28
+
29
+ ### 新增
30
+ - 支持 `get_native_live_uikit_integration` 工具,支持 LiveKit(Android/iOS) 含 UI 集成。
31
+
32
+ ## Version 1.6.0 @2026.07.01
33
+
34
+ ### 新增
35
+ - 重构 MCP 检索能力,收敛 MCP 工具数量。
36
+ - 支持 `search_trtc_knowledge` 工具,支持实时音视频(TRTC/IM)相关文档检索。
package/README.md CHANGED
@@ -1,42 +1,29 @@
1
- ## Tencent SDK Model Context Protocol (MCP) Server for Cursor IDE
1
+ ## 腾讯云 SDK MCP Server 使用说明
2
2
 
3
- This repository provides a CLI-based Model Context Protocol (MCP) server that exposes Tencent SDK documentation and Tencent API resources to LLM-powered tools.
4
- This improves the LLM AI Agent's ability to understand and interact with Tencent's SDKs and APIs.
3
+ 本仓库提供一个基于 CLI 的 Model Context Protocol(MCP)Server,向 LLM 工具暴露腾讯云 SDK 文档与 API 资料,帮助 AI Agent 更准确地理解并集成腾讯云实时音视频能力。
5
4
 
6
5
 
7
- ## Features
6
+ ## 功能特性
8
7
 
9
- - MCP server exposing tools for interacting with Tencent via JSON-RPC over STDIN/STDOUT.
10
- - MCP server supports SSE Mode by supplying `HTTP_PORT` environment variable.
11
- - Retrieve official Tencent SDK documentation (HTML → Markdown) for:
12
- - Languages: JavaScript, Java, Swift, Objective-C, Kotlin.
13
- - API reference sections: configuration, calls function.
14
- - Retrieve official Tencent TUICallKit SDK documentation (HTML → Markdown) for:
15
- - TUICallKit SDK languages: JavaScript, Kotlin, Swift.
16
- - TUICallKit SDK topics: configuration, calls function.
17
- - Converts remote HTML articles to Markdown using `jsdom` and `turndown` for consistent documentation formatting.
18
- - Input validation via Zod schemas for all tool parameters, ensuring robust error handling.
19
- - Extensible tool definitions leveraging the Model Context Protocol SDK (`@modelcontextprotocol/sdk`) with `McpServer` and `StdioServerTransport`.
8
+ - **高效检索**:支持腾讯云实时音视频文档检索,快速定位官方资料与实践说明。
9
+ - **覆盖全面**:支持 Chat(IM)、Call、Live、Room、RTCEngine(TRTC SDK)、TIMPush 等产品域。
10
+ - **智能路由**:支持产品/平台语义识别与消歧,在必要时引导补齐平台与框架信息。
11
+ - **集成友好**:除文档问答外,提供 Web / Android / iOS / Flutter 等含 UI 集成入口。
12
+ - **官方可信**:基于官方资料,提升答案可靠性与可落地性。
13
+ - **广泛兼容**:通过 STDIN/STDOUT 的 JSON-RPC 提供 MCP 能力,可接入 Cursor / Codex / Claude / CodeBuddy 等 MCP 客户端。
14
+ - **开箱即用**:`npx` 即可运行,低接入门槛。
20
15
 
16
+ ## 环境要求
17
+ - Node.js(>= 18)与 npm
18
+ - 支持 MCP 的 IDE
21
19
 
22
- ## Prerequisites
23
- - Node.js (>= 18) and npm
24
- - Cursor IDE with MCP support
25
20
 
21
+ ## 使用方式
22
+ ### 标准接入流程
26
23
 
27
- ## How To Use
28
- To set up the @tencentcloud/sdk-mcp MCP Server, follow these steps:
24
+ - Step1:在你的 Cursor 项目中,创建或编辑 `.cursor/mcp.json` 或 `~/.cursor/mcp.json`,添加如下配置。
29
25
 
30
- - Step1: Installation.
31
-
32
- ```
33
- npx -y @tencentcloud/sdk-mcp
34
- ```
35
-
36
-
37
- - Step2: In your Cursor project, create or open the `.cursor/mcp.json` or `~/.cursor/mcp.json` files and add your config.
38
-
39
- ```javascript
26
+ ```json
40
27
  {
41
28
  "mcpServers": {
42
29
  "tencentcloud-sdk-mcp": {
@@ -50,16 +37,53 @@ npx -y @tencentcloud/sdk-mcp
50
37
  }
51
38
  }
52
39
  ```
53
- When you save the file, a notification is displayed. In the prompt, click **Enable**.
40
+ 保存后会弹出提示,在提示中点击 **Enable**。
54
41
 
55
42
  ![mcp-server-prompt](https://web.sdk.qcloud.com/im/assets/images/mcp-server-prompt.png)
56
43
 
57
- - Step3: Navigate to **Cursor Settings** (the gear icon in top right corner) -> **MCP**, and check if tencentcloud-sdk-mcp server is enabled.
44
+ - Step2:进入 **Cursor Settings**(右上角齿轮)-> **MCP**,确认 `tencentcloud-sdk-mcp` 已启用。
58
45
 
59
46
  ![cursor-setting](https://web.sdk.qcloud.com/im/assets/images/cursor-setting.png)
60
47
 
48
+ - Step3:在 IDE 中直接描述你的需求,让 AI 基于腾讯云 SDK 文档完成实现或答疑。
49
+
50
+ ### 可选:全局安装
51
+
52
+ 如需在本地全局快速调用,可先执行:
53
+
54
+ ```bash
55
+ npx -y @tencentcloud/sdk-mcp
56
+ ```
57
+
58
+ ## 更新记录
59
+
60
+ > 仅展示最近更新,完整历史请查看 `CHANGELOG.md`。
61
+
62
+ ## Version 1.7.1 @2026.08.11
63
+
64
+ ### 新增
65
+ - 新增检索前置归一化:基于 prompt 真值对 `product` / `frameworks` 做归一化,解决 agent 推理错误导致的检索偏差。
66
+
67
+ ### 变更
68
+ - 优化 `search_trtc_knowledge` 输出契约:移除 "1-2 sentence per subsection" 等压缩性措辞,改为输出完整合成答案。
69
+
70
+ ### 修复
71
+ - 修复 "安卓 Web TRTC" 等 prompt 下 agent 误传双 product / 双 frameworks 时未按 prompt 真值校正的问题。
72
+
73
+ ### Version 1.7.0 @2026.08.02
74
+
75
+ #### 新增
76
+ - 补充并同步 Chat / Call / Live / Room 等产品域最佳实践文档,增强知识覆盖范围。
77
+
78
+ #### 变更
79
+ - 持续重构双语检索链路,优化 query 归一化、意图路由、候选过滤与精确命中策略。
80
+ - 优化最佳实践与 URL 类文档的召回与排序,提升问答结果可用性与稳定性。
81
+ - 优化 `search_trtc_knowledge` 相关定义与答复约束,提升回答可读性与可执行性。
82
+
83
+ #### 修复
84
+ - 修复复杂问法下部分检索结果偏题的问题,降低高分误召回概率。
61
85
 
62
- Step4: Ask the AI agent to build your Tencentcloud sdk app for you by describing the functionality yourself or by using a sample prompt.
86
+ ### Version 1.6.6 @2026.07.18
63
87
 
64
- > [!WARNING]
65
- Make sure to use the Agent mode. Depending on your Cursor rules, you may also need to explicitly ask the AI agent to use the Tencentcloud sdk-mcp server.
88
+ #### 新增
89
+ - 支持 `get_native_live_uikit_integration` 工具,支持 LiveKit(Android/iOS) 含 UI 集成。
@@ -3,7 +3,9 @@ import path from 'path';
3
3
  import { fileURLToPath } from 'url';
4
4
  import { chunkMarkdown, toManifestChunk } from './chunker.js';
5
5
  import { buildDocSourceUrlMap } from './doc-url-resolver.js';
6
+ import { mergePathMetaWithFrontmatter, resolveFrontmatterMeta } from './frontmatter-meta.js';
6
7
  import { inferPathMeta } from './path-meta.js';
8
+ import { loadChunkingPolicy, resolveChunkingRule, resolvePolicyFilePath } from './chunking-policy.js';
7
9
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
10
  // 构建期写入索引到 dist/resource/index/:
9
11
  // dist/knowledge/ → ../resource/ → dist/resource/
@@ -45,11 +47,32 @@ export function buildKnowledgeIndex() {
45
47
  ...walkMarkdownFiles(path.join(RESOURCE_ROOT, 'integration'), 'integration'),
46
48
  ];
47
49
  const chunks = [];
50
+ let frontmatterFileCount = 0;
51
+ let frontmatterMergedCount = 0;
52
+ let frontmatterParseErrorCount = 0;
53
+ const chunkingPolicyPath = resolvePolicyFilePath(__dirname);
54
+ const chunkingPolicy = loadChunkingPolicy(chunkingPolicyPath);
48
55
  for (const source of sources) {
49
56
  const abs = path.join(RESOURCE_ROOT, source);
50
- const content = fs.readFileSync(abs, 'utf-8');
51
- const meta = inferPathMeta(source);
52
- chunks.push(...chunkMarkdown(source, content, meta).map(toManifestChunk));
57
+ const rawContent = fs.readFileSync(abs, 'utf-8');
58
+ // Phase A(兼容期):frontmatter 优先,未声明字段继续走路径推断 fallback。
59
+ const pathMeta = inferPathMeta(source);
60
+ const fm = resolveFrontmatterMeta(rawContent);
61
+ const meta = mergePathMetaWithFrontmatter(pathMeta, fm.frontmatterMeta, fm.overrideFields);
62
+ if (fm.hasFrontmatter)
63
+ frontmatterFileCount += 1;
64
+ if (meta.meta_source === 'merged')
65
+ frontmatterMergedCount += 1;
66
+ if (fm.parseError) {
67
+ frontmatterParseErrorCount += 1;
68
+ console.warn(`[build-knowledge-index] frontmatter parse failed: ${source} - ${fm.parseError}`);
69
+ }
70
+ const policyRule = resolveChunkingRule(source, chunkingPolicy);
71
+ chunks.push(...chunkMarkdown(source, fm.content, meta, {
72
+ baseOffset: fm.contentOffset,
73
+ policyRule,
74
+ defaultSplitTokenThreshold: chunkingPolicy.default.split_token_threshold,
75
+ }).map(toManifestChunk));
53
76
  }
54
77
  const indexDir = path.join(RESOURCE_ROOT, 'index');
55
78
  fs.mkdirSync(indexDir, { recursive: true });
@@ -68,5 +91,6 @@ export function buildKnowledgeIndex() {
68
91
  if (fs.existsSync(staleSearchIndex)) {
69
92
  fs.unlinkSync(staleSearchIndex);
70
93
  }
71
- console.warn(`build-knowledge-index: ${chunks.length} chunks (manifest-lite) from ${sources.length} files`);
94
+ console.warn(`build-knowledge-index: ${chunks.length} chunks (manifest-lite) from ${sources.length} files; `
95
+ + `frontmatter files=${frontmatterFileCount}, merged=${frontmatterMergedCount}, parse_errors=${frontmatterParseErrorCount}`);
72
96
  }
@@ -42,6 +42,34 @@ function isFaqLike(source, content) {
42
42
  function shouldForceSectionSplit(source, meta) {
43
43
  return source.endsWith('unsupported-features.md') || meta.doc_kind === 'unsupported_summary';
44
44
  }
45
+ function isBestPracticeSource(source) {
46
+ return /\/best-practice\//i.test(source.replace(/\\/g, '/'));
47
+ }
48
+ const BEST_PRACTICE_SINGLE_CHUNK_MAX_TOKENS = 4096;
49
+ const LEGACY_SINGLE_CHUNK_CHAR_THRESHOLD = 2048;
50
+ function estimateTextTokens(text) {
51
+ let tokens = 0;
52
+ for (const ch of text) {
53
+ tokens += /[A-Za-z]/.test(ch) ? 0.5 : 1;
54
+ }
55
+ return tokens;
56
+ }
57
+ function estimateMarkdownTokens(content) {
58
+ let tokens = 0;
59
+ let cursor = 0;
60
+ const codeBlockRE = /```[\s\S]*?```/g;
61
+ for (const match of content.matchAll(codeBlockRE)) {
62
+ const start = match.index ?? cursor;
63
+ const end = start + match[0].length;
64
+ tokens += estimateTextTokens(content.slice(cursor, start));
65
+ tokens += match[0].length * 1.3;
66
+ cursor = end;
67
+ }
68
+ if (cursor < content.length) {
69
+ tokens += estimateTextTokens(content.slice(cursor));
70
+ }
71
+ return tokens;
72
+ }
45
73
  function isChatUrlMapping(source) {
46
74
  return /knowledge\/chat\/urls\/.+\.md$/i.test(source.replace(/\\/g, '/'));
47
75
  }
@@ -86,7 +114,7 @@ function extractKeyPhrases(title, body) {
86
114
  .slice(0, 8);
87
115
  return [...new Set(phrases)];
88
116
  }
89
- function buildChunk(meta, source, title, body, id, char_start, char_end) {
117
+ function buildChunk(meta, source, title, body, id, char_start, char_end, extra) {
90
118
  const api_names = extractApiNames(body);
91
119
  const key_phrases = extractKeyPhrases(title, body);
92
120
  return {
@@ -101,6 +129,8 @@ function buildChunk(meta, source, title, body, id, char_start, char_end) {
101
129
  doc_kind: meta.doc_kind,
102
130
  capability_status: meta.capability_status,
103
131
  retrievable_scope: meta.retrievable_scope,
132
+ meta_source: meta.meta_source,
133
+ meta_override_fields: meta.meta_override_fields,
104
134
  title,
105
135
  source,
106
136
  content: body,
@@ -109,14 +139,15 @@ function buildChunk(meta, source, title, body, id, char_start, char_end) {
109
139
  ...(api_names.length ? { api_names } : {}),
110
140
  ...(containsCodeBlock(body) ? { contains_code: true } : {}),
111
141
  ...(key_phrases.length ? { key_phrases } : {}),
142
+ ...(extra ?? {}),
112
143
  char_start,
113
144
  char_end,
114
145
  };
115
146
  }
116
- function parseUrlMappingRows(content) {
147
+ function parseUrlMappingRows(content, baseOffset) {
117
148
  const lines = content.split('\n');
118
149
  let sectionTitle = '';
119
- let offset = 0;
150
+ let offset = baseOffset;
120
151
  const rows = [];
121
152
  for (const line of lines) {
122
153
  const lineStart = offset;
@@ -144,8 +175,8 @@ function parseUrlMappingRows(content) {
144
175
  }
145
176
  return rows;
146
177
  }
147
- function chunkUrlMapping(source, content, meta) {
148
- const rows = parseUrlMappingRows(content);
178
+ function chunkUrlMapping(source, content, meta, baseOffset) {
179
+ const rows = parseUrlMappingRows(content, baseOffset);
149
180
  if (rows.length === 0) {
150
181
  return [];
151
182
  }
@@ -157,33 +188,70 @@ function chunkUrlMapping(source, content, meta) {
157
188
  return buildChunk(meta, source, row.label, body, `${baseSlug}#${headingSlug}`, row.lineStart, row.lineEnd);
158
189
  });
159
190
  }
160
- export function chunkMarkdown(source, content, meta) {
191
+ export function chunkMarkdown(source, content, meta, options) {
161
192
  const chunks = [];
162
193
  const baseSlug = sourceSlug(source);
163
- if (isChatUrlMapping(source)) {
164
- const urlChunks = chunkUrlMapping(source, content, meta);
194
+ const baseOffset = options?.baseOffset ?? 0;
195
+ const policyRule = options?.policyRule;
196
+ if (isChatUrlMapping(source) || policyRule?.strategy === 'by_row') {
197
+ const urlChunks = chunkUrlMapping(source, content, meta, baseOffset);
165
198
  if (urlChunks.length > 0) {
166
199
  return urlChunks;
167
200
  }
168
201
  }
169
202
  const forceSectionSplit = shouldForceSectionSplit(source, meta);
170
- if (content.length < 2048 && !forceSectionSplit) {
171
- const { body, char_start, char_end } = trimSliceOffsets(content, 0);
203
+ const isBestPractice = isBestPracticeSource(source);
204
+ const estimatedTokenCount = estimateMarkdownTokens(content);
205
+ const defaultSplitThreshold = options?.defaultSplitTokenThreshold ?? LEGACY_SINGLE_CHUNK_CHAR_THRESHOLD;
206
+ const singleChunkMaxTokens = policyRule?.max_tokens
207
+ ?? (isBestPractice ? BEST_PRACTICE_SINGLE_CHUNK_MAX_TOKENS : undefined);
208
+ const shouldSingleChunk = policyRule?.strategy === 'single_chunk_per_file'
209
+ || (isBestPractice && !policyRule?.strategy);
210
+ if (shouldSingleChunk && singleChunkMaxTokens !== undefined && estimatedTokenCount <= singleChunkMaxTokens) {
211
+ const { body, char_start, char_end } = trimSliceOffsets(content, baseOffset);
172
212
  if (!body)
173
213
  return chunks;
174
214
  chunks.push(buildChunk(meta, source, pathBasename(source), body, `${baseSlug}#full`, char_start, char_end));
175
215
  return chunks;
176
216
  }
177
- const level = forceSectionSplit ? 3 : (isFaqLike(source, content) ? 3 : 2);
217
+ if (!shouldSingleChunk
218
+ && !forceSectionSplit
219
+ && content.length < (policyRule?.split_token_threshold ?? defaultSplitThreshold)) {
220
+ const { body, char_start, char_end } = trimSliceOffsets(content, baseOffset);
221
+ if (!body)
222
+ return chunks;
223
+ chunks.push(buildChunk(meta, source, pathBasename(source), body, `${baseSlug}#full`, char_start, char_end));
224
+ return chunks;
225
+ }
226
+ const fallbackStrategy = policyRule?.split_fallback ?? policyRule?.strategy ?? 'by_h2';
227
+ const level = forceSectionSplit
228
+ ? 3
229
+ : fallbackStrategy === 'by_h3'
230
+ ? 3
231
+ : (isFaqLike(source, content) ? 3 : 2);
178
232
  const sections = splitByHeading(content, level);
179
233
  for (const section of sections) {
180
- const { body, char_start, char_end } = trimSliceOffsets(section.raw, section.start);
234
+ const { body, char_start, char_end } = trimSliceOffsets(section.raw, section.start + baseOffset);
181
235
  if (!body)
182
236
  continue;
183
237
  const headingSlug = slugify(section.title);
184
- chunks.push(buildChunk(meta, source, section.title, body, `${baseSlug}#${headingSlug}`, char_start, char_end));
238
+ const fallbackMeta = shouldSingleChunk && singleChunkMaxTokens !== undefined && estimatedTokenCount > singleChunkMaxTokens
239
+ ? {
240
+ split_reason: 'exceed_token_threshold',
241
+ original_file_token_count: Math.ceil(estimatedTokenCount),
242
+ aggregable: policyRule?.split_aggregable ?? isBestPractice,
243
+ parent_h2: section.title,
244
+ }
245
+ : undefined;
246
+ chunks.push(buildChunk(meta, source, section.title, body, `${baseSlug}#${headingSlug}`, char_start, char_end, fallbackMeta));
185
247
  }
186
- return chunks.length > 0 ? chunks : chunkMarkdown(source, content.slice(0, 2048), meta);
248
+ return chunks.length > 0
249
+ ? chunks
250
+ : chunkMarkdown(source, content.slice(0, defaultSplitThreshold), meta, {
251
+ baseOffset,
252
+ policyRule,
253
+ defaultSplitTokenThreshold: defaultSplitThreshold,
254
+ });
187
255
  }
188
256
  export function toManifestChunk(chunk) {
189
257
  const { content: _content, ...manifestChunk } = chunk;
@@ -0,0 +1,100 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import * as yaml from 'js-yaml';
4
+ const DEFAULT_POLICY = {
5
+ default: {
6
+ strategy: 'by_h2',
7
+ split_token_threshold: 2048,
8
+ },
9
+ rules: [
10
+ {
11
+ path_prefix: 'knowledge/chat/best-practice/',
12
+ strategy: 'single_chunk_per_file',
13
+ max_tokens: 4096,
14
+ hard_max_tokens: 8192,
15
+ split_fallback: 'by_h2',
16
+ split_aggregable: true,
17
+ },
18
+ {
19
+ path_prefix: 'knowledge/chat/urls/',
20
+ strategy: 'by_row',
21
+ },
22
+ ],
23
+ };
24
+ function toPolicy(raw) {
25
+ if (!raw || typeof raw !== 'object')
26
+ return DEFAULT_POLICY;
27
+ const root = raw;
28
+ const policyObj = (root.chunking_policy && typeof root.chunking_policy === 'object')
29
+ ? root.chunking_policy
30
+ : null;
31
+ if (!policyObj)
32
+ return DEFAULT_POLICY;
33
+ const rawDefault = (policyObj.default && typeof policyObj.default === 'object')
34
+ ? policyObj.default
35
+ : {};
36
+ const strategy = rawDefault.strategy === 'by_h3' ? 'by_h3' : 'by_h2';
37
+ const splitThreshold = typeof rawDefault.split_token_threshold === 'number'
38
+ ? rawDefault.split_token_threshold
39
+ : 2048;
40
+ const rules = Array.isArray(policyObj.rules)
41
+ ? policyObj.rules
42
+ .map((item) => {
43
+ if (!item || typeof item !== 'object')
44
+ return null;
45
+ const rule = item;
46
+ const pathPrefix = typeof rule.path_prefix === 'string' ? rule.path_prefix.trim() : '';
47
+ const strategyValue = typeof rule.strategy === 'string' ? rule.strategy : '';
48
+ if (!pathPrefix || !['single_chunk_per_file', 'by_h2', 'by_h3', 'by_row'].includes(strategyValue)) {
49
+ return null;
50
+ }
51
+ return {
52
+ path_prefix: pathPrefix,
53
+ strategy: strategyValue,
54
+ max_tokens: typeof rule.max_tokens === 'number' ? rule.max_tokens : undefined,
55
+ hard_max_tokens: typeof rule.hard_max_tokens === 'number' ? rule.hard_max_tokens : undefined,
56
+ split_fallback: rule.split_fallback === 'by_h3' ? 'by_h3' : (rule.split_fallback === 'by_h2' ? 'by_h2' : undefined),
57
+ split_aggregable: typeof rule.split_aggregable === 'boolean' ? rule.split_aggregable : undefined,
58
+ split_token_threshold: typeof rule.split_token_threshold === 'number' ? rule.split_token_threshold : undefined,
59
+ };
60
+ })
61
+ .filter((rule) => Boolean(rule))
62
+ : [];
63
+ return {
64
+ default: {
65
+ strategy,
66
+ split_token_threshold: splitThreshold,
67
+ },
68
+ rules,
69
+ };
70
+ }
71
+ function extractYamlBlock(markdown) {
72
+ const match = markdown.match(/```ya?ml\n([\s\S]*?)\n```/i);
73
+ return match?.[1] ?? null;
74
+ }
75
+ export function loadChunkingPolicy(policyFilePath) {
76
+ if (!fs.existsSync(policyFilePath)) {
77
+ return DEFAULT_POLICY;
78
+ }
79
+ try {
80
+ const markdown = fs.readFileSync(policyFilePath, 'utf-8');
81
+ const yamlBlock = extractYamlBlock(markdown);
82
+ if (!yamlBlock)
83
+ return DEFAULT_POLICY;
84
+ const parsed = yaml.load(yamlBlock);
85
+ return toPolicy(parsed);
86
+ }
87
+ catch {
88
+ return DEFAULT_POLICY;
89
+ }
90
+ }
91
+ export function resolveChunkingRule(source, policy) {
92
+ const normalized = source.replace(/\\/g, '/');
93
+ const matched = policy.rules
94
+ .filter((rule) => normalized.startsWith(rule.path_prefix.replace(/\\/g, '/')))
95
+ .sort((a, b) => b.path_prefix.length - a.path_prefix.length);
96
+ return matched[0];
97
+ }
98
+ export function resolvePolicyFilePath(knowledgeDirname) {
99
+ return path.resolve(knowledgeDirname, '../../../chunking-policy.md');
100
+ }
@@ -5,6 +5,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
5
5
  /** 控制台/购买页等不适合作为「参考文档」的链接 */
6
6
  const NOISE_URL_RE = /console\.cloud\.tencent|buy\.cloud\.tencent|account\.renewal|edu\/learning/i;
7
7
  const PREFERRED_DOC_HOST_RE = /cloud\.tencent\.com|web\.sdk\.qcloud\.com|github\.com\/Tencent(?:Cloud|RTC)/i;
8
+ const LOCALHOST_URL_RE = /^https?:\/\/(?:localhost|127\.0\.0\.1)(?::|\b)/i;
8
9
  let cachedSourceUrlMap = null;
9
10
  function loadSourceUrlMap() {
10
11
  if (cachedSourceUrlMap)
@@ -25,6 +26,8 @@ function isUsefulDocUrl(url) {
25
26
  return false;
26
27
  if (NOISE_URL_RE.test(url))
27
28
  return false;
29
+ if (LOCALHOST_URL_RE.test(url))
30
+ return false;
28
31
  return true;
29
32
  }
30
33
  /** 从 markdown 正文中提取可参考的官网链接(优先 cloud.tencent / web.sdk.qcloud) */
@@ -51,17 +54,42 @@ export function extractUrlMappingPrimaryUrl(content) {
51
54
  const row = content.match(/\|\s*[^|]+\s*\|\s*`?(https?:\/\/[^|`\s]+)`?\s*\|/);
52
55
  return row?.[1] ? normalizeUrl(row[1]) : null;
53
56
  }
57
+ /** 产品无关的 URL 映射行判定:knowledge/<product>/urls/... */
54
58
  function isUrlMappingSource(source) {
55
- return /knowledge\/chat\/urls\/.+\.md$/i.test(source.replace(/\\/g, '/'));
59
+ return /knowledge\/[a-z0-9_-]+\/urls\/.+\.md$/i.test(source.replace(/\\/g, '/'));
60
+ }
61
+ const RTCENGINE_WEB_API_SOURCE = 'knowledge/rtcengine/web/api.md';
62
+ const RTCENGINE_WEB_API_BASE_URL = 'https://web.sdk.qcloud.com/trtc/webrtc/doc/zh-cn/TRTC.html';
63
+ /** api.md 中的非方法标题(示例/成员枚举/类型定义等),不作为锚点生成 */
64
+ const RTCENGINE_WEB_API_NON_METHOD_RE = /^(?:Example|Members|Type|Definitions)$/i;
65
+ /**
66
+ * rtcengine/web/api.md 的 API 章节 chunk(如 `### TRTC.setCurrentSpeaker` / `### setCurrentSpeaker`)
67
+ * 生成稳定参考链接:`.../TRTC.html#.setCurrentSpeaker`
68
+ * 兼容两种标题格式:静态方法带 `TRTC.` 前缀,实例方法不带前缀。
69
+ */
70
+ function resolveRtcengineWebApiUrl(source, content) {
71
+ if (source !== RTCENGINE_WEB_API_SOURCE)
72
+ return null;
73
+ const methodMatch = content.match(/^###\s+(?:TRTC\.)?([A-Za-z0-9_]+)/m);
74
+ if (methodMatch?.[1] && !RTCENGINE_WEB_API_NON_METHOD_RE.test(methodMatch[1])) {
75
+ return `${RTCENGINE_WEB_API_BASE_URL}#.${methodMatch[1]}`;
76
+ }
77
+ // 未解析到具体方法时回退到类文档首页,避免误用整篇正文里无关 URL。
78
+ return RTCENGINE_WEB_API_BASE_URL;
56
79
  }
57
80
  /**
58
81
  * 为检索 fragment 解析可参考的官网 URL(去重、按优先级)。
59
- * 1. sync-manifest 落盘的 localPath → 官网 URL(rtcengine 等)
60
- * 2. L1 URL 映射行内目标 URL
61
- * 3. 本地文档正文中的官网链接
82
+ * 1. rtcengine/web/api.md:优先按 API 标题生成锚点 URL(例如 #.setCurrentSpeaker)
83
+ * 2. sync-manifest 落盘的 localPath → 官网 URL(rtcengine 等)
84
+ * 3. L1 URL 映射行内目标 URL
85
+ * 4. 本地文档正文中的官网链接
62
86
  */
63
87
  export function resolveFragmentDocUrls(source, content, fullSourceContent) {
64
88
  const normalized = source.replace(/\\/g, '/');
89
+ const rtcengineWebApiUrl = resolveRtcengineWebApiUrl(normalized, content);
90
+ if (rtcengineWebApiUrl) {
91
+ return [rtcengineWebApiUrl];
92
+ }
65
93
  const urls = [];
66
94
  const canonical = loadSourceUrlMap()[normalized];
67
95
  if (canonical)
@@ -84,35 +112,58 @@ export function resolveFragmentDocUrls(source, content, fullSourceContent) {
84
112
  }
85
113
  return urls.slice(0, 3);
86
114
  }
87
- /** 从 fragments 汇总去重后的参考链接列表(按 fragment 相关度排序) */
115
+ /** 从 fragments 汇总去重后的参考链接列表(按 fragment 相关度排序)。
116
+ * 规则:每个 fragment 仅取一个参考链接(优先 doc_urls[0]),避免 1 个片段膨胀出多个 references。 */
88
117
  export function buildReferences(fragments) {
89
118
  const refs = [];
90
119
  const seen = new Set();
91
120
  for (const fragment of fragments) {
92
- for (const url of fragment.doc_urls) {
93
- if (seen.has(url))
94
- continue;
95
- seen.add(url);
96
- refs.push({
97
- rank: refs.length + 1,
98
- title: fragment.title,
99
- url,
100
- source: fragment.source,
101
- });
102
- }
121
+ const primaryUrl = fragment.doc_urls.find((u) => isUsefulDocUrl(u));
122
+ if (!primaryUrl)
123
+ continue;
124
+ if (seen.has(primaryUrl))
125
+ continue;
126
+ seen.add(primaryUrl);
127
+ refs.push({
128
+ rank: refs.length + 1,
129
+ title: fragment.title,
130
+ url: primaryUrl,
131
+ source: fragment.source,
132
+ });
103
133
  }
104
134
  return refs;
105
135
  }
136
+ function coerceHttpUrl(value) {
137
+ if (typeof value !== 'string')
138
+ return null;
139
+ const normalized = value.replace(/\\/g, '/').trim();
140
+ if (!normalized.startsWith('http'))
141
+ return null;
142
+ return normalized;
143
+ }
144
+ function resolveManifestDocUrl(entry) {
145
+ return coerceHttpUrl(entry.ref) ?? coerceHttpUrl(entry.nodeId);
146
+ }
106
147
  /** build 时从 sync-manifest 生成 localPath → 官网 URL 映射 */
107
148
  export function buildDocSourceUrlMap(syncManifestPath) {
108
149
  if (!fs.existsSync(syncManifestPath))
109
150
  return {};
110
151
  const manifest = JSON.parse(fs.readFileSync(syncManifestPath, 'utf-8'));
111
152
  const map = {};
112
- for (const entry of Object.values(manifest.entries ?? {})) {
113
- if (entry.nodeId?.startsWith('http') && entry.localPath) {
114
- map[entry.localPath.replace(/\\/g, '/')] = entry.nodeId;
153
+ const entries = manifest.entries ?? {};
154
+ for (const entry of Object.values(entries)) {
155
+ const url = resolveManifestDocUrl(entry);
156
+ if (url && entry.localPath) {
157
+ map[entry.localPath.replace(/\\/g, '/')] = url;
115
158
  }
116
159
  }
160
+ for (const [localPath, nodeId] of Object.entries(manifest.byLocalPath ?? {})) {
161
+ const normalized = localPath.replace(/\\/g, '/');
162
+ if (map[normalized])
163
+ continue;
164
+ const url = resolveManifestDocUrl(entries[nodeId] ?? { nodeId });
165
+ if (url)
166
+ map[normalized] = url;
167
+ }
117
168
  return map;
118
169
  }