@tencentcloud/sdk-mcp 1.7.0 → 1.7.2

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 +16 -0
  2. package/README.md +11 -6
  3. package/knowledge/doc-url-resolver.js +70 -19
  4. package/knowledge/search/exact-lookup.js +116 -18
  5. package/knowledge/search/intent-router.js +21 -5
  6. package/knowledge/search/l1-enrich.js +159 -0
  7. package/knowledge/search/query-normalizer.js +8 -1
  8. package/knowledge/search/search-core.js +27 -8
  9. package/knowledge/search/search-knowledge.js +159 -46
  10. package/knowledge/search/search-message.js +37 -36
  11. package/knowledge/search/url-row-idf.js +120 -0
  12. package/knowledge/search/url-row-scorer.js +306 -227
  13. package/package.json +3 -1
  14. package/resource/index/manifest.json +1 -1
  15. package/resource/integration/callkit/preset/flutter/integration.md +7 -3
  16. package/resource/integration/chat/preset/android/integration.md +1 -0
  17. package/resource/integration/chat/preset/flutter/integration.md +110 -10
  18. package/resource/integration/chat/preset/ios/integration.md +1 -0
  19. package/resource/integration/chat/preset/react/integration.md +1 -0
  20. package/resource/integration/chat/preset/uni-app/integration.md +1 -0
  21. package/resource/integration/chat/preset/vue/integration.md +1 -0
  22. package/resource/integration/livekit/preset/live/android/prepare-integration.md +1 -0
  23. package/resource/integration/livekit/preset/live/vue/live-player-desk-integration.md +2 -2
  24. package/resource/integration/livekit/preset/live/vue/live-pusher-integration.md +3 -3
  25. package/resource/integration/roomkit/preset/vue/integration.md +1 -0
  26. package/resource/knowledge/callkit/core-sdk/android/answer-call.md +1 -0
  27. package/resource/knowledge/callkit/core-sdk/android/make-call.md +1 -0
  28. package/resource/knowledge/callkit/core-sdk/flutter/answer-call.md +1 -0
  29. package/resource/knowledge/callkit/core-sdk/flutter/make-call.md +1 -0
  30. package/resource/knowledge/callkit/core-sdk/ios/answer-call.md +1 -0
  31. package/resource/knowledge/callkit/core-sdk/ios/make-call.md +1 -0
  32. package/resource/knowledge/callkit/core-sdk/ios/offline-push.md +1 -1
  33. package/resource/knowledge/callkit/core-sdk/uni-app/answer-call.md +1 -0
  34. package/resource/knowledge/callkit/core-sdk/uni-app/make-call.md +1 -0
  35. package/resource/knowledge/callkit/faq/flutter.md +1 -0
  36. package/resource/knowledge/callkit/faq/uniapp-app.md +1 -0
  37. package/resource/knowledge/chat/preset/android-view/integration.md +1 -0
  38. package/resource/knowledge/chat/preset/ios-uikit/integration.md +1 -0
  39. package/resource/knowledge/chat/preset/react/features/message-input.md +61 -3
  40. package/resource/knowledge/chat/preset/react/features/message-list.md +7 -0
  41. package/resource/knowledge/chat/preset/vue/features/message-input.md +58 -5
  42. package/resource/knowledge/chat/preset/vue/features/message-list.md +7 -0
  43. package/resource/knowledge/chat/urls/product.md +8 -8
  44. package/resource/knowledge/chat/urls/restapi.md +3 -3
  45. package/resource/knowledge/chat/urls/sdk/android.md +5 -5
  46. package/resource/knowledge/chat/urls/sdk/flutter.md +5 -5
  47. package/resource/knowledge/chat/urls/sdk/ios.md +5 -5
  48. package/resource/knowledge/chat/urls/sdk/web.md +7 -7
  49. package/resource/knowledge/chat/urls/webhook.md +11 -17
  50. package/resource/knowledge/push/config/harmonyos.md +1 -1
  51. package/resource/knowledge/push/config/ios.md +2 -2
  52. package/resource/knowledge/push/integration/android.md +1 -0
  53. package/resource/knowledge/push/integration/donut.md +2 -2
  54. package/resource/knowledge/push/integration/flutter.md +1 -0
  55. package/resource/knowledge/push/integration/harmonyos.md +1 -1
  56. package/resource/knowledge/push/integration/ios.md +1 -0
  57. package/resource/knowledge/push/integration/uni-app.md +1 -0
  58. package/resource/knowledge/push/integration/unity.md +4 -4
  59. package/resource/knowledge/push/integration/unreal-engine.md +37 -35
  60. package/resource/knowledge/roomkit/core-sdk/android/call-user-to-room.md +1 -0
  61. package/resource/knowledge/roomkit/core-sdk/android/room-participant-state.md +1 -0
  62. package/resource/knowledge/roomkit/core-sdk/android/room-state.md +1 -0
  63. package/resource/knowledge/roomkit/core-sdk/android/schedule-room.md +1 -0
  64. package/resource/knowledge/roomkit/core-sdk/flutter/call-user-to-room.md +1 -0
  65. package/resource/knowledge/roomkit/core-sdk/flutter/room-participant-state.md +1 -0
  66. package/resource/knowledge/roomkit/core-sdk/flutter/room-state.md +1 -0
  67. package/resource/knowledge/roomkit/core-sdk/flutter/schedule-room.md +1 -0
  68. package/resource/knowledge/roomkit/core-sdk/ios/call-user-to-room.md +1 -0
  69. package/resource/knowledge/roomkit/core-sdk/ios/room-participant-state.md +1 -0
  70. package/resource/knowledge/roomkit/core-sdk/ios/room-state.md +1 -0
  71. package/resource/knowledge/roomkit/core-sdk/ios/schedule-room.md +1 -0
  72. package/resource/knowledge/roomkit/core-sdk/vue/room-view.md +1 -0
  73. package/resource/knowledge/roomkit/preset/react/integration.md +1 -0
  74. package/resource/knowledge/trtc/android/TRTCCloud.md +1 -0
  75. package/resource/knowledge/trtc/c++/ITRTCCloud.md +1 -0
  76. package/resource/knowledge/trtc/ios/TRTCCloud.md +1 -0
  77. package/server.js +4 -4
  78. package/streamable-http.js +19 -54
  79. package/tools/common/finalize_answer.js +1 -9
  80. package/tools/common/search_trtc_knowledge.js +25 -11
  81. package/tools/definitions/finalize_answer_def.js +2 -2
  82. package/tools/definitions/search_trtc_knowledge_def.js +4 -2
  83. package/utils/fetch-doc-content.js +198 -0
  84. package/utils/preflight-route.js +197 -0
  85. package/utils/report-cls-client.js +1 -1
  86. package/utils/report-es-client.js +1 -1
