@tencent-rtc/mcp 1.6.8 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/README.md +58 -39
- package/knowledge/build-index.js +30 -6
- package/knowledge/chunker.js +82 -14
- package/knowledge/chunking-policy.js +100 -0
- package/knowledge/content-loader.js +3 -3
- package/knowledge/doc-url-resolver.js +43 -16
- package/knowledge/frontmatter-meta.js +167 -0
- package/knowledge/path-meta.js +28 -6
- package/knowledge/search/exact-lookup.js +120 -239
- package/knowledge/search/filter-pool.js +12 -29
- package/knowledge/search/framework-fallback.js +22 -0
- package/knowledge/search/intent-router.js +29 -0
- package/knowledge/search/l1-enrich.js +159 -0
- package/knowledge/search/query-normalizer.js +63 -3
- package/knowledge/search/search-core.js +410 -0
- package/knowledge/search/search-knowledge.js +421 -379
- package/knowledge/search/search-message.js +88 -0
- package/knowledge/search/search-pass.js +428 -0
- package/knowledge/search/search-types.js +1 -0
- package/knowledge/search/url-row-idf.js +120 -0
- package/knowledge/search/url-row-scorer.js +337 -0
- package/package.json +3 -1
- package/resource/_schema/feature-aliases.yaml +1 -1
- package/resource/index/manifest.json +1 -1
- package/resource/knowledge/callkit/best-practice/callkit-miniprogram/uniapp-shrink.md +60 -0
- package/resource/knowledge/callkit/best-practice/common/call-autoplay-blocked.md +77 -0
- package/resource/knowledge/callkit/best-practice/common/call-message-in-chat.md +78 -0
- package/resource/knowledge/callkit/best-practice/common/call-vs-calls.md +110 -0
- package/resource/knowledge/callkit/best-practice/common/callengine-call-flow.md +123 -0
- package/resource/knowledge/callkit/best-practice/common/callengine-log-level.md +67 -0
- package/resource/knowledge/callkit/best-practice/common/callkit-get-engine-instance.md +81 -0
- package/resource/knowledge/callkit/best-practice/common/callkit-get-roomid.md +78 -0
- package/resource/knowledge/callkit/best-practice/common/callkit-limitations.md +37 -0
- package/resource/knowledge/callkit/best-practice/common/callkit-user-register-and-kicked.md +98 -0
- package/resource/knowledge/chat/best-practice/common/avchatroom-message-loss-diagnosis.md +65 -0
- package/resource/knowledge/chat/best-practice/common/group-conversation-cleanup-on-system-notice.md +62 -0
- package/resource/knowledge/chat/best-practice/common/image-message-url-lifecycle.md +59 -0
- package/resource/knowledge/chat/best-practice/common/login-repeat-2025-diagnosis.md +61 -0
- package/resource/knowledge/chat/best-practice/common/message-read-report-and-receipt.md +64 -0
- package/resource/knowledge/chat/best-practice/common/message-receive-event.md +65 -0
- package/resource/knowledge/chat/best-practice/common/message-risk-error-and-render.md +65 -0
- package/resource/knowledge/chat/best-practice/common/message-unread-count.md +71 -0
- package/resource/knowledge/chat/best-practice/miniprogram/audio-message-send.md +113 -0
- package/resource/knowledge/chat/best-practice/miniprogram/login-timeout-2801-diagnosis.md +63 -0
- package/resource/knowledge/chat/best-practice/miniprogram/media-upload-fail-diagnosis.md +64 -0
- package/resource/knowledge/chat/best-practice/web/audio-message-send.md +118 -0
- package/resource/knowledge/chat/urls/product.md +17 -16
- package/resource/knowledge/chat/urls/restapi.md +3 -2
- package/resource/knowledge/chat/urls/sdk/android.md +6 -5
- package/resource/knowledge/chat/urls/sdk/flutter.md +6 -5
- package/resource/knowledge/chat/urls/sdk/ios.md +4 -3
- package/resource/knowledge/chat/urls/sdk/web.md +7 -6
- package/resource/knowledge/chat/urls/webhook.md +0 -5
- package/resource/knowledge/livekit/best-practice/android/voice-room-anchor-start.md +120 -0
- package/resource/knowledge/livekit/best-practice/common/admin-take-seat.md +73 -0
- package/resource/knowledge/livekit/best-practice/common/audience-leave-live.md +81 -0
- package/resource/knowledge/livekit/best-practice/common/voice-room-no-audio-troubleshoot.md +100 -0
- package/resource/knowledge/livekit/best-practice/common/voice-room-seat-list-change.md +65 -0
- package/resource/knowledge/livekit/best-practice/flutter/voice-room-anchor-start.md +122 -0
- package/resource/knowledge/livekit/best-practice/ios/voice-room-anchor-start.md +126 -0
- package/resource/knowledge/roomkit/best-practice/common/room-ghost-member.md +70 -0
- package/resource/knowledge/roomkit/best-practice/common/room-limit-and-multi.md +83 -0
- package/resource/knowledge/roomkit/best-practice/common/room-screen-share-preview.md +83 -0
- package/resource/knowledge/roomkit/best-practice/common/room-status-after-all-leave.md +69 -0
- package/resource/knowledge/roomkit/best-practice/vue/room-uikit-vs-atomicx.md +111 -0
- package/resource/knowledge/rtcengine/web/plugin/30-advanced-cross-room-link.md +2 -0
- package/resource/knowledge/rtcengine/web/plugin/39-advanced-video-decoder.md +2 -0
- package/server.js +28 -23
- package/streamable-http.js +19 -54
- package/tools/common/search_trtc_knowledge.js +24 -12
- package/tools/definitions/finalize_answer_def.js +2 -2
- package/tools/definitions/integration-boundary.js +2 -1
- package/tools/definitions/search_trtc_knowledge_def.js +5 -3
- package/utils/fetch-doc-content.js +198 -0
- package/utils/preflight-route.js +197 -0
- package/utils/report-cls-client.js +1 -1
- package/utils/report-es-client.js +1 -1
- package/cursor-setting.png +0 -0
- package/mcp-server-prompt.png +0 -0
- package/resource/knowledge/callkit/best-practice/web/callengine-invitation.md +0 -80
- package/resource/knowledge/callkit/best-practice/web/callengine-log-control.md +0 -24
- package/resource/knowledge/callkit/best-practice/web/callkit-call-vs-calls.md +0 -116
- package/resource/knowledge/callkit/preset/react/uikit-api-reference.md +0 -444
- package/resource/knowledge/callkit/preset/vue/uikit-api-reference.md +0 -464
- package/resource/knowledge/chat/best-practice/web/image-message-url-handling.md +0 -16
package/streamable-http.js
CHANGED
|
@@ -3,66 +3,31 @@ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/
|
|
|
3
3
|
import { createServer } from './server.js';
|
|
4
4
|
const app = express();
|
|
5
5
|
app.use(express.json());
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
// Stateless Streamable HTTP: create a fresh McpServer + transport per request,
|
|
7
|
+
// to avoid reusing an already-connected Protocol instance ("Already connected").
|
|
8
|
+
async function handleMcpRequest(req, res) {
|
|
9
|
+
const server = createServer();
|
|
10
|
+
const transport = new StreamableHTTPServerTransport({
|
|
11
|
+
sessionIdGenerator: undefined, // stateless: no persistent session
|
|
12
|
+
});
|
|
12
13
|
await server.connect(transport);
|
|
13
|
-
|
|
14
|
+
await transport.handleRequest(req, res, req.body);
|
|
15
|
+
}
|
|
14
16
|
app.post('/mcp', async (req, res) => {
|
|
15
|
-
|
|
16
|
-
try {
|
|
17
|
-
await transport.handleRequest(req, res, req.body);
|
|
18
|
-
}
|
|
19
|
-
catch (error) {
|
|
20
|
-
console.error('Error handling MCP request:', error);
|
|
21
|
-
if (!res.headersSent) {
|
|
22
|
-
res.status(500).json({
|
|
23
|
-
jsonrpc: '2.0',
|
|
24
|
-
error: {
|
|
25
|
-
code: -32603,
|
|
26
|
-
message: 'Internal server error',
|
|
27
|
-
},
|
|
28
|
-
id: null,
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
}
|
|
17
|
+
await handleMcpRequest(req, res);
|
|
32
18
|
});
|
|
33
19
|
app.get('/mcp', async (req, res) => {
|
|
34
|
-
|
|
35
|
-
res.writeHead(405).end(JSON.stringify({
|
|
36
|
-
jsonrpc: "2.0",
|
|
37
|
-
error: {
|
|
38
|
-
code: -32000,
|
|
39
|
-
message: "Method not allowed."
|
|
40
|
-
},
|
|
41
|
-
id: null
|
|
42
|
-
}));
|
|
20
|
+
await handleMcpRequest(req, res);
|
|
43
21
|
});
|
|
44
22
|
app.delete('/mcp', async (req, res) => {
|
|
45
|
-
|
|
46
|
-
res.writeHead(405).end(JSON.stringify({
|
|
47
|
-
jsonrpc: "2.0",
|
|
48
|
-
error: {
|
|
49
|
-
code: -32000,
|
|
50
|
-
message: "Method not allowed."
|
|
51
|
-
},
|
|
52
|
-
id: null
|
|
53
|
-
}));
|
|
23
|
+
await handleMcpRequest(req, res);
|
|
54
24
|
});
|
|
55
25
|
// Start the server
|
|
56
|
-
const PORT = process.env.PORT ||
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
console.log(`Weather MCP Server listening on port ${PORT}`);
|
|
64
|
-
});
|
|
65
|
-
}).catch(error => {
|
|
66
|
-
console.error('Failed to set up the server:', error);
|
|
67
|
-
process.exit(1);
|
|
26
|
+
const PORT = process.env.PORT || 8081;
|
|
27
|
+
app.listen(PORT, (error) => {
|
|
28
|
+
if (error) {
|
|
29
|
+
console.error('Failed to start server:', error);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
console.log(`RTC MCP Server (streamable HTTP) listening on port ${PORT}`);
|
|
68
33
|
});
|
|
@@ -2,7 +2,9 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { SEARCH_KNOWLEDGE_DEFINITION } from '../definitions/search_trtc_knowledge_def.js';
|
|
4
4
|
import { searchKnowledge } from '../../knowledge/search/search-knowledge.js';
|
|
5
|
+
import { enrichL1Fragments } from '../../knowledge/search/l1-enrich.js';
|
|
5
6
|
import { reportCLSClient } from '../../utils/report-cls-client.js';
|
|
7
|
+
import { preflightRoute } from '../../utils/preflight-route.js';
|
|
6
8
|
const registrySearchTRTCKnowledgeTool = (mcpServer) => {
|
|
7
9
|
const { name, description, parameter_descriptions } = SEARCH_KNOWLEDGE_DEFINITION;
|
|
8
10
|
mcpServer.registerTool(name, {
|
|
@@ -10,7 +12,7 @@ const registrySearchTRTCKnowledgeTool = (mcpServer) => {
|
|
|
10
12
|
inputSchema: {
|
|
11
13
|
prompt: z.string().describe(parameter_descriptions.prompt),
|
|
12
14
|
product: z.array(z.enum(['chat', 'call', 'live', 'room', 'push', 'rtcengine', 'native_trtc_sdk'])).min(1).max(4).optional().describe(parameter_descriptions.product),
|
|
13
|
-
frameworks: z.array(z.enum(['react', 'vue', 'web', 'miniprogram', 'android', 'ios', 'flutter', 'c++', 'uni-app', 'harmonyos', 'react-native', 'unity', 'unreal-engine', 'donut'])).
|
|
15
|
+
frameworks: z.array(z.enum(['react', 'vue', 'web', 'miniprogram', 'android', 'ios', 'flutter', 'c++', 'uni-app', 'harmonyos', 'react-native', 'unity', 'unreal-engine', 'donut'])).default([]).describe(parameter_descriptions.frameworks),
|
|
14
16
|
queryID: z.string().describe(parameter_descriptions.queryID),
|
|
15
17
|
ide: z.string().describe(parameter_descriptions.ide),
|
|
16
18
|
types: z.array(z.enum([
|
|
@@ -21,18 +23,11 @@ const registrySearchTRTCKnowledgeTool = (mcpServer) => {
|
|
|
21
23
|
limit: z.number().min(1).max(16).default(8).describe(parameter_descriptions.limit),
|
|
22
24
|
from: z.string().default('').describe(parameter_descriptions.from),
|
|
23
25
|
},
|
|
24
|
-
}, (params) => {
|
|
26
|
+
}, async (params) => {
|
|
25
27
|
const qid = params.queryID || randomUUID();
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
prompt: params.prompt,
|
|
30
|
-
framework: params.frameworks?.join(',') ?? '',
|
|
31
|
-
product: params.product ? params.product.join(',') : '',
|
|
32
|
-
ide: params.ide,
|
|
33
|
-
from: params.from,
|
|
34
|
-
});
|
|
35
|
-
const response = searchKnowledge({
|
|
28
|
+
// preflight 只做归一化(归类、矛盾校正),不做 multi-path 编排。
|
|
29
|
+
// multi-platform 由 search core 的 filterPool(按 frameworks 交集)原生处理。
|
|
30
|
+
const routePlan = preflightRoute({
|
|
36
31
|
prompt: params.prompt,
|
|
37
32
|
product: params.product,
|
|
38
33
|
frameworks: params.frameworks,
|
|
@@ -40,6 +35,23 @@ const registrySearchTRTCKnowledgeTool = (mcpServer) => {
|
|
|
40
35
|
scenario: params.scenario,
|
|
41
36
|
limit: params.limit,
|
|
42
37
|
});
|
|
38
|
+
reportCLSClient({
|
|
39
|
+
method: 'search_trtc_knowledge',
|
|
40
|
+
queryID: qid,
|
|
41
|
+
prompt: params.prompt,
|
|
42
|
+
framework: routePlan.primary.frameworks?.join(',') ?? '',
|
|
43
|
+
product: routePlan.primary.product?.join(',') ?? '',
|
|
44
|
+
ide: params.ide,
|
|
45
|
+
from: params.from,
|
|
46
|
+
info: JSON.stringify({
|
|
47
|
+
preflight: routePlan.meta,
|
|
48
|
+
normalized_product: routePlan.primary.product,
|
|
49
|
+
normalized_frameworks: routePlan.primary.frameworks,
|
|
50
|
+
}),
|
|
51
|
+
});
|
|
52
|
+
// 单次检索:multi-platform same-product 由 frameworks 数组承接;TRC web+native 由 product 数组承接。
|
|
53
|
+
const result = searchKnowledge(routePlan.primary);
|
|
54
|
+
const response = await enrichL1Fragments(result);
|
|
43
55
|
return {
|
|
44
56
|
content: [{ type: 'text', text: JSON.stringify({ ...response, queryID: qid }) }],
|
|
45
57
|
};
|
|
@@ -9,7 +9,7 @@ When to call:
|
|
|
9
9
|
|
|
10
10
|
Input:
|
|
11
11
|
- prompt: Original user question. Must match the search_trtc_knowledge prompt.
|
|
12
|
-
- answer:
|
|
12
|
+
- answer: Full synthesized answer (same text sent to the user).
|
|
13
13
|
- queryID: REQUIRED. Must match the non-empty queryID returned by search_trtc_knowledge.
|
|
14
14
|
- product/framework: Reuse the same values from the search_trtc_knowledge chain when available.
|
|
15
15
|
|
|
@@ -19,7 +19,7 @@ Returns:
|
|
|
19
19
|
`;
|
|
20
20
|
const PARAMETER_DESCRIPTIONS = {
|
|
21
21
|
prompt: 'Original user question. Must match search_trtc_knowledge.',
|
|
22
|
-
answer: '
|
|
22
|
+
answer: 'Full synthesized answer (same text sent to the user).',
|
|
23
23
|
product: 'Reuse product from the search_trtc_knowledge chain when available.',
|
|
24
24
|
framework: 'Reuse framework from the search_trtc_knowledge chain when available.',
|
|
25
25
|
queryID: 'REQUIRED. Non-empty queryID returned by search_trtc_knowledge.',
|
|
@@ -2,4 +2,5 @@
|
|
|
2
2
|
export const INTEGRATION_BOUNDARY_SUFFIX = `
|
|
3
3
|
- **Codegen only**: Call this tool only when the user explicitly wants integration code added into a project, wants you to implement the integration, or wants generated starter code.
|
|
4
4
|
- **Consultation / lookup only** (how it works, what it is, setup steps, troubleshooting, API or component explanations) -> call \`search_trtc_knowledge\`, not this tool.
|
|
5
|
-
- This tool belongs to the codegen flow: after answering, do **not** call \`finalize_answer\` or \`submit_feedback\` (those belong only to the \`search_trtc_knowledge\` flow)
|
|
5
|
+
- This tool belongs to the codegen flow: after answering, do **not** call \`finalize_answer\` or \`submit_feedback\` (those belong only to the \`search_trtc_knowledge\` flow).
|
|
6
|
+
- **User-visible output rule (MUST)**: routing/tool choices above are internal only. Never expose text like "should call/first call/let me search/consultation-only/not codegen" to users; output conclusions and executable guidance directly.`;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
const TOOL_DESCRIPTION = `
|
|
2
2
|
For Tencent RTC documentation lookup (Chat / Call / Live / Room / Push / Web TRTC / Native TRTC): FAQ, error code, API, component, feature, plugin, integration lookup, troubleshooting -> MUST use this tool first. Do not use web_search. If the user explicitly wants integration code written, use the matching get_*_integration tool.
|
|
3
3
|
|
|
4
|
+
**prompt must preserve the user's original question verbatim**: do NOT expand, paraphrase, rewrite, or append API usage / scenario details. Pass exactly what the user asked. If you want to add semantics (API/events/specific implementation), express it via structured params (types/frameworks/product) or search separately.
|
|
5
|
+
|
|
4
6
|
**product** (if inferable, MUST pass; if not, ask first):
|
|
5
7
|
push: TIMPush / push / vendor channel / FCM / Huawei / Xiaomi / OPPO (not chat) | chat: conversation / TUIKit / IM / theme
|
|
6
8
|
call: CallKit | live: LiveKit | room: RoomKit / meeting | rtcengine: Web TRTC | native_trtc_sdk: TRTCCloud | cross-domain: ["push","chat"]
|
|
@@ -19,14 +21,14 @@ if types include api / component / feature / plugin / sdk / integration -> frame
|
|
|
19
21
|
|
|
20
22
|
**L0/L1**: For local fragments, answer only from the returned text. For chat/urls/ results, fetch the official URL before stating facts. Never invent content.
|
|
21
23
|
|
|
22
|
-
**Returns**: status, confidence, fragments(doc_urls), meta.references/exact_hits, top_score/second_score, queryID, message, meta.answer_contract. The answer MUST
|
|
24
|
+
**Returns**: status, confidence, fragments(doc_urls), meta.references/exact_hits, top_score/second_score, queryID, message, meta.answer_contract. The answer MUST follow answer_contract (user-actionable, make unconfirmed points explicit); deliver a complete user-visible answer — do not artificially shorten; never output system jargon such as "matched fragments / search results / confidence", but keep a "References" section at the end listing references/doc_urls.
|
|
23
25
|
|
|
24
26
|
**Post-answer workflow**: After the final answer is drafted, MUST call \`finalize_answer\` with the same prompt + queryID before replying. Append its follow-up text. On explicit follow-up feedback, call \`submit_feedback\`.
|
|
25
27
|
`;
|
|
26
28
|
const PARAMETER_DESCRIPTIONS = {
|
|
27
|
-
prompt: "The user's original question
|
|
29
|
+
prompt: "The user's original question VERBATIM. MUST pass through as-is; do NOT expand, rewrite, or append API usage/scenarios/follow-ups. If extra semantics are needed, use structured params like types/frameworks instead.",
|
|
28
30
|
product: 'Product array: chat | call | live | room | push | rtcengine | native_trtc_sdk. If inferable, MUST pass it. Push takes priority for TIMPush / push topics and must not be misrouted to chat. Common live keywords: LiveKit / live streaming / voice room / TUILiveKit / AtomicXCore / host streaming / watch live. If uncertain, ask the user first.',
|
|
29
|
-
frameworks: 'Platform array: react | vue | web | miniprogram | android | ios | flutter | c++ | uni-app | harmonyos | react-native | unity | unreal-engine | donut. Multiple platforms can be passed for cross-platform lookup. For CallKit, TUICallKit is best searched separately by react/vue, while TUICallEngine/TUICallEvent still map to web; use miniprogram for Mini Program. harmonyos / react-native / unity / unreal-engine / donut are only valid for push.',
|
|
31
|
+
frameworks: 'Platform array: react | vue | web | miniprogram | android | ios | flutter | c++ | uni-app | harmonyos | react-native | unity | unreal-engine | donut. Multiple platforms can be passed for cross-platform lookup. For CallKit, TUICallKit is best searched separately by react/vue, while TUICallEngine/TUICallEvent still map to web; use miniprogram for Mini Program. harmonyos / react-native / unity / unreal-engine / donut are only valid for push. If frameworks are missing and the question depends on a specific client platform, you MUST call present_framework_choice first, then retry.',
|
|
30
32
|
types: 'Usually omit. Allowed values: integration | component | feature | api | plugin | sdk | faq | error_code | product | server_api | webhook. component and feature are equivalent (match both). Discovery/listing questions ("what components are available") MUST omit types. Console/billing/product-capability config -> product (not feature). Do not proactively over-narrow.',
|
|
31
33
|
scenario: 'live | voice. Only valid for the live product.',
|
|
32
34
|
queryID: "First call: ''. Follow-up calls in the same consultation MUST pass through the original queryID returned by the server.",
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fetch-doc-content.ts
|
|
3
|
+
*
|
|
4
|
+
* 在命中 chat/urls L1 URL 映射行时,由 server 内部 fetch 官网目标文档并清洗成
|
|
5
|
+
* markdown 返回给 AI,避免 AI 自行联网抓取(不可靠、拿到的还是整页 HTML)。
|
|
6
|
+
*
|
|
7
|
+
* 设计要点:
|
|
8
|
+
* - 白名单域名:仅允许 fetch 我们自己的 urls 映射里出现的官方域名,防 SSRF。
|
|
9
|
+
* - Node 22 原生 fetch 会自动解 gzip(解决 cloud.tencent.com 返回 gzip 导致乱码的问题)。
|
|
10
|
+
* - 内部分流函数 fetch 按 hostname 选策略:
|
|
11
|
+
* - trtc.io(en 端 urls 主域名):`url + '.md'` 直拿 text/markdown,跳过 cheerio 清洗
|
|
12
|
+
* - 其他:cheerio + turndown 清洗 HTML(cloud.tencent.com / web.sdk.qcloud.com)
|
|
13
|
+
* - 内存缓存 + 超时 + 失败兜底(返回 null,不阻断原有检索)。
|
|
14
|
+
* - 对外函数 fetchDocContent 签名不变(l1-enrich 调用方无感知)。
|
|
15
|
+
* - 缓存 key 始终用原 URL(不带 .md 后缀),避免缓存 key 污染。
|
|
16
|
+
*/
|
|
17
|
+
import * as cheerio from 'cheerio';
|
|
18
|
+
import TurndownService from 'turndown';
|
|
19
|
+
/**
|
|
20
|
+
* 白名单域名:只允许这些域名的文档页被 fetch。
|
|
21
|
+
* 与 zh/resource/knowledge/chat/urls/ 和 en/resource/knowledge/chat/urls/ 下的映射保持一致。
|
|
22
|
+
*/
|
|
23
|
+
const ALLOWED_HOSTS = new Set([
|
|
24
|
+
'cloud.tencent.com',
|
|
25
|
+
'web.sdk.qcloud.com',
|
|
26
|
+
'trtc.io',
|
|
27
|
+
]);
|
|
28
|
+
/**
|
|
29
|
+
* 走"url + '.md' 直拿 markdown"策略的域名。
|
|
30
|
+
* 这类域名提供原生 markdown 视图(content-type=text/markdown),
|
|
31
|
+
* 比 cheerio+turndown 清洗 HTML 更干净、token 更省。
|
|
32
|
+
*/
|
|
33
|
+
const DIRECT_MD_HOSTS = new Set([
|
|
34
|
+
'trtc.io',
|
|
35
|
+
]);
|
|
36
|
+
const REQUEST_TIMEOUT_MS = 15000;
|
|
37
|
+
/** 默认截断长度(字符),超过则按段落截断 */
|
|
38
|
+
const DEFAULT_MAX_CHARS = 50000;
|
|
39
|
+
/** 内存缓存:url -> markdown,避免同一咨询/相近请求重复打官网 */
|
|
40
|
+
const cache = new Map();
|
|
41
|
+
/** 判断 URL 是否命中白名单域名 */
|
|
42
|
+
function isAllowedUrl(url) {
|
|
43
|
+
try {
|
|
44
|
+
const { hostname } = new URL(url);
|
|
45
|
+
return ALLOWED_HOSTS.has(hostname);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 从 HTML 中提取正文容器(cheerio 按域名选择稳定容器)。
|
|
53
|
+
* 找不到时回退到更宽泛的选择器;仍找不到返回 null,交由调用方退化为整页清洗。
|
|
54
|
+
*/
|
|
55
|
+
function extractArticleSelector(hostname) {
|
|
56
|
+
if (hostname === 'cloud.tencent.com') {
|
|
57
|
+
// 腾讯云文档:正文在 #docArticleContent 内(SSR 直出),依次回退
|
|
58
|
+
return ['#docArticleContent', '#doc-slate-root', '.J-markdown-box'];
|
|
59
|
+
}
|
|
60
|
+
if (hostname === 'web.sdk.qcloud.com') {
|
|
61
|
+
// JSDoc 静态文档:正文在 #main 的 article 内
|
|
62
|
+
return ['#main article', '#main', 'article'];
|
|
63
|
+
}
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
/** 使用 turndown 将选中的 HTML 节点转成 markdown */
|
|
67
|
+
function convertToMarkdown(html) {
|
|
68
|
+
const turndown = new TurndownService({
|
|
69
|
+
headingStyle: 'atx', // ## 风格标题
|
|
70
|
+
codeBlockStyle: 'fenced', // ``` 围栏代码块
|
|
71
|
+
bulletListMarker: '-',
|
|
72
|
+
});
|
|
73
|
+
turndown.remove(['script', 'style', 'nav', 'noscript', 'iframe']);
|
|
74
|
+
let md = turndown.turndown(html);
|
|
75
|
+
md = md
|
|
76
|
+
.replace(/[ \t]+\n/g, '\n')
|
|
77
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
78
|
+
.trim();
|
|
79
|
+
return md;
|
|
80
|
+
}
|
|
81
|
+
/** 按段落截断到 maxChars */
|
|
82
|
+
function truncateMarkdown(md, maxChars) {
|
|
83
|
+
if (md.length <= maxChars)
|
|
84
|
+
return md;
|
|
85
|
+
const head = md.slice(0, maxChars);
|
|
86
|
+
const lastBreak = head.lastIndexOf('\n\n');
|
|
87
|
+
return `${head.slice(0, lastBreak > 0 ? lastBreak : maxChars)}\n…[已截断]`;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* 公共 fetch helper:带 timeout + 错误吞咽,把 Response 交给 handle 解析。
|
|
91
|
+
* 用 globalThis.fetch 避免与下方内部分流函数 fetch 同名冲突。
|
|
92
|
+
*/
|
|
93
|
+
async function fetchWithTimeout(url, handle) {
|
|
94
|
+
const controller = new AbortController();
|
|
95
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
96
|
+
try {
|
|
97
|
+
const res = await globalThis.fetch(url, {
|
|
98
|
+
signal: controller.signal,
|
|
99
|
+
headers: {
|
|
100
|
+
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36',
|
|
101
|
+
// 加 text/markdown 偏好,让 trtc.io 等支持 .md 后缀的站点优先返回 markdown
|
|
102
|
+
'Accept': 'text/markdown,text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
103
|
+
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
|
104
|
+
},
|
|
105
|
+
// Node fetch 自动处理 gzip/deflate/br
|
|
106
|
+
redirect: 'follow',
|
|
107
|
+
});
|
|
108
|
+
if (!res.ok)
|
|
109
|
+
return null;
|
|
110
|
+
return await handle(res);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
clearTimeout(timer);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/** cheerio+turndown 路径:处理 cloud.tencent.com / web.sdk.qcloud.com 等 HTML 文档 */
|
|
120
|
+
async function fetchAndConvert(url, maxChars) {
|
|
121
|
+
return fetchWithTimeout(url, async (res) => {
|
|
122
|
+
const html = await res.text();
|
|
123
|
+
const { hostname } = new URL(url);
|
|
124
|
+
// cheerio 定位正文容器
|
|
125
|
+
const $ = cheerio.load(html);
|
|
126
|
+
const selectors = extractArticleSelector(hostname);
|
|
127
|
+
let article = selectors.map((sel) => $(sel)).find((node) => node.length > 0);
|
|
128
|
+
if (!article || article.length === 0) {
|
|
129
|
+
// 找不到容器时退化为 body 清洗(会偏大,但不丢正文)
|
|
130
|
+
article = $('body');
|
|
131
|
+
}
|
|
132
|
+
const markdown = convertToMarkdown(article.html() ?? '');
|
|
133
|
+
if (!markdown)
|
|
134
|
+
return null;
|
|
135
|
+
const truncated = markdown.length > maxChars;
|
|
136
|
+
const final = truncated ? truncateMarkdown(markdown, maxChars) : markdown;
|
|
137
|
+
return {
|
|
138
|
+
markdown: final,
|
|
139
|
+
truncated,
|
|
140
|
+
htmlBytes: Buffer.byteLength(html, 'utf-8'),
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* direct-md 路径:trtc.io 提供 `url + '.md'` 视图,
|
|
146
|
+
* content-type=text/markdown,可直接消费,跳过 cheerio 清洗。
|
|
147
|
+
* 失败时(content-type 不对 / 404)返回 null,让调用方走 fallback 路径。
|
|
148
|
+
*/
|
|
149
|
+
async function fetchDirectMarkdown(url, maxChars) {
|
|
150
|
+
const mdUrl = `${url}.md`;
|
|
151
|
+
return fetchWithTimeout(mdUrl, async (res) => {
|
|
152
|
+
const ct = res.headers.get('content-type') ?? '';
|
|
153
|
+
if (!/text\/markdown/i.test(ct))
|
|
154
|
+
return null;
|
|
155
|
+
const md = await res.text();
|
|
156
|
+
const truncated = md.length > maxChars;
|
|
157
|
+
return {
|
|
158
|
+
markdown: truncated ? truncateMarkdown(md, maxChars) : md,
|
|
159
|
+
truncated,
|
|
160
|
+
htmlBytes: 0,
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* 内部分流 fetch:按 hostname 选择策略。
|
|
166
|
+
* - DIRECT_MD_HOSTS 内的域名走 fetchDirectMarkdown;失败 fallback 到 fetchAndConvert。
|
|
167
|
+
* - 其他域名走 fetchAndConvert(cheerio+turndown)。
|
|
168
|
+
*/
|
|
169
|
+
async function fetch(url, hostname, maxChars) {
|
|
170
|
+
if (DIRECT_MD_HOSTS.has(hostname)) {
|
|
171
|
+
const direct = await fetchDirectMarkdown(url, maxChars);
|
|
172
|
+
// 防御:.md 视图失败时(如 content-type 不对、404)fallback 到 cheerio 路径
|
|
173
|
+
return direct ?? fetchAndConvert(url, maxChars);
|
|
174
|
+
}
|
|
175
|
+
return fetchAndConvert(url, maxChars);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* fetch 官网文档并清洗为 markdown。
|
|
179
|
+
* 命中白名单才请求;失败/超时/非白名单一律返回 null,不抛错。
|
|
180
|
+
*/
|
|
181
|
+
export async function fetchDocContent(url, opts) {
|
|
182
|
+
if (!isAllowedUrl(url))
|
|
183
|
+
return null;
|
|
184
|
+
// 缓存 key 用原 URL(不带 .md),让 fetchDocContent 调用方透明
|
|
185
|
+
const cached = cache.get(url);
|
|
186
|
+
if (cached)
|
|
187
|
+
return { markdown: cached, truncated: false, htmlBytes: 0 };
|
|
188
|
+
const { hostname } = new URL(url);
|
|
189
|
+
const maxChars = opts?.maxChars ?? DEFAULT_MAX_CHARS;
|
|
190
|
+
const result = await fetch(url, hostname, maxChars);
|
|
191
|
+
if (result)
|
|
192
|
+
cache.set(url, result.markdown);
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
/** 清空内存缓存(测试/热更时用) */
|
|
196
|
+
export function clearFetchDocCache() {
|
|
197
|
+
cache.clear();
|
|
198
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// ========== 信号分层(方案 §3,中英双语) ==========
|
|
2
|
+
// 产品词:产品线路标识
|
|
3
|
+
const PRODUCT_SIGNALS = {
|
|
4
|
+
call: /CallKit|TUICallKit|TUICallEngine|语音通话|视频通话|来电|去电|拨打|\b(?:voice|video)\s+calls?\b|incoming\s+calls?|outgoing\s+calls?|make\s+a\s+call|\bdial(?:ing)?\b/i,
|
|
5
|
+
live: /LiveKit|TUILiveKit|直播|语聊房|连麦|开播|观看直播|\blive\s+stream(?:ing)?\b|\bvoice\s+room\b|co-?host(?:ing)?|go\s+live|start\s+stream(?:ing)?|watch(?:ing)?\s+live|\bbroadcast(?:ing)?\b/i,
|
|
6
|
+
room: /RoomKit|会议|会中|会议室|屏幕分享|白板|\bconference\b|\bmeetings?\b|in-?meeting|meeting\s+room|screen\s+shar(?:e|ing)|white\s*board/i,
|
|
7
|
+
chat: /\bchat\b|\bim\b|会话|消息|TUIKit|ChatKit|\bconversation\b|\bmessaging\b|\binstant\s+messaging\b/i,
|
|
8
|
+
push: /TIMPush|推送|离线推送|FCM|APNs|华为|小米|OPPO|vivo|\bpush(?:\s+notification)?s?\b|offline\s+push|vendor\s+channel|\bHuawei\b|\bXiaomi\b|\bHonor\b/i,
|
|
9
|
+
};
|
|
10
|
+
// 平台词(纯运行形态,不含任何产品词)
|
|
11
|
+
const PLATFORM_NATIVE_SIGNAL = /\b(android|ios|flutter|c\+\+|native)\b|安卓\s*(sdk|端|原生)|iOS\s*(SDK|端|原生)|原生\s*(sdk|端)?|native\s+(?:sdk|client|app)|mobile\s+(?:sdk|native)/i;
|
|
12
|
+
const PLATFORM_WEB_SIGNAL = /\b(web|h5|html5|browser|webview)\b|浏览器|网页|Web端|H5端|网页端|web\s+(?:app|client|page)|in-?browser|mobile\s+browser|安卓\s*(浏览器|网页|H5|Web|webview)|iOS\s*(浏览器|网页|H5|Web|webview)|Android\s*(?:浏览器|网页|H5|Web|webview|browser|web)|iPad\s*(?:浏览器|网页|H5|Web|browser|web)|\b(Safari|Chrome|Edge|UC|QQ浏览器|QQ\s+Browser)\b/i;
|
|
13
|
+
// 平台+产品复合词(运行形态 + 产品线路标识)
|
|
14
|
+
const WEB_PRODUCT_PAIR_SIGNAL = /\bweb\s+(trtc|sdk|im|chat|roomkit)\b|\b(trtc|sdk|im|chat|roomkit)\s+web\b|trtc[-\s]?sdk|trtc\.js/i;
|
|
15
|
+
// API 词(客户端调用形态)
|
|
16
|
+
const NATIVE_TRTC_CLASS_SIGNAL = /\b(TRTCCloud|TRTCCloudListener|TRTCCloudDelegate|ITRTCCloud|TRTCTypeDef|TRTCCloudCallback|TXAudioEffectManager|TXBeautyManager|TXDeviceManager|TXLiteAVCode|TXLiveSDKTypeDef)\b/i;
|
|
17
|
+
const NATIVE_TRTC_METHOD_SIGNAL = /\b(?:TRTCCloud|ITRTCCloud|TX[A-Za-z0-9_]+)\s*(?:\.|::|->)\s*(?:set|get|start|stop|enable|disable)[A-Z][A-Za-z0-9_]+\b/i;
|
|
18
|
+
const WEB_TRTC_API_RE = /TRTC\.(setCurrentSpeaker|getMicrophoneList|getSpeakerList|getCameraList|isSupported|create)|trtc\.(startLocalAudio|startLocalVideo|updateLocalAudio|updateLocalVideo)/i;
|
|
19
|
+
const WEB_TRTC_TOKEN_RE = /^TRTC\.(setCurrentSpeaker|getMicrophoneList|getSpeakerList|getCameraList|isSupported|create)$/i;
|
|
20
|
+
// 协议级 TRTC 信号
|
|
21
|
+
const TRTC_SIGNAL = /\bTRTC\b|\bWebRTC\b|rtcengine|TRTCCloud|腾讯实时音视频/i;
|
|
22
|
+
// ========== 工具函数 ==========
|
|
23
|
+
function uniq(list) {
|
|
24
|
+
return [...new Set(list)];
|
|
25
|
+
}
|
|
26
|
+
function extractQueryApiTokens(prompt) {
|
|
27
|
+
const matches = prompt.match(/\b(?:TRTC|trtc)\.[A-Za-z_][A-Za-z0-9_]*\b/g) ?? [];
|
|
28
|
+
return uniq(matches.map((m) => m.replace(/^trtc\./i, 'TRTC.')));
|
|
29
|
+
}
|
|
30
|
+
function collectPromptSignals(prompt) {
|
|
31
|
+
const products = new Set();
|
|
32
|
+
if (PRODUCT_SIGNALS.call.test(prompt))
|
|
33
|
+
products.add('call');
|
|
34
|
+
if (PRODUCT_SIGNALS.live.test(prompt))
|
|
35
|
+
products.add('live');
|
|
36
|
+
if (PRODUCT_SIGNALS.room.test(prompt))
|
|
37
|
+
products.add('room');
|
|
38
|
+
if (PRODUCT_SIGNALS.chat.test(prompt))
|
|
39
|
+
products.add('chat');
|
|
40
|
+
if (PRODUCT_SIGNALS.push.test(prompt))
|
|
41
|
+
products.add('push');
|
|
42
|
+
if (TRTC_SIGNAL.test(prompt))
|
|
43
|
+
products.add('trtc');
|
|
44
|
+
// 平台 tokens:web signal 锁定 → 压制 native 单字(安卓/iOS 仅归 hostEnv);
|
|
45
|
+
// 真复合词(unreal/unity/harmonyos/react-native/flutter/c++)不受影响。
|
|
46
|
+
const platforms = [];
|
|
47
|
+
const webLocked = PLATFORM_WEB_SIGNAL.test(prompt) || WEB_PRODUCT_PAIR_SIGNAL.test(prompt);
|
|
48
|
+
if (webLocked) {
|
|
49
|
+
platforms.push('web');
|
|
50
|
+
}
|
|
51
|
+
const nativeSpecificHits = [];
|
|
52
|
+
if (/\b(?:unreal|UE[45]?|unreal-engine)\b/i.test(prompt))
|
|
53
|
+
nativeSpecificHits.push('unreal-engine');
|
|
54
|
+
if (/\b(?:unity)\b/i.test(prompt))
|
|
55
|
+
nativeSpecificHits.push('unity');
|
|
56
|
+
if (/\b(?:harmonyos|鸿蒙|HarmonyOS)\b/i.test(prompt))
|
|
57
|
+
nativeSpecificHits.push('harmonyos');
|
|
58
|
+
if (/\b(?:react-native)\b/i.test(prompt))
|
|
59
|
+
nativeSpecificHits.push('react-native');
|
|
60
|
+
if (/\b(?:flutter)\b/i.test(prompt))
|
|
61
|
+
nativeSpecificHits.push('flutter');
|
|
62
|
+
if (/\b(?:c\+\+)\b/i.test(prompt))
|
|
63
|
+
nativeSpecificHits.push('c++');
|
|
64
|
+
// 安卓/iOS 单字只在非 web-locked 时进 platforms(web-locked 时仅归 hostEnv)
|
|
65
|
+
if (!webLocked && /\b(?:ios|iphone|ipad|苹果)\b/i.test(prompt))
|
|
66
|
+
nativeSpecificHits.push('ios');
|
|
67
|
+
if (!webLocked && /\b(?:android|安卓)\b/i.test(prompt))
|
|
68
|
+
nativeSpecificHits.push('android');
|
|
69
|
+
// 真复合词(如 unity/unreal/flutter/harmonyos/react-native/c++)无条件 push;单字在 webLocked 时被压制
|
|
70
|
+
if (nativeSpecificHits.length > 0) {
|
|
71
|
+
platforms.push(...nativeSpecificHits);
|
|
72
|
+
}
|
|
73
|
+
else if (!webLocked && PLATFORM_NATIVE_SIGNAL.test(prompt)) {
|
|
74
|
+
// 仅 "native" 词,未指定细分 → 默认 android(webLocked 时不触发 fallback,避免被 \b(android|ios|...) 反向命中把 web 路径覆盖成 native)
|
|
75
|
+
platforms.push('android');
|
|
76
|
+
}
|
|
77
|
+
// Host env:仅用于标记,不进 platforms 归一化
|
|
78
|
+
let hostEnv = null;
|
|
79
|
+
if (/\bios\b|iphone|ipad|苹果/i.test(prompt))
|
|
80
|
+
hostEnv = 'ios';
|
|
81
|
+
else if (/\bandroid\b|安卓/i.test(prompt))
|
|
82
|
+
hostEnv = 'android';
|
|
83
|
+
const queryApiTokens = extractQueryApiTokens(prompt);
|
|
84
|
+
const hasWebTrtcApi = PLATFORM_WEB_SIGNAL.test(prompt)
|
|
85
|
+
|| WEB_TRTC_API_RE.test(prompt)
|
|
86
|
+
|| queryApiTokens.some((t) => WEB_TRTC_TOKEN_RE.test(t));
|
|
87
|
+
const hasNativeTrtcApi = NATIVE_TRTC_CLASS_SIGNAL.test(prompt) || NATIVE_TRTC_METHOD_SIGNAL.test(prompt);
|
|
88
|
+
return { products, platforms, hostEnv, hasWebTrtcApi, hasNativeTrtcApi, queryApiTokens };
|
|
89
|
+
}
|
|
90
|
+
function reconcileWithPrompt(promptValues, explicit) {
|
|
91
|
+
// 核心规则:prompt 是真值源,explicit 只在 prompt 为空时启用。
|
|
92
|
+
// 这条规则必须严格遵守——
|
|
93
|
+
// 「prompt 是用户问题表达出来的真实意图」,explicit 只在外部上下文(IDE/文件/历史)有补充价值,
|
|
94
|
+
// 但 prompt 既然明示了「Web」,就要按 Web 来,不要因为 explicit 多了 'android' 就把它"补"进来当成双路。
|
|
95
|
+
// 情形 1:prompt 真值存在 → 信任 prompt 真值
|
|
96
|
+
// - 即便 explicit 扩张(prompt 外多列了平台)→ 也只输出 prompt 真值(prompt_override=true 表示 agent 推理被忽略)
|
|
97
|
+
// - 即便 explicit 收缩(prompt 真值的子集)→ 也只输出 prompt 真值(避免丢失用户意图)
|
|
98
|
+
if (promptValues.length > 0) {
|
|
99
|
+
const promptSet = new Set(promptValues);
|
|
100
|
+
const sameAsExplicit = !!explicit?.length
|
|
101
|
+
&& explicit.length === promptValues.length
|
|
102
|
+
&& explicit.every((v) => promptSet.has(v));
|
|
103
|
+
return { values: uniq(promptValues), corrected: !sameAsExplicit };
|
|
104
|
+
}
|
|
105
|
+
// 情形 2:prompt 真值无平台信息 → 信任上游 agent 基于上下文(IDE/文件/历史)的推理
|
|
106
|
+
if (!promptValues.length) {
|
|
107
|
+
return { values: uniq(explicit ?? []), corrected: false };
|
|
108
|
+
}
|
|
109
|
+
return { values: [], corrected: false };
|
|
110
|
+
}
|
|
111
|
+
// ========== Phase 3: 单 SearchParams 构造 ==========
|
|
112
|
+
//
|
|
113
|
+
// 关键约束:**永远只返回单 primary**,consumer 只跑一次 searchKnowledge。
|
|
114
|
+
// multi-platform / multi-product 通过把 frameworks / product 设成数组交给 search core 处理。
|
|
115
|
+
// search core 的 filterPool.applyRankMultipliers 已原生支持多值归并。
|
|
116
|
+
function passthroughPlan(params, reason, env) {
|
|
117
|
+
return { primary: params, meta: { route_reason: reason, environment_downgraded: env } };
|
|
118
|
+
}
|
|
119
|
+
/** 单产品路径:product 是单值;frameworks 是数组(可能多平台共享一个 product) */
|
|
120
|
+
function singleProductPlan(params, product, frameworks, hostEnv, reason) {
|
|
121
|
+
const envDiff = hostEnv != null && !frameworks.includes(hostEnv);
|
|
122
|
+
return {
|
|
123
|
+
primary: { ...params, product: [product], frameworks },
|
|
124
|
+
meta: { route_reason: reason, environment_downgraded: envDiff },
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/** TRC 路径:product 是数组(rtcengine/native_trtc_sdk 两个产品码,按 platforms 各命中其一);frameworks 也是数组 */
|
|
128
|
+
function trcMultiProductPlan(params, products, frameworks, reason, env) {
|
|
129
|
+
return {
|
|
130
|
+
primary: { ...params, product: products, frameworks },
|
|
131
|
+
meta: { route_reason: reason, environment_downgraded: env },
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
// ========== 主入口(基于 prompt 真值归一化) ==========
|
|
135
|
+
export function preflightRoute(params) {
|
|
136
|
+
const prompt = params.prompt ?? '';
|
|
137
|
+
const explicitFrameworks = params.frameworks?.length ? uniq(params.frameworks) : undefined;
|
|
138
|
+
const explicitProducts = params.product?.length ? uniq(params.product) : undefined;
|
|
139
|
+
// Phase 1
|
|
140
|
+
const signals = collectPromptSignals(prompt);
|
|
141
|
+
// —— 真跨主题(多产品信号)直接 passthrough
|
|
142
|
+
const nonTrtcProducts = [...signals.products].filter((p) => p !== 'trtc');
|
|
143
|
+
if (nonTrtcProducts.length > 1) {
|
|
144
|
+
return passthroughPlan(params, 'cross_product_passthrough', false);
|
|
145
|
+
}
|
|
146
|
+
// Phase 2: frameworks 调和
|
|
147
|
+
const fwRecon = reconcileWithPrompt(signals.platforms, explicitFrameworks);
|
|
148
|
+
const frameworks = fwRecon.values;
|
|
149
|
+
const envDowngraded = signals.hostEnv != null && !frameworks.includes(signals.hostEnv);
|
|
150
|
+
// Phase 2.5: products 调和(仅当 prompt 有非 TRTC 单产品时取此产品;TRTC 由 platform 推断 rtcengine/native_trtc_sdk)
|
|
151
|
+
const explicitTrtcBoth = explicitProducts?.includes('rtcengine') && explicitProducts?.includes('native_trtc_sdk');
|
|
152
|
+
const explicitTrtcAny = explicitProducts?.some((p) => p === 'rtcengine' || p === 'native_trtc_sdk') ?? false;
|
|
153
|
+
const isTrtcFamily = signals.products.has('trtc') || explicitTrtcBoth || (explicitTrtcAny && !nonTrtcProducts[0]);
|
|
154
|
+
// —— TRTC 家族:产物随平台分流
|
|
155
|
+
if (isTrtcFamily) {
|
|
156
|
+
const hasWeb = frameworks.includes('web');
|
|
157
|
+
const natives = frameworks.filter((f) => f !== 'web');
|
|
158
|
+
// TRTC + 多平台(web + native)→ product 数组,search core filterPool 原生接受多 product 交集
|
|
159
|
+
if (hasWeb && natives.length > 0) {
|
|
160
|
+
const reason = fwRecon.corrected ? 'trtc_multi_platform_prompt_override' : 'trtc_multi_platform';
|
|
161
|
+
return trcMultiProductPlan(params, ['rtcengine', 'native_trtc_sdk'], frameworks, reason, envDowngraded);
|
|
162
|
+
}
|
|
163
|
+
// TRTC + 仅 web
|
|
164
|
+
if (hasWeb) {
|
|
165
|
+
const reason = fwRecon.corrected ? 'trtc_web_prompt_override' : 'trtc_web';
|
|
166
|
+
return singleProductPlan(params, 'rtcengine', ['web'], signals.hostEnv, reason);
|
|
167
|
+
}
|
|
168
|
+
// TRTC + 仅 native
|
|
169
|
+
if (natives.length > 0) {
|
|
170
|
+
const reason = fwRecon.corrected ? 'trtc_native_prompt_override' : 'trtc_native';
|
|
171
|
+
return singleProductPlan(params, 'native_trtc_sdk', natives, signals.hostEnv, reason);
|
|
172
|
+
}
|
|
173
|
+
// TRTC 但 prompt / explicit 都没有可用平台 → 透传
|
|
174
|
+
return passthroughPlan(params, 'trtc_no_platform_resolved', false);
|
|
175
|
+
}
|
|
176
|
+
// —— 单非 TRTC 产品
|
|
177
|
+
if (nonTrtcProducts.length === 1) {
|
|
178
|
+
const product = nonTrtcProducts[0];
|
|
179
|
+
// 多平台 → product 单值,frameworks 多值(search core filterPool 原生按 framework 交集过滤)
|
|
180
|
+
if (frameworks.length >= 2) {
|
|
181
|
+
const reason = fwRecon.corrected ? 'multi_platform_prompt_override' : 'multi_platform';
|
|
182
|
+
return singleProductPlan(params, product, frameworks, signals.hostEnv, reason);
|
|
183
|
+
}
|
|
184
|
+
// 单平台
|
|
185
|
+
if (frameworks.length === 1) {
|
|
186
|
+
const reason = fwRecon.corrected ? 'single_platform_prompt_override' : 'single_platform';
|
|
187
|
+
return singleProductPlan(params, product, frameworks, signals.hostEnv, reason);
|
|
188
|
+
}
|
|
189
|
+
// 无平台信号:透传(保留 explicit frameworks)
|
|
190
|
+
if (explicitFrameworks?.length) {
|
|
191
|
+
return singleProductPlan(params, product, uniq(explicitFrameworks), signals.hostEnv, 'product_no_platform_explicit');
|
|
192
|
+
}
|
|
193
|
+
return passthroughPlan(params, 'product_no_platform_signal', false);
|
|
194
|
+
}
|
|
195
|
+
// —— 无产品信号:纯透传
|
|
196
|
+
return passthroughPlan(params, 'no_product_signal', false);
|
|
197
|
+
}
|
package/cursor-setting.png
DELETED
|
Binary file
|
package/mcp-server-prompt.png
DELETED
|
Binary file
|