ms-vite-plugin 1.1.16 → 1.1.18

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/dist/mcp/tools.js CHANGED
@@ -1,935 +1,13 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  Object.defineProperty(exports, "__esModule", { value: true });
36
3
  exports.DEFAULT_DEVICE_PORT = void 0;
37
4
  exports.createMcpServer = createMcpServer;
38
5
  const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
39
- const z = __importStar(require("zod/v4"));
40
- const fsExtra = __importStar(require("fs-extra"));
41
- const os = __importStar(require("os"));
42
- const path = __importStar(require("path"));
43
- const packager_1 = require("../packager");
44
- const project_1 = require("../project");
45
- const project_2 = require("./project");
6
+ const version_1 = require("../version");
46
7
  const device_config_1 = require("./device-config");
47
8
  Object.defineProperty(exports, "DEFAULT_DEVICE_PORT", { enumerable: true, get: function () { return device_config_1.DEFAULT_DEVICE_PORT; } });
48
- const docs_service_1 = require("./docs-service");
49
- const types_1 = require("./types");
50
- const version_1 = require("../version");
51
- /**
52
- * 设备日志内存缓存上限
53
- * - 采用环形窗口思路,仅保留最新 5000 条,避免 MCP 进程长期运行时内存无限增长
54
- */
55
- const DEVICE_LOG_MEMORY_LIMIT = 5000;
56
- /**
57
- * SSE 重连等待时间(毫秒)
58
- * - 当设备日志流异常断开时,后台会自动尝试重连
59
- */
60
- const DEVICE_LOG_RECONNECT_DELAY_MS = 1500;
61
- /**
62
- * 视为“仍在工作中”的日志订阅状态集合
63
- * - 用于判断同一设备是否可以直接复用已有后台连接
64
- */
65
- const ACTIVE_DEVICE_LOG_SUBSCRIPTION_STATUSES = new Set([
66
- "connecting",
67
- "connected",
68
- "reconnecting",
69
- ]);
70
- /**
71
- * 进程级设备日志订阅状态
72
- * - 当前 MCP 仅维护单设备日志缓存
73
- * - MCP Tool 处理函数会复用同一份内存缓存
74
- */
75
- const deviceLogSubscriptionState = {
76
- target: null,
77
- status: "idle",
78
- lastError: null,
79
- lastEventAt: null,
80
- startedAt: null,
81
- logs: [],
82
- runtimeStatus: [],
83
- generation: 0,
84
- controller: null,
85
- reconnectTimer: null,
86
- };
87
- /**
88
- * 向环形缓冲区追加数据
89
- * @param buffer 目标数组
90
- * @param entry 待写入的数据
91
- * @param limit 数组最大长度
92
- * @returns 无返回值
93
- * @example
94
- * pushRingBuffer([1, 2], 3, 2)
95
- */
96
- function pushRingBuffer(buffer, entry, limit) {
97
- if (buffer.length >= limit) {
98
- buffer.splice(0, buffer.length - limit + 1);
99
- }
100
- buffer.push(entry);
101
- }
102
- /**
103
- * 创建标准 MCP 文本内容对象
104
- * @param text 返回文本
105
- * @returns 返回 `{ type: "text", text }` 结构
106
- * @example
107
- * createTextContent("ok")
108
- */
109
- function createTextContent(text) {
110
- return {
111
- type: "text",
112
- text,
113
- };
114
- }
115
- /**
116
- * 创建标准 MCP 文本工具返回值
117
- * @param text 返回文本
118
- * @param isError 是否标记为错误响应
119
- * @returns 返回包含单条文本内容的 MCP Tool 结果
120
- * @example
121
- * createTextToolResult("done")
122
- */
123
- function createTextToolResult(text, isError = false) {
124
- return {
125
- content: [createTextContent(text)],
126
- ...(isError ? { isError: true } : {}),
127
- };
128
- }
129
- /**
130
- * 构建运行时 HTTP 请求参数
131
- * @param target 已解析的设备目标
132
- * @returns 返回项目内部设备请求参数
133
- * @example
134
- * const options = createRuntimeHttpRequestOptions(target)
135
- */
136
- function createRuntimeHttpRequestOptions(target) {
137
- return {
138
- ip: target.ip,
139
- port: target.port,
140
- transport: "http",
141
- };
142
- }
143
- /**
144
- * 重置当前日志缓存快照
145
- * @returns 无返回值
146
- * @example
147
- * resetDeviceLogSnapshotState()
148
- */
149
- function resetDeviceLogSnapshotState() {
150
- deviceLogSubscriptionState.lastError = null;
151
- deviceLogSubscriptionState.lastEventAt = null;
152
- deviceLogSubscriptionState.startedAt = new Date().toISOString();
153
- deviceLogSubscriptionState.logs = [];
154
- deviceLogSubscriptionState.runtimeStatus = [];
155
- }
156
- /**
157
- * 判断日志订阅是否仍然活跃
158
- * @returns 活跃返回 true,否则返回 false
159
- * @example
160
- * const active = isDeviceLogSubscriptionActive()
161
- */
162
- function isDeviceLogSubscriptionActive() {
163
- return (ACTIVE_DEVICE_LOG_SUBSCRIPTION_STATUSES.has(deviceLogSubscriptionState.status) || deviceLogSubscriptionState.reconnectTimer !== null);
164
- }
165
- /**
166
- * 解析单个 SSE 文本块
167
- * @param rawBlock 原始 SSE 文本块
168
- * @returns 返回事件名与 data 字符串
169
- * @example
170
- * parseSseEventBlock("event: log\ndata: {\"message\":\"ok\"}")
171
- */
172
- function parseSseEventBlock(rawBlock) {
173
- const lines = rawBlock.split(/\r?\n/);
174
- let eventName = "message";
175
- const dataLines = [];
176
- for (const line of lines) {
177
- if (line.startsWith("event:")) {
178
- eventName = line.slice(6).trim() || "message";
179
- continue;
180
- }
181
- if (line.startsWith("data:")) {
182
- dataLines.push(line.slice(5).trimStart());
183
- }
184
- }
185
- return {
186
- event: eventName,
187
- data: dataLines.join("\n"),
188
- };
189
- }
190
- /**
191
- * 将原始日志对象标准化为统一结构
192
- * @param payload 日志原始对象
193
- * @returns 返回标准化后的日志
194
- * @example
195
- * normalizeBufferedLogEntry({ level: "info", message: "started" })
196
- */
197
- function normalizeBufferedLogEntry(payload) {
198
- const levelText = String(payload.level ?? "info").toLowerCase();
199
- const messageText = String(payload.message ?? "");
200
- const timestampText = typeof payload.timestamp === "string" && payload.timestamp
201
- ? payload.timestamp
202
- : new Date().toISOString();
203
- return {
204
- level: levelText,
205
- message: messageText,
206
- timestamp: timestampText,
207
- };
208
- }
209
- /**
210
- * 清理当前后台日志订阅
211
- * @returns 无返回值
212
- * @example
213
- * stopDeviceLogSubscription()
214
- */
215
- function stopDeviceLogSubscription() {
216
- if (deviceLogSubscriptionState.reconnectTimer) {
217
- clearTimeout(deviceLogSubscriptionState.reconnectTimer);
218
- deviceLogSubscriptionState.reconnectTimer = null;
219
- }
220
- if (deviceLogSubscriptionState.controller) {
221
- deviceLogSubscriptionState.controller.abort();
222
- deviceLogSubscriptionState.controller = null;
223
- }
224
- }
225
- /**
226
- * 安排后台重连
227
- * @param generation 当前订阅代次
228
- * @returns 无返回值
229
- * @example
230
- * scheduleDeviceLogReconnect(1)
231
- */
232
- function scheduleDeviceLogReconnect(generation) {
233
- if (generation !== deviceLogSubscriptionState.generation ||
234
- !deviceLogSubscriptionState.target) {
235
- return;
236
- }
237
- if (deviceLogSubscriptionState.reconnectTimer) {
238
- return;
239
- }
240
- deviceLogSubscriptionState.status = "reconnecting";
241
- deviceLogSubscriptionState.reconnectTimer = setTimeout(() => {
242
- deviceLogSubscriptionState.reconnectTimer = null;
243
- void startDeviceLogSubscriptionLoop(generation);
244
- }, DEVICE_LOG_RECONNECT_DELAY_MS);
245
- }
246
- /**
247
- * 启动一次 SSE 读取循环
248
- * - 该函数仅负责单次连接;若连接中断,会由外层自动调度重连
249
- * @param generation 当前订阅代次
250
- * @returns 读取流程结束后返回 Promise<void>
251
- * @example
252
- * await startDeviceLogSubscriptionLoop(1)
253
- */
254
- async function startDeviceLogSubscriptionLoop(generation) {
255
- if (generation !== deviceLogSubscriptionState.generation ||
256
- !deviceLogSubscriptionState.target) {
257
- return;
258
- }
259
- const { ip, port } = deviceLogSubscriptionState.target;
260
- const controller = new AbortController();
261
- const url = `http://${ip}:${port}/logger/sse`;
262
- deviceLogSubscriptionState.controller = controller;
263
- deviceLogSubscriptionState.status =
264
- deviceLogSubscriptionState.lastEventAt === null
265
- ? "connecting"
266
- : "reconnecting";
267
- try {
268
- const response = await fetch(url, {
269
- method: "GET",
270
- signal: controller.signal,
271
- headers: {
272
- Accept: "text/event-stream",
273
- },
274
- });
275
- if (!response.ok || !response.body) {
276
- throw new Error(`连接日志流失败: ${response.status} ${response.statusText || "unknown"}`);
277
- }
278
- deviceLogSubscriptionState.status = "connected";
279
- deviceLogSubscriptionState.lastError = null;
280
- const reader = response.body.getReader();
281
- const decoder = new TextDecoder("utf-8");
282
- let buffer = "";
283
- while (generation === deviceLogSubscriptionState.generation) {
284
- const { value, done } = await reader.read();
285
- if (done) {
286
- break;
287
- }
288
- buffer += decoder.decode(value, { stream: true });
289
- let splitIndex = buffer.search(/\r?\n\r?\n/);
290
- while (splitIndex >= 0) {
291
- const rawBlock = buffer.slice(0, splitIndex);
292
- buffer = buffer.slice(splitIndex + (buffer[splitIndex] === "\r" ? 4 : 2));
293
- const event = parseSseEventBlock(rawBlock);
294
- if (event.data) {
295
- try {
296
- const parsed = JSON.parse(event.data);
297
- deviceLogSubscriptionState.lastEventAt = new Date().toISOString();
298
- if (event.event === "log") {
299
- pushRingBuffer(deviceLogSubscriptionState.logs, normalizeBufferedLogEntry(parsed), DEVICE_LOG_MEMORY_LIMIT);
300
- }
301
- else if (event.event === "runtime_status") {
302
- pushRingBuffer(deviceLogSubscriptionState.runtimeStatus, {
303
- raw: parsed,
304
- timestamp: new Date().toISOString(),
305
- }, DEVICE_LOG_MEMORY_LIMIT);
306
- }
307
- }
308
- catch {
309
- // 忽略单条非法 JSON,避免因为异常日志阻塞整个后台连接
310
- }
311
- }
312
- splitIndex = buffer.search(/\r?\n\r?\n/);
313
- }
314
- }
315
- if (generation === deviceLogSubscriptionState.generation) {
316
- scheduleDeviceLogReconnect(generation);
317
- }
318
- }
319
- catch (error) {
320
- if (error instanceof Error &&
321
- (error.name === "AbortError" ||
322
- error.message === "The operation was aborted.")) {
323
- return;
324
- }
325
- if (generation === deviceLogSubscriptionState.generation) {
326
- deviceLogSubscriptionState.status = "error";
327
- deviceLogSubscriptionState.lastError =
328
- error instanceof Error ? error.message : String(error);
329
- scheduleDeviceLogReconnect(generation);
330
- }
331
- }
332
- finally {
333
- if (deviceLogSubscriptionState.controller === controller) {
334
- deviceLogSubscriptionState.controller = null;
335
- }
336
- }
337
- }
338
- /**
339
- * 确保后台日志订阅处于目标设备上
340
- * - 若目标未变化且订阅仍在运行,则直接复用已有连接
341
- * - 若目标发生变化,则重置缓存并重新建立订阅
342
- * @param ip 设备 IP
343
- * @param port 设备端口
344
- * @returns 返回是否复用了现有订阅
345
- * @example
346
- * const reused = ensureDeviceLogSubscription("192.168.1.10", 9800)
347
- */
348
- function ensureDeviceLogSubscription(ip, port) {
349
- const sameTarget = deviceLogSubscriptionState.target?.ip === ip &&
350
- deviceLogSubscriptionState.target?.port === port;
351
- const isActive = isDeviceLogSubscriptionActive();
352
- if (sameTarget && isActive) {
353
- return true;
354
- }
355
- stopDeviceLogSubscription();
356
- deviceLogSubscriptionState.generation += 1;
357
- deviceLogSubscriptionState.target = { ip, port };
358
- deviceLogSubscriptionState.status = "connecting";
359
- resetDeviceLogSnapshotState();
360
- void startDeviceLogSubscriptionLoop(deviceLogSubscriptionState.generation);
361
- return false;
362
- }
363
- /**
364
- * 生成日志缓存快照文本
365
- * @param limit 需要返回的日志条数
366
- * @param runtimeStatusLimit 需要返回的 runtime_status 条数
367
- * @returns 返回用于 MCP 文本响应的快照内容
368
- * @example
369
- * const text = buildDeviceLogSnapshotText(100, 20)
370
- */
371
- function buildDeviceLogSnapshotText(limit, runtimeStatusLimit) {
372
- const recentLogs = deviceLogSubscriptionState.logs
373
- .slice(-limit)
374
- .map((log) => `[${formatDisplayTimestamp(log.timestamp)}] [${log.level.toUpperCase()}] ${log.message}`);
375
- const recentRuntimeStatus = deviceLogSubscriptionState.runtimeStatus
376
- .slice(-runtimeStatusLimit)
377
- .map((entry) => formatRuntimeStatusEntry(entry));
378
- return [
379
- `设备: ${deviceLogSubscriptionState.target
380
- ? `${deviceLogSubscriptionState.target.ip}:${deviceLogSubscriptionState.target.port}`
381
- : "未订阅"}`,
382
- `状态: ${formatDeviceLogSubscriptionStatus(deviceLogSubscriptionState.status)}`,
383
- ...(deviceLogSubscriptionState.lastError
384
- ? [`最近错误: ${deviceLogSubscriptionState.lastError}`]
385
- : []),
386
- "",
387
- `最新日志(最多 ${limit} 条):`,
388
- recentLogs.length > 0 ? recentLogs.join("\n") : "无日志",
389
- "",
390
- `最新运行状态(最多 ${runtimeStatusLimit} 条):`,
391
- recentRuntimeStatus.length > 0
392
- ? recentRuntimeStatus.join("\n")
393
- : "无运行状态",
394
- ].join("\n");
395
- }
396
- /**
397
- * 按日志页面风格格式化时间
398
- * @param value ISO 时间字符串
399
- * @returns 返回 `yyyy/mm/dd hh:mm:ss.xxx` 风格文本,异常时回退原值
400
- * @example
401
- * formatDisplayTimestamp("2026-05-07T11:34:48.776Z")
402
- */
403
- function formatDisplayTimestamp(value) {
404
- const date = new Date(value);
405
- if (Number.isNaN(date.getTime())) {
406
- return value;
407
- }
408
- const pad = (number, length = 2) => String(number).padStart(length, "0");
409
- return `${date.getFullYear()}/${pad(date.getMonth() + 1)}/${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds(), 3)}`;
410
- }
411
- /**
412
- * 将日志订阅连接状态转为中文文本
413
- * @param status 原始连接状态
414
- * @returns 返回中文状态说明
415
- * @example
416
- * formatDeviceLogSubscriptionStatus("connected")
417
- */
418
- function formatDeviceLogSubscriptionStatus(status) {
419
- switch (status) {
420
- case "idle":
421
- return "未订阅";
422
- case "connecting":
423
- return "连接中";
424
- case "connected":
425
- return "已连接";
426
- case "reconnecting":
427
- return "重连中";
428
- case "error":
429
- return "连接异常";
430
- default:
431
- return status;
432
- }
433
- }
434
- /**
435
- * 将 runtime_status 中的内存数值格式化为可读文本
436
- * @param value 原始数值
437
- * @param suffix 数值单位后缀
438
- * @returns 数值存在时返回格式化结果,否则返回 unknown
439
- * @example
440
- * formatRuntimeMetricText(12.3456, " MB")
441
- */
442
- function formatRuntimeMetricText(value, suffix = "") {
443
- if (typeof value === "number" && Number.isFinite(value)) {
444
- return `${value.toFixed(2)}${suffix}`;
445
- }
446
- return "未知";
447
- }
448
- /**
449
- * 将单条 runtime_status 事件格式化为中文运行状态摘要
450
- * @param entry runtime_status 条目
451
- * @returns 返回更易读的状态文本
452
- * @example
453
- * formatRuntimeStatusEntry({ raw: { isRunning: false }, timestamp: "2026-01-01T00:00:00.000Z" })
454
- */
455
- function formatRuntimeStatusEntry(entry) {
456
- const raw = entry.raw;
457
- const memory = raw.memory && typeof raw.memory === "object"
458
- ? raw.memory
459
- : {};
460
- return [
461
- `[${formatDisplayTimestamp(entry.timestamp)}]`,
462
- `UI: ${typeof raw.isUIShowing === "boolean"
463
- ? raw.isUIShowing
464
- ? "显示中"
465
- : "未显示"
466
- : "未知"}`,
467
- `脚本: ${typeof raw.isRunning === "boolean"
468
- ? raw.isRunning
469
- ? "运行中"
470
- : "未运行"
471
- : "未知"}`,
472
- `总内存: ${formatRuntimeMetricText(memory.total, " MB")}`,
473
- `可用: ${formatRuntimeMetricText(memory.available, " MB")}`,
474
- `已用: ${formatRuntimeMetricText(memory.used, " MB")}`,
475
- `占用: ${formatRuntimeMetricText(memory.usagePercentage, "%")}`,
476
- ].join(" | ");
477
- }
478
- /**
479
- * 格式化单条 API 文档摘要
480
- * @param language 文档语言
481
- * @param title 文档标题
482
- * @param slug 文档 slug
483
- * @param index 列表序号
484
- * @returns 返回用于列表展示的文本块
485
- * @example
486
- * formatApiDocSummary("js", "Tap", "tap", 0)
487
- */
488
- function formatApiDocSummary(language, title, slug, index) {
489
- return `${index + 1}. ${title}\nslug: ${slug}\nuri: ${(0, docs_service_1.getDocUri)(language, slug)}`;
490
- }
491
- /**
492
- * 注册文档资源
493
- * @param server MCP 服务实例
494
- * @returns 无返回值
495
- * @example
496
- * registerDocResources(server)
497
- */
498
- function registerDocResources(server) {
499
- server.registerResource("current-language", "kuaijs://docs/current-language", {
500
- title: "Current KuaiJS API language",
501
- mimeType: "application/json",
502
- }, async () => {
503
- const language = (0, docs_service_1.getCurrentDocsLanguage)();
504
- return {
505
- contents: [
506
- {
507
- uri: "kuaijs://docs/current-language",
508
- mimeType: "application/json",
509
- text: JSON.stringify({
510
- currentLanguage: language,
511
- label: types_1.DOC_LANGUAGE_LABELS[language],
512
- }, null, 2),
513
- },
514
- ],
515
- };
516
- });
517
- server.registerResource("api-doc", new mcp_js_1.ResourceTemplate("kuaijs://docs/current/{slug}", {
518
- list: async () => {
519
- const language = (0, docs_service_1.getCurrentDocsLanguage)();
520
- const docs = await (0, docs_service_1.getApiDocsByLanguage)(language);
521
- return {
522
- resources: docs.map((doc) => ({
523
- uri: `kuaijs://docs/current/${doc.slug}`,
524
- name: doc.title,
525
- description: `[${types_1.DOC_LANGUAGE_LABELS[language]}] ${doc.slug}`,
526
- mimeType: "text/markdown",
527
- })),
528
- };
529
- },
530
- }), {
531
- title: "KuaiJS API documents",
532
- mimeType: "text/markdown",
533
- }, async (_uri, variables) => {
534
- const language = (0, docs_service_1.getCurrentDocsLanguage)();
535
- const docs = await (0, docs_service_1.getApiDocsByLanguage)(language);
536
- const slug = String(variables.slug || "");
537
- const target = docs.find((doc) => doc.slug === slug);
538
- if (!target) {
539
- throw new Error(`文档不存在: ${slug}`);
540
- }
541
- return {
542
- contents: [
543
- {
544
- uri: `kuaijs://docs/current/${target.slug}`,
545
- mimeType: "text/markdown",
546
- text: target.content,
547
- },
548
- ],
549
- };
550
- });
551
- }
552
- /**
553
- * 注册文档工具
554
- * @param server MCP 服务实例
555
- * @returns 无返回值
556
- * @example
557
- * registerDocTools(server)
558
- */
559
- function registerDocTools(server) {
560
- server.registerTool("get_docs_language", {
561
- title: "Get Docs Language",
562
- description: "获取当前 KuaiJS API 文档语言。",
563
- inputSchema: {},
564
- }, async () => {
565
- const language = (0, docs_service_1.getCurrentDocsLanguage)();
566
- return createTextToolResult(`当前文档语言: ${language} (${types_1.DOC_LANGUAGE_LABELS[language]})`);
567
- });
568
- server.registerTool("set_docs_language", {
569
- title: "Set Docs Language",
570
- description: "设置当前 KuaiJS API 文档语言,影响后续文档查询与读取。",
571
- inputSchema: {
572
- language: z
573
- .enum(["js", "js_zh", "python"])
574
- .describe("文档语言:js | js_zh | python"),
575
- },
576
- }, async ({ language }) => {
577
- const active = (0, docs_service_1.setCurrentDocsLanguage)(language);
578
- const docsDir = (0, docs_service_1.getDocsDirByLanguage)(active);
579
- return createTextToolResult(`文档语言已切换为 ${active} (${types_1.DOC_LANGUAGE_LABELS[active]})\n目录: ${docsDir}`);
580
- });
581
- server.registerTool("list_api_docs", {
582
- title: "List API Docs",
583
- description: "列出当前(或指定)语言下可用的 API 文档。",
584
- inputSchema: {
585
- language: z
586
- .enum(["js", "js_zh", "python"])
587
- .optional()
588
- .describe("可选语言,不传则使用当前语言"),
589
- },
590
- }, async ({ language }) => {
591
- const activeLanguage = (0, docs_service_1.resolveDocsLanguage)(language);
592
- const docs = await (0, docs_service_1.getApiDocsByLanguage)(activeLanguage);
593
- const lines = docs.map((doc, index) => formatApiDocSummary(activeLanguage, doc.title, doc.slug, index));
594
- return createTextToolResult(lines.length === 0
595
- ? `当前语言 ${activeLanguage} 下没有可用文档。`
596
- : [
597
- `当前语言: ${activeLanguage} (${types_1.DOC_LANGUAGE_LABELS[activeLanguage]})`,
598
- "",
599
- ...lines,
600
- ].join("\n"));
601
- });
602
- server.registerTool("search_api_docs", {
603
- title: "Search API Docs",
604
- description: "在当前(或指定)语言文档中按关键字搜索。",
605
- inputSchema: {
606
- query: z.string().min(1).describe("搜索关键字"),
607
- limit: z
608
- .number()
609
- .int()
610
- .min(1)
611
- .max(20)
612
- .optional()
613
- .default(5)
614
- .describe("返回条数上限,默认 5"),
615
- language: z
616
- .enum(["js", "js_zh", "python"])
617
- .optional()
618
- .describe("可选语言,不传则使用当前语言"),
619
- },
620
- }, async ({ query, limit, language }) => {
621
- const activeLanguage = (0, docs_service_1.resolveDocsLanguage)(language);
622
- const docs = await (0, docs_service_1.getApiDocsByLanguage)(activeLanguage);
623
- const normalizedQuery = query.toLowerCase();
624
- const matches = docs
625
- .map((doc) => ({ doc, score: (0, docs_service_1.scoreApiDoc)(doc, normalizedQuery) }))
626
- .filter((item) => item.score > 0)
627
- .sort((a, b) => b.score - a.score || a.doc.slug.localeCompare(b.doc.slug))
628
- .slice(0, limit)
629
- .map((item) => item.doc);
630
- const text = matches.length === 0
631
- ? `当前语言: ${activeLanguage} (${types_1.DOC_LANGUAGE_LABELS[activeLanguage]})\n未找到与 "${query}" 匹配的文档。`
632
- : [
633
- `当前语言: ${activeLanguage} (${types_1.DOC_LANGUAGE_LABELS[activeLanguage]})`,
634
- "",
635
- ...matches.map((doc, index) => formatApiDocSummary(activeLanguage, doc.title, doc.slug, index)),
636
- ].join("\n");
637
- return createTextToolResult(text);
638
- });
639
- server.registerTool("read_api_doc", {
640
- title: "Read API Doc",
641
- description: "读取当前(或指定)语言下某个文档的完整 markdown 内容。",
642
- inputSchema: {
643
- slug: z.string().min(1).describe("文档 slug(文件名,不含 .md)"),
644
- language: z
645
- .enum(["js", "js_zh", "python"])
646
- .optional()
647
- .describe("可选语言,不传则使用当前语言"),
648
- },
649
- }, async ({ slug, language }) => {
650
- const activeLanguage = (0, docs_service_1.resolveDocsLanguage)(language);
651
- const docs = await (0, docs_service_1.getApiDocsByLanguage)(activeLanguage);
652
- const target = docs.find((doc) => doc.slug === slug);
653
- if (!target) {
654
- return createTextToolResult(`未找到文档 ${slug}.md(语言: ${activeLanguage})。`, true);
655
- }
656
- return createTextToolResult(`标题: ${target.title}\n语言: ${activeLanguage}\nURI: ${(0, docs_service_1.getDocUri)(activeLanguage, target.slug)}\n\n${target.content}`);
657
- });
658
- }
659
- /**
660
- * 注册设备与项目执行工具
661
- * @param server MCP 服务实例
662
- * @returns 无返回值
663
- * @example
664
- * registerRuntimeTools(server)
665
- */
666
- function registerRuntimeTools(server) {
667
- /**
668
- * MCP 进程内当前工作目录(仅内存态,不落盘)
669
- * 说明:用于同一轮 MCP 进程内复用目录,进程重启后需由 AI 再次调用 set_workspace 设置。
670
- */
671
- let currentWorkspacePath;
672
- /**
673
- * 解析本次调用使用的工作目录
674
- * 优先级:workspacePath 参数 > set_workspace 记忆值
675
- * @param workspacePath 可选工作目录
676
- * @returns 返回生效的绝对路径
677
- * @example
678
- * const workspace = resolveWorkspacePath("/Users/demo/project")
679
- */
680
- function resolveWorkspacePath(workspacePath) {
681
- if (workspacePath && workspacePath.trim()) {
682
- currentWorkspacePath = path.resolve(workspacePath.trim());
683
- return currentWorkspacePath;
684
- }
685
- if (currentWorkspacePath) {
686
- return currentWorkspacePath;
687
- }
688
- throw new Error("未设置工作目录:请先调用 set_workspace,或在本次工具调用时传 workspacePath。");
689
- }
690
- /**
691
- * 校验并返回有效工作目录
692
- * @param workspacePath 可选工作目录
693
- * @returns 返回校验通过的绝对路径
694
- * @example
695
- * const workspace = await ensureWorkspacePath("/Users/demo/project")
696
- */
697
- async function ensureWorkspacePath(workspacePath) {
698
- const workspace = resolveWorkspacePath(workspacePath);
699
- await (0, project_2.ensureValidKuaiJSProject)(workspace);
700
- return workspace;
701
- }
702
- server.registerTool("set_workspace", {
703
- title: "Set Workspace",
704
- description: "设置当前 MCP 进程内的默认工作目录。后续 build/run/package 优先使用该目录。",
705
- inputSchema: {
706
- workspacePath: z
707
- .string()
708
- .min(1)
709
- .describe("快点JS 项目目录绝对路径(需包含 package.json 与 scripts)"),
710
- },
711
- }, async ({ workspacePath }) => {
712
- const workspace = await ensureWorkspacePath(workspacePath);
713
- return createTextToolResult(`默认工作目录已设置为: ${workspace}`);
714
- });
715
- server.registerTool("get_workspace", {
716
- title: "Get Workspace",
717
- description: "查看当前 MCP 进程内记忆的工作目录。",
718
- inputSchema: {},
719
- }, async () => {
720
- if (!currentWorkspacePath) {
721
- return createTextToolResult("当前未设置工作目录,请先调用 set_workspace。");
722
- }
723
- return createTextToolResult(`当前工作目录: ${currentWorkspacePath}`);
724
- });
725
- /**
726
- * 解析当前默认 HTTP 设备
727
- * - 当前 MCP 仅支持单设备模型
728
- * - 所有设备工具默认复用 set_device 保存的连接信息
729
- * @returns 返回标准化后的 HTTP 请求目标
730
- * @example
731
- * const target = await resolveRuntimeHttpTarget()
732
- */
733
- async function resolveRuntimeHttpTarget() {
734
- let device;
735
- try {
736
- device = await (0, device_config_1.resolveDeviceConfig)();
737
- }
738
- catch {
739
- throw new Error("未设置设备:请先调用 set_device。");
740
- }
741
- return {
742
- ip: device.ip,
743
- port: (0, device_config_1.normalizePort)(device.port),
744
- label: `${device.ip}:${device.port}`,
745
- };
746
- }
747
- server.registerTool("set_device", {
748
- title: "Set Device",
749
- description: "设置当前唯一设备连接信息。设置后其余设备工具均复用该设备。",
750
- inputSchema: {
751
- ip: z.string().min(1).describe("设备 IP 地址,例如 192.168.1.100"),
752
- port: z
753
- .number()
754
- .int()
755
- .min(1)
756
- .max(65535)
757
- .optional()
758
- .describe("设备端口,默认 9800"),
759
- },
760
- }, async ({ ip, port }) => {
761
- const config = await (0, device_config_1.setDeviceConfig)(ip, port);
762
- const reusedSubscription = ensureDeviceLogSubscription(config.ip, config.port);
763
- return createTextToolResult([
764
- `默认设备已设置为 ${config.ip}:${config.port}`,
765
- reusedSubscription
766
- ? "SSE 日志后台订阅已复用现有连接"
767
- : `SSE 日志后台订阅已启动,内存缓存上限 ${DEVICE_LOG_MEMORY_LIMIT} 条`,
768
- ].join("\n"));
769
- });
770
- server.registerTool("get_device", {
771
- title: "Get Device",
772
- description: "查看当前默认设备配置。",
773
- inputSchema: {},
774
- }, async () => {
775
- const config = await (0, device_config_1.readDeviceConfig)();
776
- if (!config) {
777
- return createTextToolResult("当前未设置默认设备,请先调用 set_device。");
778
- }
779
- return createTextToolResult(`当前默认设备: ${config.ip}:${config.port}`);
780
- });
781
- server.registerTool("take_screenshot", {
782
- title: "Take Screenshot",
783
- description: "获取当前默认设备截图。可返回 base64,或落地到文件后返回文件路径(默认写入系统临时目录)。",
784
- inputSchema: {
785
- format: z
786
- .enum(["file", "base64"])
787
- .optional()
788
- .default("file")
789
- .describe("截图返回格式:file=文件路径,base64=图片 base64"),
790
- outputPath: z
791
- .string()
792
- .min(1)
793
- .optional()
794
- .describe("当 format=file 时可指定输出路径,不传则写入系统临时目录"),
795
- },
796
- }, async ({ format, outputPath }) => {
797
- const target = await resolveRuntimeHttpTarget();
798
- const requestOptions = createRuntimeHttpRequestOptions(target);
799
- if (format === "base64") {
800
- const base64 = await (0, project_1.getScreenshotBase64OnDevice)(requestOptions);
801
- return createTextToolResult(`截图成功: ${target.label}\nformat: base64\n${base64}`);
802
- }
803
- const image = await (0, project_1.getScreenshotOnDevice)(requestOptions);
804
- const targetPath = outputPath && outputPath.trim()
805
- ? path.resolve(outputPath.trim())
806
- : path.join(os.tmpdir(), `ms-mcp-screenshot-${Date.now()}-${Math.random()
807
- .toString(36)
808
- .slice(2, 8)}.jpg`);
809
- await fsExtra.ensureDir(path.dirname(targetPath));
810
- await fsExtra.writeFile(targetPath, image);
811
- return createTextToolResult(`截图成功: ${target.label}\nformat: file\npath: ${targetPath}\nsize: ${image.length} bytes`);
812
- });
813
- server.registerTool("get_node_source", {
814
- title: "Get Node Source",
815
- description: "获取当前默认设备页面节点 XML(/api/source)。",
816
- inputSchema: {
817
- maxDepth: z
818
- .number()
819
- .int()
820
- .min(1)
821
- .max(200)
822
- .optional()
823
- .default(50)
824
- .describe("节点树最大深度,默认 50"),
825
- timeout: z
826
- .number()
827
- .int()
828
- .min(1)
829
- .max(600)
830
- .optional()
831
- .default(120)
832
- .describe("设备端节点抓取超时秒数,默认 120"),
833
- },
834
- }, async ({ maxDepth, timeout }) => {
835
- const target = await resolveRuntimeHttpTarget();
836
- const requestOptions = createRuntimeHttpRequestOptions(target);
837
- const source = await (0, project_1.getSourceOnDevice)(requestOptions, maxDepth, timeout);
838
- return createTextToolResult(`节点获取成功: ${target.label}\nmaxDepth: ${maxDepth}\ntimeout: ${timeout}\n\n${source}`);
839
- });
840
- server.registerTool("get_logs", {
841
- title: "Get Device Logs",
842
- description: "获取当前默认设备的日志缓存快照。调用 set_device 后会自动建立 SSE 后台订阅,此工具只返回当前已缓存的日志内容。",
843
- inputSchema: {
844
- limit: z
845
- .number()
846
- .int()
847
- .min(1)
848
- .max(5000)
849
- .optional()
850
- .default(200)
851
- .describe("返回最近日志条数,默认 200,最大 5000"),
852
- runtimeStatusLimit: z
853
- .number()
854
- .int()
855
- .min(0)
856
- .max(200)
857
- .optional()
858
- .default(20)
859
- .describe("返回最近 runtime_status 条数,默认 20"),
860
- },
861
- }, async ({ limit, runtimeStatusLimit }) => {
862
- if (!deviceLogSubscriptionState.target) {
863
- const device = await (0, device_config_1.readDeviceConfig)();
864
- if (device) {
865
- ensureDeviceLogSubscription(device.ip, device.port);
866
- }
867
- }
868
- return createTextToolResult(buildDeviceLogSnapshotText(limit, runtimeStatusLimit));
869
- });
870
- server.registerTool("package_project", {
871
- title: "Package Project",
872
- description: "执行生产构建并加密,返回 enc.msbundle 路径。",
873
- inputSchema: {
874
- workspacePath: z
875
- .string()
876
- .min(1)
877
- .optional()
878
- .describe("可选工作目录;不传时使用 set_workspace 记忆值"),
879
- },
880
- }, async ({ workspacePath }) => {
881
- const workspace = await ensureWorkspacePath(workspacePath);
882
- const encryptedPath = await (0, packager_1.packageProject)(workspace);
883
- return createTextToolResult(`打包完成: ${encryptedPath}`);
884
- });
885
- server.registerTool("run_project", {
886
- title: "Run Project",
887
- description: "构建并同步到当前默认设备后运行项目,仅使用 HTTP 设备连接。",
888
- inputSchema: {
889
- workspacePath: z
890
- .string()
891
- .min(1)
892
- .optional()
893
- .describe("可选工作目录;不传时使用 set_workspace 记忆值"),
894
- },
895
- }, async ({ workspacePath }) => {
896
- const workspace = await ensureWorkspacePath(workspacePath);
897
- const target = await resolveRuntimeHttpTarget();
898
- await (0, project_1.runOnDevice)({
899
- ...createRuntimeHttpRequestOptions(target),
900
- workspacePath: workspace,
901
- });
902
- return createTextToolResult(`运行请求已发送到 ${target.label}`);
903
- });
904
- server.registerTool("run_ui_project", {
905
- title: "Run UI Project",
906
- description: "构建并同步到当前默认设备后预览 UI,仅使用 HTTP 设备连接。",
907
- inputSchema: {
908
- workspacePath: z
909
- .string()
910
- .min(1)
911
- .optional()
912
- .describe("可选工作目录;不传时使用 set_workspace 记忆值"),
913
- },
914
- }, async ({ workspacePath }) => {
915
- const workspace = await ensureWorkspacePath(workspacePath);
916
- const target = await resolveRuntimeHttpTarget();
917
- await (0, project_1.runUIOnDevice)({
918
- ...createRuntimeHttpRequestOptions(target),
919
- workspacePath: workspace,
920
- });
921
- return createTextToolResult(`UI 预览请求已发送到 ${target.label}`);
922
- });
923
- server.registerTool("stop_project", {
924
- title: "Stop Project",
925
- description: "停止当前默认设备上的项目,仅使用 HTTP 设备连接。",
926
- inputSchema: {},
927
- }, async () => {
928
- const target = await resolveRuntimeHttpTarget();
929
- await (0, project_1.stopOnDevice)(createRuntimeHttpRequestOptions(target));
930
- return createTextToolResult(`停止请求已发送到 ${target.label}`);
931
- });
932
- }
9
+ const doc_tools_1 = require("./doc-tools");
10
+ const runtime_tools_1 = require("./runtime-tools");
933
11
  /**
934
12
  * 创建并注册 MCP 工具
935
13
  * @returns 返回已注册工具的 MCP Server 实例
@@ -941,8 +19,8 @@ function createMcpServer() {
941
19
  name: "ms-vite-plugin-mcp",
942
20
  version: version_1.PACKAGE_VERSION,
943
21
  });
944
- registerDocResources(server);
945
- registerDocTools(server);
946
- registerRuntimeTools(server);
22
+ (0, doc_tools_1.registerDocResources)(server);
23
+ (0, doc_tools_1.registerDocTools)(server);
24
+ (0, runtime_tools_1.registerRuntimeTools)(server);
947
25
  return server;
948
26
  }