ms-vite-plugin 1.1.12 → 1.1.14

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 (2) hide show
  1. package/dist/mcp/tools.js +520 -502
  2. package/package.json +1 -1
package/dist/mcp/tools.js CHANGED
@@ -40,16 +40,444 @@ const z = __importStar(require("zod/v4"));
40
40
  const fsExtra = __importStar(require("fs-extra"));
41
41
  const os = __importStar(require("os"));
42
42
  const path = __importStar(require("path"));
43
- const build_1 = require("../build");
44
43
  const packager_1 = require("../packager");
45
44
  const project_1 = require("../project");
46
- const ws_manager_1 = require("../ws-manager");
47
45
  const project_2 = require("./project");
48
46
  const device_config_1 = require("./device-config");
49
47
  Object.defineProperty(exports, "DEFAULT_DEVICE_PORT", { enumerable: true, get: function () { return device_config_1.DEFAULT_DEVICE_PORT; } });
50
48
  const docs_service_1 = require("./docs-service");
51
49
  const types_1 = require("./types");
52
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) => `[${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 status 原始连接状态
399
+ * @returns 返回中文状态说明
400
+ * @example
401
+ * formatDeviceLogSubscriptionStatus("connected")
402
+ */
403
+ function formatDeviceLogSubscriptionStatus(status) {
404
+ switch (status) {
405
+ case "idle":
406
+ return "未订阅";
407
+ case "connecting":
408
+ return "连接中";
409
+ case "connected":
410
+ return "已连接";
411
+ case "reconnecting":
412
+ return "重连中";
413
+ case "error":
414
+ return "连接异常";
415
+ default:
416
+ return status;
417
+ }
418
+ }
419
+ /**
420
+ * 将 runtime_status 中的布尔值转为中文状态文本
421
+ * @param value 原始布尔值
422
+ * @returns true 返回“是”,false 返回“否”,其它情况返回“unknown”
423
+ * @example
424
+ * formatBooleanStatusText(true)
425
+ */
426
+ function formatBooleanStatusText(value) {
427
+ if (typeof value === "boolean") {
428
+ return value ? "是" : "否";
429
+ }
430
+ return "未知";
431
+ }
432
+ /**
433
+ * 将 runtime_status 中的内存数值格式化为可读文本
434
+ * @param value 原始数值
435
+ * @param suffix 数值单位后缀
436
+ * @returns 数值存在时返回格式化结果,否则返回 unknown
437
+ * @example
438
+ * formatRuntimeMetricText(12.3456, " MB")
439
+ */
440
+ function formatRuntimeMetricText(value, suffix = "") {
441
+ if (typeof value === "number" && Number.isFinite(value)) {
442
+ return `${value.toFixed(2)}${suffix}`;
443
+ }
444
+ return "未知";
445
+ }
446
+ /**
447
+ * 将单条 runtime_status 事件格式化为中文运行状态摘要
448
+ * @param entry runtime_status 条目
449
+ * @returns 返回更易读的状态文本
450
+ * @example
451
+ * formatRuntimeStatusEntry({ raw: { isRunning: false }, timestamp: "2026-01-01T00:00:00.000Z" })
452
+ */
453
+ function formatRuntimeStatusEntry(entry) {
454
+ const raw = entry.raw;
455
+ const memory = raw.memory && typeof raw.memory === "object"
456
+ ? raw.memory
457
+ : {};
458
+ return [
459
+ `[${entry.timestamp}]`,
460
+ `UI: ${formatBooleanStatusText(raw.isUIShowing)}`,
461
+ `脚本: ${formatBooleanStatusText(raw.isRunning)}`,
462
+ `总内存: ${formatRuntimeMetricText(memory.total, " MB")}`,
463
+ `可用: ${formatRuntimeMetricText(memory.available, " MB")}`,
464
+ `已用: ${formatRuntimeMetricText(memory.used, " MB")}`,
465
+ `占用: ${formatRuntimeMetricText(memory.usagePercentage, "%")}`,
466
+ ].join(" | ");
467
+ }
468
+ /**
469
+ * 格式化单条 API 文档摘要
470
+ * @param language 文档语言
471
+ * @param title 文档标题
472
+ * @param slug 文档 slug
473
+ * @param index 列表序号
474
+ * @returns 返回用于列表展示的文本块
475
+ * @example
476
+ * formatApiDocSummary("js", "Tap", "tap", 0)
477
+ */
478
+ function formatApiDocSummary(language, title, slug, index) {
479
+ return `${index + 1}. ${title}\nslug: ${slug}\nuri: ${(0, docs_service_1.getDocUri)(language, slug)}`;
480
+ }
53
481
  /**
54
482
  * 注册文档资源
55
483
  * @param server MCP 服务实例
@@ -125,14 +553,7 @@ function registerDocTools(server) {
125
553
  inputSchema: {},
126
554
  }, async () => {
127
555
  const language = (0, docs_service_1.getCurrentDocsLanguage)();
128
- return {
129
- content: [
130
- {
131
- type: "text",
132
- text: `当前文档语言: ${language} (${types_1.DOC_LANGUAGE_LABELS[language]})`,
133
- },
134
- ],
135
- };
556
+ return createTextToolResult(`当前文档语言: ${language} (${types_1.DOC_LANGUAGE_LABELS[language]})`);
136
557
  });
137
558
  server.registerTool("set_docs_language", {
138
559
  title: "Set Docs Language",
@@ -145,14 +566,7 @@ function registerDocTools(server) {
145
566
  }, async ({ language }) => {
146
567
  const active = (0, docs_service_1.setCurrentDocsLanguage)(language);
147
568
  const docsDir = (0, docs_service_1.getDocsDirByLanguage)(active);
148
- return {
149
- content: [
150
- {
151
- type: "text",
152
- text: `文档语言已切换为 ${active} (${types_1.DOC_LANGUAGE_LABELS[active]})\n目录: ${docsDir}`,
153
- },
154
- ],
155
- };
569
+ return createTextToolResult(`文档语言已切换为 ${active} (${types_1.DOC_LANGUAGE_LABELS[active]})\n目录: ${docsDir}`);
156
570
  });
157
571
  server.registerTool("list_api_docs", {
158
572
  title: "List API Docs",
@@ -166,21 +580,14 @@ function registerDocTools(server) {
166
580
  }, async ({ language }) => {
167
581
  const activeLanguage = (0, docs_service_1.resolveDocsLanguage)(language);
168
582
  const docs = await (0, docs_service_1.getApiDocsByLanguage)(activeLanguage);
169
- const lines = docs.map((doc, index) => `${index + 1}. ${doc.title}\nslug: ${doc.slug}\nuri: ${(0, docs_service_1.getDocUri)(activeLanguage, doc.slug)}`);
170
- return {
171
- content: [
172
- {
173
- type: "text",
174
- text: lines.length === 0
175
- ? `当前语言 ${activeLanguage} 下没有可用文档。`
176
- : [
177
- `当前语言: ${activeLanguage} (${types_1.DOC_LANGUAGE_LABELS[activeLanguage]})`,
178
- "",
179
- ...lines,
180
- ].join("\n"),
181
- },
182
- ],
183
- };
583
+ const lines = docs.map((doc, index) => formatApiDocSummary(activeLanguage, doc.title, doc.slug, index));
584
+ return createTextToolResult(lines.length === 0
585
+ ? `当前语言 ${activeLanguage} 下没有可用文档。`
586
+ : [
587
+ `当前语言: ${activeLanguage} (${types_1.DOC_LANGUAGE_LABELS[activeLanguage]})`,
588
+ "",
589
+ ...lines,
590
+ ].join("\n"));
184
591
  });
185
592
  server.registerTool("search_api_docs", {
186
593
  title: "Search API Docs",
@@ -215,16 +622,9 @@ function registerDocTools(server) {
215
622
  : [
216
623
  `当前语言: ${activeLanguage} (${types_1.DOC_LANGUAGE_LABELS[activeLanguage]})`,
217
624
  "",
218
- ...matches.map((doc, index) => `${index + 1}. ${doc.title}\nslug: ${doc.slug}\nuri: ${(0, docs_service_1.getDocUri)(activeLanguage, doc.slug)}`),
625
+ ...matches.map((doc, index) => formatApiDocSummary(activeLanguage, doc.title, doc.slug, index)),
219
626
  ].join("\n");
220
- return {
221
- content: [
222
- {
223
- type: "text",
224
- text,
225
- },
226
- ],
227
- };
627
+ return createTextToolResult(text);
228
628
  });
229
629
  server.registerTool("read_api_doc", {
230
630
  title: "Read API Doc",
@@ -241,24 +641,9 @@ function registerDocTools(server) {
241
641
  const docs = await (0, docs_service_1.getApiDocsByLanguage)(activeLanguage);
242
642
  const target = docs.find((doc) => doc.slug === slug);
243
643
  if (!target) {
244
- return {
245
- content: [
246
- {
247
- type: "text",
248
- text: `未找到文档 ${slug}.md(语言: ${activeLanguage})。`,
249
- },
250
- ],
251
- isError: true,
252
- };
644
+ return createTextToolResult(`未找到文档 ${slug}.md(语言: ${activeLanguage})。`, true);
253
645
  }
254
- return {
255
- content: [
256
- {
257
- type: "text",
258
- text: `标题: ${target.title}\n语言: ${activeLanguage}\nURI: ${(0, docs_service_1.getDocUri)(activeLanguage, target.slug)}\n\n${target.content}`,
259
- },
260
- ],
261
- };
646
+ return createTextToolResult(`标题: ${target.title}\n语言: ${activeLanguage}\nURI: ${(0, docs_service_1.getDocUri)(activeLanguage, target.slug)}\n\n${target.content}`);
262
647
  });
263
648
  }
264
649
  /**
@@ -315,14 +700,7 @@ function registerRuntimeTools(server) {
315
700
  },
316
701
  }, async ({ workspacePath }) => {
317
702
  const workspace = await ensureWorkspacePath(workspacePath);
318
- return {
319
- content: [
320
- {
321
- type: "text",
322
- text: `默认工作目录已设置为: ${workspace}`,
323
- },
324
- ],
325
- };
703
+ return createTextToolResult(`默认工作目录已设置为: ${workspace}`);
326
704
  });
327
705
  server.registerTool("get_workspace", {
328
706
  title: "Get Workspace",
@@ -330,56 +708,27 @@ function registerRuntimeTools(server) {
330
708
  inputSchema: {},
331
709
  }, async () => {
332
710
  if (!currentWorkspacePath) {
333
- return {
334
- content: [
335
- {
336
- type: "text",
337
- text: "当前未设置工作目录,请先调用 set_workspace。",
338
- },
339
- ],
340
- };
711
+ return createTextToolResult("当前未设置工作目录,请先调用 set_workspace。");
341
712
  }
342
- return {
343
- content: [
344
- {
345
- type: "text",
346
- text: `当前工作目录: ${currentWorkspacePath}`,
347
- },
348
- ],
349
- };
713
+ return createTextToolResult(`当前工作目录: ${currentWorkspacePath}`);
350
714
  });
351
715
  /**
352
- * 解析运行目标:
353
- * - 显式 transport=ws: 强制走 ws
354
- * - 显式 transport=http: 强制走 http
355
- * - 未显式 transport:
356
- * 1) 若传入 ip/port,则走 http
357
- * 2) 若当前已有 ws 设备连接,则优先走 ws
358
- * 3) 否则回退到 http 默认设备配置
359
- * @param options 工具调用参数
360
- * @returns 返回标准化后的请求目标
716
+ * 解析当前默认 HTTP 设备
717
+ * - 当前 MCP 仅支持单设备模型
718
+ * - 所有设备工具默认复用 set_device 保存的连接信息
719
+ * @returns 返回标准化后的 HTTP 请求目标
361
720
  * @example
362
- * const target = await resolvePreferredRuntimeTarget({ ip: "192.168.1.10" })
721
+ * const target = await resolveRuntimeHttpTarget()
363
722
  */
364
- async function resolvePreferredRuntimeTarget(options) {
365
- const hasHttpHint = options.ip !== undefined || options.port !== undefined;
366
- const shouldUseWs = options.transport === "ws" ||
367
- (options.transport === undefined &&
368
- !hasHttpHint &&
369
- ws_manager_1.WSManager.isConnected());
370
- if (shouldUseWs) {
371
- const wsPort = String(options.wsPort ?? 31111);
372
- const wsWaitMs = String(options.wsWaitMs ?? 30000);
373
- return {
374
- transport: "ws",
375
- wsPort,
376
- wsWaitMs,
377
- label: `transport=ws (wsPort=${wsPort})`,
378
- };
723
+ async function resolveRuntimeHttpTarget() {
724
+ let device;
725
+ try {
726
+ device = await (0, device_config_1.resolveDeviceConfig)();
727
+ }
728
+ catch {
729
+ throw new Error("未设置设备:请先调用 set_device。");
379
730
  }
380
- const device = await (0, device_config_1.resolveDeviceConfig)(options.ip, options.port);
381
731
  return {
382
- transport: "http",
383
732
  ip: device.ip,
384
733
  port: (0, device_config_1.normalizePort)(device.port),
385
734
  label: `${device.ip}:${device.port}`,
@@ -387,7 +736,7 @@ function registerRuntimeTools(server) {
387
736
  }
388
737
  server.registerTool("set_device", {
389
738
  title: "Set Device",
390
- description: "设置默认设备连接信息。首次配置后,后续 run/stop 可不再传 ip。",
739
+ description: "设置当前唯一设备连接信息。设置后其余设备工具均复用该设备。",
391
740
  inputSchema: {
392
741
  ip: z.string().min(1).describe("设备 IP 地址,例如 192.168.1.100"),
393
742
  port: z
@@ -400,14 +749,13 @@ function registerRuntimeTools(server) {
400
749
  },
401
750
  }, async ({ ip, port }) => {
402
751
  const config = await (0, device_config_1.setDeviceConfig)(ip, port);
403
- return {
404
- content: [
405
- {
406
- type: "text",
407
- text: `默认设备已设置为 ${config.ip}:${config.port}`,
408
- },
409
- ],
410
- };
752
+ const reusedSubscription = ensureDeviceLogSubscription(config.ip, config.port);
753
+ return createTextToolResult([
754
+ `默认设备已设置为 ${config.ip}:${config.port}`,
755
+ reusedSubscription
756
+ ? "SSE 日志后台订阅已复用现有连接"
757
+ : `SSE 日志后台订阅已启动,内存缓存上限 ${DEVICE_LOG_MEMORY_LIMIT} 条`,
758
+ ].join("\n"));
411
759
  });
412
760
  server.registerTool("get_device", {
413
761
  title: "Get Device",
@@ -416,52 +764,14 @@ function registerRuntimeTools(server) {
416
764
  }, async () => {
417
765
  const config = await (0, device_config_1.readDeviceConfig)();
418
766
  if (!config) {
419
- return {
420
- content: [
421
- { type: "text", text: "当前未设置默认设备,请先调用 set_device。" },
422
- ],
423
- };
767
+ return createTextToolResult("当前未设置默认设备,请先调用 set_device。");
424
768
  }
425
- return {
426
- content: [
427
- { type: "text", text: `当前默认设备: ${config.ip}:${config.port}` },
428
- ],
429
- };
769
+ return createTextToolResult(`当前默认设备: ${config.ip}:${config.port}`);
430
770
  });
431
771
  server.registerTool("take_screenshot", {
432
772
  title: "Take Screenshot",
433
- description: "获取设备截图。可返回 base64,或落地到文件后返回文件路径(默认写入系统临时目录)。",
773
+ description: "获取当前默认设备截图。可返回 base64,或落地到文件后返回文件路径(默认写入系统临时目录)。",
434
774
  inputSchema: {
435
- transport: z
436
- .enum(["http", "ws"])
437
- .optional()
438
- .describe("传输方式:http|ws;不传时自动优先 ws(若已连接)"),
439
- ip: z
440
- .string()
441
- .min(1)
442
- .optional()
443
- .describe("设备 IP 地址(transport=http 时可传;未传则使用 set_device 默认值)"),
444
- port: z
445
- .number()
446
- .int()
447
- .min(1)
448
- .max(65535)
449
- .optional()
450
- .describe("设备端口,默认 9800"),
451
- wsPort: z
452
- .number()
453
- .int()
454
- .min(1)
455
- .max(65535)
456
- .optional()
457
- .describe("WS 服务端口(transport=ws 时可选,默认 31111)"),
458
- wsWaitMs: z
459
- .number()
460
- .int()
461
- .min(1)
462
- .max(600000)
463
- .optional()
464
- .describe("WS 等待连接超时毫秒(transport=ws 时可选,默认 30000)"),
465
775
  format: z
466
776
  .enum(["file", "base64"])
467
777
  .optional()
@@ -473,35 +783,12 @@ function registerRuntimeTools(server) {
473
783
  .optional()
474
784
  .describe("当 format=file 时可指定输出路径,不传则写入系统临时目录"),
475
785
  },
476
- }, async ({ transport, ip, port, wsPort, wsWaitMs, format, outputPath }) => {
477
- const target = await resolvePreferredRuntimeTarget({
478
- transport,
479
- ip,
480
- port,
481
- wsPort,
482
- wsWaitMs,
483
- });
484
- const requestOptions = target.transport === "ws"
485
- ? {
486
- transport: "ws",
487
- wsPort: target.wsPort,
488
- wsWaitMs: target.wsWaitMs,
489
- }
490
- : {
491
- ip: target.ip,
492
- port: target.port,
493
- transport: "http",
494
- };
786
+ }, async ({ format, outputPath }) => {
787
+ const target = await resolveRuntimeHttpTarget();
788
+ const requestOptions = createRuntimeHttpRequestOptions(target);
495
789
  if (format === "base64") {
496
790
  const base64 = await (0, project_1.getScreenshotBase64OnDevice)(requestOptions);
497
- return {
498
- content: [
499
- {
500
- type: "text",
501
- text: `截图成功: ${target.label}\nformat: base64\n${base64}`,
502
- },
503
- ],
504
- };
791
+ return createTextToolResult(`截图成功: ${target.label}\nformat: base64\n${base64}`);
505
792
  }
506
793
  const image = await (0, project_1.getScreenshotOnDevice)(requestOptions);
507
794
  const targetPath = outputPath && outputPath.trim()
@@ -511,49 +798,12 @@ function registerRuntimeTools(server) {
511
798
  .slice(2, 8)}.jpg`);
