@tencentcloud/sdk-mcp 1.7.0 → 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 +11 -0
  2. package/README.md +11 -6
  3. package/knowledge/doc-url-resolver.js +70 -19
  4. package/knowledge/search/exact-lookup.js +116 -18
  5. package/knowledge/search/intent-router.js +21 -5
  6. package/knowledge/search/l1-enrich.js +159 -0
  7. package/knowledge/search/query-normalizer.js +8 -1
  8. package/knowledge/search/search-core.js +27 -8
  9. package/knowledge/search/search-knowledge.js +159 -46
  10. package/knowledge/search/search-message.js +37 -36
  11. package/knowledge/search/url-row-idf.js +120 -0
  12. package/knowledge/search/url-row-scorer.js +306 -227
  13. package/package.json +3 -1
  14. package/resource/index/manifest.json +1 -1
  15. package/resource/integration/callkit/preset/flutter/integration.md +7 -3
  16. package/resource/integration/chat/preset/android/integration.md +1 -0
  17. package/resource/integration/chat/preset/flutter/integration.md +110 -10
  18. package/resource/integration/chat/preset/ios/integration.md +1 -0
  19. package/resource/integration/chat/preset/react/integration.md +1 -0
  20. package/resource/integration/chat/preset/uni-app/integration.md +1 -0
  21. package/resource/integration/chat/preset/vue/integration.md +1 -0
  22. package/resource/integration/livekit/preset/live/android/prepare-integration.md +1 -0
  23. package/resource/integration/livekit/preset/live/vue/live-player-desk-integration.md +2 -2
  24. package/resource/integration/livekit/preset/live/vue/live-pusher-integration.md +3 -3
  25. package/resource/integration/roomkit/preset/vue/integration.md +1 -0
  26. package/resource/knowledge/callkit/core-sdk/android/answer-call.md +1 -0
  27. package/resource/knowledge/callkit/core-sdk/android/make-call.md +1 -0
  28. package/resource/knowledge/callkit/core-sdk/flutter/answer-call.md +1 -0
  29. package/resource/knowledge/callkit/core-sdk/flutter/make-call.md +1 -0
  30. package/resource/knowledge/callkit/core-sdk/ios/answer-call.md +1 -0
  31. package/resource/knowledge/callkit/core-sdk/ios/make-call.md +1 -0
  32. package/resource/knowledge/callkit/core-sdk/ios/offline-push.md +1 -1
  33. package/resource/knowledge/callkit/core-sdk/uni-app/answer-call.md +1 -0
  34. package/resource/knowledge/callkit/core-sdk/uni-app/make-call.md +1 -0
  35. package/resource/knowledge/callkit/faq/flutter.md +1 -0
  36. package/resource/knowledge/callkit/faq/uniapp-app.md +1 -0
  37. package/resource/knowledge/chat/preset/android-view/integration.md +1 -0
  38. package/resource/knowledge/chat/preset/ios-uikit/integration.md +1 -0
  39. package/resource/knowledge/chat/preset/react/features/message-input.md +61 -3
  40. package/resource/knowledge/chat/preset/react/features/message-list.md +7 -0
  41. package/resource/knowledge/chat/preset/vue/features/message-input.md +58 -5
  42. package/resource/knowledge/chat/preset/vue/features/message-list.md +7 -0
  43. package/resource/knowledge/chat/urls/product.md +8 -8
  44. package/resource/knowledge/chat/urls/restapi.md +3 -3
  45. package/resource/knowledge/chat/urls/sdk/android.md +5 -5
  46. package/resource/knowledge/chat/urls/sdk/flutter.md +5 -5
  47. package/resource/knowledge/chat/urls/sdk/ios.md +5 -5
  48. package/resource/knowledge/chat/urls/sdk/web.md +7 -7
  49. package/resource/knowledge/chat/urls/webhook.md +11 -17
  50. package/resource/knowledge/push/config/harmonyos.md +1 -1
  51. package/resource/knowledge/push/config/ios.md +2 -2
  52. package/resource/knowledge/push/integration/android.md +1 -0
  53. package/resource/knowledge/push/integration/donut.md +2 -2
  54. package/resource/knowledge/push/integration/flutter.md +1 -0
  55. package/resource/knowledge/push/integration/harmonyos.md +1 -1
  56. package/resource/knowledge/push/integration/ios.md +1 -0
  57. package/resource/knowledge/push/integration/uni-app.md +1 -0
  58. package/resource/knowledge/push/integration/unity.md +4 -4
  59. package/resource/knowledge/push/integration/unreal-engine.md +37 -35
  60. package/resource/knowledge/roomkit/core-sdk/android/call-user-to-room.md +1 -0
  61. package/resource/knowledge/roomkit/core-sdk/android/room-participant-state.md +1 -0
  62. package/resource/knowledge/roomkit/core-sdk/android/room-state.md +1 -0
  63. package/resource/knowledge/roomkit/core-sdk/android/schedule-room.md +1 -0
  64. package/resource/knowledge/roomkit/core-sdk/flutter/call-user-to-room.md +1 -0
  65. package/resource/knowledge/roomkit/core-sdk/flutter/room-participant-state.md +1 -0
  66. package/resource/knowledge/roomkit/core-sdk/flutter/room-state.md +1 -0
  67. package/resource/knowledge/roomkit/core-sdk/flutter/schedule-room.md +1 -0
  68. package/resource/knowledge/roomkit/core-sdk/ios/call-user-to-room.md +1 -0
  69. package/resource/knowledge/roomkit/core-sdk/ios/room-participant-state.md +1 -0
  70. package/resource/knowledge/roomkit/core-sdk/ios/room-state.md +1 -0
  71. package/resource/knowledge/roomkit/core-sdk/ios/schedule-room.md +1 -0
  72. package/resource/knowledge/roomkit/core-sdk/vue/room-view.md +1 -0
  73. package/resource/knowledge/roomkit/preset/react/integration.md +1 -0
  74. package/resource/knowledge/trtc/android/TRTCCloud.md +1 -0
  75. package/resource/knowledge/trtc/c++/ITRTCCloud.md +1 -0
  76. package/resource/knowledge/trtc/ios/TRTCCloud.md +1 -0
  77. package/server.js +4 -4
  78. package/streamable-http.js +19 -54
  79. package/tools/common/finalize_answer.js +1 -9
  80. package/tools/common/search_trtc_knowledge.js +23 -11
  81. package/tools/definitions/finalize_answer_def.js +2 -2
  82. package/tools/definitions/search_trtc_knowledge_def.js +4 -2
  83. package/utils/fetch-doc-content.js +198 -0
  84. package/utils/preflight-route.js +197 -0
  85. package/utils/report-cls-client.js +1 -1
  86. package/utils/report-es-client.js +1 -1
