@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.
Files changed (86) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +58 -39
  3. package/knowledge/build-index.js +30 -6
  4. package/knowledge/chunker.js +82 -14
  5. package/knowledge/chunking-policy.js +100 -0
  6. package/knowledge/content-loader.js +3 -3
  7. package/knowledge/doc-url-resolver.js +43 -16
  8. package/knowledge/frontmatter-meta.js +167 -0
  9. package/knowledge/path-meta.js +28 -6
  10. package/knowledge/search/exact-lookup.js +120 -239
  11. package/knowledge/search/filter-pool.js +12 -29
  12. package/knowledge/search/framework-fallback.js +22 -0
  13. package/knowledge/search/intent-router.js +29 -0
  14. package/knowledge/search/l1-enrich.js +159 -0
  15. package/knowledge/search/query-normalizer.js +63 -3
  16. package/knowledge/search/search-core.js +410 -0
  17. package/knowledge/search/search-knowledge.js +421 -379
  18. package/knowledge/search/search-message.js +88 -0
  19. package/knowledge/search/search-pass.js +428 -0
  20. package/knowledge/search/search-types.js +1 -0
  21. package/knowledge/search/url-row-idf.js +120 -0
  22. package/knowledge/search/url-row-scorer.js +337 -0
  23. package/package.json +3 -1
  24. package/resource/_schema/feature-aliases.yaml +1 -1
  25. package/resource/index/manifest.json +1 -1
  26. package/resource/knowledge/callkit/best-practice/callkit-miniprogram/uniapp-shrink.md +60 -0
  27. package/resource/knowledge/callkit/best-practice/common/call-autoplay-blocked.md +77 -0
  28. package/resource/knowledge/callkit/best-practice/common/call-message-in-chat.md +78 -0
  29. package/resource/knowledge/callkit/best-practice/common/call-vs-calls.md +110 -0
  30. package/resource/knowledge/callkit/best-practice/common/callengine-call-flow.md +123 -0
  31. package/resource/knowledge/callkit/best-practice/common/callengine-log-level.md +67 -0
  32. package/resource/knowledge/callkit/best-practice/common/callkit-get-engine-instance.md +81 -0
  33. package/resource/knowledge/callkit/best-practice/common/callkit-get-roomid.md +78 -0
  34. package/resource/knowledge/callkit/best-practice/common/callkit-limitations.md +37 -0
  35. package/resource/knowledge/callkit/best-practice/common/callkit-user-register-and-kicked.md +98 -0
  36. package/resource/knowledge/chat/best-practice/common/avchatroom-message-loss-diagnosis.md +65 -0
  37. package/resource/knowledge/chat/best-practice/common/group-conversation-cleanup-on-system-notice.md +62 -0
  38. package/resource/knowledge/chat/best-practice/common/image-message-url-lifecycle.md +59 -0
  39. package/resource/knowledge/chat/best-practice/common/login-repeat-2025-diagnosis.md +61 -0
  40. package/resource/knowledge/chat/best-practice/common/message-read-report-and-receipt.md +64 -0
  41. package/resource/knowledge/chat/best-practice/common/message-receive-event.md +65 -0
  42. package/resource/knowledge/chat/best-practice/common/message-risk-error-and-render.md +65 -0
  43. package/resource/knowledge/chat/best-practice/common/message-unread-count.md +71 -0
  44. package/resource/knowledge/chat/best-practice/miniprogram/audio-message-send.md +113 -0
  45. package/resource/knowledge/chat/best-practice/miniprogram/login-timeout-2801-diagnosis.md +63 -0
  46. package/resource/knowledge/chat/best-practice/miniprogram/media-upload-fail-diagnosis.md +64 -0
  47. package/resource/knowledge/chat/best-practice/web/audio-message-send.md +118 -0
  48. package/resource/knowledge/chat/urls/product.md +17 -16
  49. package/resource/knowledge/chat/urls/restapi.md +3 -2
  50. package/resource/knowledge/chat/urls/sdk/android.md +6 -5
  51. package/resource/knowledge/chat/urls/sdk/flutter.md +6 -5
  52. package/resource/knowledge/chat/urls/sdk/ios.md +4 -3
  53. package/resource/knowledge/chat/urls/sdk/web.md +7 -6
  54. package/resource/knowledge/chat/urls/webhook.md +0 -5
  55. package/resource/knowledge/livekit/best-practice/android/voice-room-anchor-start.md +120 -0
  56. package/resource/knowledge/livekit/best-practice/common/admin-take-seat.md +73 -0
  57. package/resource/knowledge/livekit/best-practice/common/audience-leave-live.md +81 -0
  58. package/resource/knowledge/livekit/best-practice/common/voice-room-no-audio-troubleshoot.md +100 -0
  59. package/resource/knowledge/livekit/best-practice/common/voice-room-seat-list-change.md +65 -0
  60. package/resource/knowledge/livekit/best-practice/flutter/voice-room-anchor-start.md +122 -0
  61. package/resource/knowledge/livekit/best-practice/ios/voice-room-anchor-start.md +126 -0
  62. package/resource/knowledge/roomkit/best-practice/common/room-ghost-member.md +70 -0
  63. package/resource/knowledge/roomkit/best-practice/common/room-limit-and-multi.md +83 -0
  64. package/resource/knowledge/roomkit/best-practice/common/room-screen-share-preview.md +83 -0
  65. package/resource/knowledge/roomkit/best-practice/common/room-status-after-all-leave.md +69 -0
  66. package/resource/knowledge/roomkit/best-practice/vue/room-uikit-vs-atomicx.md +111 -0
  67. package/resource/knowledge/rtcengine/web/plugin/30-advanced-cross-room-link.md +2 -0
  68. package/resource/knowledge/rtcengine/web/plugin/39-advanced-video-decoder.md +2 -0
  69. package/server.js +28 -23
  70. package/streamable-http.js +19 -54
  71. package/tools/common/search_trtc_knowledge.js +24 -12
  72. package/tools/definitions/finalize_answer_def.js +2 -2
  73. package/tools/definitions/integration-boundary.js +2 -1
  74. package/tools/definitions/search_trtc_knowledge_def.js +5 -3
  75. package/utils/fetch-doc-content.js +198 -0
  76. package/utils/preflight-route.js +197 -0
  77. package/utils/report-cls-client.js +1 -1
  78. package/utils/report-es-client.js +1 -1
  79. package/cursor-setting.png +0 -0
  80. package/mcp-server-prompt.png +0 -0
  81. package/resource/knowledge/callkit/best-practice/web/callengine-invitation.md +0 -80
  82. package/resource/knowledge/callkit/best-practice/web/callengine-log-control.md +0 -24
  83. package/resource/knowledge/callkit/best-practice/web/callkit-call-vs-calls.md +0 -116
  84. package/resource/knowledge/callkit/preset/react/uikit-api-reference.md +0 -444
  85. package/resource/knowledge/callkit/preset/vue/uikit-api-reference.md +0 -464
  86. package/resource/knowledge/chat/best-practice/web/image-message-url-handling.md +0 -16
