@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
@@ -1,5 +1,6 @@
1
1
  import { downgradeToProductType } from './product-scope-intent.js';
2
2
  import { normalizeQueryPrompt } from './query-normalizer.js';
3
+ import { hasExplicitFrameworkMatch, hasFrameworkFallbackMatch } from './framework-fallback.js';
3
4
  /**
4
5
  * MCP framework enum → index 中对应的 framework 值列表。
5
6
  * path-meta.ts 已通过 normalizeFrameworkDir 把目录名归一到标准 framework,
@@ -42,14 +43,6 @@ function resolveTypes(products, types, prompt, frameworks) {
42
43
  return undefined;
43
44
  const downgraded = downgradeToProductType(products, types, prompt, frameworks);
44
45
  const expanded = expandTypeAliases(downgraded);
45
- // Live 生命周期兼容:用户常传 types=['api'],但核心生命周期方法来自 core-sdk/live|voice/*/integration.md。
46
- // 这里补充 core/integration,避免被类型硬过滤提前排除。
47
- if (products?.includes('live') && expanded.includes('api') && LIVE_LIFECYCLE_PROMPT.test(prompt)) {
48
- if (!expanded.includes('core'))
49
- expanded.push('core');
50
- if (!expanded.includes('integration'))
51
- expanded.push('integration');
52
- }
53
46
  return expanded;
54
47
  }
55
48
  /** 单平台产品的隐式 framework 推断:避免对显然唯一的平台再发起询问 */
@@ -133,33 +126,23 @@ export function filterPool(chunks, norm) {
133
126
  // 收紧:framework=['*'] 仅在该 chunk 明确是跨平台 type(product/server_api/webhook)时放行;
134
127
  // 否则要求 chunk.framework 与请求 framework 有交集。
135
128
  const wildcardAllowed = isWildcardChunk && chunkIsCrossPlatform(chunk.types);
136
- const explicitMatch = norm.frameworks.some((fw) => chunk.framework.includes(fw));
137
- // CallKit 特例:core-sdk 仍维持 web 单套(纯 JS),允许 react/vue 查询命中 core-sdk/web。
138
- const callCoreWebFallback = Boolean(norm.product?.includes('call')
139
- && (norm.framework === 'react' || norm.framework === 'vue')
140
- && chunk.variant === 'core-sdk'
141
- && chunk.framework.includes('web'));
142
- // Live 特例:写写侧把 'Web' 平台归到 livekit/core-sdk/api-reference/{vue,web} 目录(写写节点名是 'Web'),
143
- // 本地目录重命名为 'vue' 后 framework 标签变为 'vue'。'web' 查询仍应能命中这些 chunk。
144
- const liveWebVueFallback = Boolean(norm.product?.includes('live')
145
- && norm.framework === 'web'
146
- && chunk.variant === 'core-sdk'
147
- && chunk.framework.includes('vue')
148
- && chunk.source.includes('/api-reference/'));
149
- // Room special-case: Conference Web docs are under roomkit/core-sdk/vue.
150
- const roomWebVueFallback = Boolean(norm.product?.includes('room')
151
- && norm.framework === 'web'
152
- && chunk.variant === 'core-sdk'
153
- && chunk.framework.includes('vue')
154
- && chunk.source.includes('knowledge/roomkit/core-sdk/vue/'));
155
- if (!explicitMatch && !wildcardAllowed && !callCoreWebFallback && !liveWebVueFallback && !roomWebVueFallback)
129
+ const explicitMatch = hasExplicitFrameworkMatch(chunk.framework, norm.frameworks);
130
+ const fallbackMatch = hasFrameworkFallbackMatch(chunk, {
131
+ product: norm.product,
132
+ framework: norm.framework,
133
+ frameworks: norm.frameworks,
134
+ });
135
+ if (!explicitMatch && !wildcardAllowed && !fallbackMatch)
156
136
  return false;
157
137
  }
158
138
  if (norm.scenario && chunk.scenario && chunk.scenario !== norm.scenario) {
159
139
  return false;
160
140
  }