@@ -7,9 +7,11 @@ import { classifyRetrievalMode } from './intent-router.js';
7
7
  import { rankSubsetWithIndex } from '../bm25.js';
8
8
  import { getProductsBM25Index } from '../bm25-cache.js';
9
9
  import { hasExplicitFrameworkMatch } from './framework-fallback.js';
10
- import { assessConfidence, assessNarrativeCompleteness, buildDiscoveryCatalog, buildNarrativeDocument, hasNativeClientFramework, hasNativeTrtcApiSignal, hasProductScopeIntent, inferQueryApiTokens, pickNarrativeSource, } from './search-core.js';
10
+ import { assessConfidence, assessNarrativeCompleteness, buildNarrativeDocument, hasNativeClientFramework, hasNativeTrtcApiSignal, hasProductScopeIntent, inferQueryApiTokens, pickNarrativeSource, } from './search-core.js';
11
+ import { scoreUrlRow } from './url-row-scorer.js';
11
12
  import { buildAnswerContract, buildAgentMessage } from './search-message.js';
12
13
  import { runSearchPass } from './search-pass.js';
14
+ import { buildReferences } from '../doc-url-resolver.js';
13
15
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
16
  let cachedManifest = null;
15
17
  function resolveResourcePath(...segments) {
@@ -63,6 +65,97 @@ function buildCoverageReport(allChunks, fragments, preferredSource) {
63
65
  retry_suggested: primaryCompleteness < 0.6,
64
66
  };
65
67
  }
68
+ /**
69
+ * 提取 title 的主题特征 token 集合(跨语言统一)。
70
+ * - CJK 路径: 字符 2-gram 滑动(与 V2 版本 bit-identical,zh 端 0 退化)。
71
+ * - EN 路径: 原子 token + word 2-gram。
72
+ * - 原子 token: 单词 [a-z0-9]+ 或带连字符的复合词 [a-z0-9]+(?:-[a-z0-9]+)+
73
+ * 关键: `one-to-one` 整体作为一个 token,不拆成 ['one', 'to', 'one'](域语义:C2C 类别标签,
74
+ * 跟 CJK「单聊」等价;拆开会产生 'one one' 这种无意义 2-gram 噪声)。
75
+ * - word 2-gram: 原子 token 之间的 2-gram 滑动(不跨复合词内部),跟 CJK 2-gram 同构。
76
+ * - stopword 过滤: the/and/for/with/from/how/use/api/sdk/app/set/get/add/to/of/in/on/by 等功能词。
77
+ *
78
+ * 产品无关: 纯字面特征,不依赖 IDF / 分词词表。
79
+ */
80
+ function extractTitleTokens(text) {
81
+ const out = new Set();
82
+ // CJK 路径: 字符 2-gram(与原 extractTitleBigrams bit-identical)
83
+ for (const seg of text.match(/[\u4e00-\u9fff]{2,}/g) ?? []) {
84
+ for (let i = 0; i < seg.length - 1; i += 1) {
85
+ out.add(seg.slice(i, i + 2));
86
+ }
87
+ }
88
+ // EN 路径: 原子 token(含连字符复合词整体保留)+ 原子间的 2-gram
89
+ // 正则说明: [a-z][a-z0-9]*(?:-[a-z][a-z0-9]*)* — 匹配单词或带连字符的复合词,
90
+ // 一次匹配即可拿到完整 token,不会在复合词内部产生 2-gram。
91
+ const EN_STOPS = new Set([
92
+ 'the', 'and', 'for', 'with', 'from', 'how', 'what', 'why', 'when', 'where',
93
+ 'use', 'api', 'sdk', 'app', 'set', 'get', 'add', 'to', 'of', 'in', 'on', 'by',
94
+ ]);
95
+ const atomic = (text.toLowerCase().match(/[a-z][a-z0-9]*(?:-[a-z][a-z0-9]*)*/g) ?? [])
96
+ .filter((w) => w.length >= 3 && !EN_STOPS.has(w));
97
+ for (const w of atomic)
98
+ out.add(w);
99
+ for (let i = 0; i < atomic.length - 1; i += 1) {
100
+ out.add(`${atomic[i]} ${atomic[i + 1]}`);
101
+ }
102
+ return out;
103
+ }
104
+ /**
105
+ * 单主题截断:基于 scoreUrlRow + 字面 token Jaccard 判定 query 是否有「精确单主题」命中。
106
+ *
107
+ * 设计(V3 跨语言对称版,V2 基础上扩展 EN 路径):
108
+ * - scoreUrlRow 是「URL row 跟 query 的纯净相关度」(不被 E7 boost 稀释),
109
+ * 500+ 代表覆盖率 + 有效信号占比都达标的语义对齐强信号。
110
+ * - **但 scoreUrlRow 数值相近(615-700)时无法区分「同主题相关 row」和「泛命中 row」**。
111
+ * 例:query「单聊消息发送前回调」+ types=['webhook']:
112
+ * - top1=单聊消息发送之前回调 (scoreUrlRow=700) ← 真正答案
113
+ * - top2=单聊消息发送之后回调 (scoreUrlRow=672) ← 时态近义,保留
114
+ * - top3-6=单聊消息撤回/扩展/编辑/已读 (scoreUrlRow=615) ← 不同动作,应砍
115
+ * - top7-8=群消息发送之前/之后 (scoreUrlRow=643/615) ← 动作相同时态差异,保留"前"砍"后"
116
+ * 同样例 EN 端: query "One-to-one message send before webhook" 返回 5 个 webhook 切片
117
+ * (before/after C2C、before/after group、group exception),其中 group 类 3 个应砍。
118
+ * - 解决: 加 **token Jaccard** 作为「是否跟 top1 同主题」的二次过滤——
119
+ * 只保留跟 top1 字面 Jaccard ≥ 0.5 的 row(共享核心动作词/时态词)。
120
+ * - CJK: 2-gram 字符滑动(与 V2 bit-identical,zh 端 0 退化)
121
+ * - EN: 原子 token(含连字符复合词 one-to-one 整体保留)+ 原子间 word 2-gram,
122
+ * 跟 CJK 2-gram 同构
123
+ * - 多主题(保留 ≥ 2 个 row)/ 弱信号(top1 < 500)/ 不足 2 个候选 → 保持原 limit。
124
+ *
125
+ * 关键不变量:
126
+ * - 不动 top1(避免误判导致用户拿不到正确答案)
127
+ * - 不减少「真的有第二个相关 row」的场景(如「群消息发送前/之后」同主题)
128
+ * - 截断发生在 narrative 模式之外(narrative 模式本身按 full document 走,不受影响)
129
+ */
130
+ const SINGLE_TOPIC_URL_SCORE_THRESHOLD = 500;
131
+ const SINGLE_TOPIC_JACCARD_THRESHOLD = 0.5;
132
+ function applySingleTopicTrim(fragments, prompt, limit) {
133
+ if (fragments.length < 2)
134
+ return fragments;
135
+ // top1 必须有强 scoreUrlRow 信号(避免弱信号 top1 误判导致砍掉正确答案)
136
+ const top1UrlScore = scoreUrlRow(prompt, fragments[0].title).score;
137
+ if (top1UrlScore < SINGLE_TOPIC_URL_SCORE_THRESHOLD)
138
+ return fragments;
139
+ // 用 top1 标题的 token 集合作为「主题特征」,过滤出字面同主题的 row。
140
+ // CJK 标题走 2-gram 字符路径(与 V2 等价,zh 端 0 退化),
141
+ // EN 标题走原子 token + word 2-gram 路径(与 CJK 2-gram 同构,en 端获得对等的单主题判定)。
142
+ const top1Tokens = extractTitleTokens(fragments[0].title);
143
+ if (top1Tokens.size < 2)
144
+ return fragments; // 标题太短或纯 stopword,不做主题判定
145
+ const related = fragments.filter((f) => {
146
+ const fTokens = extractTitleTokens(f.title);
147
+ if (fTokens.size === 0)
148
+ return false;
149
+ const intersection = [...fTokens].filter((t) => top1Tokens.has(t)).length;
150
+ const union = new Set([...fTokens, ...top1Tokens]).size;
151
+ return intersection / union >= SINGLE_TOPIC_JACCARD_THRESHOLD;
152
+ });
153
+ // 过滤后只剩 1 个 → 单主题精确返回 [top1]
154
+ if (related.length === 1)
155
+ return [related[0]];
156
+ // 多个同主题 row(topN)→ 保留全部(受 limit 约束)
157
+ return related.slice(0, limit);
158
+ }
66
159
  /** 轻量覆盖度:仅算主文件完整度,用于非收敛场景的 allow_pending_confirm 判定,不产出逐文件明细 */
