@tencentcloud/sdk-mcp 1.6.8 → 1.7.0
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 +25 -0
- package/README.md +54 -35
- package/knowledge/build-index.js +28 -4
- package/knowledge/chunker.js +82 -14
- package/knowledge/chunking-policy.js +100 -0
- package/knowledge/frontmatter-meta.js +167 -0
- package/knowledge/path-meta.js +28 -6
- package/knowledge/search/exact-lookup.js +26 -123
- package/knowledge/search/filter-pool.js +12 -29
- package/knowledge/search/framework-fallback.js +22 -0
- package/knowledge/search/intent-router.js +13 -0
- package/knowledge/search/query-normalizer.js +56 -3
- package/knowledge/search/search-core.js +391 -0
- package/knowledge/search/search-knowledge.js +290 -361
- package/knowledge/search/search-message.js +87 -0
- package/knowledge/search/search-pass.js +428 -0
- package/knowledge/search/search-types.js +1 -0
- package/knowledge/search/url-row-scorer.js +258 -0
- package/package.json +1 -1
- package/resource/index/manifest.json +1 -1
- package/resource/integration/callkit/preset/ios/integration.md +1 -0
- package/resource/integration/chat/preset/flutter/integration.md +1 -0
- package/resource/integration/livekit/preset/live/vue/live-player-desk-integration.md +11 -9
- package/resource/integration/livekit/preset/live/vue/live-pusher-integration.md +11 -11
- package/resource/knowledge/callkit/best-practice/callkit-miniprogram/uniapp-shrink.md +64 -0
- package/resource/knowledge/callkit/best-practice/common/call-autoplay-blocked.md +81 -0
- package/resource/knowledge/callkit/best-practice/common/call-message-in-chat.md +84 -0
- package/resource/knowledge/callkit/best-practice/common/call-vs-calls.md +114 -0
- package/resource/knowledge/callkit/best-practice/common/callengine-call-flow.md +128 -0
- package/resource/knowledge/callkit/best-practice/common/callengine-log-level.md +72 -0
- package/resource/knowledge/callkit/best-practice/common/callkit-get-engine-instance.md +87 -0
- package/resource/knowledge/callkit/best-practice/common/callkit-get-roomid.md +83 -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 +103 -0
- package/resource/knowledge/callkit/preset/react/api.md +1 -150
- package/resource/knowledge/callkit/preset/vue/api.md +1 -38
- package/resource/knowledge/chat/best-practice/common/avchatroom-message-loss-diagnosis.md +71 -0
- package/resource/knowledge/chat/best-practice/common/group-conversation-cleanup-on-system-notice.md +67 -0
- package/resource/knowledge/chat/best-practice/common/image-message-url-lifecycle.md +63 -0
- package/resource/knowledge/chat/best-practice/common/login-repeat-2025-diagnosis.md +65 -0
- package/resource/knowledge/chat/best-practice/common/message-read-report-and-receipt.md +72 -0
- package/resource/knowledge/chat/best-practice/common/message-receive-event.md +70 -0
- package/resource/knowledge/chat/best-practice/common/message-risk-error-and-render.md +69 -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 +118 -0
- package/resource/knowledge/chat/best-practice/miniprogram/login-timeout-2801-diagnosis.md +67 -0
- package/resource/knowledge/chat/best-practice/miniprogram/media-upload-fail-diagnosis.md +68 -0
- package/resource/knowledge/chat/best-practice/web/audio-message-send.md +123 -0
- package/resource/knowledge/chat/preset/android/features/conversation-list.md +1 -0
- package/resource/knowledge/chat/preset/android/features/language.md +1 -0
- package/resource/knowledge/chat/preset/android/features/message-input.md +1 -0
- package/resource/knowledge/chat/preset/android/features/message-list.md +1 -0
- package/resource/knowledge/chat/preset/flutter/features/conversation-list.md +1 -0
- package/resource/knowledge/chat/preset/flutter/features/message-input.md +1 -0
- package/resource/knowledge/chat/preset/flutter/features/message-list.md +1 -0
- package/resource/knowledge/chat/preset/ios/features/conversation-list.md +1 -0
- package/resource/knowledge/chat/preset/ios/features/message-input.md +1 -0
- package/resource/knowledge/chat/preset/ios/features/message-list.md +1 -0
- package/resource/knowledge/chat/preset/react/features/conversation-preview.md +1 -0
- package/resource/knowledge/chat/urls/sdk/web.md +1 -1
- package/resource/knowledge/livekit/best-practice/android/voice-room-anchor-start.md +124 -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 +126 -0
- package/resource/knowledge/livekit/best-practice/ios/voice-room-anchor-start.md +130 -0
- package/resource/knowledge/push/config/react-native.md +1 -0
- package/resource/knowledge/push/config/unity.md +1 -0
- package/resource/knowledge/push/config/unreal-engine.md +1 -0
- package/resource/knowledge/push/integration/android.md +16 -16
- package/resource/knowledge/push/integration/flutter.md +3 -3
- package/resource/knowledge/push/integration/ios.md +3 -3
- package/resource/knowledge/push/integration/react-native.md +1 -1
- package/resource/knowledge/roomkit/best-practice/common/room-ghost-member.md +75 -0
- package/resource/knowledge/roomkit/best-practice/common/room-limit-and-multi.md +88 -0
- package/resource/knowledge/roomkit/best-practice/common/room-screen-share-preview.md +87 -0
- package/resource/knowledge/roomkit/best-practice/common/room-status-after-all-leave.md +73 -0
- package/resource/knowledge/roomkit/best-practice/vue/room-uikit-vs-atomicx.md +115 -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 +8 -3
- package/tools/common/finalize_answer.js +9 -1
- package/tools/common/search_trtc_knowledge.js +1 -1
- 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 +1 -1
- 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 -20
- 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 -469
- package/resource/knowledge/callkit/preset/vue/uikit-api-reference.md +0 -581
- package/resource/knowledge/callkit/preset/web/api.md +0 -79
- package/resource/knowledge/chat/best-practice/web/image-message-url-handling.md +0 -16
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { hasBestPracticeFragment, partitionFragments, } from './search-core.js';
|
|
2
|
+
export function buildAnswerContract(status, confidence, retrievalMode, opts) {
|
|
3
|
+
const uncertaintyRule = '不得出现"可能/大概/应该/推测"等猜测措辞。';
|
|
4
|
+
const riskRule = opts?.fabricationRisk ? 'fabrication_risk=true:禁止补全未命中 API 细节。' : '';
|
|
5
|
+
const aggregatedRule = opts?.hasAggregatedFragments
|
|
6
|
+
? '若字段来自 aggregated fragment,引用处需显式标注"[补全]"或脚注,避免与直接命中片段混淆。'
|
|
7
|
+
: '';
|
|
8
|
+
const modeRule = retrievalMode === 'narrative'
|
|
9
|
+
? (opts?.fullDocumentRequired
|
|
10
|
+
? 'narrative 模式(全文优先):必须按 documents[0].sections 顺序输出完整正文,保留示例代码块/命令与关键说明,不得压缩成目录或摘要。'
|
|
11
|
+
: 'narrative 模式:优先按 documents.sections 的顺序组织答案,不要跳节。')
|
|
12
|
+
: retrievalMode === 'discovery'
|
|
13
|
+
? 'discovery 模式:先给目录/清单,再给简短选择建议;不要伪造不存在的条目。'
|
|
14
|
+
: 'point 模式:优先给直接结论,再引用命中片段补充关键细节。';
|
|
15
|
+
const structureRule = opts?.fullDocumentRequired
|
|
16
|
+
? '当用户明确要求"完整指南/完整 md/全文/含示例代码"时,正文必须以文档章节顺序完整展开;除必要的章节标题整理外,不得省略步骤与代码。'
|
|
17
|
+
: '使用"问题导向"标题组织答案,优先复用用户提问原句:一、<问题1> 二、<问题2> 三、<问题3> …';
|
|
18
|
+
const sectionRule = opts?.fullDocumentRequired
|
|
19
|
+
? '每个章节可包含完整步骤、命令与代码,不受"1-2句"限制。'
|
|
20
|
+
: '每个小节先给直接结论,再补 1-2 句必要说明;避免模板化术语(如"依据/操作"字样)。';
|
|
21
|
+
return [
|
|
22
|
+
'【输出模板(必须遵守)】',
|
|
23
|
+
structureRule,
|
|
24
|
+
sectionRule,
|
|
25
|
+
'信息不足的延展细节可直接省略,不要为凑完整度强行补全。',
|
|
26
|
+
'末尾保留"参考文档"小节,仅列文档标题或 URL;正文不展示检索过程、证据链、打分细节。',
|
|
27
|
+
'用户可见正文禁止出现系统检索话术(如"命中片段/检索结果/置信度/分数/召回/重排")。',
|
|
28
|
+
'用户可见正文禁止出现工具与链路决策话术(如"应调用/先调用/让我先搜索/属于咨询类问题/而非 codegen/search_trtc_knowledge/finalize_answer/submit_feedback")。',
|
|
29
|
+
'硬约束:不得引入 fragments 未出现的 API 名、字段值、枚举项、事件名。',
|
|
30
|
+
modeRule,
|
|
31
|
+
uncertaintyRule,
|
|
32
|
+
riskRule,
|
|
33
|
+
aggregatedRule,
|
|
34
|
+
].filter(Boolean).join(' ');
|
|
35
|
+
}
|
|
36
|
+
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\`.`;
|
|
40
|
+
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
|
+
}
|
|
43
|
+
if (status === 'success' && retrievalMode === 'narrative' && opts?.hasDocuments) {
|
|
44
|
+
if (opts?.fullDocumentRequired) {
|
|
45
|
+
return `当前请求属于 narrative(完整指南)模式,且用户明确要求完整正文:必须按 documents[0].sections 顺序输出完整内容,保留示例代码与命令,不要摘要化;fragments 仅用于交叉校验。${tail}`;
|
|
46
|
+
}
|
|
47
|
+
return `当前请求属于 narrative(完整指南)模式:优先按 documents.sections 顺序组织答案,不要跳节;fragments 仅用于交叉校验。${tail}`;
|
|
48
|
+
}
|
|
49
|
+
if (status === 'success' && retrievalMode === 'discovery' && opts?.hasCatalog) {
|
|
50
|
+
return `当前请求属于 discovery(目录清单)模式:先输出候选项清单,再给简短选择建议;不要把单个片段当完整流程。${tail}`;
|
|
51
|
+
}
|
|
52
|
+
if (status === 'success' && retrievalMode === 'point' && opts?.preferDocuments) {
|
|
53
|
+
return `当前命中已收敛到单一 best-practice 标准文档:fragments 已按原文章节顺序完整返回,请按章节顺序组织答案,保留示例代码与关键说明,不要压缩成摘要或跳节。${tail}`;
|
|
54
|
+
}
|
|
55
|
+
const { urlMapping, localDoc, urlMappingKinds } = partitionFragments(fragments);
|
|
56
|
+
const hasUrlMapping = urlMapping.length > 0;
|
|
57
|
+
const hasLocalDoc = localDoc.length > 0;
|
|
58
|
+
const hasBestPractice = hasBestPracticeFragment(fragments);
|
|
59
|
+
const urlKindLabel = urlMappingKinds.join(' / ') || 'urls';
|
|
60
|
+
if (status === 'success' && fragments.length > 0) {
|
|
61
|
+
const topFragment = fragments[0];
|
|
62
|
+
if (topFragment?.capability_status === 'unsupported') {
|
|
63
|
+
const scopeLabel = topFragment.scope ? `(${topFragment.scope})` : '';
|
|
64
|
+
return `当前问题在该平台 / 形态下 **不支持**${scopeLabel}。回答 MUST:① 第一段先明确写"**不支持**";② 说明这是当前平台 Core 的能力边界,而不是缺少示例代码;③ 仅提供当前平台已支持的替代方案(如对应平台 preset 预制 UI);若当前知识库未给出明确替代形态,应直接说明暂无可执行替代路径;④ **禁止**输出当前平台 Core 的 API 调用步骤或集成步骤。${tail}`;
|
|
65
|
+
}
|
|
66
|
+
if (hasUrlMapping && !hasLocalDoc) {
|
|
67
|
+
return `当前可用信息主要是文档入口链接(${urlKindLabel})。请先匹配用户问题对应的目标 URL,抓取官网文档后再合成答案;禁止仅复述映射表或臆造 API/错误码细节。${tail}`;
|
|
68
|
+
}
|
|
69
|
+
if (hasUrlMapping && hasLocalDoc) {
|
|
70
|
+
if (hasBestPractice) {
|
|
71
|
+
return `优先基于已命中的 best-practice 与本地片段直接作答;URL 映射仅作补充参考,不要再抓官网做二次求证。${tail}`;
|
|
72
|
+
}
|
|
73
|
+
return `本地文档片段可直接用于作答;若本地片段未覆盖关键细节,再从 URL 映射中提取目标 URL 抓取官网文档补齐。${tail}`;
|
|
74
|
+
}
|
|
75
|
+
return `请仅基于已返回 fragments 合成答案;不要并行 web_search 或额外抓取官网文档。${tail}`;
|
|
76
|
+
}
|
|
77
|
+
if (status === 'low_confidence') {
|
|
78
|
+
if (hasUrlMapping) {
|
|
79
|
+
if (hasBestPractice) {
|
|
80
|
+
return `优先基于 best-practice 与本地片段给出可执行结论。${tail}`;
|
|
81
|
+
}
|
|
82
|
+
return `请先从 URL 映射里提取目标 URL 并抓取官网文档补齐关键信息,再给出答案。${tail}`;
|
|
83
|
+
}
|
|
84
|
+
return `请基于已有 fragments 给出最可执行答案;可引导用户补充组件名/API 名/错误码后重新检索。不要默认并行 web_search。${tail}`;
|
|
85
|
+
}
|
|
86
|
+
return `知识库未命中有效片段。请向用户说明并确认 product/framework 是否准确;若仍无 platform 信息且问题依赖具体客户端,应先 \`present_framework_choice\`。${tail}`;
|
|
87
|
+
}
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import { attachPoolContent } from '../content-loader.js';
|
|
2
|
+
import { buildReferences } from '../doc-url-resolver.js';
|
|
3
|
+
import { rankSubsetWithIndex } from '../bm25.js';
|
|
4
|
+
import { getProductsBM25Index } from '../bm25-cache.js';
|
|
5
|
+
import { filterPool } from './filter-pool.js';
|
|
6
|
+
import { runExactLookup } from './exact-lookup.js';
|
|
7
|
+
import { applyRankMultipliers, computeApiNameCoverage, computeMissingApiTokens, isUrlMappingSource, postProcess, } from './search-core.js';
|
|
8
|
+
function isBestPracticeSource(source) {
|
|
9
|
+
return /\/best-practice\//i.test(source);
|
|
10
|
+
}
|
|
11
|
+
function normalizeTopicText(text) {
|
|
12
|
+
return text
|
|
13
|
+
.toLowerCase()
|
|
14
|
+
.replace(/[(())【】\[\]{}《》"""'`]/g, '')
|
|
15
|
+
.replace(/[\s/\\,,。::;;|·\-—_]+/g, '');
|
|
16
|
+
}
|
|
17
|
+
const TOPIC_EN_NOISE_WORDS = new Set([
|
|
18
|
+
'a', 'an', 'the', 'and', 'or', 'to', 'of', 'in', 'on', 'for', 'with',
|
|
19
|
+
'is', 'are', 'was', 'were', 'be', 'how', 'what', 'why', 'when', 'where',
|
|
20
|
+
'chat', 'message', 'messages', 'api', 'sdk', 'doc', 'docs',
|
|
21
|
+
]);
|
|
22
|
+
function extractTopicTokens(text) {
|
|
23
|
+
const tokens = new Set();
|
|
24
|
+
for (const match of text.matchAll(/\b\d{2,}\b/g)) {
|
|
25
|
+
tokens.add(match[0]);
|
|
26
|
+
}
|
|
27
|
+
for (const match of text.matchAll(/\b[a-z][a-z0-9_]{1,}\b/gi)) {
|
|
28
|
+
const token = match[0].toLowerCase();
|
|
29
|
+
if (TOPIC_EN_NOISE_WORDS.has(token))
|
|
30
|
+
continue;
|
|
31
|
+
tokens.add(token);
|
|
32
|
+
}
|
|
33
|
+
const cjkSegments = text.match(/[\u4e00-\u9fff]+/g) ?? [];
|
|
34
|
+
for (const seg of cjkSegments) {
|
|
35
|
+
if (seg.length >= 2)
|
|
36
|
+
tokens.add(seg);
|
|
37
|
+
for (let i = 0; i < seg.length - 1; i += 1) {
|
|
38
|
+
tokens.add(seg.slice(i, i + 2));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return [...tokens].filter((token) => token.length >= 2);
|
|
42
|
+
}
|
|
43
|
+
function countTopicTokenHits(tokens, normalizedTarget) {
|
|
44
|
+
if (!tokens.length || !normalizedTarget)
|
|
45
|
+
return 0;
|
|
46
|
+
let hits = 0;
|
|
47
|
+
for (const token of tokens) {
|
|
48
|
+
const normalizedToken = normalizeTopicText(token);
|
|
49
|
+
if (normalizedToken.length < 2)
|
|
50
|
+
continue;
|
|
51
|
+
if (normalizedTarget.includes(normalizedToken))
|
|
52
|
+
hits += 1;
|
|
53
|
+
}
|
|
54
|
+
return hits;
|
|
55
|
+
}
|
|
56
|
+
const TOPIC_GENERIC_GUARD_WORDS = new Set([
|
|
57
|
+
...TOPIC_EN_NOISE_WORDS,
|
|
58
|
+
'web', 'ios', 'android', 'flutter', 'react', 'vue',
|
|
59
|
+
'直播', '消息', '文档', '教程', '指南', '重要', '保证', '丢失', '聊天',
|
|
60
|
+
]);
|
|
61
|
+
/**
|
|
62
|
+
* 提取更强区分度的话题锚点(通用策略,不针对任何单 case):
|
|
63
|
+
* - 英文:长度 >= 4 或包含数字(如 AVChatRoom / trtc123)
|
|
64
|
+
* - 中文:连续 3 字以上片段(避免 2-gram 过宽)
|
|
65
|
+
*/
|
|
66
|
+
function extractStrongTopicAnchors(text) {
|
|
67
|
+
const anchors = new Set();
|
|
68
|
+
for (const match of text.matchAll(/\b[a-z][a-z0-9_]{3,}\b/gi)) {
|
|
69
|
+
const token = match[0].toLowerCase();
|
|
70
|
+
if (TOPIC_GENERIC_GUARD_WORDS.has(token))
|
|
71
|
+
continue;
|
|
72
|
+
anchors.add(token);
|
|
73
|
+
}
|
|
74
|
+
for (const segment of text.match(/[\u4e00-\u9fff]{3,}/g) ?? []) {
|
|
75
|
+
if (TOPIC_GENERIC_GUARD_WORDS.has(segment))
|
|
76
|
+
continue;
|
|
77
|
+
anchors.add(segment);
|
|
78
|
+
}
|
|
79
|
+
return [...anchors];
|
|
80
|
+
}
|
|
81
|
+
function extractPrimaryHeading(content) {
|
|
82
|
+
const heading = content.split('\n').find((line) => /^#\s+/.test(line.trim()));
|
|
83
|
+
return heading ? heading.replace(/^#\s+/, '').trim() : '';
|
|
84
|
+
}
|
|
85
|
+
function buildBestPracticeTopicText(chunk) {
|
|
86
|
+
if (!chunk)
|
|
87
|
+
return '';
|
|
88
|
+
return normalizeTopicText([
|
|
89
|
+
chunk.title,
|
|
90
|
+
...(chunk.key_phrases ?? []),
|
|
91
|
+
extractPrimaryHeading(chunk.content),
|
|
92
|
+
].filter(Boolean).join(' '));
|
|
93
|
+
}
|
|
94
|
+
const DIAGNOSIS_INTENT_RE = /丢消息|消息丢失|message\s*loss|消息可靠性|message\s*reliability|超时|timeout|失败|fail(?:ed|ure)?|排查|诊断|troubleshoot(?:ing)?/i;
|
|
95
|
+
const DIAGNOSIS_DOC_RE = /diagnosis|troubleshoot|排查|诊断|丢失|message\s*loss|reliability|可靠性|timeout|fail(?:ed|ure)?/i;
|
|
96
|
+
const TUTORIAL_DOC_RE = /audio[\s-]*message|voice[\s-]*message|语音消息|录音|recorder|create[a-z]+message|sendmessage|standard\s*(approach|usage)|标准用法|如何发送|how\s*to\s*send/i;
|
|
97
|
+
function isDiagnosisIntent(prompt) {
|
|
98
|
+
return DIAGNOSIS_INTENT_RE.test(prompt);
|
|
99
|
+
}
|
|
100
|
+
function buildChunkIntentText(chunk) {
|
|
101
|
+
return [
|
|
102
|
+
chunk.title,
|
|
103
|
+
...(chunk.key_phrases ?? []),
|
|
104
|
+
extractPrimaryHeading(chunk.content),
|
|
105
|
+
].filter(Boolean).join(' ');
|
|
106
|
+
}
|
|
107
|
+
function isDiagnosisStyleChunk(chunk) {
|
|
108
|
+
return DIAGNOSIS_DOC_RE.test(buildChunkIntentText(chunk));
|
|
109
|
+
}
|
|
110
|
+
function isTutorialStyleChunk(chunk) {
|
|
111
|
+
return TUTORIAL_DOC_RE.test(buildChunkIntentText(chunk));
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* 通用门控:诊断/排查类问法下,抑制教程型文档误抢首位。
|
|
115
|
+
* 仅对“教程型且非诊断型”文档施加轻度降权,不做任何单文档特化。
|
|
116
|
+
*/
|
|
117
|
+
function applyDiagnosisIntentGuard(ranked, chunksById, prompt) {
|
|
118
|
+
if (!isDiagnosisIntent(prompt) || ranked.length < 2)
|
|
119
|
+
return ranked;
|
|
120
|
+
const topChunk = chunksById.get(ranked[0].id);
|
|
121
|
+
const topIsTutorial = topChunk ? isTutorialStyleChunk(topChunk) && !isDiagnosisStyleChunk(topChunk) : false;
|
|
122
|
+
const penalty = topIsTutorial ? 90 : 60;
|
|
123
|
+
let adjusted = false;
|
|
124
|
+
const next = ranked.map((item) => {
|
|
125
|
+
const chunk = chunksById.get(item.id);
|
|
126
|
+
if (!chunk)
|
|
127
|
+
return item;
|
|
128
|
+
if (!isTutorialStyleChunk(chunk) || isDiagnosisStyleChunk(chunk))
|
|
129
|
+
return item;
|
|
130
|
+
adjusted = true;
|
|
131
|
+
return { ...item, score: item.score - penalty };
|
|
132
|
+
});
|
|
133
|
+
return adjusted ? next.sort((a, b) => b.score - a.score) : ranked;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* A3: 同 prompt 下 best-practice 优先(仅在分数接近时触发,避免召回退化)
|
|
137
|
+
* 触发条件:
|
|
138
|
+
* - 当前 Top1 不是 best-practice
|
|
139
|
+
* - 存在 best-practice 候选
|
|
140
|
+
* - 该候选分数与 Top1 差距不超过 18%
|
|
141
|
+
*/
|
|
142
|
+
function prioritizeBestPracticeWhenCompetitive(ranked, chunksById, prompt) {
|
|
143
|
+
if (ranked.length < 2)
|
|
144
|
+
return ranked;
|
|
145
|
+
const top = ranked[0];
|
|
146
|
+
const topChunk = chunksById.get(top.id);
|
|
147
|
+
if (!topChunk || isBestPracticeSource(topChunk.source) || top.score <= 0) {
|
|
148
|
+
return ranked;
|
|
149
|
+
}
|
|
150
|
+
// 收集所有 best-practice 候选,取分数最高者,避免"首个 best-practice 不一定最相关"的偏置。
|
|
151
|
+
let bestPracticeIdx = -1;
|
|
152
|
+
let bestPracticeScore = -Infinity;
|
|
153
|
+
for (let i = 1; i < ranked.length; i += 1) {
|
|
154
|
+
const chunk = chunksById.get(ranked[i].id);
|
|
155
|
+
if (!chunk)
|
|
156
|
+
continue;
|
|
157
|
+
if (!isBestPracticeSource(chunk.source))
|
|
158
|
+
continue;
|
|
159
|
+
if (ranked[i].score > bestPracticeScore) {
|
|
160
|
+
bestPracticeScore = ranked[i].score;
|
|
161
|
+
bestPracticeIdx = i;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (bestPracticeIdx < 0)
|
|
165
|
+
return ranked;
|
|
166
|
+
const candidate = ranked[bestPracticeIdx];
|
|
167
|
+
const gapRatio = (top.score - candidate.score) / top.score;
|
|
168
|
+
// 常规场景:仅在分数接近时提升(避免召回退化)
|
|
169
|
+
const normalCompetitive = gapRatio <= 0.35;
|
|
170
|
+
// URL 映射/FAQ 抢首位场景:只要 best-practice 达到基础相关阈值,也应优先返回可执行内容
|
|
171
|
+
// 这样可避免"只有 URL 映射行或 FAQ 简答,没有本地内容"的体验退化。
|
|
172
|
+
// chat/urls 的 URL 行:需额外校验 BP 和 URL 行是否同一话题,
|
|
173
|
+
// 避免"问地理位置消息,URL 行是地理位置,但 BP 是语音消息"的跨话题 shadow。
|
|
174
|
+
const topIsUrlMapping = Boolean(isUrlMappingSource(topChunk.source));
|
|
175
|
+
const topIsChatUrlRow = topIsUrlMapping && topChunk.source.includes('knowledge/chat/urls/');
|
|
176
|
+
const topIsFaq = topChunk.source.includes('/faq/');
|
|
177
|
+
// chat/urls 话题一致性:收紧为“高精度 + 多信号”
|
|
178
|
+
// 1) prompt 中数字/错误码(高区分信号)
|
|
179
|
+
// 2) prompt 切词与 BP 主题 token 命中
|
|
180
|
+
// 3) URL 行标题切词与 BP 主题 token 命中(跨语言兜底)
|
|
181
|
+
// 4) 通用强锚点(长 token / CJK>=3)需与 BP 主题有交集(若 prompt 存在强锚点)
|
|
182
|
+
let chatUrlRowTopicMatch = true;
|
|
183
|
+
let chatUrlTopicSignalCount = 0;
|
|
184
|
+
if (topIsChatUrlRow) {
|
|
185
|
+
const bpChunk = chunksById.get(candidate.id);
|
|
186
|
+
const bpTopicText = buildBestPracticeTopicText(bpChunk);
|
|
187
|
+
const promptDigits = prompt.match(/\b\d{2,}\b/g) ?? [];
|
|
188
|
+
const digitMatch = promptDigits.some((d) => bpTopicText.includes(d));
|
|
189
|
+
const promptTokens = extractTopicTokens(prompt);
|
|
190
|
+
const promptTokenHitCount = countTopicTokenHits(promptTokens, bpTopicText);
|
|
191
|
+
const promptTokenMatch = promptTokenHitCount >= 1;
|
|
192
|
+
const urlTopicRaw = (topChunk.title ?? '').replace(/[((].*$/, '').trim();
|
|
193
|
+
const urlTopicTokens = extractTopicTokens(urlTopicRaw);
|
|
194
|
+
const urlTitleHitCount = countTopicTokenHits(urlTopicTokens, bpTopicText);
|
|
195
|
+
const urlTitleMatch = urlTitleHitCount >= 1;
|
|
196
|
+
const strongAnchors = extractStrongTopicAnchors(prompt);
|
|
197
|
+
const strongAnchorHitCount = countTopicTokenHits(strongAnchors, bpTopicText);
|
|
198
|
+
const strongAnchorMatch = strongAnchors.length === 0 || strongAnchorHitCount >= 1;
|
|
199
|
+
chatUrlTopicSignalCount = Number(digitMatch) + Number(promptTokenMatch) + Number(urlTitleMatch);
|
|
200
|
+
const hasHighPrecisionSignal = digitMatch || urlTitleMatch;
|
|
201
|
+
chatUrlRowTopicMatch = hasHighPrecisionSignal
|
|
202
|
+
&& chatUrlTopicSignalCount >= 2
|
|
203
|
+
&& strongAnchorMatch;
|
|
204
|
+
}
|
|
205
|
+
const bestPracticeStrongEnough = candidate.score >= Math.max(12, top.score * 0.05);
|
|
206
|
+
const urlRatioThreshold = topIsChatUrlRow ? 0.045 : 0.05;
|
|
207
|
+
const urlAbsoluteThreshold = topIsChatUrlRow ? 20 : 16;
|
|
208
|
+
const urlStrongEnough = candidate.score >= Math.max(urlAbsoluteThreshold, top.score * urlRatioThreshold);
|
|
209
|
+
const urlMappingShadowing = topIsUrlMapping
|
|
210
|
+
&& urlStrongEnough
|
|
211
|
+
&& chatUrlRowTopicMatch;
|
|
212
|
+
const faqShadowing = topIsFaq && bestPracticeStrongEnough;
|
|
213
|
+
// Feature/API 文档抢首位场景:top1 是功能文档(非 integration、非 BP),
|
|
214
|
+
// BP 的精准实践应优先于功能文档的泛匹配。
|
|
215
|
+
// 条件更严格:BP 分数需达到 top 的 40%(避免完全不相关的 BP 被误提)。
|
|
216
|
+
const topIsFeatureDoc = !topIsUrlMapping && !topIsFaq
|
|
217
|
+
&& !isBestPracticeSource(topChunk.source)
|
|
218
|
+
&& !isIntegrationSource(topChunk.source);
|
|
219
|
+
const featureDocShadowing = topIsFeatureDoc && candidate.score >= top.score * 0.4;
|
|
220
|
+
if (!normalCompetitive && !urlMappingShadowing && !faqShadowing && !featureDocShadowing)
|
|
221
|
+
return ranked;
|
|
222
|
+
const next = [...ranked];
|
|
223
|
+
next[bestPracticeIdx] = { ...candidate, score: top.score + 2 };
|
|
224
|
+
return next.sort((a, b) => b.score - a.score);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* P0-R1/P0-R2:best-practice 单 chunk 直通。
|
|
228
|
+
* 在高置信且无强竞争时,直接锁定 preferredSource + fullDocumentMode。
|
|
229
|
+
* 当 promote 已发生(faqShadowing 触发后 best-practice 排到首位),
|
|
230
|
+
* 即使分数未达 80 也信任 promote 结果执行收敛。
|
|
231
|
+
*/
|
|
232
|
+
function pickDominantBestPracticeSource(ranked, chunksById) {
|
|
233
|
+
const top = ranked[0];
|
|
234
|
+
if (!top || top.score <= 0)
|
|
235
|
+
return { fullDocumentMode: false };
|
|
236
|
+
const topChunk = chunksById.get(top.id);
|
|
237
|
+
if (!topChunk || !isBestPracticeSource(topChunk.source)) {
|
|
238
|
+
return { fullDocumentMode: false };
|
|
239
|
+
}
|
|
240
|
+
const source = topChunk.source;
|
|
241
|
+
const topWindow = ranked.slice(0, 12);
|
|
242
|
+
// URL 映射行和 FAQ 语料是导航指针/概览而非内容竞争者:
|
|
243
|
+
// 若计入两者竞争,1.2 倍 dominance 门槛在 best-practice 被命中时永远无法达成。
|
|
244
|
+
const isCrossCompetitor = (item) => {
|
|
245
|
+
const chunk = chunksById.get(item.id);
|
|
246
|
+
if (!chunk || chunk.source === source)
|
|
247
|
+
return false;
|
|
248
|
+
if (isUrlMappingSource(chunk.source))
|
|
249
|
+
return false;
|
|
250
|
+
if (chunk.source.includes('/faq/'))
|
|
251
|
+
return false;
|
|
252
|
+
return true;
|
|
253
|
+
};
|
|
254
|
+
const secondCrossSource = ranked.find((item) => isCrossCompetitor(item));
|
|
255
|
+
const secondCrossScore = secondCrossSource?.score ?? 0;
|
|
256
|
+
const strongCrossSourceCount = topWindow
|
|
257
|
+
.filter((item) => isCrossCompetitor(item) && item.score >= top.score * 0.92)
|
|
258
|
+
.length;
|
|
259
|
+
const sourceSections = [...chunksById.values()].filter((chunk) => chunk.source === source);
|
|
260
|
+
const sourceTotalChars = sourceSections.reduce((sum, chunk) => sum + chunk.content.length, 0);
|
|
261
|
+
const isPromotedResult = top.score >= 20 && ranked.length >= 2 && (top.score - ranked[1].score) >= 0.05;
|
|
262
|
+
const scoreStrongEnough = top.score >= 80 || isPromotedResult;
|
|
263
|
+
const dominanceStrongEnough = secondCrossScore === 0 || (top.score / secondCrossScore) >= 1.2 || isPromotedResult;
|
|
264
|
+
const crossSourceNotCompeting = strongCrossSourceCount === 0 || isPromotedResult;
|
|
265
|
+
const withinLengthBudget = sourceTotalChars <= 7000;
|
|
266
|
+
if (scoreStrongEnough && dominanceStrongEnough && crossSourceNotCompeting && withinLengthBudget) {
|
|
267
|
+
return {
|
|
268
|
+
preferredSource: source,
|
|
269
|
+
fullDocumentMode: true,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
return { fullDocumentMode: false };
|
|
273
|
+
}
|
|
274
|
+
function isIntegrationSource(source) {
|
|
275
|
+
return /\/integration\.md$/i.test(source)
|
|
276
|
+
|| /\/preset\/.+integration/i.test(source);
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* 覆盖度竞争:在 A3 之前执行。
|
|
280
|
+
* 当 integration 的多个 section 被查询广泛命中时,选择 integration 完整文档;
|
|
281
|
+
* 否则返回 null,交由 A3 + BP 收敛处理。
|
|
282
|
+
*
|
|
283
|
+
* 规则:
|
|
284
|
+
* - 按 source 聚合 integration 的 section 命中数,选命中数最多的 source
|
|
285
|
+
* - ≥3 个 section 命中 + integration top 分数 ≥ BP top × 0.6 → 选 integration
|
|
286
|
+
* - BP 分数远高于 integration(>1.2×)→ 直接返回 null
|
|
287
|
+
* - 否则 → 返回 null
|
|
288
|
+
*/
|
|
289
|
+
function resolveContentPriority(ranked, chunksById) {
|
|
290
|
+
if (ranked.length < 2)
|
|
291
|
+
return null;
|
|
292
|
+
// 找 BP 最高分(在 top-30 范围内)
|
|
293
|
+
let bpTopScore = -1;
|
|
294
|
+
for (let i = 0; i < Math.min(ranked.length, 30); i++) {
|
|
295
|
+
const chunk = chunksById.get(ranked[i].id);
|
|
296
|
+
if (!chunk || !isBestPracticeSource(chunk.source))
|
|
297
|
+
continue;
|
|
298
|
+
if (ranked[i].score > bpTopScore) {
|
|
299
|
+
bpTopScore = ranked[i].score;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// 按 source 聚合 integration 的 section 命中(在全部 ranked 范围内,但有 minScore 门槛)
|
|
303
|
+
const minScore = bpTopScore > 0 ? Math.max(bpTopScore * 0.3, 5) : 5;
|
|
304
|
+
const intSourceStats = new Map();
|
|
305
|
+
for (const item of ranked) {
|
|
306
|
+
const chunk = chunksById.get(item.id);
|
|
307
|
+
if (!chunk || !isIntegrationSource(chunk.source))
|
|
308
|
+
continue;
|
|
309
|
+
if (item.score < minScore)
|
|
310
|
+
continue;
|
|
311
|
+
const prev = intSourceStats.get(chunk.source);
|
|
312
|
+
if (!prev) {
|
|
313
|
+
intSourceStats.set(chunk.source, { hitCount: 1, topScore: item.score });
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
intSourceStats.set(chunk.source, {
|
|
317
|
+
hitCount: prev.hitCount + 1,
|
|
318
|
+
topScore: Math.max(prev.topScore, item.score),
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (intSourceStats.size === 0)
|
|
323
|
+
return null;
|
|
324
|
+
// 选命中数最多的 integration source(同命中数则选分数最高的)
|
|
325
|
+
let bestIntSource = '';
|
|
326
|
+
let bestHitCount = 0;
|
|
327
|
+
let bestIntTopScore = 0;
|
|
328
|
+
for (const [source, stats] of intSourceStats) {
|
|
329
|
+
if (stats.hitCount > bestHitCount
|
|
330
|
+
|| (stats.hitCount === bestHitCount && stats.topScore > bestIntTopScore)) {
|
|
331
|
+
bestIntSource = source;
|
|
332
|
+
bestHitCount = stats.hitCount;
|
|
333
|
+
bestIntTopScore = stats.topScore;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
// 无 BP 候选 → 选 integration(如果有足够 section 命中)
|
|
337
|
+
if (bpTopScore < 0) {
|
|
338
|
+
return bestHitCount >= 2
|
|
339
|
+
? { preferredSource: bestIntSource, fullDocumentMode: true }
|
|
340
|
+
: null;
|
|
341
|
+
}
|
|
342
|
+
// BP 分数远高于 integration → 不走 integration 路径
|
|
343
|
+
if (bpTopScore > bestIntTopScore * 1.2)
|
|
344
|
+
return null;
|
|
345
|
+
// 覆盖度判定
|
|
346
|
+
const BREADTH_THRESHOLD = 3;
|
|
347
|
+
if (bestHitCount >= BREADTH_THRESHOLD && bestIntTopScore >= bpTopScore * 0.6) {
|
|
348
|
+
return { preferredSource: bestIntSource, fullDocumentMode: true };
|
|
349
|
+
}
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
export function runSearchPass(params) {
|
|
353
|
+
const { allChunks, passNorm, retrievalMode, queryApiTokens, opts, } = params;
|
|
354
|
+
const effectivePassNorm = retrievalMode === 'discovery'
|
|
355
|
+
? { ...passNorm, types: undefined }
|
|
356
|
+
: passNorm;
|
|
357
|
+
let pool = filterPool(allChunks, effectivePassNorm);
|
|
358
|
+
// types 误传自愈:首轮保留,二次扩检也同样保留该能力
|
|
359
|
+
if (pool.length === 0 && effectivePassNorm.types?.length) {
|
|
360
|
+
pool = filterPool(allChunks, { ...effectivePassNorm, types: undefined });
|
|
361
|
+
}
|
|
362
|
+
const { boosts, exactHits, urlRowHitChunkIds } = runExactLookup(effectivePassNorm.prompt, pool, effectivePassNorm);
|
|
363
|
+
// A2 补充正向路由:UserSig 问法下允许 share 命中且优先于无关 urls 噪声
|
|
364
|
+
if (/usersig|sdkappid|secretkey|鉴权|签名|gentestusersig/i.test(passNorm.prompt)) {
|
|
365
|
+
for (const chunk of pool) {
|
|
366
|
+
if (chunk.product === '*' && chunk.source.includes('knowledge/share/userSig.md')) {
|
|
367
|
+
boosts[chunk.id] = (boosts[chunk.id] ?? 0) + 220;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
// B2: API 覆盖不足扩检时,对 api/integration 语料做轻量重排提权
|
|
372
|
+
if (opts?.apiRetry) {
|
|
373
|
+
for (const chunk of pool) {
|
|
374
|
+
if (chunk.types.includes('api') || chunk.types.includes('integration')) {
|
|
375
|
+
boosts[chunk.id] = (boosts[chunk.id] ?? 0) + 40;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
const productIndex = getProductsBM25Index(allChunks, passNorm.product);
|
|
380
|
+
const poolWithContent = attachPoolContent(pool);
|
|
381
|
+
const chunksById = new Map(poolWithContent.map((chunk) => [chunk.id, chunk]));
|
|
382
|
+
const ranked = rankSubsetWithIndex(effectivePassNorm.prompt, pool.map((chunk) => chunk.id), productIndex, boosts);
|
|
383
|
+
const rankedWithMultipliers = applyRankMultipliers(ranked, chunksById, {
|
|
384
|
+
product: passNorm.product,
|
|
385
|
+
framework: passNorm.framework,
|
|
386
|
+
frameworks: passNorm.frameworks,
|
|
387
|
+
});
|
|
388
|
+
const rankedAfterIntentGuard = applyDiagnosisIntentGuard(rankedWithMultipliers, chunksById, passNorm.prompt);
|
|
389
|
+
// 覆盖度竞争先于 A3 执行
|
|
390
|
+
const integrationPriority = resolveContentPriority(rankedAfterIntentGuard, chunksById);
|
|
391
|
+
let dominantBestPractice;
|
|
392
|
+
let rankedAfterBestPractice;
|
|
393
|
+
if (integrationPriority) {
|
|
394
|
+
// integration 胜出 → 跳过 A3,直接走完整文档路径
|
|
395
|
+
rankedAfterBestPractice = rankedAfterIntentGuard;
|
|
396
|
+
dominantBestPractice = integrationPriority;
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
// integration 未胜出 → A3 提升 BP → BP 收敛判定
|
|
400
|
+
rankedAfterBestPractice = prioritizeBestPracticeWhenCompetitive(rankedAfterIntentGuard, chunksById, passNorm.prompt);
|
|
401
|
+
dominantBestPractice = pickDominantBestPracticeSource(rankedAfterBestPractice, chunksById);
|
|
402
|
+
}
|
|
403
|
+
const pinnedUrlRowChunkIds = new Set(urlRowHitChunkIds);
|
|
404
|
+
const fragments = postProcess(rankedAfterBestPractice, chunksById, passNorm.limit, {
|
|
405
|
+
pinnedUrlRowChunkIds,
|
|
406
|
+
preferredSource: dominantBestPractice.preferredSource,
|
|
407
|
+
fullDocumentMode: dominantBestPractice.fullDocumentMode,
|
|
408
|
+
});
|
|
409
|
+
const urlMappingSources = [...new Set(fragments.map((f) => f.source).filter((source) => isUrlMappingSource(source)))];
|
|
410
|
+
const references = buildReferences(fragments);
|
|
411
|
+
const apiNameCoverage = computeApiNameCoverage(queryApiTokens, poolWithContent);
|
|
412
|
+
const missingApiTokens = computeMissingApiTokens(queryApiTokens, poolWithContent);
|
|
413
|
+
return {
|
|
414
|
+
pool,
|
|
415
|
+
poolWithContent,
|
|
416
|
+
rankedWithMultipliers: rankedAfterBestPractice,
|
|
417
|
+
fragments,
|
|
418
|
+
exactHits,
|
|
419
|
+
urlMappingSources,
|
|
420
|
+
references,
|
|
421
|
+
apiNameCoverage,
|
|
422
|
+
missingApiTokens,
|
|
423
|
+
urlMapRelaxed: pinnedUrlRowChunkIds.size > 0,
|
|
424
|
+
urlMapExactHitCount: pinnedUrlRowChunkIds.size,
|
|
425
|
+
preferredSource: dominantBestPractice.preferredSource,
|
|
426
|
+
fullDocumentMode: dominantBestPractice.fullDocumentMode,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|