141
+ // best-practice 文档对 types 过滤免疫:内容不依赖 types 分类,应始终进 pool 靠 BM25 自然排名。
142
+ // 典型场景:Agent 传 types=['api'] 时,types=['faq'] 的 best-practice 不应被过滤掉。
161
143
  if (norm.types?.length && !norm.types.some((t) => chunk.types.includes(t))) {
162
- return false;
144
+ if (!chunk.source.includes('/best-practice/'))
145
+ return false;
163
146
  }
164
147
  return true;
165
148
  });
@@ -0,0 +1,22 @@
1
+ export function hasExplicitFrameworkMatch(chunkFrameworks, requestFrameworks) {
2
+ if (!requestFrameworks?.length)
3
+ return false;
4
+ return requestFrameworks.some((fw) => chunkFrameworks.includes(fw));
5
+ }
6
+ export function hasFrameworkFallbackMatch(chunk, opts) {
7
+ const callCoreWebFallback = Boolean(opts.product?.includes('call')
8
+ && (opts.framework === 'react' || opts.framework === 'vue')
9
+ && chunk.variant === 'core-sdk'
10
+ && chunk.framework.includes('web'));
11
+ const liveWebVueFallback = Boolean(opts.product?.includes('live')
12
+ && opts.framework === 'web'
13
+ && chunk.variant === 'core-sdk'
14
+ && chunk.framework.includes('vue')
15
+ && chunk.source.includes('/api-reference/'));
16
+ const roomWebVueFallback = Boolean(opts.product?.includes('room')
17
+ && opts.framework === 'web'
18
+ && chunk.variant === 'core-sdk'
19
+ && chunk.framework.includes('vue')
20
+ && chunk.source.includes('knowledge/roomkit/core-sdk/vue/'));
21
+ return callCoreWebFallback || liveWebVueFallback || roomWebVueFallback;
22
+ }
@@ -0,0 +1,29 @@
1
+ /** 枚举/目录浏览意图 */
2
+ const DISCOVERY_INTENT_RE = /有哪些|支持哪些|列一下|列出来|清单|目录|可用|全部|全量|对比|怎么选|选哪个|哪些\s+(组件|功能|接口|能力)|which\s+one|compare|comparison|what\s+(is|are)\s+available|what\s+.*\b(components?|features?|apis?)\b|available\s+(components?|features?|apis?)|supported\s+(components?|features?|apis?)|list\s+(all|available)|catalog/i;
3
+ /** 显式完整文档意图(用户明确说"完整/从零/step by step") */
4
+ const NARRATIVE_INTENT_RE = /从头到尾|一步一步|完整指南|完整教程|完整流程|完整接入|完整集成|完整的\s*(接入|集成|教程|指南|流程)|从零|全流程|接入指南|集成指南|step\s*by(?:\s*step)?|end[-\s]?to[-\s]?end|full\s+(guide|tutorial|flow|integration)|complete\s+(guide|tutorial|integration)|quick\s*start|getting\s*started/i;
5
+ /** 强 point 信号:API token / 错误码 / 具体操作词 → 优先走 point。
6
+ * 注意 camelCase 检测必须区分大小写(不加 /i),否则 React/RoomKit 这类普通专有名词会被误判为 API。 */
7
+ const POINT_STRONG_SIGNAL_RE = /\b\d{5,7}\b|\bERR_[A-Z0-9_]+\b|\buse[A-Z]\w*\b|\b[A-Z][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*\b|\b(TRTC|TX)[A-Za-z0-9_]+\b|\bprops?\b|参数|事件|回调|error\s*code|错误码|怎么用|如何用|怎么实现|如何实现|怎么做|怎么发送|怎么获取|怎么监听|怎么接收|怎么调用|如何发送|如何获取|如何监听|如何调用|参数是什么/i;
8
+ /** 判断是否含 API token(驼峰/点号/错误码/数字),这是最强的 point 结构信号。
9
+ * camelCase 检测区分大小写,避免把 React/RoomKit 等专有名词误判为 API。 */
10
+ function hasApiToken(prompt) {
11
+ return /\b[a-z]+[A-Z][A-Za-z0-9_]*\b|\b[A-Z][A-Za-z0-9_]*\.[A-Za-z_]\w*\b|\b(TRTC|TX)[A-Za-z0-9_]+\b|\b\d{5,7}\b|\bERR_[A-Z0-9_]+\b/.test(prompt);
12
+ }
13
+ export function classifyRetrievalMode(prompt) {
14
+ // 优先级分层(结构化信号,不依赖句末问号):
15
+ // 1) narrative 优先(保守):用户明确要完整指南/从零/step by step,
16
+ // 即使带操作词(如"完整指南怎么做")也走 narrative,因为意图是完整文档。
17
+ if (NARRATIVE_INTENT_RE.test(prompt) && !hasApiToken(prompt)) {
18
+ return 'narrative';
19
+ }
20
+ // 2) API token 是最强 point 信号:query 出现具体 API/错误码/数字 → point
21
+ if (hasApiToken(prompt)) {
22
+ return 'point';
23
+ }
24
+ // 3) discovery:枚举词 + 无具体操作词("有哪些接口 怎么实现"这类混合问法 → 走 point)
25
+ if (DISCOVERY_INTENT_RE.test(prompt) && !POINT_STRONG_SIGNAL_RE.test(prompt)) {
26
+ return 'discovery';
27
+ }
28
+ return 'point';
29
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * l1-enrich.ts
3
+ *
4
+ * 当检索结果命中 chat/urls 的 L1 URL 映射行(内容只是官网链接,无本地正文)时,
5
+ * server 内部 fetch 目标文档并回填 markdown 正文,避免把"只有一行 URL"的结果交给 AI。
6
+ *
7
+ * 设计:
8
+ * - 复用 fetch-doc-content 的清洗能力(白名单 + gzip 解压 + md 转换 + 截断)。
9
+ * - 仅对"纯 URL 映射行"的 fragment 回填;已有本地正文的 fragment 不动。
10
+ * - 逐条 fetch,但并发受限 + 超时 + 缓存兜底,失败静默(保留原 URL 行)。
11
+ */
12
+ import { fetchDocContent } from '../../utils/fetch-doc-content.js';
13
+ import { buildReferences } from '../doc-url-resolver.js';
14
+ import { partitionFragments } from './search-core.js';
15
+ import { isUrlMappingSource } from './url-row-scorer.js';
16
+ /** 从 fragment 中取第一个可 fetch 的官方 URL */
17
+ function primaryDocUrl(fragment) {
18
+ const urls = fragment.doc_urls ?? [];
19
+ return urls[0];
20
+ }
21
+ /**
22
+ * 单条 fragment 回填:
23
+ * 拉取到正文后,content 替换为官网 markdown,source 改写为官网 URL。
24
+ * 这样已回填片段不再命中 `chat/urls/` 的 L1 判定,AI 会把它当本地正文(L0)直接作答,无需再 fetch。
25
+ */
26
+ async function enrichFragment(fragment, maxChars) {
27
+ const url = primaryDocUrl(fragment);
28
+ if (!url)
29
+ return null;
30
+ const result = await fetchDocContent(url, { maxChars });
31
+ if (!result)
32
+ return null;
33
+ return {
34
+ ...fragment,
35
+ content: result.markdown,
36
+ source: url,
37
+ };
38
+ }
39
+ /** 服务端 L1 领域类型:query 为客户端 SDK 意图时跳过,避免给 SDK 问题回填 REST/回调正文。
40
+ * 注意:'product' 不在集合中——产品概念/全局方案(如「内网代理实现方案」)跨平台适用,
41
+ * 客户端 query 命中 product 文档应正常 enrich(如 web SDK 用户搜「内网代理」应拿到正文)。
42
+ */
43
+ const SERVER_SIDE_TYPES = new Set(['server_api', 'webhook']);
44
+ /**
45
+ * 判定 L1 行是否属于「query 主领域」,用于限领域回填(避免 token 浪费在无关领域)。
46
+ * - query 有 frameworks(客户端 SDK 意图):只回填 sdk/api 等客户端行,跳过 server_api/webhook/product。
47
+ * - query 无 frameworks(服务端/跨域意图):只回填服务端行,跳过 sdk/api 客户端行。
48
+ * 语言无关、产品无关。
49
+ */
50
+ function isInQueryDomain(fragment, queryFrameworks) {
51
+ if (queryFrameworks?.length) {
52
+ // 客户端意图:跳过服务端领域行
53
+ if (fragment.types.some((t) => SERVER_SIDE_TYPES.has(t)))
54
+ return false;
55
+ return true;
56
+ }
57
+ // 无 frameworks:跨域/服务端意图,跳过纯客户端 sdk/api 行
58
+ if (fragment.types.includes('sdk') || fragment.types.includes('api'))
59
+ return false;
60
+ return true;
61
+ }
62
+ /**
63
+ * 全部主领域 L1 行回填成功后,重写 message:
64
+ * 原始 message 是在回填前由 searchKnowledge 生成的(可能提示"需抓官网文档"),
65
+ * 回填后 source 已指向官网正文,应改为"直接基于正文作答",避免 AI 被旧话术误导再去抓官网。
66
+ * 保留原 message 中 finalize_answer / submit_feedback 等必要工作流指令。
67
+ */
68
+ function buildEnrichedMessage(response) {
69
+ const original = response.message ?? '';
70
+ const tail = original.includes('Final answer drafted')
71
+ ? original.slice(original.indexOf('Final answer drafted'))
72
+ : '';
73
+ return `Official content has been fetched and provided below; answer directly. ${tail}`;
74
+ }
75
+ /**
76
+ * 对 searchKnowledge 的结果做 L1 内容回填(异步,不影响同步检索主流程)。
77
+ * 返回回填后的新 response 副本。
78
+ *
79
+ * 修复(与 V3 `applySingleTopicTrim` 同步改造):
80
+ * - 跨领域 L1 行 filter 提到早 return 之前,覆盖「无主领域 L1 行可 enrich」分支(A: 无 L1 / B: L1 全跨领域 / C: L1 全已回填)。
81
+ * 修复前 B case 早 return,跨领域 L1 行残留 + message 假 "fetch 提示",LLM 拿到 URL 表行 content 无法作答。
82
+ * - targets 用 origIndex 跟踪原数组位置,确保回填后 fragments/references 仍按原顺序、保持一致。
83
+ */
84
+ export async function enrichL1Fragments(response, opts) {
85
+ const maxChars = opts?.maxChars ?? 50000;
86
+ const concurrency = opts?.concurrency ?? 3;
87
+ const queryFrameworks = response.query?.frameworks;
88
+ const originalFragments = response.fragments ?? [];
89
+ // 公共 filter:跨领域 L1 行一律过滤(无论是否走 enrich 路径)
90
+ const afterFilter = originalFragments.filter((f) => {
91
+ if (!isUrlMappingSource(f.source))
92
+ return true;
93
+ // L1 行:已回填(source 已是官网 URL)或属于主领域 → 保留;否则过滤
94
+ return /^https?:\/\//i.test(f.source) || isInQueryDomain(f, queryFrameworks);
95
+ });
96
+ // 收集需要回填的 L1 fragment(仅纯 URL 映射行 + 属于 query 主领域 + 未回填)
97
+ // 关键:用 origIndex 跟踪原数组位置,enrich 后能正确回填,避免与公共 filter 后的 fragments 错位
98
+ const targets = [];
99
+ for (let i = 0; i < originalFragments.length; i += 1) {
100
+ const f = originalFragments[i];
101
+ if (isUrlMappingSource(f.source)
102
+ && !/^https?:\/\//i.test(f.source) // 未回填
103
+ && isInQueryDomain(f, queryFrameworks) // 属于主领域
104
+ ) {
105
+ targets.push({ f, origIndex: i });
106
+ }
107
+ }
108
+ // 无主领域 L1 行可 enrich(A: 无 L1 / B: L1 全跨领域 / C: L1 全已回填)
109
+ // 仍按公共 filter 后的 fragments 返回,并重建 references 保持 fragments/references 一致
110
+ if (targets.length === 0) {
111
+ const refs = afterFilter.length > 0 ? buildReferences(afterFilter) : [];
112
+ return {
113
+ ...response,
114
+ fragments: afterFilter,
115
+ meta: { ...response.meta, references: refs },
116
+ };
117
+ }
118
+ // 并发受限地逐条回填:仅成功拉取到正文的 fragment 才写入 Map
119
+ const enriched = new Map();
120
+ let cursor = 0;
121
+ const worker = async () => {
122
+ while (cursor < targets.length) {
123
+ const { f, origIndex } = targets[cursor];
124
+ cursor += 1;
125
+ const enrichedFragment = await enrichFragment(f, maxChars);
126
+ if (enrichedFragment)
127
+ enriched.set(origIndex, enrichedFragment);
128
+ }
129
+ };
130
+ await Promise.all(Array.from({ length: Math.min(concurrency, targets.length) }, () => worker()));
131
+ // 组装新 fragments:
132
+ // - 基于 originalFragments 替换已回填的 L1 行
133
+ // - 跨领域 L1 行再次过滤(保险,理论上已被 afterFilter 过滤)
134
+ const newFragments = originalFragments
135
+ .map((f, i) => enriched.get(i) ?? f)
136
+ .filter((f) => {
137
+ if (!isUrlMappingSource(f.source))
138
+ return true;
139
+ return /^https?:\/\//i.test(f.source) || isInQueryDomain(f, queryFrameworks);
140
+ });
141
+ // 基于回填后的新 fragments 重建 references,
142
+ // 使 meta.references 与 fragments 的 source 保持一致(回填后 source=官网 URL,不再指向 chat/urls 本地路径)。
143
+ const references = newFragments.length > 0 ? buildReferences(newFragments) : [];
144
+ // 仅当回填前「有 L1 行且无本地正文」(对应 buildAgentMessage 的"需抓官网"分支)且本次全部回填成功时,
145
+ // 重写 message 为「已提供官网正文」。用结构判断(而非正则匹配 message 文案),更稳、不随文案变化失效。
146
+ const { urlMapping, localDoc } = partitionFragments(originalFragments);
147
+ const originalNeedsFetch = urlMapping.length > 0 && localDoc.length === 0;
148
+ const allEnriched = originalNeedsFetch && enriched.size > 0 && enriched.size === targets.length;
149
+ const message = allEnriched ? buildEnrichedMessage(response) : response.message;
150
+ return {
151
+ ...response,
152
+ fragments: newFragments,
153
+ message,
154
+ meta: {
155
+ ...response.meta,
156
+ references,
157
+ },
158
+ };
159
+ }
@@ -5,6 +5,45 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
5
5
  const SCHEMA_ROOT = path.resolve(__dirname, '../../resource/_schema');
6
6
  const QUERY_NORMALIZATION_FILE = path.join(SCHEMA_ROOT, 'query-normalization.yaml');
7
7
  const MAX_APPEND_TOKENS = 6;
8
+ const BASE_QUERY_NORMALIZATION = {
9
+ literal: {
10
+ 登录: ['login', 'signin', 'authentication', 'auth'],
11
+ 鉴权: ['authentication', 'auth', 'usersig'],
12
+ 错误码: ['error', 'error code', 'code'],
13
+ 重复登录: ['repeat login', 'multi-device login', 'login policy'],
14
+ 单聊: ['one-to-one', 'c2c', 'single chat'],
15
+ 群聊: ['group chat', 'group message'],
16
+ 直播群: ['avchatroom'],
17
+ 直播间: ['avchatroom', 'live room'],
18
+ 丢消息: ['message loss', 'reliability', 'priority'],
19
+ 消息丢失: ['message loss', 'reliability', 'priority'],
20
+ 重要消息: ['priority', 'high priority', 'critical message'],
21
+ 用户: ['user', 'account'],
22
+ 消息: ['message', 'messages'],
23
+ userSig: ['usersig', 'auth', 'authentication'],
24
+ AVChatRoom: ['直播群'],
25
+ 'error code': ['错误码', '错误', 'code'],
26
+ 'repeat login': ['重复登录', '多端登录', 'login policy'],
27
+ 'one-to-one': ['单聊', 'c2c', 'single chat'],
28
+ },
29
+ word: {
30
+ login: ['登录', '鉴权', 'authentication', 'auth'],
31
+ auth: ['鉴权', '登录', 'authentication'],
32
+ authentication: ['鉴权', '登录', 'auth'],
33
+ usersig: ['userSig', '鉴权', '登录'],
34
+ error: ['错误', '错误码', 'error code'],
35
+ code: ['错误码', 'error code'],
36
+ message: ['消息', 'chat', 'conversation'],
37
+ messages: ['消息', 'chat', 'conversation'],
38
+ group: ['群组', '群聊', 'group chat'],
39
+ c2c: ['单聊', 'one-to-one', 'single chat'],
40
+ avchatroom: ['直播群'],
41
+ priority: ['重要消息', '优先级'],
42
+ reliability: ['消息可靠性'],
43
+ 'message loss': ['丢消息', '消息丢失'],
44
+ 'rate limit': ['限频', '刷屏'],
45
+ },
46
+ };
8
47
  let cachedConfig = null;
9
48
  function unquote(input) {
10
49
  return input.trim().replace(/^['"]|['"]$/g, '');
@@ -12,7 +51,10 @@ function unquote(input) {
12
51
  function loadQueryNormalizationConfig() {
13
52
  if (cachedConfig)
14
53
  return cachedConfig;
15
- const config = { literal: {}, word: {} };
54
+ const config = {
55
+ literal: { ...BASE_QUERY_NORMALIZATION.literal },
56
+ word: { ...BASE_QUERY_NORMALIZATION.word },
57
+ };
16
58
  if (!fs.existsSync(QUERY_NORMALIZATION_FILE)) {
17
59
  cachedConfig = config;
18
60
  return config;
@@ -46,16 +88,35 @@ function loadQueryNormalizationConfig() {
46
88
  function escapeRegExp(value) {
47
89
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
48
90
  }
91
+ function hasCjk(text) {
92
+ return /[\u4e00-\u9fff]/.test(text);
93
+ }
94
+ function hasWordMatch(prompt, promptLower, needle) {
95
+ const normalizedNeedle = needle.toLowerCase();
96
+ if (hasCjk(normalizedNeedle)) {
97
+ return prompt.includes(needle);
98
+ }
99
+ const pattern = new RegExp(`\\b${escapeRegExp(normalizedNeedle)}\\b`, 'i');
100
+ return pattern.test(promptLower);
101
+ }
49
102
  export function normalizeQueryPrompt(prompt) {
50
103
  const config = loadQueryNormalizationConfig();
51
104
  const terms = new Set(prompt.split(/\s+/).filter(Boolean));
52
105
  const promptLower = prompt.toLowerCase();
106
+ // 纯 EN 提问时跳过 CJK 注入:query expansion 的 CJK 翻译(群组/群聊/回调 等)
107
+ // 会进入 qCjk 触发「混合 query 只走 CJK 路径」分支,导致纯 EN title 行(CJK 路径
108
+ // 返回 0)失去 E7 boost,BM25 排名错误。典型回归:webhook 行「X webhook」全部
109
+ // 被「Before create group webhook」抢首位(+7 row top1 退化)。
110
+ // zh 端几乎都是 CJK 提问 → isPureEn=false → 行为完全不变。
111
+ const isPureEn = !hasCjk(prompt);
53
112
  let appendedCount = 0;
54
113
  const tryAppend = (tokens) => {
55
114
  for (const token of tokens) {
56
115
  if (appendedCount >= MAX_APPEND_TOKENS)
57
116
  return;
58
117
  if (!terms.has(token)) {
118
+ if (isPureEn && hasCjk(token))
119
+ continue;
59
120
  terms.add(token);
60
121
  appendedCount += 1;
61
122
  }
@@ -67,8 +128,7 @@ export function normalizeQueryPrompt(prompt) {
67
128
  tryAppend(expansion);
68
129
  }
69
130
  for (const [needle, expansion] of Object.entries(config.word)) {
70
- const pattern = new RegExp(`\\b${escapeRegExp(needle.toLowerCase())}\\b`, 'i');
71
- if (!pattern.test(promptLower))
131
+ if (!hasWordMatch(prompt, promptLower, needle))
72
132
  continue;
73
133
  tryAppend(expansion);
74
134
  }