67
160
  function computePrimaryCompleteness(allChunks, fragments, preferredSource) {
68
161
  if (!fragments.length)
@@ -85,7 +178,7 @@ export function searchKnowledge(params) {
85
178
  const norm = normalizeParams(params);
86
179
  const retrievalMode = classifyRetrievalMode(norm.prompt);
87
180
  if (requiresFramework(norm)) {
88
- const manifest = loadManifest();
181
+ loadManifest();
89
182
  return {
90
183
  status: 'needs_framework',
91
184
  confidence: 'none',
@@ -95,9 +188,7 @@ export function searchKnowledge(params) {
95
188
  documents: [],
96
189
  catalog: [],
97
190
  meta: {
98
- pool_size: manifest.chunks.length,
99
- filtered_size: 0,
100
- exact_hits: [],
191
+ answer_contract: buildAnswerContract('needs_framework', 'none', retrievalMode, { fabricationRisk: false }),
101
192
  },
102
193
  fragments: [],
103
194
  };
@@ -115,9 +206,26 @@ export function searchKnowledge(params) {
115
206
  // 典型场景:Agent 传 types=['sdk'],filterPool 把 best-practice(type=faq)硬过滤掉,
116
207
  // 只剩 URL 映射行(有 E7 boost 但内容只是一个 URL 链接)。
117
208
  // 不侵入 runSearchPass 层,避免每次调用都执行重复检索。
209
+ // 修复 B1 副作用:之前 `types: undefined` 会同时把 chat/preset/{flutter,android,...}/integration.md
210
+ // (types=['integration'])也放进 pool,integration 文档多 section 命中 generic 词会把 URL row 顶掉。
211
+ // 改为 `types: ['faq']` 后:
212
+ // - best-practice(types=['faq'])走 filterPool 的 bp-bypass 保留
213
+ // - chat/preset/.../integration.md(types=['integration'])被硬过滤
214
+ // - URL row(types 与原请求一致)继续保留
215
+ // 合并策略:append BP fragments 到原 pass(不去重 URL row),保留 URL row 在 top。
118
216
  const passRetryReasons = [];
119
217
  const hasLocalContent = pass.fragments.some((f) => !f.source.includes('/urls/'));
120
- if (!hasLocalContent && norm.product?.length && !norm.types?.includes('faq')) {
218
+ // 当主 pass 命中一个明确的 URL row(scoreUrlRow ≥ 300 强信号)时,**跳过 B1 补充**,
219
+ // 避免 BP 兜底把无关 BP 文档(message-unread-count、avchatroom-message-loss-diagnosis 等)塞满 fragments。
220
+ // 典型反例:query「im 内网代理」types=['product'],pass 命中「内网代理实现方案」(scoreUrlRow=460),
221
+ // 旧 B1 会触发 → 拉 8 个无关 BP → fragments 膨胀到 9 个。
222
+ // 进一步保护:当 top1 finalScore < 20(零命中场景,如「im 怎么发火箭」finalScore=16.8),
223
+ // 也不触发 B1——无意义 query 不该被 BP 兜底撑满。
224
+ const top1UrlScore = !hasLocalContent && pass.fragments[0]?.title
225
+ ? scoreUrlRow(norm.prompt, pass.fragments[0].title).score
226
+ : 0;
227
+ const top1FinalScore = pass.fragments[0]?.score ?? 0;
228
+ if (!hasLocalContent && top1UrlScore < 300 && top1FinalScore >= 20 && norm.product?.length && !norm.types?.includes('faq')) {
121
229
  const primaryProduct = norm.product[0];
122
230
  const bpCandidates = allChunks.filter((chunk) => {
123
231
  if (!/\/best-practice\//i.test(chunk.source))
@@ -138,13 +246,26 @@ export function searchKnowledge(params) {
138
246
  if ((bpRanked[0]?.score ?? 0) >= 6) {
139
247
  const bpPass = runSearchPass({
140
248
  allChunks,
141
- passNorm: { ...norm, types: undefined },
249
+ passNorm: { ...norm, types: ['faq'] },
142
250
  retrievalMode,
143
251
  queryApiTokens,
144
252
  });
145
253
  if (bpPass.fragments.length > 0) {
146
- pass = bpPass;
147
- passRetryReasons.push('best_practice_supplement');
254
+ // 合并:保留原 URL row 在前,追加 BP fragments 去重
255
+ const existingIds = new Set(pass.fragments.map((f) => f.chunk_id));
256
+ const newBpFragments = bpPass.fragments.filter((f) => !existingIds.has(f.chunk_id));
257
+ if (newBpFragments.length > 0) {
258
+ // 重新编号 rank:原 URL row 保持 1..N,BP 从 N+1 开始
259
+ const merged = [
260
+ ...pass.fragments,
261
+ ...newBpFragments.map((f, i) => ({ ...f, rank: pass.fragments.length + i + 1 })),
262
+ ];
263
+ pass = {
264
+ ...pass,
265
+ fragments: merged,
266
+ };
267
+ passRetryReasons.push('best_practice_supplement');
268
+ }
148
269
  }
149
270
  }
150
271
  }
@@ -296,21 +417,30 @@ export function searchKnowledge(params) {
296
417
  };
297
418
  }
298
419
  narrativeState = resolveNarrativeState(pass);
299
- const retryReason = retryReasons.length ? retryReasons.join(';') : undefined;
300
- const routeInconsistencyWarning = pass.fragments.length > 0
301
- ? pass.fragments.filter((f) => f.product === '*').length / pass.fragments.length > 0.3
302
- : false;
420
+ // P0 单主题截断:基于 scoreUrlRow(URL row 跟 query 的纯净相关度,不被 E7 boost 污染)
421
+ // + 2-gram 字面 Jaccard 判定 query 是否有「精确单主题」命中。
422
+ // 详见 applySingleTopicTrim 函数注释。
423
+ let passFragments = applySingleTopicTrim(pass.fragments, norm.prompt, norm.limit ?? 8);
424
+ // P1 低置信裁剪:0 命中 query(top1 finalScore < 20)的 fragments 是 BM25 凑出来的
425
+ // 无关内容(如「im 怎么发火箭」top1=audio-message-send finalScore=16.8),
426
+ // 直接裁剪到 1 个 top fragment,避免 BP 凑数撑爆。
427
+ // 适用:first pass 已经拉了 BP 文档但 top1 仍很弱的情况。
428
+ if (passFragments.length > 1 && (passFragments[0]?.score ?? 0) < 20) {
429
+ passFragments = [passFragments[0]];
430
+ }
431
+ if (passFragments !== pass.fragments) {
432
+ // 同步重建 references:V3 applySingleTopicTrim 砍 fragment 时不砍 references 会导致
433
+ // fragments/references 数量错位(实测:5→3 fragment 砍后 references 仍 5 个,LLM 困惑)。
434
+ // 用 trim 后的 fragments 重建 references,保持一致。
435
+ pass = { ...pass, fragments: passFragments, references: buildReferences(passFragments) };
436
+ }
303
437
  const { confidence, status } = assessConfidence(pass.fragments, pass.exactHits, pass.apiNameCoverage, queryApiTokens);
304
438
  const fabricationRisk = (queryApiTokens.length > 0 && pass.apiNameCoverage < 0.3)
305
439
  || (confidence === 'low' && pass.fragments.length < 2);
306
- // point 模式不再返回 documents:fullDocumentMode 下 fragments 已按 char_start 顺序返回完整章节,
307
- // 再回 documents 会造成同一篇内容重复外发。
440
+ // narrative 模式返回完整文档;point/discovery 通过 fragments 提供内容(已含正文)。
308
441
  const documents = retrievalMode === 'narrative'
309
442
  ? (narrativeState.document ? [narrativeState.document] : [])
310
443
  : [];
311
- const catalog = retrievalMode === 'discovery'
312
- ? buildDiscoveryCatalog(pass.rankedWithMultipliers, pass.poolWithContent, norm.limit)
313
- : [];
314
444
  let finalStatus = status;
315
445
  let finalConfidence = confidence;
316
446
  if (retrievalMode === 'narrative' && documents.length > 0) {
@@ -323,18 +453,16 @@ export function searchKnowledge(params) {
323
453
  finalConfidence = 'low';
324
454
  }
325
455
  }
326
- if (retrievalMode === 'discovery' && catalog.length > 0) {
456
+ if (retrievalMode === 'discovery' && pass.fragments.length > 0) {
327
457
  finalStatus = 'success';
328
458
  finalConfidence = finalConfidence === 'none' ? 'medium' : finalConfidence;
329
459
  }
330
- const topScore = pass.fragments[0]?.score;
331
- const secondScore = pass.fragments[1]?.score;
332
460
  const aggregatedFragments = pass.fragments
333
461
  .filter((fragment) => fragment.aggregated || fragment.citation?.includes('[aggregated]'))
334
462
  .map((fragment) => ({
335
463
  chunk_id: fragment.chunk_id,
336
464
  source: fragment.source,
337
- citation: fragment.citation ?? `[来源: ${fragment.chunk_id}][aggregated]`,
465
+ citation: fragment.citation ?? `[Source: ${fragment.chunk_id}][aggregated]`,
338
466
  }));
339
467
  const fullDocumentRequired = retrievalMode === 'narrative'
340
468
  && /完整的?\s*(接入|集成|教程|指南|流程)|完整\s*md|全文|原文|不要总结|含\s*(示例)?代码|全部步骤|完整步骤/i.test(norm.prompt);
@@ -358,7 +486,7 @@ export function searchKnowledge(params) {
358
486
  const narrativeConverged = retrievalMode !== 'narrative'
359
487
  || (documents.length > 0 && narrativeState.quality.isComplete);
360
488
  const convergenceReached = (retrievalMode === 'narrative' && narrativeConverged)
361
- || (retrievalMode === 'discovery' && catalog.length > 0)
489
+ || (retrievalMode === 'discovery' && pass.fragments.length > 0)
362
490
  || (finalStatus === 'success'
363
491
  && (finalConfidence === 'high' || finalConfidence === 'medium')
364
492
  && (!hasApiQueryTokens || pass.apiNameCoverage >= 0.5)
@@ -393,33 +521,23 @@ export function searchKnowledge(params) {
393
521
  message: buildAgentMessage(finalStatus, retrievalMode, pass.fragments, finalConfidence, pass.references, {
394
522
  fabricationRisk,
395
523
  hasDocuments: documents.length > 0,
396
- hasCatalog: catalog.length > 0,
397
524
  fullDocumentRequired,
398
525
  preferDocuments: pass.fullDocumentMode,
399
526
  hasAggregatedFragments: aggregatedFragments.length > 0,
400
527
  }),
528
+ // query 保留:l1-enrich 依赖 response.query.frameworks 做限领域回填(内部消费)
401
529
  query: params,
402
530
  retrieval_mode: retrievalMode,
403
531
  documents,
404
- catalog,
405
532
  meta: {
406
- pool_size: allChunks.length,
407
- filtered_size: pass.pool.length,
408
- exact_hits: pass.exactHits,
409
- ...(pass.urlMappingSources.length ? { url_mapping_sources: pass.urlMappingSources } : {}),
410
- ...(pass.urlMapRelaxed ? { url_map_relaxed: pass.urlMapRelaxed } : {}),
411
- ...(pass.urlMapExactHitCount > 0 ? { url_map_exact_hit_count: pass.urlMapExactHitCount } : {}),
533
+ // 参考文档(AI 输出「参考文档」小节依据):
412
534
  ...(pass.references.length ? { references: pass.references } : {}),
413
- ...(topScore !== undefined ? { top_score: Number(topScore.toFixed(2)) } : {}),
414
- ...(secondScore !== undefined ? { second_score: Number(secondScore.toFixed(2)) } : {}),
415
- ...(queryApiTokens.length ? { query_api_tokens: queryApiTokens } : {}),
416
- ...(pass.missingApiTokens.length ? { missing_api_tokens: pass.missingApiTokens } : {}),
417
- ...(retryReason ? { retry_reason: retryReason } : {}),
418
- ...(retrievalMode === 'narrative' && narrativeState.source ? { narrative_source: narrativeState.source } : {}),
419
- ...(pass.preferredSource ? { preferred_source: pass.preferredSource } : {}),
420
- ...(pass.fullDocumentMode ? { full_document_mode: pass.fullDocumentMode } : {}),
421
- coverage_report: coverageReport,
422
- ...(aggregatedFragments.length ? { aggregated_fragments: aggregatedFragments } : {}),
535
+ answer_contract: answerContract,
536
+ // 防幻觉信号(search-message 依赖):
537
+ fabrication_risk: fabricationRisk,
538
+ // 内部重试决策(供上游判断是否需重搜):
539
+ retry_advice: retryAdvice,
540
+ // narrative 模式的完整度报告(仅 narrative 模式返回,用于验收/判断完整指南是否完整)
423
541
  ...(retrievalMode === 'narrative'
424
542
  ? {
425
543
  narrative_completeness: {
@@ -431,11 +549,6 @@ export function searchKnowledge(params) {
431
549
  },
432
550
  }
433
551
  : {}),
434
- answer_contract: answerContract,
435
- api_name_coverage: pass.apiNameCoverage,
436
- fabrication_risk: fabricationRisk,
437
- route_inconsistency_warning: routeInconsistencyWarning,
438
- retry_advice: retryAdvice,
439
552
  },
440
553
  fragments: pass.fragments,
441
554
  };
@@ -1,32 +1,33 @@
1
1
  import { hasBestPracticeFragment, partitionFragments, } from './search-core.js';
2
2
  export function buildAnswerContract(status, confidence, retrievalMode, opts) {
3
- const uncertaintyRule = '不得出现"可能/大概/应该/推测"等猜测措辞。';
4
- const riskRule = opts?.fabricationRisk ? 'fabrication_risk=true:禁止补全未命中 API 细节。' : '';
3
+ const uncertaintyRule = 'Do not use speculative wording (e.g., "maybe/probably/should/it seems"). When the user asks for a specific detail (API/method/enum/event/error code) that is not present in the returned fragments, do not invent it; instead, explicitly mark it as "[not found in docs]" or "[to be confirmed]" in the answer so the gap is visible to the user.';
4
+ const riskRule = opts?.fabricationRisk ? 'fabrication_risk=true: do not fill unmatched API details. For every detail that cannot be grounded in the returned fragments, explicitly mark it as "[not found in docs]" or "[to be confirmed]"; the user-facing answer must surface every gap, not omit silently.' : '';
5
5
  const aggregatedRule = opts?.hasAggregatedFragments
6
- ? '若字段来自 aggregated fragment,引用处需显式标注"[补全]"或脚注,避免与直接命中片段混淆。'
6
+ ? 'If a field comes from an aggregated fragment, explicitly mark it as "[supplement]" or a footnote to avoid confusion with directly matched fragments.'
7
7
  : '';
8
8
  const modeRule = retrievalMode === 'narrative'
9
9
  ? (opts?.fullDocumentRequired
10
- ? 'narrative 模式(全文优先):必须按 documents[0].sections 顺序输出完整正文,保留示例代码块/命令与关键说明,不得压缩成目录或摘要。'
11
- : 'narrative 模式:优先按 documents.sections 的顺序组织答案,不要跳节。')
10
+ ? 'Narrative mode (full-document priority): output the complete body in documents[0].sections order, preserving sample code blocks/commands/key notes; do not compress into a summary.'
11
+ : 'Narrative mode: organize the answer in documents.sections order; do not skip sections.')
12
12
  : retrievalMode === 'discovery'
13
- ? 'discovery 模式:先给目录/清单,再给简短选择建议;不要伪造不存在的条目。'
14
- : 'point 模式:优先给直接结论,再引用命中片段补充关键细节。';
13
+ ? 'Discovery mode: list every candidate from the returned fragments with full identifying detail (name, purpose, platform scope); then give complete selection guidance with trade-offs; do not fabricate nonexistent items.'
14
+ : 'Point mode: synthesize a complete answer derived from the matched fragments, preserving relevant depth and source-faithful detail; do not artificially shorten, the reporting layer enforces any necessary truncation.';
15
15
  const structureRule = opts?.fullDocumentRequired
16
- ? '当用户明确要求"完整指南/完整 md/全文/含示例代码"时,正文必须以文档章节顺序完整展开;除必要的章节标题整理外,不得省略步骤与代码。'
17
- : '使用"问题导向"标题组织答案,优先复用用户提问原句:一、<问题1> 二、<问题2> 三、<问题3> …';
16
+ ? 'If the user explicitly asks for a "full guide/full markdown/full text/include sample code", expand the body in full section order; do not omit steps or code except minimal heading cleanup.'
17
+ : 'Use problem-oriented headings when the answer benefits from sections; otherwise write a continuous synthesized answer (no per-section length constraint).';
18
18
  const sectionRule = opts?.fullDocumentRequired
19
- ? '每个章节可包含完整步骤、命令与代码,不受"1-2句"限制。'
20
- : '每个小节先给直接结论,再补 1-2 句必要说明;避免模板化术语(如"依据/操作"字样)。';
19
+ ? 'Each section may contain complete steps/commands/code; no "1-2 sentence" limit applies.'
20
+ : 'For each section (or in continuous prose): synthesize a complete answer from the matched fragments, including problem analysis, code samples, citations, and examples as relevant. Preserve the source depth. Avoid template wording. The reporting layer enforces truncation, so do not artificially summarize.';
21
21
  return [
22
- '【输出模板(必须遵守)】',
22
+ '[Output contract — MUST follow]',
23
+ 'User-visible answer MUST be complete; never artificially shorten or summarize for brevity.',
23
24
  structureRule,
24
25
  sectionRule,
25
- '信息不足的延展细节可直接省略,不要为凑完整度强行补全。',
26
- '末尾保留"参考文档"小节,仅列文档标题或 URL;正文不展示检索过程、证据链、打分细节。',
27
- '用户可见正文禁止出现系统检索话术(如"命中片段/检索结果/置信度/分数/召回/重排")。',
28
- '用户可见正文禁止出现工具与链路决策话术(如"应调用/先调用/让我先搜索/属于咨询类问题/而非 codegen/search_trtc_knowledge/finalize_answer/submit_feedback")。',
29
- '硬约束:不得引入 fragments 未出现的 API 名、字段值、枚举项、事件名。',
26
+ 'If details are not covered by fragments, mark them as "[not found in docs]" or "[to be confirmed]" instead of inventing or silently omitting.',
27
+ 'Keep a final "References" section with only document titles or URLs; do not show retrieval process/evidence chain/scoring details in the body.',
28
+ 'User-visible content MUST NOT include retrieval-system wording (e.g., "matched fragments / retrieval result / confidence / score / recall / rerank").',
29
+ 'User-visible content MUST NOT include tool/routing decision wording (e.g., "should call / call first / let me search / consultation task / not codegen / search_trtc_knowledge / finalize_answer / submit_feedback").',
30
+ 'Hard constraint: do not introduce API names/field values/enums/events that are absent from fragments.',
30
31
  modeRule,
31
32
  uncertaintyRule,
32
33
  riskRule,
@@ -34,23 +35,23 @@ export function buildAnswerContract(status, confidence, retrievalMode, opts) {
34
35
  ].filter(Boolean).join(' ');
35
36
  }
36
37
  export function buildAgentMessage(status, retrievalMode, fragments, confidence, _references, opts) {
37
- const riskRule = opts?.fabricationRisk ? '当前检索存在幻觉风险(fabrication_risk=true):禁止补全未命中 API 细节。' : '';
38
- // 输出契约只在 meta.answer_contract 保留一份,避免 message 内嵌造成双份指令。
39
- const tail = `${riskRule} 输出模板契约见 meta.answer_contract,必须严格遵守。用户可见输出必须直接回答问题,禁止先叙述工具选择或检索计划。 Final answer drafted -> MUST call \`finalize_answer\` before replying,prompt、answer、queryID 都是必填参数,必须传入(queryID 即本工具返回的 queryID)。Explicit follow-up feedback -> call \`submit_feedback\`.`;
38
+ const riskRule = opts?.fabricationRisk ? 'Current retrieval has hallucination risk (fabrication_risk=true): do not fill unmatched API details.' : '';
39
+ // Keep the output contract in meta.answer_contract only, to avoid duplicated instruction blocks in message.
40
+ const tail = `${riskRule} Output contract is in meta.answer_contract and MUST be followed. User-visible output must answer the question directly; do not start with tool-selection or retrieval-plan narrative. Final answer drafted -> MUST call \`finalize_answer\` before replying, and \`prompt\`, \`answer\`, \`queryID\` are all required (queryID is from this tool output). Explicit follow-up feedback -> call \`submit_feedback\`.`;
40
41
  if (status === 'needs_framework') {
41
- return '【硬规则】问题依赖具体平台实现,但未指定 frameworks,全平台混检信息密度低且被禁止。MUST 先调用 `present_framework_choice`(传入 product),等用户选定 web/miniprogram/android/ios/flutter/react/vue/uni-app 等后再调用本工具,并 MUST 带上 **product + frameworks**。禁止传空 frameworks 硬搜。';
42
+ return '[Hard rule] The question depends on a concrete platform implementation, but frameworks are missing. Cross-platform mixed retrieval is disallowed due to low precision. MUST call `present_framework_choice` first (with product), then call this tool again after user selects web/miniprogram/android/ios/flutter/react/vue/uni-app etc, and MUST include both **product + frameworks**. Do not hard-search with empty frameworks.';
42
43
  }
43
44
  if (status === 'success' && retrievalMode === 'narrative' && opts?.hasDocuments) {
44
45
  if (opts?.fullDocumentRequired) {
45
- return `当前请求属于 narrative(完整指南)模式,且用户明确要求完整正文:必须按 documents[0].sections 顺序输出完整内容,保留示例代码与命令,不要摘要化;fragments 仅用于交叉校验。${tail}`;
46
+ return `This request is narrative (full guide) mode and user explicitly asks for full text: output complete content in documents[0].sections order, preserving sample code and commands; do not summarize. Fragments are for cross-checking only. ${tail}`;
46
47
  }
47
- return `当前请求属于 narrative(完整指南)模式:优先按 documents.sections 顺序组织答案,不要跳节;fragments 仅用于交叉校验。${tail}`;
48
+ return `This request is narrative (full guide) mode: prioritize documents.sections order and do not skip sections. Fragments are for cross-checking only. ${tail}`;
48
49
  }
49
- if (status === 'success' && retrievalMode === 'discovery' && opts?.hasCatalog) {
50
- return `当前请求属于 discovery(目录清单)模式:先输出候选项清单,再给简短选择建议;不要把单个片段当完整流程。${tail}`;
50
+ if (status === 'success' && retrievalMode === 'discovery' && fragments.length > 0) {
51
+ return `This request is discovery (catalog/list) mode: list every candidate from the returned fragments with full identifying detail, then give complete selection guidance; do not treat a single fragment as a full workflow, and do not fabricate nonexistent items. ${tail}`;
51
52
  }
52
53
  if (status === 'success' && retrievalMode === 'point' && opts?.preferDocuments) {
53
- return `当前命中已收敛到单一 best-practice 标准文档:fragments 已按原文章节顺序完整返回,请按章节顺序组织答案,保留示例代码与关键说明,不要压缩成摘要或跳节。${tail}`;
54
+ return `Current results have converged to a single best-practice document: fragments already contain the full document in original section order. Answer in that order, preserve code samples and key notes, and do not compress into a short summary. ${tail}`;
54
55
  }
55
56
  const { urlMapping, localDoc, urlMappingKinds } = partitionFragments(fragments);
56
57
  const hasUrlMapping = urlMapping.length > 0;
@@ -60,28 +61,28 @@ export function buildAgentMessage(status, retrievalMode, fragments, confidence,
60
61
  if (status === 'success' && fragments.length > 0) {
61
62
  const topFragment = fragments[0];
62
63
  if (topFragment?.capability_status === 'unsupported') {
63
- const scopeLabel = topFragment.scope ? `(${topFragment.scope})` : '';
64
- return `当前问题在该平台 / 形态下 **不支持**${scopeLabel}。回答 MUST:① 第一段先明确写"**不支持**";② 说明这是当前平台 Core 的能力边界,而不是缺少示例代码;③ 仅提供当前平台已支持的替代方案(如对应平台 preset 预制 UI);若当前知识库未给出明确替代形态,应直接说明暂无可执行替代路径;④ **禁止**输出当前平台 Core 的 API 调用步骤或集成步骤。${tail}`;
64
+ const scopeLabel = topFragment.scope ? ` (${topFragment.scope})` : '';
65
+ return `This capability is **NOT supported** on the current platform/form${scopeLabel}. Answer MUST: (1) clearly state "**not supported**" in the first paragraph; (2) explain this is a current platform Core capability boundary, not a missing sample-code issue; (3) provide only supported alternatives on this platform (e.g., corresponding preset UI); if no explicit alternative exists in knowledge, clearly state no executable alternative path is available; (4) DO NOT output Core API invocation steps or integration steps for this unsupported capability. ${tail}`;
65
66
  }
66
67
  if (hasUrlMapping && !hasLocalDoc) {
67
- return `当前可用信息主要是文档入口链接(${urlKindLabel})。请先匹配用户问题对应的目标 URL,抓取官网文档后再合成答案;禁止仅复述映射表或臆造 API/错误码细节。${tail}`;
68
+ return `Available information is mainly documentation-entry links (${urlKindLabel}). First map user question to target URL(s), fetch official docs, then synthesize the answer. Do not merely repeat URL mappings or invent API/error-code details. ${tail}`;
68
69
  }
69
70
  if (hasUrlMapping && hasLocalDoc) {
70
71
  if (hasBestPractice) {
71
- return `优先基于已命中的 best-practice 与本地片段直接作答;URL 映射仅作补充参考,不要再抓官网做二次求证。${tail}`;
72
+ return `Prioritize direct answering from matched best-practice and local fragments; use URL mapping only as supplemental reference. Do not fetch official docs again for secondary verification by default. ${tail}`;
72
73
  }
73
- return `本地文档片段可直接用于作答;若本地片段未覆盖关键细节,再从 URL 映射中提取目标 URL 抓取官网文档补齐。${tail}`;
74
+ return `Local documentation fragments are sufficient for direct answering. Only if key details are still missing, extract target URL from URL mapping and fetch official docs to fill gaps. ${tail}`;
74
75
  }
75
- return `请仅基于已返回 fragments 合成答案;不要并行 web_search 或额外抓取官网文档。${tail}`;
76
+ return `Synthesize the answer strictly from returned fragments; do not run parallel web_search or extra official-doc fetching. ${tail}`;
76
77
  }
77
78
  if (status === 'low_confidence') {
78
79
  if (hasUrlMapping) {
79
80
  if (hasBestPractice) {
80
- return `优先基于 best-practice 与本地片段给出可执行结论。${tail}`;
81
+ return `Prioritize actionable conclusions from best-practice and local fragments. ${tail}`;
81
82
  }
82
- return `请先从 URL 映射里提取目标 URL 并抓取官网文档补齐关键信息,再给出答案。${tail}`;
83
+ return `First extract target URL(s) from URL mapping and fetch official docs for key details, then answer. ${tail}`;
83
84
  }
84
- return `请基于已有 fragments 给出最可执行答案;可引导用户补充组件名/API 名/错误码后重新检索。不要默认并行 web_search。${tail}`;
85
+ return `Provide the most actionable answer from existing fragments; you may guide user to provide component/API/error-code specifics for another retrieval round. Do not run parallel web_search by default. ${tail}`;
85
86
  }
86
- return `知识库未命中有效片段。请向用户说明并确认 product/framework 是否准确;若仍无 platform 信息且问题依赖具体客户端,应先 \`present_framework_choice\`。${tail}`;
87
+ return `No effective fragment was retrieved from the knowledge base. Ask user to verify product/framework; if platform is still missing and the question depends on client-specific implementation, call \`present_framework_choice\` first. ${tail}`;
87
88
  }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * url-row-idf.ts
3
+ *
4
+ * 为 url-row 匹配提供 **IDF(逆文档频率)加权**,用于自动降权高频泛词
5
+ * (`消息`/`message`/`group` 等),而不依赖任何枚举词表。
6
+ *
7
+ * 语言无关:同时支持中文(CJK 切词)与英文(空格切词)。
8
+ * 产品无关:词频统计对当前语料中所有 `urls/` 行的标题构建,未来新增产品自动纳入。
9
+ *
10
+ * 懒加载 + 内存缓存:首次调用时从 manifest 索引统计,之后复用。
11
+ */
12
+ import fs from 'fs';
13
+ import path from 'path';
14
+ import { fileURLToPath } from 'url';
15
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
+ let cachedIdf = null;
17
+ let cachedManifestPath = '';
18
+ /** 判断 source 是否 url 映射行(产品无关:knowledge/<product>/urls/) */
19
+ function isUrlMappingSource(source) {
20
+ return /knowledge\/[a-z0-9_-]+\/urls\/.+\.md$/i.test(source.replace(/\\/g, '/'));
21
+ }
22
+ /**
23
+ * 中文连续段切为 2-gram 词片;英文按空白切词。
24
+ * 返回小写 token 列表(未去重,用于统计文档频率)。
25
+ */
26
+ function tokenizeTitle(title) {
27
+ const tokens = [];
28
+ const lower = title.toLowerCase();
29
+ // 英文词(>=2 字母)
30
+ for (const match of lower.matchAll(/[a-z][a-z0-9]{1,}/g)) {
31
+ tokens.push(match[0]);
32
+ }
33
+ // 中文连续段 → 2-gram(避免单字「消息」过于泛,同时保留「消息回应」这类相邻组合的贡献)
34
+ for (const seg of lower.match(/[\u4e00-\u9fff]{2,}/g) ?? []) {
35
+ for (let i = 0; i < seg.length - 1; i += 1) {
36
+ tokens.push(seg.slice(i, i + 2));
37
+ }
38
+ // 整段作为复合词也计入(比 2-gram 更有区分度)
39
+ tokens.push(seg);
40
+ }
41
+ return tokens;
42
+ }
43
+ function resolveManifestPath() {
44
+ return path.resolve(__dirname, '../../resource/index/manifest.json');
45
+ }
46
+ /** 构建 IDF 索引:统计所有 urls 行标题的文档频率 */
47
+ function buildIdfIndex() {
48
+ const manifestPath = resolveManifestPath();
49
+ const df = new Map();
50
+ const seenTitles = new Set();
51
+ let totalDocs = 0;
52
+ if (fs.existsSync(manifestPath)) {
53
+ try {
54
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
55
+ for (const chunk of manifest.chunks ?? []) {
56
+ if (!chunk.title)
57
+ continue;
58
+ if (!isUrlMappingSource(chunk.source))
59
+ continue;
60
+ if (seenTitles.has(chunk.title))
61
+ continue;
62
+ seenTitles.add(chunk.title);
63
+ totalDocs += 1;
64
+ const seen = new Set();
65
+ for (const token of tokenizeTitle(chunk.title)) {
66
+ if (seen.has(token))
67
+ continue;
68
+ seen.add(token);
69
+ df.set(token, (df.get(token) ?? 0) + 1);
70
+ }
71
+ }
72
+ }
73
+ catch {
74
+ // manifest 读取失败时退化为空索引(IDF 全视为高区分度)
75
+ }
76
+ }
77
+ return { df, totalDocs };
78
+ }
79
+ /**
80
+ * 计算单个 token 的 IDF 权重。
81
+ * - 未出现在统计中(稀有/新词)→ 高权重(IDF 高)
82
+ * - 高频泛词 → 低权重
83
+ * - totalDocs 为 0 时退化为 1(不降权)
84
+ */
85
+ export function idfWeight(token) {
86
+ if (!cachedIdf)
87
+ loadIdf();
88
+ const { df, totalDocs } = cachedIdf;
89
+ if (!totalDocs)
90
+ return 1;
91
+ const docFreq = df.get(token.toLowerCase()) ?? 0;
92
+ if (docFreq <= 0)
93
+ return 1;
94
+ // IDF = ln((N - df + 0.5) / (df + 0.5)) + 1,平滑处理避免极端
95
+ const idf = Math.log((totalDocs - docFreq + 0.5) / (docFreq + 0.5)) + 1;
96
+ return Math.max(idf, 0.2);
97
+ }
98
+ /** 加载(或复用)IDF 索引 */
99
+ export function loadIdf() {
100
+ const manifestPath = resolveManifestPath();
101
+ if (cachedIdf && cachedManifestPath === manifestPath)
102
+ return;
103
+ cachedIdf = buildIdfIndex();
104
+ cachedManifestPath = manifestPath;
105
+ }
106
+ /**
107
+ * 判断 2-gram 是否为「有效词」(在 urls 标题语料中出现过,df>0)。
108
+ * 跨词边界的 2-gram(如「送前」「前回」「端群」)在标题语料中 df=0(从未作为词出现),
109
+ * 属于 N-gram 滑窗产生的噪声,不应参与覆盖率分母。df>0 的才是真实语义词。
110
+ * 产品无关:对所有产品的 urls 标题统计,不依赖枚举词表。
111
+ */
112
+ export function isValidBigram(token) {
113
+ if (!cachedIdf)
114
+ loadIdf();
115
+ return (cachedIdf.df.get(token.toLowerCase()) ?? 0) > 0;
116
+ }
117
+ /** 供测试/热更使用 */
118
+ export function resetIdfCache() {
119
+ cachedIdf = null;
120
+ }