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.
- package/dist/mcp/tools.js +520 -502
- 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) =>
|
|
170
|
-
return
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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) =>
|
|
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
|
-
* -
|
|
354
|
-
* -
|
|
355
|
-
*
|
|
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
|
|
721
|
+
* const target = await resolveRuntimeHttpTarget()
|
|
363
722
|
*/
|
|
364
|
-
async function
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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: "
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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: "
|
|
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 ({
|
|
477
|
-
const target = await
|
|
478
|
-
|
|
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: "
|
|
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 ({
|
|
575
|
-
const target = await
|
|
576
|
-
|
|
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("
|
|
604
|
-
title: "
|
|
605
|
-
description: "
|
|
830
|
+
server.registerTool("get_logs", {
|
|
831
|
+
title: "Get Device Logs",
|
|
832
|
+
description: "获取当前默认设备的日志缓存快照。调用 set_device 后会自动建立 SSE 后台订阅,此工具只返回当前已缓存的日志内容。",
|
|
606
833
|
inputSchema: {
|
|
607
|
-
|
|
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(
|
|
838
|
+
.max(5000)
|
|
617
839
|
.optional()
|
|
618
|
-
.
|
|
619
|
-
|
|
840
|
+
.default(200)
|
|
841
|
+
.describe("返回最近日志条数,默认 200,最大 5000"),
|
|
842
|
+
runtimeStatusLimit: z
|
|
620
843
|
.number()
|
|
621
844
|
.int()
|
|
622
845
|
.min(0)
|
|
623
|
-
.max(
|
|
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
|
-
.
|
|
848
|
+
.default(20)
|
|
849
|
+
.describe("返回最近 runtime_status 条数,默认 20"),
|
|
672
850
|
},
|
|
673
|
-
}, async ({
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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: "
|
|
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 ({
|
|
885
|
+
}, async ({ workspacePath }) => {
|
|
740
886
|
const workspace = await ensureWorkspacePath(workspacePath);
|
|
741
|
-
const target = await
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
wsPort,
|
|
746
|
-
wsWaitMs,
|
|
887
|
+
const target = await resolveRuntimeHttpTarget();
|
|
888
|
+
await (0, project_1.runOnDevice)({
|
|
889
|
+
...createRuntimeHttpRequestOptions(target),
|
|
890
|
+
workspacePath: workspace,
|
|
747
891
|
});
|
|
748
|
-
|
|
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: "
|
|
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 ({
|
|
904
|
+
}, async ({ workspacePath }) => {
|
|
811
905
|
const workspace = await ensureWorkspacePath(workspacePath);
|
|
812
|
-
const target = await
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
wsPort,
|
|
817
|
-
wsWaitMs,
|
|
906
|
+
const target = await resolveRuntimeHttpTarget();
|
|
907
|
+
await (0, project_1.runUIOnDevice)({
|
|
908
|
+
...createRuntimeHttpRequestOptions(target),
|
|
909
|
+
workspacePath: workspace,
|
|
818
910
|
});
|
|
819
|
-
|
|
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: "
|
|
844
|
-
inputSchema: {
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
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
|
/**
|