ms-vite-plugin 1.1.15 → 1.1.17

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.
@@ -0,0 +1,9 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ /**
3
+ * 注册 Control API 对应 MCP 工具
4
+ * @param server MCP 服务实例
5
+ * @returns 无返回值
6
+ * @example
7
+ * registerControlTools(server)
8
+ */
9
+ export declare function registerControlTools(server: McpServer): void;
@@ -0,0 +1,201 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.registerControlTools = registerControlTools;
37
+ const z = __importStar(require("zod/v4"));
38
+ const tool_utils_1 = require("./tool-utils");
39
+ /**
40
+ * 请求控制路由
41
+ * @param endpoint 控制 API 名称
42
+ * @param query 可选查询参数
43
+ * @returns 返回设备端 JSON 响应
44
+ * @example
45
+ * const payload = await requestControlApi("home")
46
+ */
47
+ async function requestControlApi(endpoint, query) {
48
+ const target = await (0, tool_utils_1.resolveRuntimeHttpTarget)();
49
+ return (0, tool_utils_1.requestRuntimePathJson)(target, `/api/control/${endpoint}`, query);
50
+ }
51
+ /**
52
+ * 将控制类 API 响应格式化为 MCP 文本
53
+ * @param actionName 操作名称
54
+ * @param payload 设备端响应
55
+ * @returns 返回标准文本工具响应
56
+ * @example
57
+ * formatControlResult("返回桌面", { success: true })
58
+ */
59
+ function formatControlResult(actionName, payload) {
60
+ const success = payload.success === true;
61
+ if ("data" in payload) {
62
+ return (0, tool_utils_1.createTextToolResult)(`${actionName}${success ? "成功" : "失败"}:\n${(0, tool_utils_1.formatRuntimeJsonText)(payload.data)}`, !success);
63
+ }
64
+ return (0, tool_utils_1.createTextToolResult)(`${actionName}${success ? "成功" : "失败"}`, !success);
65
+ }
66
+ /**
67
+ * 注册 Control API 对应 MCP 工具
68
+ * @param server MCP 服务实例
69
+ * @returns 无返回值
70
+ * @example
71
+ * registerControlTools(server)
72
+ */
73
+ function registerControlTools(server) {
74
+ server.registerTool("control_home", {
75
+ title: "Control Home",
76
+ description: "返回桌面。",
77
+ inputSchema: {},
78
+ }, async () => formatControlResult("返回桌面", await requestControlApi("home")));
79
+ server.registerTool("control_click", {
80
+ title: "Control Click",
81
+ description: "点击指定坐标。",
82
+ inputSchema: {
83
+ x: z.number().int().min(0).describe("点击坐标 x"),
84
+ y: z.number().int().min(0).describe("点击坐标 y"),
85
+ duration: z
86
+ .number()
87
+ .int()
88
+ .min(0)
89
+ .optional()
90
+ .describe("按压时长,默认 20"),
91
+ jitter: z.boolean().optional().describe("是否启用抖动,默认 false"),
92
+ },
93
+ }, async ({ x, y, duration, jitter }) => formatControlResult("点击", await requestControlApi("click", { x, y, duration, jitter })));
94
+ server.registerTool("control_double_click", {
95
+ title: "Control Double Click",
96
+ description: "双击指定坐标。",
97
+ inputSchema: {
98
+ x: z.number().int().min(0).describe("双击坐标 x"),
99
+ y: z.number().int().min(0).describe("双击坐标 y"),
100
+ duration: z
101
+ .number()
102
+ .int()
103
+ .min(0)
104
+ .optional()
105
+ .describe("单次按压时长,默认 20"),
106
+ interval: z
107
+ .number()
108
+ .int()
109
+ .min(0)
110
+ .optional()
111
+ .describe("两次点击间隔,默认 20"),
112
+ jitter: z.boolean().optional().describe("是否启用抖动,默认 false"),
113
+ },
114
+ }, async ({ x, y, duration, interval, jitter }) => formatControlResult("双击", await requestControlApi("doubleClick", {
115
+ x,
116
+ y,
117
+ duration,
118
+ interval,
119
+ jitter,
120
+ })));
121
+ server.registerTool("control_swipe", {
122
+ title: "Control Swipe",
123
+ description: "从起点滑动到终点。",
124
+ inputSchema: {
125
+ startX: z.number().int().min(0).describe("起点 x"),
126
+ startY: z.number().int().min(0).describe("起点 y"),
127
+ endX: z.number().int().min(0).describe("终点 x"),
128
+ endY: z.number().int().min(0).describe("终点 y"),
129
+ duration: z
130
+ .number()
131
+ .int()
132
+ .min(0)
133
+ .optional()
134
+ .describe("滑动时长,默认 100"),
135
+ jitter: z.boolean().optional().describe("是否启用抖动,默认 false"),
136
+ steps: z.number().int().min(1).optional().describe("分段步数,默认 6"),
137
+ },
138
+ }, async ({ startX, startY, endX, endY, duration, jitter, steps }) => formatControlResult("滑动", await requestControlApi("swipe", {
139
+ startX,
140
+ startY,
141
+ endX,
142
+ endY,
143
+ duration,
144
+ jitter,
145
+ steps,
146
+ })));
147
+ server.registerTool("control_press_button", {
148
+ title: "Control Press Button",
149
+ description: "按下指定系统按键。",
150
+ inputSchema: {
151
+ button: z.string().min(1).describe("按钮名称"),
152
+ },
153
+ }, async ({ button }) => formatControlResult("按键", await requestControlApi("pressButton", { button })));
154
+ server.registerTool("control_input", {
155
+ title: "Control Input",
156
+ description: "输入文本。",
157
+ inputSchema: {
158
+ text: z.string().min(1).describe("待输入文本"),
159
+ },
160
+ }, async ({ text }) => formatControlResult("输入", await requestControlApi("input", { text })));
161
+ server.registerTool("control_backspace", {
162
+ title: "Control Backspace",
163
+ description: "执行退格操作。",
164
+ inputSchema: {
165
+ count: z.number().int().min(1).optional().describe("退格次数,默认 1"),
166
+ },
167
+ }, async ({ count }) => formatControlResult("退格", await requestControlApi("backspace", { count })));
168
+ server.registerTool("control_enter", {
169
+ title: "Control Enter",
170
+ description: "执行回车操作。",
171
+ inputSchema: {},
172
+ }, async () => formatControlResult("回车", await requestControlApi("enter")));
173
+ server.registerTool("control_start_app", {
174
+ title: "Control Start App",
175
+ description: "启动指定应用。",
176
+ inputSchema: {
177
+ bundleId: z.string().min(1).describe("应用 bundleId"),
178
+ },
179
+ }, async ({ bundleId }) => formatControlResult("启动应用", await requestControlApi("startApp", { bundleId })));
180
+ server.registerTool("control_activate_app", {
181
+ title: "Control Activate App",
182
+ description: "激活指定应用。",
183
+ inputSchema: {
184
+ bundleId: z.string().min(1).describe("应用 bundleId"),
185
+ },
186
+ }, async ({ bundleId }) => formatControlResult("激活应用", await requestControlApi("activateApp", { bundleId })));
187
+ server.registerTool("control_stop_app", {
188
+ title: "Control Stop App",
189
+ description: "停止指定应用。",
190
+ inputSchema: {
191
+ bundleId: z.string().min(1).describe("应用 bundleId"),
192
+ },
193
+ }, async ({ bundleId }) => formatControlResult("停止应用", await requestControlApi("stopApp", { bundleId })));
194
+ server.registerTool("control_open_url", {
195
+ title: "Control Open URL",
196
+ description: "打开指定 URL。",
197
+ inputSchema: {
198
+ url: z.string().min(1).describe("目标 URL"),
199
+ },
200
+ }, async ({ url }) => formatControlResult("打开 URL", await requestControlApi("openURL", { url })));
201
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * 设备日志内存缓存上限
3
+ * - 采用环形窗口思路,仅保留最新 5000 条,避免 MCP 进程长期运行时内存无限增长
4
+ */
5
+ export declare const DEVICE_LOG_MEMORY_LIMIT = 5000;
6
+ /**
7
+ * 确保后台日志订阅处于目标设备上
8
+ * - 若目标未变化且订阅仍在运行,则直接复用已有连接
9
+ * - 若目标发生变化,则重置缓存并重新建立订阅
10
+ * @param ip 设备 IP
11
+ * @param port 设备端口
12
+ * @returns 返回是否复用了现有订阅
13
+ * @example
14
+ * const reused = ensureDeviceLogSubscription("192.168.1.10", 9800)
15
+ */
16
+ export declare function ensureDeviceLogSubscription(ip: string, port: number): boolean;
17
+ /**
18
+ * 生成日志缓存快照文本
19
+ * @param limit 需要返回的日志条数
20
+ * @param runtimeStatusLimit 需要返回的 runtime_status 条数
21
+ * @returns 返回用于 MCP 文本响应的快照内容
22
+ * @example
23
+ * const text = buildDeviceLogSnapshotText(100, 20)
24
+ */
25
+ export declare function buildDeviceLogSnapshotText(limit: number, runtimeStatusLimit: number): string;
@@ -0,0 +1,391 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEVICE_LOG_MEMORY_LIMIT = void 0;
4
+ exports.ensureDeviceLogSubscription = ensureDeviceLogSubscription;
5
+ exports.buildDeviceLogSnapshotText = buildDeviceLogSnapshotText;
6
+ /**
7
+ * 设备日志内存缓存上限
8
+ * - 采用环形窗口思路,仅保留最新 5000 条,避免 MCP 进程长期运行时内存无限增长
9
+ */
10
+ exports.DEVICE_LOG_MEMORY_LIMIT = 5000;
11
+ /**
12
+ * SSE 重连等待时间(毫秒)
13
+ * - 当设备日志流异常断开时,后台会自动尝试重连
14
+ */
15
+ const DEVICE_LOG_RECONNECT_DELAY_MS = 1500;
16
+ /**
17
+ * 视为“仍在工作中”的日志订阅状态集合
18
+ * - 用于判断同一设备是否可以直接复用已有后台连接
19
+ */
20
+ const ACTIVE_DEVICE_LOG_SUBSCRIPTION_STATUSES = new Set([
21
+ "connecting",
22
+ "connected",
23
+ "reconnecting",
24
+ ]);
25
+ /**
26
+ * 进程级设备日志订阅状态
27
+ * - 当前 MCP 仅维护单设备日志缓存
28
+ * - MCP Tool 处理函数会复用同一份内存缓存
29
+ */
30
+ const deviceLogSubscriptionState = {
31
+ target: null,
32
+ status: "idle",
33
+ lastError: null,
34
+ lastEventAt: null,
35
+ startedAt: null,
36
+ logs: [],
37
+ runtimeStatus: [],
38
+ generation: 0,
39
+ controller: null,
40
+ reconnectTimer: null,
41
+ };
42
+ /**
43
+ * 向环形缓冲区追加数据
44
+ * @param buffer 目标数组
45
+ * @param entry 待写入的数据
46
+ * @param limit 数组最大长度
47
+ * @returns 无返回值
48
+ * @example
49
+ * pushRingBuffer([1, 2], 3, 2)
50
+ */
51
+ function pushRingBuffer(buffer, entry, limit) {
52
+ if (buffer.length >= limit) {
53
+ buffer.splice(0, buffer.length - limit + 1);
54
+ }
55
+ buffer.push(entry);
56
+ }
57
+ /**
58
+ * 重置当前日志缓存快照
59
+ * @returns 无返回值
60
+ * @example
61
+ * resetDeviceLogSnapshotState()
62
+ */
63
+ function resetDeviceLogSnapshotState() {
64
+ deviceLogSubscriptionState.lastError = null;
65
+ deviceLogSubscriptionState.lastEventAt = null;
66
+ deviceLogSubscriptionState.startedAt = new Date().toISOString();
67
+ deviceLogSubscriptionState.logs = [];
68
+ deviceLogSubscriptionState.runtimeStatus = [];
69
+ }
70
+ /**
71
+ * 判断日志订阅是否仍然活跃
72
+ * @returns 活跃返回 true,否则返回 false
73
+ * @example
74
+ * const active = isDeviceLogSubscriptionActive()
75
+ */
76
+ function isDeviceLogSubscriptionActive() {
77
+ return (ACTIVE_DEVICE_LOG_SUBSCRIPTION_STATUSES.has(deviceLogSubscriptionState.status) || deviceLogSubscriptionState.reconnectTimer !== null);
78
+ }
79
+ /**
80
+ * 解析单个 SSE 文本块
81
+ * @param rawBlock 原始 SSE 文本块
82
+ * @returns 返回事件名与 data 字符串
83
+ * @example
84
+ * parseSseEventBlock("event: log\ndata: {\"message\":\"ok\"}")
85
+ */
86
+ function parseSseEventBlock(rawBlock) {
87
+ const lines = rawBlock.split(/\r?\n/);
88
+ let eventName = "message";
89
+ const dataLines = [];
90
+ for (const line of lines) {
91
+ if (line.startsWith("event:")) {
92
+ eventName = line.slice(6).trim() || "message";
93
+ continue;
94
+ }
95
+ if (line.startsWith("data:")) {
96
+ dataLines.push(line.slice(5).trimStart());
97
+ }
98
+ }
99
+ return {
100
+ event: eventName,
101
+ data: dataLines.join("\n"),
102
+ };
103
+ }
104
+ /**
105
+ * 将原始日志对象标准化为统一结构
106
+ * @param payload 日志原始对象
107
+ * @returns 返回标准化后的日志
108
+ * @example
109
+ * normalizeBufferedLogEntry({ level: "info", message: "started" })
110
+ */
111
+ function normalizeBufferedLogEntry(payload) {
112
+ const levelText = String(payload.level ?? "info").toLowerCase();
113
+ const messageText = String(payload.message ?? "");
114
+ const timestampText = typeof payload.timestamp === "string" && payload.timestamp
115
+ ? payload.timestamp
116
+ : new Date().toISOString();
117
+ return {
118
+ level: levelText,
119
+ message: messageText,
120
+ timestamp: timestampText,
121
+ };
122
+ }
123
+ /**
124
+ * 清理当前后台日志订阅
125
+ * @returns 无返回值
126
+ * @example
127
+ * stopDeviceLogSubscription()
128
+ */
129
+ function stopDeviceLogSubscription() {
130
+ if (deviceLogSubscriptionState.reconnectTimer) {
131
+ clearTimeout(deviceLogSubscriptionState.reconnectTimer);
132
+ deviceLogSubscriptionState.reconnectTimer = null;
133
+ }
134
+ if (deviceLogSubscriptionState.controller) {
135
+ deviceLogSubscriptionState.controller.abort();
136
+ deviceLogSubscriptionState.controller = null;
137
+ }
138
+ }
139
+ /**
140
+ * 安排后台重连
141
+ * @param generation 当前订阅代次
142
+ * @returns 无返回值
143
+ * @example
144
+ * scheduleDeviceLogReconnect(1)
145
+ */
146
+ function scheduleDeviceLogReconnect(generation) {
147
+ if (generation !== deviceLogSubscriptionState.generation ||
148
+ !deviceLogSubscriptionState.target) {
149
+ return;
150
+ }
151
+ if (deviceLogSubscriptionState.reconnectTimer) {
152
+ return;
153
+ }
154
+ deviceLogSubscriptionState.status = "reconnecting";
155
+ deviceLogSubscriptionState.reconnectTimer = setTimeout(() => {
156
+ deviceLogSubscriptionState.reconnectTimer = null;
157
+ void startDeviceLogSubscriptionLoop(generation);
158
+ }, DEVICE_LOG_RECONNECT_DELAY_MS);
159
+ }
160
+ /**
161
+ * 启动一次 SSE 读取循环
162
+ * - 该函数仅负责单次连接;若连接中断,会由外层自动调度重连
163
+ * @param generation 当前订阅代次
164
+ * @returns 读取流程结束后返回 Promise<void>
165
+ * @example
166
+ * await startDeviceLogSubscriptionLoop(1)
167
+ */
168
+ async function startDeviceLogSubscriptionLoop(generation) {
169
+ if (generation !== deviceLogSubscriptionState.generation ||
170
+ !deviceLogSubscriptionState.target) {
171
+ return;
172
+ }
173
+ const { ip, port } = deviceLogSubscriptionState.target;
174
+ const controller = new AbortController();
175
+ const url = `http://${ip}:${port}/logger/sse`;
176
+ deviceLogSubscriptionState.controller = controller;
177
+ deviceLogSubscriptionState.status =
178
+ deviceLogSubscriptionState.lastEventAt === null
179
+ ? "connecting"
180
+ : "reconnecting";
181
+ try {
182
+ const response = await fetch(url, {
183
+ method: "GET",
184
+ signal: controller.signal,
185
+ headers: {
186
+ Accept: "text/event-stream",
187
+ },
188
+ });
189
+ if (!response.ok || !response.body) {
190
+ throw new Error(`连接日志流失败: ${response.status} ${response.statusText || "unknown"}`);
191
+ }
192
+ deviceLogSubscriptionState.status = "connected";
193
+ deviceLogSubscriptionState.lastError = null;
194
+ const reader = response.body.getReader();
195
+ const decoder = new TextDecoder("utf-8");
196
+ let buffer = "";
197
+ while (generation === deviceLogSubscriptionState.generation) {
198
+ const { value, done } = await reader.read();
199
+ if (done) {
200
+ break;
201
+ }
202
+ buffer += decoder.decode(value, { stream: true });
203
+ let splitIndex = buffer.search(/\r?\n\r?\n/);
204
+ while (splitIndex >= 0) {
205
+ const rawBlock = buffer.slice(0, splitIndex);
206
+ buffer = buffer.slice(splitIndex + (buffer[splitIndex] === "\r" ? 4 : 2));
207
+ const event = parseSseEventBlock(rawBlock);
208
+ if (event.data) {
209
+ try {
210
+ const parsed = JSON.parse(event.data);
211
+ deviceLogSubscriptionState.lastEventAt = new Date().toISOString();
212
+ if (event.event === "log") {
213
+ pushRingBuffer(deviceLogSubscriptionState.logs, normalizeBufferedLogEntry(parsed), exports.DEVICE_LOG_MEMORY_LIMIT);
214
+ }
215
+ else if (event.event === "runtime_status") {
216
+ pushRingBuffer(deviceLogSubscriptionState.runtimeStatus, {
217
+ raw: parsed,
218
+ timestamp: new Date().toISOString(),
219
+ }, exports.DEVICE_LOG_MEMORY_LIMIT);
220
+ }
221
+ }
222
+ catch {
223
+ // 忽略单条非法 JSON,避免因为异常日志阻塞整个后台连接
224
+ }
225
+ }
226
+ splitIndex = buffer.search(/\r?\n\r?\n/);
227
+ }
228
+ }
229
+ if (generation === deviceLogSubscriptionState.generation) {
230
+ scheduleDeviceLogReconnect(generation);
231
+ }
232
+ }
233
+ catch (error) {
234
+ if (error instanceof Error &&
235
+ (error.name === "AbortError" ||
236
+ error.message === "The operation was aborted.")) {
237
+ return;
238
+ }
239
+ if (generation === deviceLogSubscriptionState.generation) {
240
+ deviceLogSubscriptionState.status = "error";
241
+ deviceLogSubscriptionState.lastError =
242
+ error instanceof Error ? error.message : String(error);
243
+ scheduleDeviceLogReconnect(generation);
244
+ }
245
+ }
246
+ finally {
247
+ if (deviceLogSubscriptionState.controller === controller) {
248
+ deviceLogSubscriptionState.controller = null;
249
+ }
250
+ }
251
+ }
252
+ /**
253
+ * 确保后台日志订阅处于目标设备上
254
+ * - 若目标未变化且订阅仍在运行,则直接复用已有连接
255
+ * - 若目标发生变化,则重置缓存并重新建立订阅
256
+ * @param ip 设备 IP
257
+ * @param port 设备端口
258
+ * @returns 返回是否复用了现有订阅
259
+ * @example
260
+ * const reused = ensureDeviceLogSubscription("192.168.1.10", 9800)
261
+ */
262
+ function ensureDeviceLogSubscription(ip, port) {
263
+ const sameTarget = deviceLogSubscriptionState.target?.ip === ip &&
264
+ deviceLogSubscriptionState.target?.port === port;
265
+ const isActive = isDeviceLogSubscriptionActive();
266
+ if (sameTarget && isActive) {
267
+ return true;
268
+ }
269
+ stopDeviceLogSubscription();
270
+ deviceLogSubscriptionState.generation += 1;
271
+ deviceLogSubscriptionState.target = { ip, port };
272
+ deviceLogSubscriptionState.status = "connecting";
273
+ resetDeviceLogSnapshotState();
274
+ void startDeviceLogSubscriptionLoop(deviceLogSubscriptionState.generation);
275
+ return false;
276
+ }
277
+ /**
278
+ * 按日志页面风格格式化时间
279
+ * @param value ISO 时间字符串
280
+ * @returns 返回 `yyyy/mm/dd hh:mm:ss.xxx` 风格文本,异常时回退原值
281
+ * @example
282
+ * formatDisplayTimestamp("2026-05-07T11:34:48.776Z")
283
+ */
284
+ function formatDisplayTimestamp(value) {
285
+ const date = new Date(value);
286
+ if (Number.isNaN(date.getTime())) {
287
+ return value;
288
+ }
289
+ const pad = (number, length = 2) => String(number).padStart(length, "0");
290
+ return `${date.getFullYear()}/${pad(date.getMonth() + 1)}/${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds(), 3)}`;
291
+ }
292
+ /**
293
+ * 将日志订阅连接状态转为中文文本
294
+ * @param status 原始连接状态
295
+ * @returns 返回中文状态说明
296
+ * @example
297
+ * formatDeviceLogSubscriptionStatus("connected")
298
+ */
299
+ function formatDeviceLogSubscriptionStatus(status) {
300
+ switch (status) {
301
+ case "idle":
302
+ return "未订阅";
303
+ case "connecting":
304
+ return "连接中";
305
+ case "connected":
306
+ return "已连接";
307
+ case "reconnecting":
308
+ return "重连中";
309
+ case "error":
310
+ return "连接异常";
311
+ default:
312
+ return status;
313
+ }
314
+ }
315
+ /**
316
+ * 将 runtime_status 中的内存数值格式化为可读文本
317
+ * @param value 原始数值
318
+ * @param suffix 数值单位后缀
319
+ * @returns 数值存在时返回格式化结果,否则返回未知
320
+ * @example
321
+ * formatRuntimeMetricText(12.3456, " MB")
322
+ */
323
+ function formatRuntimeMetricText(value, suffix = "") {
324
+ if (typeof value === "number" && Number.isFinite(value)) {
325
+ return `${value.toFixed(2)}${suffix}`;
326
+ }
327
+ return "未知";
328
+ }
329
+ /**
330
+ * 将单条 runtime_status 事件格式化为中文运行状态摘要
331
+ * @param entry runtime_status 条目
332
+ * @returns 返回更易读的状态文本
333
+ * @example
334
+ * formatRuntimeStatusEntry({ raw: { isRunning: false }, timestamp: "2026-01-01T00:00:00.000Z" })
335
+ */
336
+ function formatRuntimeStatusEntry(entry) {
337
+ const raw = entry.raw;
338
+ const memory = raw.memory && typeof raw.memory === "object"
339
+ ? raw.memory
340
+ : {};
341
+ return [
342
+ `[${formatDisplayTimestamp(entry.timestamp)}]`,
343
+ `UI: ${typeof raw.isUIShowing === "boolean"
344
+ ? raw.isUIShowing
345
+ ? "显示中"
346
+ : "未显示"
347
+ : "未知"}`,
348
+ `脚本: ${typeof raw.isRunning === "boolean"
349
+ ? raw.isRunning
350
+ ? "运行中"
351
+ : "未运行"
352
+ : "未知"}`,
353
+ `总内存: ${formatRuntimeMetricText(memory.total, " MB")}`,
354
+ `可用: ${formatRuntimeMetricText(memory.available, " MB")}`,
355
+ `已用: ${formatRuntimeMetricText(memory.used, " MB")}`,
356
+ `占用: ${formatRuntimeMetricText(memory.usagePercentage, "%")}`,
357
+ ].join(" | ");
358
+ }
359
+ /**
360
+ * 生成日志缓存快照文本
361
+ * @param limit 需要返回的日志条数
362
+ * @param runtimeStatusLimit 需要返回的 runtime_status 条数
363
+ * @returns 返回用于 MCP 文本响应的快照内容
364
+ * @example
365
+ * const text = buildDeviceLogSnapshotText(100, 20)
366
+ */
367
+ function buildDeviceLogSnapshotText(limit, runtimeStatusLimit) {
368
+ const recentLogs = deviceLogSubscriptionState.logs
369
+ .slice(-limit)
370
+ .map((log) => `[${formatDisplayTimestamp(log.timestamp)}] [${log.level.toUpperCase()}] ${log.message}`);
371
+ const recentRuntimeStatus = deviceLogSubscriptionState.runtimeStatus
372
+ .slice(-runtimeStatusLimit)
373
+ .map((entry) => formatRuntimeStatusEntry(entry));
374
+ return [
375
+ `设备: ${deviceLogSubscriptionState.target
376
+ ? `${deviceLogSubscriptionState.target.ip}:${deviceLogSubscriptionState.target.port}`
377
+ : "未订阅"}`,
378
+ `状态: ${formatDeviceLogSubscriptionStatus(deviceLogSubscriptionState.status)}`,
379
+ ...(deviceLogSubscriptionState.lastError
380
+ ? [`最近错误: ${deviceLogSubscriptionState.lastError}`]
381
+ : []),
382
+ "",
383
+ `最新日志(最多 ${limit} 条):`,
384
+ recentLogs.length > 0 ? recentLogs.join("\n") : "无日志",
385
+ "",
386
+ `最新运行状态(最多 ${runtimeStatusLimit} 条):`,
387
+ recentRuntimeStatus.length > 0
388
+ ? recentRuntimeStatus.join("\n")
389
+ : "无运行状态",
390
+ ].join("\n");
391
+ }
@@ -0,0 +1,17 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ /**
3
+ * 注册文档资源
4
+ * @param server MCP 服务实例
5
+ * @returns 无返回值
6
+ * @example
7
+ * registerDocResources(server)
8
+ */
9
+ export declare function registerDocResources(server: McpServer): void;
10
+ /**
11
+ * 注册文档工具
12
+ * @param server MCP 服务实例
13
+ * @returns 无返回值
14
+ * @example
15
+ * registerDocTools(server)
16
+ */
17
+ export declare function registerDocTools(server: McpServer): void;