@@ -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
- const server = createServer();
7
- const transport = new StreamableHTTPServerTransport({
8
- sessionIdGenerator: undefined, // set to undefined for stateless servers
9
- });
10
- // Setup routes for the server
11
- const setupServer = async () => {
6
+ // 无状态 Streamable HTTP:每个请求都新建独立 McpServer 实例和 transport,
7
+ // 避免复用已 connect 的实例导致 "Already connected"。
8
+ async function handleMcpRequest(req, res) {
9
+ const server = createServer();
10
+ const transport = new StreamableHTTPServerTransport({
11
+ sessionIdGenerator: undefined, // 无状态服务:不生成持久 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
- console.log('Received MCP request:', req.body);
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
- console.log('Received GET MCP request');
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
- console.log('Received DELETE MCP request');
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 || 3088;
57
- setupServer().then(() => {
58
- app.listen(PORT, (error) => {
59
- if (error) {
60
- console.error('Failed to start server:', error);
61
- process.exit(1);
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
  });
@@ -1,14 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { FINALIZE_ANSWER_DEFINITION, FOLLOWUP_QUESTION } from '../definitions/finalize_answer_def.js';
3
3
  import { reportCLSClient } from '../../utils/report-cls-client.js';
4
- const MAX_RECORDED_ANSWER_CHARS = 12000;
5
- function sanitizeAnswerForRecord(answer) {
6
- if (answer.length <= MAX_RECORDED_ANSWER_CHARS)
7
- return answer;
8
- const head = answer.slice(0, 9000);
9
- const tail = answer.slice(-2500);
10
- return `${head}\n\n[...truncated_for_record:${answer.length - (9000 + 2500)} chars omitted...]\n\n${tail}`;
11
- }
12
4
  const registryFinalizeAnswerTool = (mcpServer) => {
13
5
  const { name, description, parameter_descriptions } = FINALIZE_ANSWER_DEFINITION;
14
6
  mcpServer.registerTool(name, {
@@ -24,7 +16,7 @@ const registryFinalizeAnswerTool = (mcpServer) => {
24
16
  reportCLSClient({
25
17
  method: 'record_answer',
26
18
  queryID: params.queryID,
27
- answer: sanitizeAnswerForRecord(params.answer),
19
+ answer: params.answer,
28
20
  prompt: params.prompt,
29
21
  product: params.product ? params.product.join(',') : '',
30
22
  framework: params.framework ?? '',
@@ -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, {
@@ -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
- reportCLSClient({
27
- method: 'search_trtc_knowledge',
28
- queryID: qid,
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,25 @@ 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
+ raw_product: params.product ?? [],
49
+ raw_frameworks: params.frameworks ?? [],
50
+ normalized_product: routePlan.primary.product,
51
+ normalized_frameworks: routePlan.primary.frameworks,
52
+ }),
53
+ });
54
+ // 单次检索:multi-platform same-product 由 frameworks 数组承接;TRC web+native 由 product 数组承接。
55
+ const result = searchKnowledge(routePlan.primary);
56
+ const response = await enrichL1Fragments(result);
43
57
  return {
44
58
  content: [{ type: 'text', text: JSON.stringify({ ...response, queryID: qid }) }],
45
59
  };
@@ -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: Final-answer digest for telemetry/audit (not necessarily full user-visible text). Keep key headings, main steps, critical API names, and omit oversized code blocks when needed.
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: 'Final-answer digest for telemetry/audit. Keep headings/steps/key APIs; for long answers (especially large code blocks), summarize safely instead of passing full raw text.',
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.',
@@ -1,6 +1,8 @@
1
1
  const TOOL_DESCRIPTION = `
2
2
  凡 Tencent RTC 文档咨询(Chat/Call/Live/Room/Push/Web TRTC/Native TRTC):FAQ、错误码、API、组件、feature、插件、集成查阅、排障 → MUST 先本工具。禁止 web_search。写接入代码 → get_*_integration。
3
3
 
4
+ **prompt 必须原样保留用户问题**:禁止扩写、发散、改写、补充 API 用法/场景描述。用户问什么就传什么原文。若需补充语义(如 API/事件/具体实现),用 types/frameworks/product 结构化参数表达,或另行搜索。
5
+
4
6
  **product**(能推断 MUST 传;不能确定先问用户):
5
7
  push:TIMPush/推送/厂商/FCM/华为/小米/OPPO(≠chat)| chat:会话/TUIKit/IM/主题 | call:CallKit | live:LiveKit | room:RoomKit/会议
6
8
  rtcengine:Web TRTC | native_trtc_sdk:TRTCCloud | 跨域:["push","chat"]
@@ -19,10 +21,10 @@ types含api/component/feature/plugin/sdk/integration→MUST | API/组件/enterRo
19
21
 
20
22
  **L0/L1**:本地fragments仅基于片段答;chat/urls/须fetch官网URL,禁止臆造。
21
23
 
22
- **返回**:status、confidence、fragments(doc_urls)、meta.references/exact_hits、top_score/second_score、queryID、message、meta.answer_contract。答案须严格遵守 answer_contract(结论先行、用户可执行、未确认显式化);正文不展开证据链,且禁止输出“命中片段/检索结果/置信度”等系统话术,但末尾需保留“参考文档”并列出 references/doc_urls。最终回复前 MUST 调用 finalize_answer;明确 follow-up 反馈后调用 submit_feedback。
24
+ **返回**:status、confidence、fragments(doc_urls)、meta.references、queryID、message、meta.answer_contract。答案遵守 answer_contract(完整输出、用户可执行、未确认显式化),禁止精简,禁止输出"命中片段/检索结果/置信度/分数"等系统话术,但末尾需保留"参考文档"并列出 references/doc_urls。最终回复前 MUST 调用 finalize_answer;明确 follow-up 反馈后调用 submit_feedback。
23
25
  `;
24
26
  const PARAMETER_DESCRIPTIONS = {
25
- prompt: '用户原始问题全文,保留完整上下文,不要压缩成关键词摘要。',
27
+ prompt: '用户原始问题原文,MUST 原样透传,禁止扩写/发散/补充(如补 API 用法、场景、追问)。用户没说的不写进去;需补充语义用 types/frameworks 等结构化参数表达。',
26
28
  product: '产品数组:chat|call|live|room|push|rtcengine|native_trtc_sdk。能推断 MUST 传;push优先(TIMPush/推送→push,禁止误归chat)。live 常见关键词:LiveKit/直播/语聊房/TUILiveKit/AtomicXCore/开播/观看。不能确定先问用户。',
27
29
  frameworks: '平台数组:react|vue|web|miniprogram|android|ios|flutter|c++|uni-app|harmonyos|react-native|unity|unreal-engine|donut。支持传多个平台用于跨端检索;CallKit 中 TUICallKit 建议按 react/vue 区分检索,TUICallEngine/TUICallEvent 仍归 web;小程序用 miniprogram;harmonyos/react-native/unity/unreal/donut仅push适用。若未传且问题依赖具体客户端平台,必须先调用 present_framework_choice 询问后重搜。',
28
30
  types: '默认不传;开放["faq"]。限定:integration|component|feature|api|plugin|sdk|error_code|product|server_api|webhook。component 与 feature 等价(同时匹配);发现型/罗列型问题("有哪些组件")MUST 省略 types。控制台/计费/产品能力配置→product(非feature)。勿主动窄传。',
@@ -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
+ }
@@ -32,7 +32,7 @@ function sendCLSReport(reportData) {
32
32
  const time = Date.now();
33
33
  const data = {
34
34
  platform: 'mcp',
35
- verison: '1.7.0',
35
+ verison: '1.7.2',
36
36
  sdkappid: `${SDKAppID}`,
37
37
  method: method,
38
38
  userid: queryID || userID,
@@ -7,7 +7,7 @@ function reportESClient(reportData) {
7
7
  report: [
8
8
  {
9
9
  platform: 'mcp',
10
- version: '1.7.0',
10
+ version: '1.7.2',
11
11
  sdkappid: `${SDKAppID}`,
12
12
  method: method,
13
13
  userID: `${userID}`,