@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
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { getSourceFileContent } from '../content-loader.js';
|
|
2
|
+
import { resolveFragmentDocUrls } from '../doc-url-resolver.js';
|
|
3
|
+
import { hasExplicitFrameworkMatch, hasFrameworkFallbackMatch } from './framework-fallback.js';
|
|
4
|
+
import { isProductScopePrompt } from './product-scope-intent.js';
|
|
5
|
+
const CODE_HINT_PREFIX = '[Code sample - must match target platform]';
|
|
6
|
+
const CITATION_PREFIX = '[Source:';
|
|
7
|
+
export function postProcess(ranked, chunksById, limit, opts) {
|
|
8
|
+
const buildFragment = (chunk, score, rank, aggregated = false) => {
|
|
9
|
+
const contentWithHint = chunk.contains_code
|
|
10
|
+
? `${CODE_HINT_PREFIX}\n${chunk.content}`
|
|
11
|
+
: chunk.content;
|
|
12
|
+
return {
|
|
13
|
+
rank,
|
|
14
|
+
score,
|
|
15
|
+
title: chunk.title,
|
|
16
|
+
content: contentWithHint,
|
|
17
|
+
source: chunk.source,
|
|
18
|
+
doc_urls: resolveFragmentDocUrls(chunk.source, chunk.content, getSourceFileContent(chunk.source)),
|
|
19
|
+
product: chunk.product,
|
|
20
|
+
framework: chunk.framework,
|
|
21
|
+
types: chunk.types,
|
|
22
|
+
variant: chunk.variant,
|
|
23
|
+
scope: chunk.scope,
|
|
24
|
+
doc_kind: chunk.doc_kind,
|
|
25
|
+
capability_status: chunk.capability_status,
|
|
26
|
+
api_names: chunk.api_names,
|
|
27
|
+
contains_code: chunk.contains_code,
|
|
28
|
+
citation: `${CITATION_PREFIX} ${chunk.id}]${aggregated ? '[aggregated]' : ''}`,
|
|
29
|
+
...(aggregated ? { aggregated: true } : {}),
|
|
30
|
+
chunk_id: chunk.id,
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
if (opts?.fullDocumentMode && opts.preferredSource) {
|
|
34
|
+
const scoreMap = new Map(ranked.map((item) => [item.id, item.score]));
|
|
35
|
+
const sections = [...chunksById.values()]
|
|
36
|
+
.filter((chunk) => chunk.source === opts.preferredSource)
|
|
37
|
+
.sort((a, b) => a.char_start - b.char_start);
|
|
38
|
+
if (sections.length > 0) {
|
|
39
|
+
return sections.map((section, idx) => buildFragment(section, scoreMap.get(section.id) ?? 0, idx + 1));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const sourceCount = {};
|
|
43
|
+
const urlMappingSourceCount = {};
|
|
44
|
+
/** 已入选的 push config/android/{vendor} 文件,用于抑制其他厂商噪声 */
|
|
45
|
+
let selectedVendorFile = null;
|
|
46
|
+
const fragments = [];
|
|
47
|
+
for (const item of ranked) {
|
|
48
|
+
const chunk = chunksById.get(item.id);
|
|
49
|
+
if (!chunk)
|
|
50
|
+
continue;
|
|
51
|
+
if (opts?.preferredSource && chunk.source !== opts.preferredSource)
|
|
52
|
+
continue;
|
|
53
|
+
const isUrlMapping = Boolean(isUrlMappingSource(chunk.source));
|
|
54
|
+
const isPinnedUrlRow = isUrlMapping && (opts?.pinnedUrlRowChunkIds?.has(chunk.id) ?? false);
|
|
55
|
+
if (isUrlMapping) {
|
|
56
|
+
const urlCount = urlMappingSourceCount[chunk.source] ?? 0;
|
|
57
|
+
if (!isPinnedUrlRow && urlCount >= 1)
|
|
58
|
+
continue;
|
|
59
|
+
urlMappingSourceCount[chunk.source] = urlCount + 1;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
const count = sourceCount[chunk.source] ?? 0;
|
|
63
|
+
if (count >= 3)
|
|
64
|
+
continue;
|
|
65
|
+
sourceCount[chunk.source] = count + 1;
|
|
66
|
+
}
|
|
67
|
+
// 当用户明确问某厂商时,抑制同目录下其他厂商文档的噪声
|
|
68
|
+
// 规则:如果已有 config/android/{vendor}.md 入选,
|
|
69
|
+
// 后续其他 config/android/{other}.md 跳过
|
|
70
|
+
const isVendorDoc = chunk.source.match(/^knowledge\/push\/config\/android\/[a-z]+\.md$/);
|
|
71
|
+
if (isVendorDoc) {
|
|
72
|
+
if (!selectedVendorFile) {
|
|
73
|
+
selectedVendorFile = chunk.source;
|
|
74
|
+
}
|
|
75
|
+
else if (chunk.source !== selectedVendorFile) {
|
|
76
|
+
continue; // 跳过其他厂商
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// 当已明确命中了厂商配置页后,后续的 push 通用文件(integration/、其他 config/)不再有意义
|
|
80
|
+
// 只保留同厂商文件(已由 vendor dedup 处理)和 overview 综述,其他 push 通用文档视为噪声
|
|
81
|
+
if (selectedVendorFile && chunk.source.startsWith('knowledge/push/') && !chunk.source.includes('/overview/')) {
|
|
82
|
+
if (chunk.source !== selectedVendorFile)
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const firstPush = fragments.length === 0;
|
|
86
|
+
if (firstPush) {
|
|
87
|
+
// 首条不设分数截止,至少保证 1 条返回
|
|
88
|
+
}
|
|
89
|
+
else if (!isPinnedUrlRow) {
|
|
90
|
+
const topScore = fragments[0].score;
|
|
91
|
+
// 方案A:分数断崖截断。
|
|
92
|
+
// 当已有 E7 精确命中行(pinned url-row)时,后续非 pinned 行若跌破 Top1 的 20%,
|
|
93
|
+
// 视为「Top1 已精确命中、后续多为泛匹配噪声」,提前截断,避免凑满 limit 返回冗余。
|
|
94
|
+
// 无 E7 命中时不触发(此时答案依赖 best-practice/本地文档,需完整保留)。
|
|
95
|
+
if (topScore > 0) {
|
|
96
|
+
const pinnedChunkIds = opts?.pinnedUrlRowChunkIds;
|
|
97
|
+
const hasPinnedRow = pinnedChunkIds
|
|
98
|
+
? fragments.some((f) => pinnedChunkIds.has(f.chunk_id))
|
|
99
|
+
: false;
|
|
100
|
+
// 原有:分数衰减到 top 的 5% 以下时停止
|
|
101
|
+
if (item.score < topScore * 0.05)
|
|
102
|
+
break;
|
|
103
|
+
// 新增:已有 E7 精确命中 + 后续跌破 35% → 断崖截断
|
|
104
|
+
// 阈值取 35%(非 20%):best-practice 泛匹配分数常在 Top1 的 20%~35% 区间,
|
|
105
|
+
// 用 35% 才能拦住「Top1 精确命中后仍返回一堆无关 best-practice」的冗余。
|
|
106
|
+
if (hasPinnedRow && item.score < topScore * 0.35)
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const aggregated = Boolean(chunk.aggregable) && fragments.length > 0 && fragments[0].source === chunk.source;
|
|
111
|
+
fragments.push(buildFragment(chunk, item.score, fragments.length + 1, aggregated));
|
|
112
|
+
if (fragments.length >= limit)
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
return fragments;
|
|
116
|
+
}
|
|
117
|
+
/** 产品无关的 URL 映射行判定:knowledge/<product>/urls/.../xxx.md */
|
|
118
|
+
export function isUrlMappingSource(source) {
|
|
119
|
+
return /knowledge\/[a-z0-9_-]+\/urls\/.+\.md$/i.test(source.replace(/\\/g, '/'));
|
|
120
|
+
}
|
|
121
|
+
/** 产品无关地提取 URL 映射行的「子文件 kind」(如 sdk/web、error-code);非映射行返回 null */
|
|
122
|
+
export function extractUrlMappingKind(source) {
|
|
123
|
+
const normalized = source.replace(/\\/g, '/');
|
|
124
|
+
const match = normalized.match(/knowledge\/[a-z0-9_-]+\/urls\/(.+\.md)$/);
|
|
125
|
+
if (!match)
|
|
126
|
+
return null;
|
|
127
|
+
return match[1].replace(/\.md$/, '').replace(/\/index$/, '');
|
|
128
|
+
}
|
|
129
|
+
export function partitionFragments(fragments) {
|
|
130
|
+
const urlMapping = [];
|
|
131
|
+
const localDoc = [];
|
|
132
|
+
for (const fragment of fragments) {
|
|
133
|
+
if (isUrlMappingSource(fragment.source)) {
|
|
134
|
+
urlMapping.push(fragment);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
localDoc.push(fragment);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const urlMappingKinds = [...new Set(urlMapping.map((f) => extractUrlMappingKind(f.source)).filter(Boolean))];
|
|
141
|
+
return { urlMapping, localDoc, urlMappingKinds };
|
|
142
|
+
}
|
|
143
|
+
export function isNarrativeSource(source) {
|
|
144
|
+
const normalized = source.replace(/\\/g, '/');
|
|
145
|
+
return /\/preset\/.+(?:integration\.md|-integration\.md)$/i.test(normalized)
|
|
146
|
+
|| /\/integration\.md$/i.test(normalized)
|
|
147
|
+
|| /\/guide\.md$/i.test(normalized)
|
|
148
|
+
|| /\/getting-started\.md$/i.test(normalized);
|
|
149
|
+
}
|
|
150
|
+
export function sourceTitle(source) {
|
|
151
|
+
const base = source.split('/').pop()?.replace(/\.md$/i, '') ?? source;
|
|
152
|
+
return base || source;
|
|
153
|
+
}
|
|
154
|
+
export function pickNarrativeSource(_exactHits, ranked, poolWithContent) {
|
|
155
|
+
const bySource = new Map();
|
|
156
|
+
const chunksById = new Map(poolWithContent.map((chunk) => [chunk.id, chunk]));
|
|
157
|
+
const upsertSource = (source, score, weight) => {
|
|
158
|
+
const prev = bySource.get(source);
|
|
159
|
+
if (!prev) {
|
|
160
|
+
bySource.set(source, {
|
|
161
|
+
hitCount: 1,
|
|
162
|
+
bestScore: score,
|
|
163
|
+
weightedScore: score * weight,
|
|
164
|
+
});
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
bySource.set(source, {
|
|
168
|
+
hitCount: prev.hitCount + 1,
|
|
169
|
+
bestScore: Math.max(prev.bestScore, score),
|
|
170
|
+
weightedScore: prev.weightedScore + score * weight,
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
const topScore = ranked[0]?.score ?? 0;
|
|
174
|
+
for (let idx = 0; idx < ranked.length; idx += 1) {
|
|
175
|
+
const item = ranked[idx];
|
|
176
|
+
const chunk = chunksById.get(item.id);
|
|
177
|
+
if (!chunk || !isNarrativeSource(chunk.source))
|
|
178
|
+
continue;
|
|
179
|
+
if (topScore > 0 && idx > 120 && item.score < topScore * 0.03)
|
|
180
|
+
break;
|
|
181
|
+
const weight = idx < 20 ? 1 : idx < 80 ? 0.7 : 0.5;
|
|
182
|
+
upsertSource(chunk.source, item.score, weight);
|
|
183
|
+
}
|
|
184
|
+
const candidates = [...bySource.entries()].sort((a, b) => {
|
|
185
|
+
if (b[1].weightedScore !== a[1].weightedScore)
|
|
186
|
+
return b[1].weightedScore - a[1].weightedScore;
|
|
187
|
+
if (b[1].hitCount !== a[1].hitCount)
|
|
188
|
+
return b[1].hitCount - a[1].hitCount;
|
|
189
|
+
return b[1].bestScore - a[1].bestScore;
|
|
190
|
+
});
|
|
191
|
+
return candidates[0]?.[0] ?? null;
|
|
192
|
+
}
|
|
193
|
+
export function assessNarrativeCompleteness(document, prompt) {
|
|
194
|
+
if (!document) {
|
|
195
|
+
return {
|
|
196
|
+
isComplete: false,
|
|
197
|
+
score: 0,
|
|
198
|
+
sectionCount: 0,
|
|
199
|
+
totalChars: 0,
|
|
200
|
+
reason: 'no_document',
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
const isStepByStepIntent = /从零|完整|全流程|一步一步|step\s*by\s*step|full\s+(guide|tutorial|flow|integration)|complete\s+(guide|tutorial|integration)|getting\s*started|quick\s*start/i.test(prompt);
|
|
204
|
+
const minSections = isStepByStepIntent ? 4 : 3;
|
|
205
|
+
const minChars = isStepByStepIntent ? 1200 : 700;
|
|
206
|
+
const sectionCount = document.section_count;
|
|
207
|
+
const totalChars = document.sections.reduce((acc, section) => acc + section.content.length, 0);
|
|
208
|
+
const hasStepLikeSection = document.sections.some((section) => /步骤|step|准备|安装|初始化|接入|集成|创建|运行|验证|上线|部署/i.test(section.title)
|
|
209
|
+
|| /步骤|step|初始化|接入|集成|运行|验证|上线|部署/i.test(section.content.slice(0, 120)));
|
|
210
|
+
const issues = [];
|
|
211
|
+
if (sectionCount < minSections)
|
|
212
|
+
issues.push('section_count_low');
|
|
213
|
+
if (totalChars < minChars)
|
|
214
|
+
issues.push('content_too_short');
|
|
215
|
+
if (isStepByStepIntent && !hasStepLikeSection)
|
|
216
|
+
issues.push('missing_step_sections');
|
|
217
|
+
const score = Number((Math.min(sectionCount / minSections, 1) * 0.45
|
|
218
|
+
+ Math.min(totalChars / minChars, 1) * 0.45
|
|
219
|
+
+ ((isStepByStepIntent ? hasStepLikeSection : true) ? 0.1 : 0)).toFixed(2));
|
|
220
|
+
return {
|
|
221
|
+
isComplete: issues.length === 0,
|
|
222
|
+
score,
|
|
223
|
+
sectionCount,
|
|
224
|
+
totalChars,
|
|
225
|
+
reason: issues.length ? issues.join(',') : 'ok',
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
export function buildNarrativeDocument(source, allChunks) {
|
|
229
|
+
const chunks = allChunks
|
|
230
|
+
.filter((chunk) => chunk.source === source)
|
|
231
|
+
.sort((a, b) => a.char_start - b.char_start);
|
|
232
|
+
if (!chunks.length)
|
|
233
|
+
return null;
|
|
234
|
+
const full = getSourceFileContent(source);
|
|
235
|
+
const sections = chunks
|
|
236
|
+
.map((chunk, idx) => {
|
|
237
|
+
const content = full.slice(chunk.char_start, chunk.char_end).trim();
|
|
238
|
+
return {
|
|
239
|
+
rank: idx + 1,
|
|
240
|
+
title: chunk.title,
|
|
241
|
+
content,
|
|
242
|
+
char_start: chunk.char_start,
|
|
243
|
+
char_end: chunk.char_end,
|
|
244
|
+
contains_code: chunk.contains_code,
|
|
245
|
+
api_names: chunk.api_names,
|
|
246
|
+
chunk_id: chunk.id,
|
|
247
|
+
};
|
|
248
|
+
})
|
|
249
|
+
.filter((section) => section.content.length > 0);
|
|
250
|
+
if (!sections.length)
|
|
251
|
+
return null;
|
|
252
|
+
const first = chunks[0];
|
|
253
|
+
return {
|
|
254
|
+
source,
|
|
255
|
+
title: first.title === 'full' ? sourceTitle(source) : first.title,
|
|
256
|
+
doc_urls: resolveFragmentDocUrls(source, sections[0].content, full),
|
|
257
|
+
product: first.product,
|
|
258
|
+
framework: first.framework,
|
|
259
|
+
types: first.types,
|
|
260
|
+
section_count: sections.length,
|
|
261
|
+
sections,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
export function buildDiscoveryCatalog(ranked, poolWithContent, limit) {
|
|
265
|
+
const chunksById = new Map(poolWithContent.map((chunk) => [chunk.id, chunk]));
|
|
266
|
+
const seenSource = new Set();
|
|
267
|
+
const catalog = [];
|
|
268
|
+
for (const item of ranked) {
|
|
269
|
+
const chunk = chunksById.get(item.id);
|
|
270
|
+
if (!chunk)
|
|
271
|
+
continue;
|
|
272
|
+
if (isUrlMappingSource(chunk.source))
|
|
273
|
+
continue;
|
|
274
|
+
if (seenSource.has(chunk.source))
|
|
275
|
+
continue;
|
|
276
|
+
seenSource.add(chunk.source);
|
|
277
|
+
catalog.push({
|
|
278
|
+
rank: catalog.length + 1,
|
|
279
|
+
source: chunk.source,
|
|
280
|
+
title: chunk.title === 'full' ? sourceTitle(chunk.source) : chunk.title,
|
|
281
|
+
doc_urls: resolveFragmentDocUrls(chunk.source, chunk.content, getSourceFileContent(chunk.source)),
|
|
282
|
+
product: chunk.product,
|
|
283
|
+
framework: chunk.framework,
|
|
284
|
+
types: chunk.types,
|
|
285
|
+
score: Number(item.score.toFixed(2)),
|
|
286
|
+
});
|
|
287
|
+
if (catalog.length >= limit)
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
return catalog;
|
|
291
|
+
}
|
|
292
|
+
export function hasBestPracticeFragment(fragments) {
|
|
293
|
+
return fragments.some((f) => /\/best-practice\//i.test(f.source));
|
|
294
|
+
}
|
|
295
|
+
export function inferQueryApiTokens(prompt) {
|
|
296
|
+
const tokens = new Set();
|
|
297
|
+
// 仅保留 API 形态 token,剔除产品/平台品牌词,避免把“TRTC Conference Web ...”误判为 API 缺口。
|
|
298
|
+
const upperNoise = new Set([
|
|
299
|
+
'SDK', 'API', 'HTTP', 'HTTPS', 'JSON', 'CALL', 'NAME', 'STATUS',
|
|
300
|
+
'TRTC', 'TENCENT', 'WEB', 'VUE', 'REACT', 'ROOMKIT', 'LIVEKIT', 'CALLKIT',
|
|
301
|
+
]);
|
|
302
|
+
const platformNoise = new Set([
|
|
303
|
+
'ios', 'android', 'flutter', 'web', 'react', 'vue', 'miniprogram',
|
|
304
|
+
'harmonyos', 'react-native', 'unity', 'unreal', 'unreal-engine', 'donut', 'uni-app',
|
|
305
|
+
'pc', 'h5',
|
|
306
|
+
]);
|
|
307
|
+
for (const match of prompt.matchAll(/\b([a-z]+[A-Z][A-Za-z0-9_]*)\b/g)) {
|
|
308
|
+
tokens.add(match[1]);
|
|
309
|
+
}
|
|
310
|
+
for (const match of prompt.matchAll(/\b([A-Z][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*)\b/g)) {
|
|
311
|
+
tokens.add(match[1]);
|
|
312
|
+
}
|
|
313
|
+
for (const match of prompt.matchAll(/\b(use[A-Z]\w*)\b/g)) {
|
|
314
|
+
tokens.add(match[1]);
|
|
315
|
+
}
|
|
316
|
+
for (const match of prompt.matchAll(/\b(I[A-Z][A-Za-z0-9_]*)\b/g)) {
|
|
317
|
+
tokens.add(match[1]);
|
|
318
|
+
}
|
|
319
|
+
for (const match of prompt.matchAll(/\b([A-Z][A-Za-z0-9_]*\.[A-Z_][A-Z0-9_]*)\b/g)) {
|
|
320
|
+
tokens.add(match[1]);
|
|
321
|
+
}
|
|
322
|
+
for (const match of prompt.matchAll(/\b([A-Z_]{3,})\b/g)) {
|
|
323
|
+
const token = match[1];
|
|
324
|
+
if (!upperNoise.has(token))
|
|
325
|
+
tokens.add(token);
|
|
326
|
+
}
|
|
327
|
+
return [...tokens].filter((token) => !platformNoise.has(token.toLowerCase()));
|
|
328
|
+
}
|
|
329
|
+
export function tokenInChunk(token, chunk) {
|
|
330
|
+
if (chunk.api_names?.some((name) => name.toLowerCase() === token.toLowerCase()))
|
|
331
|
+
return true;
|
|
332
|
+
if (chunk.symbols?.some((name) => name.toLowerCase() === token.toLowerCase()))
|
|
333
|
+
return true;
|
|
334
|
+
const low = token.toLowerCase();
|
|
335
|
+
return chunk.title.toLowerCase().includes(low) || chunk.source.toLowerCase().includes(low);
|
|
336
|
+
}
|
|
337
|
+
export function computeApiNameCoverage(tokens, candidates) {
|
|
338
|
+
if (!tokens.length)
|
|
339
|
+
return 1;
|
|
340
|
+
const hit = tokens.filter((token) => candidates.some((chunk) => tokenInChunk(token, chunk))).length;
|
|
341
|
+
return Number((hit / tokens.length).toFixed(2));
|
|
342
|
+
}
|
|
343
|
+
export function computeMissingApiTokens(tokens, candidates) {
|
|
344
|
+
if (!tokens.length)
|
|
345
|
+
return [];
|
|
346
|
+
return tokens.filter((token) => !candidates.some((chunk) => tokenInChunk(token, chunk)));
|
|
347
|
+
}
|
|
348
|
+
export function hasProductScopeIntent(prompt) {
|
|
349
|
+
return isProductScopePrompt(prompt);
|
|
350
|
+
}
|
|
351
|
+
export function hasNativeTrtcApiSignal(prompt, queryApiTokens) {
|
|
352
|
+
if (/\bTRTCCloud\b|\bTRTCRenderParams\b|\bTXDeviceManager\b|setLocalRenderParams|startLocalPreview|stopLocalPreview/i.test(prompt)) {
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
return queryApiTokens.some((token) => /^(TRTC|TX)[A-Za-z0-9_]+$/.test(token)
|
|
356
|
+
|| /^(set|get|start|stop|enable|disable)[A-Z][A-Za-z0-9_]+$/.test(token));
|
|
357
|
+
}
|
|
358
|
+
export function hasNativeClientFramework(frameworks) {
|
|
359
|
+
if (!frameworks?.length)
|
|
360
|
+
return false;
|
|
361
|
+
return frameworks.some((fw) => ['android', 'ios', 'flutter', 'c++'].includes(fw));
|
|
362
|
+
}
|
|
363
|
+
export function applyRankMultipliers(ranked, chunksById, opts) {
|
|
364
|
+
const adjusted = ranked.map((item) => {
|
|
365
|
+
const chunk = chunksById.get(item.id);
|
|
366
|
+
if (!chunk)
|
|
367
|
+
return item;
|
|
368
|
+
let multiplier = 1;
|
|
369
|
+
// A2: 传了 product 时,命中 scope 的通配语料允许召回但降权
|
|
370
|
+
if (opts.product?.length && chunk.product === '*') {
|
|
371
|
+
multiplier *= 0.2;
|
|
372
|
+
}
|
|
373
|
+
// F2: framework 软降权(保留硬过滤,处理 fallback/边界残留)
|
|
374
|
+
if (opts.framework && opts.frameworks?.length && !chunk.framework.includes('*')) {
|
|
375
|
+
const explicitMatch = hasExplicitFrameworkMatch(chunk.framework, opts.frameworks);
|
|
376
|
+
const fallbackMatch = hasFrameworkFallbackMatch(chunk, opts);
|
|
377
|
+
if (!explicitMatch && !fallbackMatch) {
|
|
378
|
+
multiplier *= 0.2;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return { ...item, score: item.score * multiplier };
|
|
382
|
+
});
|
|
383
|
+
return adjusted.sort((a, b) => b.score - a.score);
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* 多因子置信度评估:exact 命中 + 分数梯度 + API 覆盖度。
|
|
387
|
+
*/
|
|
388
|
+
export function assessConfidence(fragments, exactHits, apiNameCoverage, queryApiTokens) {
|
|
389
|
+
if (fragments.length === 0) {
|
|
390
|
+
return { confidence: 'none', status: 'empty' };
|
|
391
|
+
}
|
|
392
|
+
const top = fragments[0].score;
|
|
393
|
+
const second = fragments[1]?.score ?? 0;
|
|
394
|
+
const ratio = second > 0 ? top / second : Infinity;
|
|
395
|
+
let confidence;
|
|
396
|
+
if (exactHits.length > 0 || top >= 50)
|
|
397
|
+
confidence = 'high';
|
|
398
|
+
else if (top >= 12 && ratio >= 1.25)
|
|
399
|
+
confidence = 'medium';
|
|
400
|
+
else if (top >= 6 && ratio >= 1.12)
|
|
401
|
+
confidence = 'medium';
|
|
402
|
+
else
|
|
403
|
+
confidence = 'low';
|
|
404
|
+
// D2: API 覆盖过低时,confidence 上限降为 low
|
|
405
|
+
if (queryApiTokens.length > 0 && apiNameCoverage < 0.3) {
|
|
406
|
+
confidence = 'low';
|
|
407
|
+
}
|
|
408
|
+
const status = confidence === 'low' ? 'low_confidence' : 'success';
|
|
409
|
+
return { confidence, status };
|
|
410
|
+
}
|