@@ -0,0 +1,337 @@
1
+ /**
2
+ * url-row-scorer.ts
3
+ *
4
+ * URL 映射行(chat/urls/...)匹配打分器。
5
+ *
6
+ * 设计目标(重构版):
7
+ * - **语义词对齐**:按「query 与 title 的高区分度语义特征是否对齐」打分,
8
+ * 而不是「子串/单字」粗匹配。
9
+ * - **IDF 自动降权**:高频泛词(`消息`/`message`/`group`)靠 IDF 自动低权重,
10
+ * 稀有语义词(`消息回应`/`reaction`)高权重,**不依赖枚举词表**。
11
+ * - **中英通吃、产品无关**:同一套特征提取同时处理中文/英文标题。
12
+ * - 保留已验证有效的 REST 端点 / API token 高分路径。
13
+ */
14
+ import { idfWeight, isValidBigram, loadIdf } from './url-row-idf.js';
15
+ /**
16
+ * 提取 CJK 有效 2-gram(df>0,过滤「送前」「前回」等滑窗噪声)。
17
+ * 口径与旧版 qBigrams / titleBigramArray **bit-identical**。
18
+ */
19
+ function extractCjkTokens(text) {
20
+ const tokens = new Set();
21
+ for (const seg of text.toLowerCase().match(/[\u4e00-\u9fff]{2,}/g) ?? []) {
22
+ for (let i = 0; i < seg.length - 1; i += 1) {
23
+ const bg = seg.slice(i, i + 2);
24
+ if (isValidBigram(bg))
25
+ tokens.add(bg);
26
+ }
27
+ }
28
+ return tokens;
29
+ }
30
+ /**
31
+ * 提取 EN 有效词(>=4 字母,形态过滤 web/h5/sdk/api/im/tim/app/pc/mini/v\d+)。
32
+ * 仅在 query **纯 EN**(无 CJK)时启用,避免混合 query 的 EN 翻译
33
+ * (如「单聊」展开成「one-to-one c2c single chat message」后,无对应 title 命中,
34
+ * 会稀释 CJK 主干的 coverage 分母,导致「单聊/群聊」互抢位)。
35
+ */
36
+ function extractEnTokens(text) {
37
+ const tokens = new Set();
38
+ for (const m of text.toLowerCase().matchAll(/[a-z][a-z0-9]{2,}/g)) {
39
+ const w = m[0];
40
+ if (w.length < 4)
41
+ continue;
42
+ if (/^(web|h5|sdk|api|im|tim|app|pc|mini|v\d+)$/.test(w))
43
+ continue;
44
+ tokens.add(w);
45
+ }
46
+ return tokens;
47
+ }
48
+ /**
49
+ * 计算单语言路径的语义打分(coverage + meaningfulRatio + titleCoverage 三维度)。
50
+ * 对纯 CJK 输入,与旧版 4 分支结构 + 公式 **bit-identical**。
51
+ * 对纯 EN 输入,引入 IDF 调节(旧版只走 650+30*N 无 IDF 导致泛词虚高)。
52
+ */
53
+ function scoreOneLang(qTokens, tTokens) {
54
+ const queryTokenCount = qTokens.size;
55
+ const titleTokenCount = tTokens.size;
56
+ if (queryTokenCount === 0) {
57
+ return { coverage: 0, titleCoverage: 0, meaningfulRatio: 0, matchedCount: 0, titleTokenCount };
58
+ }
59
+ let matchedCount = 0;
60
+ let meaningfulMatched = 0;
61
+ for (const t of qTokens) {
62
+ if (tTokens.has(t)) {
63
+ matchedCount += 1;
64
+ if (idfWeight(t) > 0.5)
65
+ meaningfulMatched += 1;
66
+ }
67
+ }
68
+ const coverage = matchedCount / queryTokenCount;
69
+ const titleCoverage = titleTokenCount > 0 ? matchedCount / titleTokenCount : 0;
70
+ const meaningfulRatio = meaningfulMatched / queryTokenCount;
71
+ return { coverage, titleCoverage, meaningfulRatio, matchedCount, titleTokenCount };
72
+ }
73
+ /** REST 端点(v4/xxx)精确匹配 */
74
+ function restEndpointHit(query, title) {
75
+ const qe = [...query.matchAll(/v4\/[\w/]+/gi)].map((m) => m[0].toLowerCase());
76
+ const te = [...title.matchAll(/v4\/[\w/]+/gi)].map((m) => m[0].toLowerCase());
77
+ if (!qe.length)
78
+ return 0;
79
+ for (const q of qe) {
80
+ if (te.some((t) => t === q || t.endsWith(q) || q.endsWith(t)))
81
+ return 1000;
82
+ }
83
+ return 0;
84
+ }
85
+ /**
86
+ * API token(驼峰 / 点号 / useXxx)精确匹配
87
+ *
88
+ * **V2 严格命中(修复 X message row-to-row tie)**:
89
+ * 旧版 `lower.includes(api)` 会把"共享部分 API"也计入命中,导致所有
90
+ * `X message (createXMessage → sendMessage)` 标题因共享 `sendMessage` 都得 940,
91
+ * BM25 决定胜负,倾向"Text message"抢首位(41+ miss)。
92
+ *
93
+ * 新版要求 query 和 title 的 **long API(≥8 字符)集合完全一致**(双向 ⊆)
94
+ * 才算 940。这样:
95
+ * - query "Send message parameter configuration (sendMessage parameters)" → long={sendMessage}
96
+ * - self title: long={sendMessage} → 940
97
+ * - "Text message" title: long={createTextMessage, sendMessage} → sizes differ → 0
98
+ * - 共享 `sendMessage` 但缺独有 API(如 createTextMessage)的 title 拿 0 分
99
+ * - query "Image message (createImageMessage → sendMessage)" → long={createImageMessage, sendMessage}
100
+ * - self title: long={createImageMessage, sendMessage} → 940
101
+ * - "Sound message" title: long={createSoundMessage, sendMessage} → 大小同但内容不同 → 0
102
+ *
103
+ * 兼容:
104
+ * - restapi.md 仍命中(query 和 title 的 v4 API 路径互为完整 token 集合)。
105
+ * - sdk/* 的 createXMessage/sendMessage 自匹配:query 和 title 双向 ⊆ → 940。
106
+ */
107
+ function apiTokenHit(query, title) {
108
+ // 提取 query/title 的 API token(驼峰 / useXxx / 点号)
109
+ const apiRe = /\b([a-z]+[A-Z][A-Za-z0-9_]*)\b|\b([A-Z][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*)\b|\b(use[A-Z]\w*)\b/g;
110
+ const extract = (text) => {
111
+ const out = new Set();
112
+ for (const m of text.matchAll(apiRe)) {
113
+ const api = (m[1] ?? m[2] ?? m[3]).toLowerCase();
114
+ if (api && api.length >= 4)
115
+ out.add(api);
116
+ }
117
+ return out;
118
+ };
119
+ const queryApis = extract(query);
120
+ const titleApis = extract(title);
121
+ if (queryApis.size === 0)
122
+ return 0;
123
+ // 只看 long API(≥8 字符)的双向 ⊆ 严格命中要求
124
+ const longQuery = new Set([...queryApis].filter((a) => a.length >= 8));
125
+ if (longQuery.size === 0) {
126
+ // 没有 long API 的 query 走 hits 计数(兼容短 token 场景,如 useXxx)
127
+ let hits = 0;
128
+ for (const a of queryApis)
129
+ if (titleApis.has(a))
130
+ hits += 1;
131
+ if (hits >= 2)
132
+ return 900;
133
+ if (hits === 1)
134
+ return 860;
135
+ return 0;
136
+ }
137
+ const longTitle = new Set([...titleApis].filter((a) => a.length >= 8));
138
+ // 严格双向命中:query.long === title.long(Set 视角)
139
+ if (longQuery.size !== longTitle.size)
140
+ return 0;
141
+ for (const a of longQuery)
142
+ if (!longTitle.has(a))
143
+ return 0;
144
+ return 940;
145
+ }
146
+ /**
147
+ * 语义词对齐打分(双路径版:CJK 路径与 EN 路径独立计算)
148
+ *
149
+ * 设计要点(与旧版兼容性 vs 修复 en 端虚高):
150
+ * 1. **CJK 路径**:与旧版 4 分支 + 公式 **bit-identical**。
151
+ * 混合 query(query expansion 注入 EN 翻译后)只走 CJK 路径,EN tokens 不参与
152
+ * 分母计算,避免「单聊/群聊」互抢位。
153
+ * 2. **EN 路径**:仅在 query 无 CJK 时启用(即纯 EN query)。
154
+ * 引入 IDF 调节:旧版 enWordHits 路径「650+30*N」无 IDF,导致 en 端 row 虚高抢位;
155
+ * 新版用 coverage+meaningfulRatio 与 CJK 路径同口径打分。
156
+ * 3. **惩罚 1/2**:仅当 score > 0 时生效,纯 EN query 不触发 CJK 整段包含惩罚。
157
+ *
158
+ * 关键修复:「修改单聊历史消息 one-to-one c2c single chat message messages」(扩 query)
159
+ * 旧版: enWordHits=0 → 走 CJK 路径 → 自己 700, 对偶 643
160
+ * 新版: CJK 路径独立 → 自己 700, 对偶 643 (**bit-identical**)
161
+ * 纯 EN query: 新版用 coverage+IDF 替代旧 650+30*N,降权泛词。
162
+ */
163
+ function semanticAlign(query, title) {
164
+ const qCjk = extractCjkTokens(query);
165
+ const tCjk = extractCjkTokens(title);
166
+ const qEn = extractEnTokens(query);
167
+ const tEn = extractEnTokens(title);
168
+ let score = 0;
169
+ if (qCjk.size > 0) {
170
+ // CJK 路径:与旧版公式 bit-identical
171
+ const m = scoreOneLang(qCjk, tCjk);
172
+ if (m.coverage >= 0.5 && m.meaningfulRatio >= 0.5) {
173
+ score = 500 + Math.round(m.coverage * 120) + Math.round(m.meaningfulRatio * 80);
174
+ }
175
+ else if (m.titleTokenCount >= 3 && m.titleCoverage >= 0.7) {
176
+ score = 620 + Math.round(m.titleCoverage * 60);
177
+ }
178
+ else if (m.coverage >= 0.3 && m.meaningfulRatio >= 0.3) {
179
+ score = 400;
180
+ }
181
+ }
182
+ else if (qEn.size > 0) {
183
+ // EN 路径:仅纯 EN query,引入 IDF 调节(旧版 650+30*N 无 IDF 致虚高)
184
+ const m = scoreOneLang(qEn, tEn);
185
+ if (m.coverage >= 0.5 && m.meaningfulRatio >= 0.5) {
186
+ score = 500 + Math.round(m.coverage * 120) + Math.round(m.meaningfulRatio * 80);
187
+ }
188
+ else if (m.titleTokenCount >= 3 && m.titleCoverage >= 0.7) {
189
+ score = 620 + Math.round(m.titleCoverage * 60);
190
+ }
191
+ else if (m.coverage >= 0.3) {
192
+ // 弱命中:旧版无此分支,给软基分 300-400
193
+ score = 300 + Math.round(m.coverage * 200);
194
+ }
195
+ }
196
+ if (score === 0)
197
+ return 0;
198
+ // 惩罚1:CJK 路径下,query 有明确 CJK 语义但覆盖双低(coverage<0.5 && titleCoverage<0.7),
199
+ // 封顶 400,避免泛词残留抬分。EN 路径不触发。
200
+ if (qCjk.size > 0) {
201
+ const qHasCjk3 = /[\u4e00-\u9fff]{3,}/.test(query);
202
+ if (qHasCjk3) {
203
+ const m = scoreOneLang(qCjk, tCjk);
204
+ if (m.coverage < 0.5 && m.titleCoverage < 0.7) {
205
+ score = Math.min(score, 400);
206
+ }
207
+ }
208
+ }
209
+ // 惩罚2:query 的完整 CJK 整段(≥4 字)「完全包含于」title,但 title 明显更长
210
+ // (多出 ≥3 字独立主题词),说明 title 主题更窄/偏移 → 降权。
211
+ // 例:query「发送单聊消息」vs title「发送单聊消息已读回执」→ 后者多出「已读回执」。
212
+ if (qCjk.size > 0) {
213
+ const qSegments = query.match(/[\u4e00-\u9fff]{3,}/g) ?? [];
214
+ const titleNorm = title.replace(/\s+/g, '');
215
+ const qFullCjk = qSegments.filter((s) => s.length >= 4 && titleNorm.includes(s));
216
+ if (qFullCjk.length > 0) {
217
+ const titleCjkLen = (title.match(/[\u4e00-\u9fff]/g) ?? []).length;
218
+ const queryCjkLen = (query.match(/[\u4e00-\u9fff]/g) ?? []).length;
219
+ const extraCjk = titleCjkLen - queryCjkLen;
220
+ if (extraCjk >= 3) {
221
+ score = Math.max(360, score - 120 * Math.floor(extraCjk / 2));
222
+ }
223
+ }
224
+ }
225
+ return score;
226
+ }
227
+ /** 兼容入口:原有 normalize 逻辑保留(供 exact-lookup 调用) */
228
+ function normalizeE7Text(text) {
229
+ return text
230
+ .toLowerCase()
231
+ .replace(/[^\w\u4e00-\u9fff/]+/g, ' ')
232
+ .replace(/\s+/g, ' ')
233
+ .trim();
234
+ }
235
+ export function normalizeUrlRowQuery(query) {
236
+ return normalizeE7Text(query);
237
+ }
238
+ /**
239
+ * 产品无关的 URL 映射行判定:knowledge/<product>/urls/.../xxx.md
240
+ * 供 exact-lookup / l1-enrich / doc-url-resolver 复用,避免各处硬编码 chat/urls。
241
+ */
242
+ export function isUrlMappingSource(source) {
243
+ return /knowledge\/[a-z0-9_-]+\/urls\/.+\.md$/i.test(source.replace(/\\/g, '/'));
244
+ }
245
+ /**
246
+ * 对 URL 映射行打分。
247
+ * 优先级:REST 端点 > API token > 语义词对齐。
248
+ */
249
+ export function scoreUrlRow(query, title) {
250
+ loadIdf();
251
+ const restScore = restEndpointHit(query, title);
252
+ const apiScore = restScore > 0 ? 0 : apiTokenHit(query, title);
253
+ const semanticScore = restScore > 0 || apiScore >= 860 ? 0 : semanticAlign(query, title);
254
+ if (restScore > 0) {
255
+ return { score: restScore, matchedBy: 'rest', debug: { restScore, apiScore, semanticScore } };
256
+ }
257
+ if (apiScore > 0) {
258
+ return { score: apiScore, matchedBy: 'api', debug: { restScore, apiScore, semanticScore } };
259
+ }
260
+ if (semanticScore > 0) {
261
+ return { score: semanticScore, matchedBy: 'semantic', debug: { restScore, apiScore, semanticScore } };
262
+ }
263
+ return {
264
+ score: 0,
265
+ matchedBy: 'none',
266
+ debug: { restScore, apiScore, semanticScore },
267
+ };
268
+ }
269
+ /** 从 chunk body 中提取开头的分组标题(chunker 会把 urls 行的 `##` 分组写成 `### 分组标题` 前缀) */
270
+ export function extractSectionTitle(content) {
271
+ const match = content.match(/^\s*###\s+([^\n]+)/);
272
+ return match ? match[1].trim() : '';
273
+ }
274
+ /**
275
+ * 计算一组分组标题的「共有词」(出现在全部分组标题中的单字/2-gram,无区分度)。
276
+ * 产品无关:对任意 urls 文件的 `##` 分组自动判定,不依赖枚举。
277
+ */
278
+ export function computeCommonSectionTokens(sectionTitles) {
279
+ const n = sectionTitles.length;
280
+ if (n === 0)
281
+ return { chars: new Set(), bigrams: new Set() };
282
+ const charFreq = new Map();
283
+ const bigramFreq = new Map();
284
+ for (const title of sectionTitles) {
285
+ const chars = new Set(title.match(/[\u4e00-\u9fff]/g) ?? []);
286
+ const bigrams = new Set();
287
+ for (const seg of title.match(/[\u4e00-\u9fff]{2,}/g) ?? []) {
288
+ for (let i = 0; i < seg.length - 1; i += 1)
289
+ bigrams.add(seg.slice(i, i + 2));
290
+ }
291
+ for (const c of chars)
292
+ charFreq.set(c, (charFreq.get(c) ?? 0) + 1);
293
+ for (const b of bigrams)
294
+ bigramFreq.set(b, (bigramFreq.get(b) ?? 0) + 1);
295
+ }
296
+ return {
297
+ chars: new Set([...charFreq].filter(([, v]) => v === n).map(([k]) => k)),
298
+ bigrams: new Set([...bigramFreq].filter(([, v]) => v === n).map(([k]) => k)),
299
+ };
300
+ }
301
+ /**
302
+ * 判定 query 是否命中分组标题(通用分组语义粗筛)。
303
+ *
304
+ * 规则(混用单字 + 2-gram,至少 2 个任意词 + 至少 1 个区分词):
305
+ * - 提取 query 与分组标题的中文单字集、2-gram 集。
306
+ * - 排除「共有词」(该文件所有分组都出现的词,如 webhook 里的「回调/相关」),避免其单独触发命中。
307
+ * - 命中条件:query 与分组标题共享「≥1 个区分词」且「≥2 个任意词(区分+共有)」。
308
+ *
309
+ * 例:query「服务端群消息发送前回调」vs 分组「群组系统相关回调」:
310
+ * 共享单字「群/回/调」,排除共有「回/调」后区分词「群」命中,且任意词 3 个 → 命中。
311
+ * 而「在线状态相关回调」只共享共有词「回/调」→ 不命中。
312
+ *
313
+ * 产品无关:对所有产品的 urls 分组都适用,不依赖具体产品词表。
314
+ */
315
+ export function queryMatchesSection(query, sectionTitle, commonTokens) {
316
+ const queryChars = new Set(query.match(/[\u4e00-\u9fff]/g) ?? []);
317
+ const queryBigrams = new Set();
318
+ for (const seg of query.match(/[\u4e00-\u9fff]{2,}/g) ?? []) {
319
+ for (let i = 0; i < seg.length - 1; i += 1)
320
+ queryBigrams.add(seg.slice(i, i + 2));
321
+ }
322
+ const sectionChars = new Set(sectionTitle.match(/[\u4e00-\u9fff]/g) ?? []);
323
+ const sectionBigrams = new Set();
324
+ for (const seg of sectionTitle.match(/[\u4e00-\u9fff]{2,}/g) ?? []) {
325
+ for (let i = 0; i < seg.length - 1; i += 1)
326
+ sectionBigrams.add(seg.slice(i, i + 2));
327
+ }
328
+ // 共享单字 + 共享 2-gram(2-gram 命中算 2 个词)
329
+ const sharedChar = [...sectionChars].filter((c) => queryChars.has(c));
330
+ const sharedBigram = [...sectionBigrams].filter((b) => queryBigrams.has(b));
331
+ // 任意词(共享单字按 1 个词、共享 2-gram 按 1 个词计;2-gram 与单字不重复计)
332
+ const anyCount = sharedChar.length + sharedBigram.length;
333
+ // 区分词:共享但不在共有词集合里
334
+ const distinctCount = sharedChar.filter((c) => !commonTokens.chars.has(c)).length
335
+ + sharedBigram.filter((b) => !commonTokens.bigrams.has(b)).length;
336
+ return distinctCount >= 1 && anyCount >= 2;
337
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tencent-rtc/mcp",
3
- "version": "1.6.8",
3
+ "version": "1.7.1",
4
4
  "description": "Tencent Cloud MCP Server for SDK",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -20,12 +20,14 @@
20
20
  "@types/uuid": "^10.0.0",
21
21
  "agentkeepalive": "^4.1.4",
22
22
  "axios": "^0.25.0",
23
+ "cheerio": "^1.2.0",
23
24
  "debug": "^4.3.1",
24
25
  "express": "^5.1.0",
25
26
  "hashring": "^3.2.0",
26
27
  "protobufjs": "^6.11.2",
27
28
  "semver": "^7.3.5",
28
29
  "shx": "^0.3.4",
30
+ "turndown": "^7.2.4",
29
31
  "typescript": "^5.8.2",
30
32
  "uuid": "^8.3.2",
31
33
  "zod": "^3.24.2"
@@ -8,4 +8,4 @@ live:
8
8
  room:
9
9
  screen-share.md: ["screen sharing", "screen share", "startScreenShare"]
10
10
  virtual-background.md: ["virtual background"]
11
- device-state.md: ["conference", "meeting", "turn on camera", "camera", "backend support", "server support", "会议", "开摄像头", "后端支持", "服务端支持"]
11
+ device-state.md: ["turn on camera", "camera", "backend support", "server support", "开摄像头", "后端支持", "服务端支持"]