@tencent-rtc/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.
- package/CHANGELOG.md +36 -0
- package/README.md +58 -39
- package/knowledge/build-index.js +30 -6
- package/knowledge/chunker.js +82 -14
- package/knowledge/chunking-policy.js +100 -0
- package/knowledge/content-loader.js +3 -3
- package/knowledge/doc-url-resolver.js +43 -16
- package/knowledge/frontmatter-meta.js +167 -0
- package/knowledge/path-meta.js +28 -6
- package/knowledge/search/exact-lookup.js +120 -239
- package/knowledge/search/filter-pool.js +12 -29
- package/knowledge/search/framework-fallback.js +22 -0
- package/knowledge/search/intent-router.js +29 -0
- package/knowledge/search/l1-enrich.js +159 -0
- package/knowledge/search/query-normalizer.js +63 -3
- package/knowledge/search/search-core.js +410 -0
- package/knowledge/search/search-knowledge.js +421 -379
- package/knowledge/search/search-message.js +88 -0
- package/knowledge/search/search-pass.js +428 -0
- package/knowledge/search/search-types.js +1 -0
- package/knowledge/search/url-row-idf.js +120 -0
- package/knowledge/search/url-row-scorer.js +337 -0
- package/package.json +3 -1
- package/resource/_schema/feature-aliases.yaml +1 -1
- package/resource/index/manifest.json +1 -1
- package/resource/knowledge/callkit/best-practice/callkit-miniprogram/uniapp-shrink.md +60 -0
- package/resource/knowledge/callkit/best-practice/common/call-autoplay-blocked.md +77 -0
- package/resource/knowledge/callkit/best-practice/common/call-message-in-chat.md +78 -0
- package/resource/knowledge/callkit/best-practice/common/call-vs-calls.md +110 -0
- package/resource/knowledge/callkit/best-practice/common/callengine-call-flow.md +123 -0
- package/resource/knowledge/callkit/best-practice/common/callengine-log-level.md +67 -0
- package/resource/knowledge/callkit/best-practice/common/callkit-get-engine-instance.md +81 -0
- package/resource/knowledge/callkit/best-practice/common/callkit-get-roomid.md +78 -0
- package/resource/knowledge/callkit/best-practice/common/callkit-limitations.md +37 -0
- package/resource/knowledge/callkit/best-practice/common/callkit-user-register-and-kicked.md +98 -0
- package/resource/knowledge/chat/best-practice/common/avchatroom-message-loss-diagnosis.md +65 -0
- package/resource/knowledge/chat/best-practice/common/group-conversation-cleanup-on-system-notice.md +62 -0
- package/resource/knowledge/chat/best-practice/common/image-message-url-lifecycle.md +59 -0
- package/resource/knowledge/chat/best-practice/common/login-repeat-2025-diagnosis.md +61 -0
- package/resource/knowledge/chat/best-practice/common/message-read-report-and-receipt.md +64 -0
- package/resource/knowledge/chat/best-practice/common/message-receive-event.md +65 -0
- package/resource/knowledge/chat/best-practice/common/message-risk-error-and-render.md +65 -0
- package/resource/knowledge/chat/best-practice/common/message-unread-count.md +71 -0
- package/resource/knowledge/chat/best-practice/miniprogram/audio-message-send.md +113 -0
- package/resource/knowledge/chat/best-practice/miniprogram/login-timeout-2801-diagnosis.md +63 -0
- package/resource/knowledge/chat/best-practice/miniprogram/media-upload-fail-diagnosis.md +64 -0
- package/resource/knowledge/chat/best-practice/web/audio-message-send.md +118 -0
- package/resource/knowledge/chat/urls/product.md +17 -16
- package/resource/knowledge/chat/urls/restapi.md +3 -2
- package/resource/knowledge/chat/urls/sdk/android.md +6 -5
- package/resource/knowledge/chat/urls/sdk/flutter.md +6 -5
- package/resource/knowledge/chat/urls/sdk/ios.md +4 -3
- package/resource/knowledge/chat/urls/sdk/web.md +7 -6
- package/resource/knowledge/chat/urls/webhook.md +0 -5
- package/resource/knowledge/livekit/best-practice/android/voice-room-anchor-start.md +120 -0
- package/resource/knowledge/livekit/best-practice/common/admin-take-seat.md +73 -0
- package/resource/knowledge/livekit/best-practice/common/audience-leave-live.md +81 -0
- package/resource/knowledge/livekit/best-practice/common/voice-room-no-audio-troubleshoot.md +100 -0
- package/resource/knowledge/livekit/best-practice/common/voice-room-seat-list-change.md +65 -0
- package/resource/knowledge/livekit/best-practice/flutter/voice-room-anchor-start.md +122 -0
- package/resource/knowledge/livekit/best-practice/ios/voice-room-anchor-start.md +126 -0
- package/resource/knowledge/roomkit/best-practice/common/room-ghost-member.md +70 -0
- package/resource/knowledge/roomkit/best-practice/common/room-limit-and-multi.md +83 -0
- package/resource/knowledge/roomkit/best-practice/common/room-screen-share-preview.md +83 -0
- package/resource/knowledge/roomkit/best-practice/common/room-status-after-all-leave.md +69 -0
- package/resource/knowledge/roomkit/best-practice/vue/room-uikit-vs-atomicx.md +111 -0
- package/resource/knowledge/rtcengine/web/plugin/30-advanced-cross-room-link.md +2 -0
- package/resource/knowledge/rtcengine/web/plugin/39-advanced-video-decoder.md +2 -0
- package/server.js +28 -23
- package/streamable-http.js +19 -54
- package/tools/common/search_trtc_knowledge.js +24 -12
- package/tools/definitions/finalize_answer_def.js +2 -2
- package/tools/definitions/integration-boundary.js +2 -1
- package/tools/definitions/search_trtc_knowledge_def.js +5 -3
- package/utils/fetch-doc-content.js +198 -0
- package/utils/preflight-route.js +197 -0
- package/utils/report-cls-client.js +1 -1
- package/utils/report-es-client.js +1 -1
- package/cursor-setting.png +0 -0
- package/mcp-server-prompt.png +0 -0
- package/resource/knowledge/callkit/best-practice/web/callengine-invitation.md +0 -80
- package/resource/knowledge/callkit/best-practice/web/callengine-log-control.md +0 -24
- package/resource/knowledge/callkit/best-practice/web/callkit-call-vs-calls.md +0 -116
- package/resource/knowledge/callkit/preset/react/uikit-api-reference.md +0 -444
- package/resource/knowledge/callkit/preset/vue/uikit-api-reference.md +0 -464
- 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
|
+
### Added
|
|
6
|
+
- Added pre-search normalization: normalizes `product` / `frameworks` based on prompt truth, fixing retrieval bias caused by wrong agent inference.
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
- Optimized the `search_trtc_knowledge` output contract: removed brevity wording such as "1-2 sentence per subsection", replaced with complete synthesized answers.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- Fixed the case where mis-passed dual product / dual frameworks (e.g., "安卓 Web TRTC") were not corrected according to the prompt truth.
|
|
13
|
+
|
|
14
|
+
## Version 1.7.0 @2026.08.02
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- Added and synchronized best-practice docs across product domains including Chat / Call / Live / Room to improve knowledge coverage.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- Continued refactoring of the bilingual retrieval pipeline, including query normalization, intent routing, candidate filtering, and exact-match strategies.
|
|
21
|
+
- Improved recall and ranking for best-practice and URL-based documents to increase answer usability and stability.
|
|
22
|
+
- Improved `search_trtc_knowledge` definitions and answer constraints for better readability and executability.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- Fixed off-topic retrieval in some complex queries, reducing high-score false-positive hits.
|
|
26
|
+
|
|
27
|
+
## Version 1.6.6 @2026.07.18
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
- Added support for `get_native_live_uikit_integration`, enabling LiveKit(Android/iOS) UI integration guidance.
|
|
31
|
+
|
|
32
|
+
## Version 1.6.0 @2026.07.01
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
- Refactored MCP retrieval capabilities and consolidated the MCP toolset.
|
|
36
|
+
- Added support for `search_trtc_knowledge` to retrieve real-time audio/video (TRTC/IM) documentation.
|
package/README.md
CHANGED
|
@@ -1,45 +1,27 @@
|
|
|
1
|
-
## Tencent SDK
|
|
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.
|
|
1
|
+
## Tencent RTC SDK MCP Server Guide
|
|
5
2
|
|
|
3
|
+
This repository provides a CLI-based Model Context Protocol (MCP) Server that exposes Tencent RTC SDK documentation and API resources to LLM tools, helping AI agents deliver more accurate implementation guidance and integration support.
|
|
6
4
|
|
|
7
5
|
## Features
|
|
8
6
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
|
|
16
|
-
- TUICallKit SDK topics: configuration, calls function.
|
|
17
|
-
- Retrieve official Tencent ChatUIKit documentation (HTML → Markdown) for:
|
|
18
|
-
- ChatUIKit SDK languages: JavaScript, Kotlin, Swift.
|
|
19
|
-
- ChatUIKit SDK topics: configuration, chat function.
|
|
20
|
-
- Converts remote HTML articles to Markdown using `jsdom` and `turndown` for consistent documentation formatting.
|
|
21
|
-
- Input validation via Zod schemas for all tool parameters, ensuring robust error handling.
|
|
22
|
-
- Extensible tool definitions leveraging the Model Context Protocol SDK (`@modelcontextprotocol/sdk`) with `McpServer` and `StdioServerTransport`.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
## Prerequisites
|
|
26
|
-
- Node.js (>= 18) and npm
|
|
27
|
-
- Cursor IDE with MCP support
|
|
28
|
-
|
|
7
|
+
- **Efficient retrieval**: Supports Tencent RTC documentation retrieval for fast access to official references and best practices.
|
|
8
|
+
- **Broad coverage**: Covers Chat (IM), Call, Live, Room, RTCEngine (TRTC SDK), and TIMPush domains.
|
|
9
|
+
- **Intelligent routing**: Supports product/platform intent disambiguation and prompts for missing platform/framework information when needed.
|
|
10
|
+
- **Integration-friendly**: In addition to documentation Q&A, provides integration entry points for Web / Android / iOS / Flutter, including UI-based integration directions.
|
|
11
|
+
- **Reliable source**: Grounded in official Tencent Cloud materials to improve answer reliability and implementation readiness.
|
|
12
|
+
- **Wide compatibility**: Exposes MCP capabilities over STDIN/STDOUT JSON-RPC, compatible with MCP clients such as Cursor / Codex / Claude / CodeBuddy.
|
|
13
|
+
- **Quick start**: Run with `npx` and get started with minimal setup.
|
|
29
14
|
|
|
30
|
-
##
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
- Step1: Installation.
|
|
34
|
-
|
|
35
|
-
```
|
|
36
|
-
npx -y @tencent-rtc/mcp
|
|
37
|
-
```
|
|
15
|
+
## Requirements
|
|
16
|
+
- Node.js (>= 18) and npm
|
|
17
|
+
- An MCP-capable IDE
|
|
38
18
|
|
|
19
|
+
## Usage
|
|
20
|
+
### Standard setup flow
|
|
39
21
|
|
|
40
|
-
-
|
|
22
|
+
- Step1: In your Cursor project, create or edit `.cursor/mcp.json` or `~/.cursor/mcp.json` and add the following configuration.
|
|
41
23
|
|
|
42
|
-
```
|
|
24
|
+
```json
|
|
43
25
|
{
|
|
44
26
|
"mcpServers": {
|
|
45
27
|
"tencent-rtc": {
|
|
@@ -53,16 +35,53 @@ npx -y @tencent-rtc/mcp
|
|
|
53
35
|
}
|
|
54
36
|
}
|
|
55
37
|
```
|
|
56
|
-
|
|
38
|
+
After saving, a prompt appears. Click **Enable** in the prompt.
|
|
57
39
|
|
|
58
40
|

|
|
59
41
|
|
|
60
|
-
-
|
|
42
|
+
- Step2: Go to **Cursor Settings** (top-right gear icon) -> **MCP**, and make sure `tencent-rtc` is enabled.
|
|
61
43
|
|
|
62
44
|

|
|
63
45
|
|
|
46
|
+
- Step3: Describe your requirements directly in the IDE, and let the AI complete implementation or troubleshooting based on Tencent Cloud SDK documentation.
|
|
47
|
+
|
|
48
|
+
### Optional: global install
|
|
49
|
+
|
|
50
|
+
If you want quick global invocation locally, run:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npx -y @tencent-rtc/mcp
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Changelog
|
|
57
|
+
|
|
58
|
+
> Only recent updates are listed here. For full history, see `CHANGELOG.md`.
|
|
59
|
+
|
|
60
|
+
## Version 1.7.1 @2026.08.11
|
|
61
|
+
|
|
62
|
+
### Added
|
|
63
|
+
- Added pre-search normalization: normalizes `product` / `frameworks` based on prompt truth, fixing retrieval bias caused by wrong agent inference.
|
|
64
|
+
|
|
65
|
+
### Changed
|
|
66
|
+
- Optimized the `search_trtc_knowledge` output contract: removed brevity wording such as "1-2 sentence per subsection", replaced with complete synthesized answers.
|
|
67
|
+
|
|
68
|
+
### Fixed
|
|
69
|
+
- Fixed the case where mis-passed dual product / dual frameworks (e.g., "安卓 Web TRTC") were not corrected according to the prompt truth.
|
|
70
|
+
|
|
71
|
+
### Version 1.7.0 @2026.08.02
|
|
72
|
+
|
|
73
|
+
#### Added
|
|
74
|
+
- Added and synchronized best-practice docs across product domains including Chat / Call / Live / Room to improve knowledge coverage.
|
|
75
|
+
|
|
76
|
+
#### Changed
|
|
77
|
+
- Continued refactoring of the bilingual retrieval pipeline, including query normalization, intent routing, candidate filtering, and exact-match strategies.
|
|
78
|
+
- Improved recall and ranking for best-practice and URL-based documents to increase answer usability and stability.
|
|
79
|
+
- Improved `search_trtc_knowledge` definitions and answer constraints for better readability and executability.
|
|
80
|
+
|
|
81
|
+
#### Fixed
|
|
82
|
+
- Fixed off-topic retrieval in some complex queries, reducing high-score false-positive hits.
|
|
64
83
|
|
|
65
|
-
|
|
84
|
+
### Version 1.6.6 @2026.07.18
|
|
66
85
|
|
|
67
|
-
|
|
68
|
-
|
|
86
|
+
#### Added
|
|
87
|
+
- Added support for `get_native_live_uikit_integration`, enabling LiveKit(Android/iOS) UI integration guidance.
|
package/knowledge/build-index.js
CHANGED
|
@@ -3,11 +3,13 @@ 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/
|
|
10
|
-
//
|
|
12
|
+
// 必须在 scripts/copy-resource.js 之后执行,否则目标目录不存在。
|
|
11
13
|
const RESOURCE_ROOT = path.resolve(__dirname, '../resource');
|
|
12
14
|
function walkMarkdownFiles(dir, baseRel) {
|
|
13
15
|
if (!fs.existsSync(dir))
|
|
@@ -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
|
|
51
|
-
|
|
52
|
-
|
|
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
|
}
|
package/knowledge/chunker.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
164
|
-
|
|
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
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|
|
@@ -4,9 +4,9 @@ import { fileURLToPath } from 'url';
|
|
|
4
4
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
5
5
|
const fileCache = new Map();
|
|
6
6
|
function resolveResourceRoot() {
|
|
7
|
-
//
|
|
8
|
-
// npm
|
|
9
|
-
//
|
|
7
|
+
// 运行时读 md 源目录(与 knowledge/ 同级的 resource/):
|
|
8
|
+
// npm 包:<pkg>/knowledge/ → <pkg>/resource/
|
|
9
|
+
// 本地: dist/knowledge/ → dist/resource/(依赖 build 时 copy-resource.js)
|
|
10
10
|
return path.resolve(__dirname, '../resource');
|
|
11
11
|
}
|
|
12
12
|
function readSourceFile(source) {
|
|
@@ -54,17 +54,42 @@ export function extractUrlMappingPrimaryUrl(content) {
|
|
|
54
54
|
const row = content.match(/\|\s*[^|]+\s*\|\s*`?(https?:\/\/[^|`\s]+)`?\s*\|/);
|
|
55
55
|
return row?.[1] ? normalizeUrl(row[1]) : null;
|
|
56
56
|
}
|
|
57
|
+
/** 产品无关的 URL 映射行判定:knowledge/<product>/urls/... */
|
|
57
58
|
function isUrlMappingSource(source) {
|
|
58
|
-
return /knowledge\/
|
|
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/en/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;
|
|
59
79
|
}
|
|
60
80
|
/**
|
|
61
81
|
* 为检索 fragment 解析可参考的官网 URL(去重、按优先级)。
|
|
62
|
-
* 1.
|
|
63
|
-
* 2.
|
|
64
|
-
* 3.
|
|
82
|
+
* 1. rtcengine/web/api.md:优先按 API 标题生成锚点 URL(例如 #.setCurrentSpeaker)
|
|
83
|
+
* 2. sync-manifest 落盘的 localPath → 官网 URL(rtcengine 等)
|
|
84
|
+
* 3. L1 URL 映射行内目标 URL
|
|
85
|
+
* 4. 本地文档正文中的官网链接
|
|
65
86
|
*/
|
|
66
87
|
export function resolveFragmentDocUrls(source, content, fullSourceContent) {
|
|
67
88
|
const normalized = source.replace(/\\/g, '/');
|
|
89
|
+
const rtcengineWebApiUrl = resolveRtcengineWebApiUrl(normalized, content);
|
|
90
|
+
if (rtcengineWebApiUrl) {
|
|
91
|
+
return [rtcengineWebApiUrl];
|
|
92
|
+
}
|
|
68
93
|
const urls = [];
|
|
69
94
|
const canonical = loadSourceUrlMap()[normalized];
|
|
70
95
|
if (canonical)
|
|
@@ -87,22 +112,24 @@ export function resolveFragmentDocUrls(source, content, fullSourceContent) {
|
|
|
87
112
|
}
|
|
88
113
|
return urls.slice(0, 3);
|
|
89
114
|
}
|
|
90
|
-
/** 从 fragments 汇总去重后的参考链接列表(按 fragment
|
|
115
|
+
/** 从 fragments 汇总去重后的参考链接列表(按 fragment 相关度排序)。
|
|
116
|
+
* 规则:每个 fragment 仅取一个参考链接(优先 doc_urls[0]),避免 1 个片段膨胀出多个 references。 */
|
|
91
117
|
export function buildReferences(fragments) {
|
|
92
118
|
const refs = [];
|
|
93
119
|
const seen = new Set();
|
|
94
120
|
for (const fragment of fragments) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
+
});
|
|
106
133
|
}
|
|
107
134
|
return refs;
|
|
108
135
|
}
|