512
799
  await fsExtra.ensureDir(path.dirname(targetPath));
513
800
  await fsExtra.writeFile(targetPath, image);
514
- return {
515
- content: [
516
- {
517
- type: "text",
518
- text: `截图成功: ${target.label}\nformat: file\npath: ${targetPath}\nsize: ${image.length} bytes`,
519
- },
520
- ],
521
- };
801
+ return createTextToolResult(`截图成功: ${target.label}\nformat: file\npath: ${targetPath}\nsize: ${image.length} bytes`);
522
802
  });
523
803
  server.registerTool("get_node_source", {
524
804
  title: "Get Node Source",
525
- description: "获取设备当前页面节点 XML(/api/source)。",
805
+ description: "获取当前默认设备页面节点 XML(/api/source)。",
526
806
  inputSchema: {
527
- transport: z
528
- .enum(["http", "ws"])
529
- .optional()
530
- .describe("传输方式:http|ws;不传时自动优先 ws(若已连接)"),
531
- ip: z
532
- .string()
533
- .min(1)
534
- .optional()
535
- .describe("设备 IP 地址(transport=http 时可传;未传则使用 set_device 默认值)"),
536
- port: z
537
- .number()
538
- .int()
539
- .min(1)
540
- .max(65535)
541
- .optional()
542
- .describe("设备端口,默认 9800"),
543
- wsPort: z
544
- .number()
545
- .int()
546
- .min(1)
547
- .max(65535)
548
- .optional()
549
- .describe("WS 服务端口(transport=ws 时可选,默认 31111)"),
550
- wsWaitMs: z
551
- .number()
552
- .int()
553
- .min(1)
554
- .max(600000)
555
- .optional()
556
- .describe("WS 等待连接超时毫秒(transport=ws 时可选,默认 30000)"),
557
807
  maxDepth: z
558
808
  .number()
559
809
  .int()
@@ -571,113 +821,41 @@ function registerRuntimeTools(server) {
571
821
  .default(120)
572
822
  .describe("设备端节点抓取超时秒数,默认 120"),
573
823
  },
574
- }, async ({ transport, ip, port, wsPort, wsWaitMs, maxDepth, timeout }) => {
575
- const target = await resolvePreferredRuntimeTarget({
576
- transport,
577
- ip,
578
- port,
579
- wsPort,
580
- wsWaitMs,
581
- });
582
- const requestOptions = target.transport === "ws"
583
- ? {
584
- transport: "ws",
585
- wsPort: target.wsPort,
586
- wsWaitMs: target.wsWaitMs,
587
- }
588
- : {
589
- ip: target.ip,
590
- port: target.port,
591
- transport: "http",
592
- };
824
+ }, async ({ maxDepth, timeout }) => {
825
+ const target = await resolveRuntimeHttpTarget();
826
+ const requestOptions = createRuntimeHttpRequestOptions(target);
593
827
  const source = await (0, project_1.getSourceOnDevice)(requestOptions, maxDepth, timeout);
594
- return {
595
- content: [
596
- {
597
- type: "text",
598
- text: `节点获取成功: ${target.label}\nmaxDepth: ${maxDepth}\ntimeout: ${timeout}\n\n${source}`,
599
- },
600
- ],
601
- };
828
+ return createTextToolResult(`节点获取成功: ${target.label}\nmaxDepth: ${maxDepth}\ntimeout: ${timeout}\n\n${source}`);
602
829
  });
603
- server.registerTool("watch_logs", {
604
- title: "Watch Logs",
605
- description: "通过 SSE 监听设备实时日志。首次可传 ip,也可复用 set_device 保存的默认设备。",
830
+ server.registerTool("get_logs", {
831
+ title: "Get Device Logs",
832
+ description: "获取当前默认设备的日志缓存快照。调用 set_device 后会自动建立 SSE 后台订阅,此工具只返回当前已缓存的日志内容。",
606
833
  inputSchema: {
607
- ip: z
608
- .string()
609
- .min(1)
610
- .optional()
611
- .describe("设备 IP 地址,未传则使用 set_device 保存的默认值"),
612
- port: z
834
+ limit: z
613
835
  .number()
614
836
  .int()
615
837
  .min(1)
616
- .max(65535)
838
+ .max(5000)
617
839
  .optional()
618
- .describe("设备端口,默认 9800"),
619
- durationSeconds: z
840
+ .default(200)
841
+ .describe("返回最近日志条数,默认 200,最大 5000"),
842
+ runtimeStatusLimit: z
620
843
  .number()
621
844
  .int()
622
845
  .min(0)
623
- .max(300)
624
- .optional()
625
- .default(15)
626
- .describe("监听时长(秒),默认 15 秒;传 0 表示持续监听直到连接断开"),
627
- maxLogs: z
628
- .number()
629
- .int()
630
- .min(10)
631
- .max(5000)
632
- .optional()
633
- .default(200)
634
- .describe("最多收集日志条数,默认 200"),
635
- },
636
- }, async ({ ip, port, durationSeconds, maxLogs }) => {
637
- const device = await (0, device_config_1.resolveDeviceConfig)(ip, port);
638
- const result = await (0, project_1.watchDeviceLogsBySse)(device.ip, device.port, durationSeconds * 1000, maxLogs);
639
- const logsPreview = result.logs
640
- .slice(-20)
641
- .map((log) => `[${log.timestamp}] [${log.level.toUpperCase()}] ${log.message}`)
642
- .join("\n");
643
- return {
644
- content: [
645
- {
646
- type: "text",
647
- text: [
648
- `监听目标: ${device.ip}:${device.port}`,
649
- `监听模式: ${durationSeconds === 0 ? "持续直到连接断开" : `${durationSeconds}s`}`,
650
- `结束原因: ${result.stopReason},收集日志: ${result.logs.length} 条,runtime_status: ${result.runtimeStatus.length} 条`,
651
- "",
652
- "最新日志(最多展示最后20条):",
653
- logsPreview || "无日志",
654
- ].join("\n"),
655
- },
656
- ],
657
- };
658
- });
659
- server.registerTool("build_project", {
660
- title: "Build Project",
661
- description: "构建 KuaiJS 项目,支持开发模式与生产模式。",
662
- inputSchema: {
663
- workspacePath: z
664
- .string()
665
- .min(1)
666
- .optional()
667
- .describe("可选工作目录;不传时使用 set_workspace 记忆值"),
668
- dev: z
669
- .boolean()
846
+ .max(200)
670
847
  .optional()
671
- .describe("是否开发模式构建,true=开发模式,false=生产模式"),
848
+ .default(20)
849
+ .describe("返回最近 runtime_status 条数,默认 20"),
672
850
  },
673
- }, async ({ workspacePath, dev }) => {
674
- const workspace = await ensureWorkspacePath(workspacePath);
675
- await (0, build_1.buildAll)(Boolean(dev), workspace);
676
- return {
677
- content: [
678
- { type: "text", text: `构建完成,模式: ${dev ? "dev" : "prod"}` },
679
- ],
680
- };
851
+ }, async ({ limit, runtimeStatusLimit }) => {
852
+ if (!deviceLogSubscriptionState.target) {
853
+ const device = await (0, device_config_1.readDeviceConfig)();
854
+ if (device) {
855
+ ensureDeviceLogSubscription(device.ip, device.port);
856
+ }
857
+ }
858
+ return createTextToolResult(buildDeviceLogSnapshotText(limit, runtimeStatusLimit));
681
859
  });
682
860
  server.registerTool("package_project", {
683
861
  title: "Package Project",
@@ -692,214 +870,54 @@ function registerRuntimeTools(server) {
692
870
  }, async ({ workspacePath }) => {
693
871
  const workspace = await ensureWorkspacePath(workspacePath);
694
872
  const encryptedPath = await (0, packager_1.packageProject)(workspace);
695
- return {
696
- content: [{ type: "text", text: `打包完成: ${encryptedPath}` }],
697
- };
873
+ return createTextToolResult(`打包完成: ${encryptedPath}`);
698
874
  });
699
875
  server.registerTool("run_project", {
700
876
  title: "Run Project",
701
- description: "构建并同步到设备后运行项目。不传 transport 时会自动优先使用已连接的 WS 设备。",
877
+ description: "构建并同步到当前默认设备后运行项目,仅使用 HTTP 设备连接。",
702
878
  inputSchema: {
703
- transport: z
704
- .enum(["http", "ws"])
705
- .optional()
706
- .describe("传输方式:http|ws;不传时自动优先 ws(若已连接)"),
707
- ip: z
708
- .string()
709
- .min(1)
710
- .optional()
711
- .describe("设备 IP 地址(transport=http 时可传;未传则使用 set_device 默认值)"),
712
- port: z
713
- .number()
714
- .int()
715
- .min(1)
716
- .max(65535)
717
- .optional()
718
- .describe("设备端口,默认 9800"),
719
- wsPort: z
720
- .number()
721
- .int()
722
- .min(1)
723
- .max(65535)
724
- .optional()
725
- .describe("WS 服务端口(transport=ws 时可选,默认 31111)"),
726
- wsWaitMs: z
727
- .number()
728
- .int()
729
- .min(1)
730
- .max(600000)
731
- .optional()
732
- .describe("WS 等待连接超时毫秒(transport=ws 时可选,默认 30000)"),
733
879
  workspacePath: z
734
880
  .string()
735
881
  .min(1)
736
882
  .optional()
737
883
  .describe("可选工作目录;不传时使用 set_workspace 记忆值"),
738
884
  },
739
- }, async ({ transport, ip, port, wsPort, wsWaitMs, workspacePath }) => {
885
+ }, async ({ workspacePath }) => {
740
886
  const workspace = await ensureWorkspacePath(workspacePath);
741
- const target = await resolvePreferredRuntimeTarget({
742
- transport,
743
- ip,
744
- port,
745
- wsPort,
746
- wsWaitMs,
887
+ const target = await resolveRuntimeHttpTarget();
888
+ await (0, project_1.runOnDevice)({
889
+ ...createRuntimeHttpRequestOptions(target),
890
+ workspacePath: workspace,
747
891
  });
748
- await (0, project_1.runOnDevice)(target.transport === "ws"
749
- ? {
750
- transport: "ws",
751
- wsPort: target.wsPort,
752
- wsWaitMs: target.wsWaitMs,
753
- workspacePath: workspace,
754
- }
755
- : {
756
- ip: target.ip,
757
- port: target.port,
758
- transport: "http",
759
- workspacePath: workspace,
760
- });
761
- return {
762
- content: [
763
- {
764
- type: "text",
765
- text: `运行请求已发送到 ${target.label}`,
766
- },
767
- ],
768
- };
892
+ return createTextToolResult(`运行请求已发送到 ${target.label}`);
769
893
  });
770
894
  server.registerTool("run_ui_project", {
771
895
  title: "Run UI Project",
772
- description: "构建并同步到设备后预览 UI。不传 transport 时会自动优先使用已连接的 WS 设备。",
896
+ description: "构建并同步到当前默认设备后预览 UI,仅使用 HTTP 设备连接。",
773
897
  inputSchema: {
774
- transport: z
775
- .enum(["http", "ws"])
776
- .optional()
777
- .describe("传输方式:http|ws;不传时自动优先 ws(若已连接)"),
778
- ip: z
779
- .string()
780
- .min(1)
781
- .optional()
782
- .describe("设备 IP 地址(transport=http 时可传;未传则使用 set_device 默认值)"),
783
- port: z
784
- .number()
785
- .int()
786
- .min(1)
787
- .max(65535)
788
- .optional()
789
- .describe("设备端口,默认 9800"),
790
- wsPort: z
791
- .number()
792
- .int()
793
- .min(1)
794
- .max(65535)
795
- .optional()
796
- .describe("WS 服务端口(transport=ws 时可选,默认 31111)"),
797
- wsWaitMs: z
798
- .number()
799
- .int()
800
- .min(1)
801
- .max(600000)
802
- .optional()
803
- .describe("WS 等待连接超时毫秒(transport=ws 时可选,默认 30000)"),
804
898
  workspacePath: z
805
899
  .string()
806
900
  .min(1)
807
901
  .optional()
808
902
  .describe("可选工作目录;不传时使用 set_workspace 记忆值"),
809
903
  },
810
- }, async ({ transport, ip, port, wsPort, wsWaitMs, workspacePath }) => {
904
+ }, async ({ workspacePath }) => {
811
905
  const workspace = await ensureWorkspacePath(workspacePath);
812
- const target = await resolvePreferredRuntimeTarget({
813
- transport,
814
- ip,
815
- port,
816
- wsPort,
817
- wsWaitMs,
906
+ const target = await resolveRuntimeHttpTarget();
907
+ await (0, project_1.runUIOnDevice)({
908
+ ...createRuntimeHttpRequestOptions(target),
909
+ workspacePath: workspace,
818
910
  });
819
- await (0, project_1.runUIOnDevice)(target.transport === "ws"
820
- ? {
821
- transport: "ws",
822
- wsPort: target.wsPort,
823
- wsWaitMs: target.wsWaitMs,
824
- workspacePath: workspace,
825
- }
826
- : {
827
- ip: target.ip,
828
- port: target.port,
829
- transport: "http",
830
- workspacePath: workspace,
831
- });
832
- return {
833
- content: [
834
- {
835
- type: "text",
836
- text: `UI 预览请求已发送到 ${target.label}`,
837
- },
838
- ],
839
- };
911
+ return createTextToolResult(`UI 预览请求已发送到 ${target.label}`);
840
912
  });
841
913
  server.registerTool("stop_project", {
842
914
  title: "Stop Project",
843
- description: "停止设备上的项目。不传 transport 时会自动优先使用已连接的 WS 设备。",
844
- inputSchema: {
845
- transport: z
846
- .enum(["http", "ws"])
847
- .optional()
848
- .describe("传输方式:http|ws;不传时自动优先 ws(若已连接)"),
849
- ip: z
850
- .string()
851
- .min(1)
852
- .optional()
853
- .describe("设备 IP 地址(transport=http 时可传;未传则使用 set_device 默认值)"),
854
- port: z
855
- .number()
856
- .int()
857
- .min(1)
858
- .max(65535)
859
- .optional()
860
- .describe("设备端口,默认 9800"),
861
- wsPort: z
862
- .number()
863
- .int()
864
- .min(1)
865
- .max(65535)
866
- .optional()
867
- .describe("WS 服务端口(transport=ws 时可选,默认 31111)"),
868
- wsWaitMs: z
869
- .number()
870
- .int()
871
- .min(1)
872
- .max(600000)
873
- .optional()
874
- .describe("WS 等待连接超时毫秒(transport=ws 时可选,默认 30000)"),
875
- },
876
- }, async ({ transport, ip, port, wsPort, wsWaitMs }) => {
877
- const target = await resolvePreferredRuntimeTarget({
878
- transport,
879
- ip,
880
- port,
881
- wsPort,
882
- wsWaitMs,
883
- });
884
- await (0, project_1.stopOnDevice)(target.transport === "ws"
885
- ? {
886
- transport: "ws",
887
- wsPort: target.wsPort,
888
- wsWaitMs: target.wsWaitMs,
889
- }
890
- : {
891
- ip: target.ip,
892
- port: target.port,
893
- transport: "http",
894
- });
895
- return {
896
- content: [
897
- {
898
- type: "text",
899
- text: `停止请求已发送到 ${target.label}`,
900
- },
901
- ],
902
- };
915
+ description: "停止当前默认设备上的项目,仅使用 HTTP 设备连接。",
916
+ inputSchema: {},
917
+ }, async () => {
918
+ const target = await resolveRuntimeHttpTarget();
919
+ await (0, project_1.stopOnDevice)(createRuntimeHttpRequestOptions(target));
920
+ return createTextToolResult(`停止请求已发送到 ${target.label}`);
903
921
  });
904
922
  }
905
923
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ms-vite-plugin",
3
- "version": "1.1.12",
3
+ "version": "1.1.14",
4
4
  "type": "commonjs",
5
5
  "license": "MIT",
6
6
  "publishConfig": {