ms-vite-plugin 1.1.11 → 1.1.13
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 +451 -601
- package/package.json +1 -1
package/dist/mcp/tools.js
CHANGED
|
@@ -40,16 +40,375 @@ 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 targetText = deviceLogSubscriptionState.target
|
|
373
|
+
? `${deviceLogSubscriptionState.target.ip}:${deviceLogSubscriptionState.target.port}`
|
|
374
|
+
: "未订阅";
|
|
375
|
+
const recentLogs = deviceLogSubscriptionState.logs
|
|
376
|
+
.slice(-limit)
|
|
377
|
+
.map((log) => `[${log.timestamp}] [${log.level.toUpperCase()}] ${log.message}`);
|
|
378
|
+
const recentRuntimeStatus = deviceLogSubscriptionState.runtimeStatus
|
|
379
|
+
.slice(-runtimeStatusLimit)
|
|
380
|
+
.map((entry) => `[${entry.timestamp}] ${JSON.stringify(entry.raw, null, 2).replace(/\n/g, " ")}`);
|
|
381
|
+
return [
|
|
382
|
+
`订阅目标: ${targetText}`,
|
|
383
|
+
`连接状态: ${deviceLogSubscriptionState.status}`,
|
|
384
|
+
`缓存日志: ${deviceLogSubscriptionState.logs.length}/${DEVICE_LOG_MEMORY_LIMIT}`,
|
|
385
|
+
`缓存 runtime_status: ${deviceLogSubscriptionState.runtimeStatus.length}/${DEVICE_LOG_MEMORY_LIMIT}`,
|
|
386
|
+
`startedAt: ${deviceLogSubscriptionState.startedAt ?? "unknown"}`,
|
|
387
|
+
`lastEventAt: ${deviceLogSubscriptionState.lastEventAt ?? "unknown"}`,
|
|
388
|
+
`lastError: ${deviceLogSubscriptionState.lastError ?? "none"}`,
|
|
389
|
+
"",
|
|
390
|
+
`最新日志(最多 ${limit} 条):`,
|
|
391
|
+
recentLogs.length > 0 ? recentLogs.join("\n") : "无日志",
|
|
392
|
+
"",
|
|
393
|
+
`最新 runtime_status(最多 ${runtimeStatusLimit} 条):`,
|
|
394
|
+
recentRuntimeStatus.length > 0
|
|
395
|
+
? recentRuntimeStatus.join("\n")
|
|
396
|
+
: "无 runtime_status",
|
|
397
|
+
].join("\n");
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* 格式化单条 API 文档摘要
|
|
401
|
+
* @param language 文档语言
|
|
402
|
+
* @param title 文档标题
|
|
403
|
+
* @param slug 文档 slug
|
|
404
|
+
* @param index 列表序号
|
|
405
|
+
* @returns 返回用于列表展示的文本块
|
|
406
|
+
* @example
|
|
407
|
+
* formatApiDocSummary("js", "Tap", "tap", 0)
|
|
408
|
+
*/
|
|
409
|
+
function formatApiDocSummary(language, title, slug, index) {
|
|
410
|
+
return `${index + 1}. ${title}\nslug: ${slug}\nuri: ${(0, docs_service_1.getDocUri)(language, slug)}`;
|
|
411
|
+
}
|
|
53
412
|
/**
|
|
54
413
|
* 注册文档资源
|
|
55
414
|
* @param server MCP 服务实例
|
|
@@ -125,14 +484,7 @@ function registerDocTools(server) {
|
|
|
125
484
|
inputSchema: {},
|
|
126
485
|
}, async () => {
|
|
127
486
|
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
|
-
};
|
|
487
|
+
return createTextToolResult(`当前文档语言: ${language} (${types_1.DOC_LANGUAGE_LABELS[language]})`);
|
|
136
488
|
});
|
|
137
489
|
server.registerTool("set_docs_language", {
|
|
138
490
|
title: "Set Docs Language",
|
|
@@ -145,14 +497,7 @@ function registerDocTools(server) {
|
|
|
145
497
|
}, async ({ language }) => {
|
|
146
498
|
const active = (0, docs_service_1.setCurrentDocsLanguage)(language);
|
|
147
499
|
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
|
-
};
|
|
500
|
+
return createTextToolResult(`文档语言已切换为 ${active} (${types_1.DOC_LANGUAGE_LABELS[active]})\n目录: ${docsDir}`);
|
|
156
501
|
});
|
|
157
502
|
server.registerTool("list_api_docs", {
|
|
158
503
|
title: "List API Docs",
|
|
@@ -166,21 +511,14 @@ function registerDocTools(server) {
|
|
|
166
511
|
}, async ({ language }) => {
|
|
167
512
|
const activeLanguage = (0, docs_service_1.resolveDocsLanguage)(language);
|
|
168
513
|
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
|
-
};
|
|
514
|
+
const lines = docs.map((doc, index) => formatApiDocSummary(activeLanguage, doc.title, doc.slug, index));
|
|
515
|
+
return createTextToolResult(lines.length === 0
|
|
516
|
+
? `当前语言 ${activeLanguage} 下没有可用文档。`
|
|
517
|
+
: [
|
|
518
|
+
`当前语言: ${activeLanguage} (${types_1.DOC_LANGUAGE_LABELS[activeLanguage]})`,
|
|
519
|
+
"",
|
|
520
|
+
...lines,
|
|
521
|
+
].join("\n"));
|
|
184
522
|
});
|
|
185
523
|
server.registerTool("search_api_docs", {
|
|
186
524
|
title: "Search API Docs",
|
|
@@ -215,16 +553,9 @@ function registerDocTools(server) {
|
|
|
215
553
|
: [
|
|
216
554
|
`当前语言: ${activeLanguage} (${types_1.DOC_LANGUAGE_LABELS[activeLanguage]})`,
|
|
217
555
|
"",
|
|
218
|
-
...matches.map((doc, index) =>
|
|
556
|
+
...matches.map((doc, index) => formatApiDocSummary(activeLanguage, doc.title, doc.slug, index)),
|
|
219
557
|
].join("\n");
|
|
220
|
-
return
|
|
221
|
-
content: [
|
|
222
|
-
{
|
|
223
|
-
type: "text",
|
|
224
|
-
text,
|
|
225
|
-
},
|
|
226
|
-
],
|
|
227
|
-
};
|
|
558
|
+
return createTextToolResult(text);
|
|
228
559
|
});
|
|
229
560
|
server.registerTool("read_api_doc", {
|
|
230
561
|
title: "Read API Doc",
|
|
@@ -241,24 +572,9 @@ function registerDocTools(server) {
|
|
|
241
572
|
const docs = await (0, docs_service_1.getApiDocsByLanguage)(activeLanguage);
|
|
242
573
|
const target = docs.find((doc) => doc.slug === slug);
|
|
243
574
|
if (!target) {
|
|
244
|
-
return {
|
|
245
|
-
content: [
|
|
246
|
-
{
|
|
247
|
-
type: "text",
|
|
248
|
-
text: `未找到文档 ${slug}.md(语言: ${activeLanguage})。`,
|
|
249
|
-
},
|
|
250
|
-
],
|
|
251
|
-
isError: true,
|
|
252
|
-
};
|
|
575
|
+
return createTextToolResult(`未找到文档 ${slug}.md(语言: ${activeLanguage})。`, true);
|
|
253
576
|
}
|
|
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
|
-
};
|
|
577
|
+
return createTextToolResult(`标题: ${target.title}\n语言: ${activeLanguage}\nURI: ${(0, docs_service_1.getDocUri)(activeLanguage, target.slug)}\n\n${target.content}`);
|
|
262
578
|
});
|
|
263
579
|
}
|
|
264
580
|
/**
|
|
@@ -315,14 +631,7 @@ function registerRuntimeTools(server) {
|
|
|
315
631
|
},
|
|
316
632
|
}, async ({ workspacePath }) => {
|
|
317
633
|
const workspace = await ensureWorkspacePath(workspacePath);
|
|
318
|
-
return {
|
|
319
|
-
content: [
|
|
320
|
-
{
|
|
321
|
-
type: "text",
|
|
322
|
-
text: `默认工作目录已设置为: ${workspace}`,
|
|
323
|
-
},
|
|
324
|
-
],
|
|
325
|
-
};
|
|
634
|
+
return createTextToolResult(`默认工作目录已设置为: ${workspace}`);
|
|
326
635
|
});
|
|
327
636
|
server.registerTool("get_workspace", {
|
|
328
637
|
title: "Get Workspace",
|
|
@@ -330,98 +639,27 @@ function registerRuntimeTools(server) {
|
|
|
330
639
|
inputSchema: {},
|
|
331
640
|
}, async () => {
|
|
332
641
|
if (!currentWorkspacePath) {
|
|
333
|
-
return
|
|
334
|
-
content: [
|
|
335
|
-
{
|
|
336
|
-
type: "text",
|
|
337
|
-
text: "当前未设置工作目录,请先调用 set_workspace。",
|
|
338
|
-
},
|
|
339
|
-
],
|
|
340
|
-
};
|
|
642
|
+
return createTextToolResult("当前未设置工作目录,请先调用 set_workspace。");
|
|
341
643
|
}
|
|
342
|
-
return {
|
|
343
|
-
content: [
|
|
344
|
-
{
|
|
345
|
-
type: "text",
|
|
346
|
-
text: `当前工作目录: ${currentWorkspacePath}`,
|
|
347
|
-
},
|
|
348
|
-
],
|
|
349
|
-
};
|
|
644
|
+
return createTextToolResult(`当前工作目录: ${currentWorkspacePath}`);
|
|
350
645
|
});
|
|
351
646
|
/**
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
* @
|
|
356
|
-
* @param result 日志监听结果
|
|
357
|
-
* @returns 可拼接到 tool 响应的文本
|
|
358
|
-
* @example
|
|
359
|
-
* const text = formatRunLogSection("192.168.1.10", 9800, 10, result)
|
|
360
|
-
*/
|
|
361
|
-
function formatRunLogSection(ip, port, result) {
|
|
362
|
-
const logsPreview = result.logs
|
|
363
|
-
.slice(-20)
|
|
364
|
-
.map((log) => `[${log.timestamp}] [${log.level.toUpperCase()}] ${log.message}`)
|
|
365
|
-
.join("\n");
|
|
366
|
-
return [
|
|
367
|
-
`运行日志来源: ${ip}:${port}`,
|
|
368
|
-
`结束原因: ${result.stopReason},日志: ${result.logs.length} 条,runtime_status: ${result.runtimeStatus.length} 条`,
|
|
369
|
-
"最新日志(最多展示最后20条):",
|
|
370
|
-
logsPreview || "无日志",
|
|
371
|
-
].join("\n");
|
|
372
|
-
}
|
|
373
|
-
/**
|
|
374
|
-
* 启动持续日志监听(后台,不阻塞当前 tool 返回)
|
|
375
|
-
* @param ip 设备 IP
|
|
376
|
-
* @param port 设备端口
|
|
377
|
-
* @param maxLogs 日志环形缓冲上限
|
|
378
|
-
* @returns 返回提示文本
|
|
379
|
-
* @example
|
|
380
|
-
* const tip = startBackgroundContinuousLogWatch("192.168.1.10", 9800, 300)
|
|
381
|
-
*/
|
|
382
|
-
function startBackgroundContinuousLogWatch(ip, port, maxLogs) {
|
|
383
|
-
void (0, project_1.watchDeviceLogsBySse)(ip, port, 0, maxLogs)
|
|
384
|
-
.then((result) => {
|
|
385
|
-
console.log(`[mcp] continuous logs ${ip}:${port} ended, reason=${result.stopReason}, logs=${result.logs.length}, runtime_status=${result.runtimeStatus.length}`);
|
|
386
|
-
})
|
|
387
|
-
.catch((error) => {
|
|
388
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
389
|
-
console.warn(`[mcp] continuous logs ${ip}:${port} failed: ${message}`);
|
|
390
|
-
});
|
|
391
|
-
return `已切换为持续日志监听: ${ip}:${port}(直到连接断开,max ${maxLogs})`;
|
|
392
|
-
}
|
|
393
|
-
/**
|
|
394
|
-
* 解析运行目标:
|
|
395
|
-
* - 显式 transport=ws: 强制走 ws
|
|
396
|
-
* - 显式 transport=http: 强制走 http
|
|
397
|
-
* - 未显式 transport:
|
|
398
|
-
* 1) 若传入 ip/port,则走 http
|
|
399
|
-
* 2) 若当前已有 ws 设备连接,则优先走 ws
|
|
400
|
-
* 3) 否则回退到 http 默认设备配置
|
|
401
|
-
* @param options 工具调用参数
|
|
402
|
-
* @returns 返回标准化后的请求目标
|
|
647
|
+
* 解析当前默认 HTTP 设备
|
|
648
|
+
* - 当前 MCP 仅支持单设备模型
|
|
649
|
+
* - 所有设备工具默认复用 set_device 保存的连接信息
|
|
650
|
+
* @returns 返回标准化后的 HTTP 请求目标
|
|
403
651
|
* @example
|
|
404
|
-
* const target = await
|
|
652
|
+
* const target = await resolveRuntimeHttpTarget()
|
|
405
653
|
*/
|
|
406
|
-
async function
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
const wsPort = String(options.wsPort ?? 31111);
|
|
414
|
-
const wsWaitMs = String(options.wsWaitMs ?? 30000);
|
|
415
|
-
return {
|
|
416
|
-
transport: "ws",
|
|
417
|
-
wsPort,
|
|
418
|
-
wsWaitMs,
|
|
419
|
-
label: `transport=ws (wsPort=${wsPort})`,
|
|
420
|
-
};
|
|
654
|
+
async function resolveRuntimeHttpTarget() {
|
|
655
|
+
let device;
|
|
656
|
+
try {
|
|
657
|
+
device = await (0, device_config_1.resolveDeviceConfig)();
|
|
658
|
+
}
|
|
659
|
+
catch {
|
|
660
|
+
throw new Error("未设置设备:请先调用 set_device。");
|
|
421
661
|
}
|
|
422
|
-
const device = await (0, device_config_1.resolveDeviceConfig)(options.ip, options.port);
|
|
423
662
|
return {
|
|
424
|
-
transport: "http",
|
|
425
663
|
ip: device.ip,
|
|
426
664
|
port: (0, device_config_1.normalizePort)(device.port),
|
|
427
665
|
label: `${device.ip}:${device.port}`,
|
|
@@ -429,7 +667,7 @@ function registerRuntimeTools(server) {
|
|
|
429
667
|
}
|
|
430
668
|
server.registerTool("set_device", {
|
|
431
669
|
title: "Set Device",
|
|
432
|
-
description: "
|
|
670
|
+
description: "设置当前唯一设备连接信息。设置后其余设备工具均复用该设备。",
|
|
433
671
|
inputSchema: {
|
|
434
672
|
ip: z.string().min(1).describe("设备 IP 地址,例如 192.168.1.100"),
|
|
435
673
|
port: z
|
|
@@ -442,14 +680,13 @@ function registerRuntimeTools(server) {
|
|
|
442
680
|
},
|
|
443
681
|
}, async ({ ip, port }) => {
|
|
444
682
|
const config = await (0, device_config_1.setDeviceConfig)(ip, port);
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
};
|
|
683
|
+
const reusedSubscription = ensureDeviceLogSubscription(config.ip, config.port);
|
|
684
|
+
return createTextToolResult([
|
|
685
|
+
`默认设备已设置为 ${config.ip}:${config.port}`,
|
|
686
|
+
reusedSubscription
|
|
687
|
+
? "SSE 日志后台订阅已复用现有连接"
|
|
688
|
+
: `SSE 日志后台订阅已启动,内存缓存上限 ${DEVICE_LOG_MEMORY_LIMIT} 条`,
|
|
689
|
+
].join("\n"));
|
|
453
690
|
});
|
|
454
691
|
server.registerTool("get_device", {
|
|
455
692
|
title: "Get Device",
|
|
@@ -458,52 +695,14 @@ function registerRuntimeTools(server) {
|
|
|
458
695
|
}, async () => {
|
|
459
696
|
const config = await (0, device_config_1.readDeviceConfig)();
|
|
460
697
|
if (!config) {
|
|
461
|
-
return
|
|
462
|
-
content: [
|
|
463
|
-
{ type: "text", text: "当前未设置默认设备,请先调用 set_device。" },
|
|
464
|
-
],
|
|
465
|
-
};
|
|
698
|
+
return createTextToolResult("当前未设置默认设备,请先调用 set_device。");
|
|
466
699
|
}
|
|
467
|
-
return {
|
|
468
|
-
content: [
|
|
469
|
-
{ type: "text", text: `当前默认设备: ${config.ip}:${config.port}` },
|
|
470
|
-
],
|
|
471
|
-
};
|
|
700
|
+
return createTextToolResult(`当前默认设备: ${config.ip}:${config.port}`);
|
|
472
701
|
});
|
|
473
702
|
server.registerTool("take_screenshot", {
|
|
474
703
|
title: "Take Screenshot",
|
|
475
|
-
description: "
|
|
704
|
+
description: "获取当前默认设备截图。可返回 base64,或落地到文件后返回文件路径(默认写入系统临时目录)。",
|
|
476
705
|
inputSchema: {
|
|
477
|
-
transport: z
|
|
478
|
-
.enum(["http", "ws"])
|
|
479
|
-
.optional()
|
|
480
|
-
.describe("传输方式:http|ws;不传时自动优先 ws(若已连接)"),
|
|
481
|
-
ip: z
|
|
482
|
-
.string()
|
|
483
|
-
.min(1)
|
|
484
|
-
.optional()
|
|
485
|
-
.describe("设备 IP 地址(transport=http 时可传;未传则使用 set_device 默认值)"),
|
|
486
|
-
port: z
|
|
487
|
-
.number()
|
|
488
|
-
.int()
|
|
489
|
-
.min(1)
|
|
490
|
-
.max(65535)
|
|
491
|
-
.optional()
|
|
492
|
-
.describe("设备端口,默认 9800"),
|
|
493
|
-
wsPort: z
|
|
494
|
-
.number()
|
|
495
|
-
.int()
|
|
496
|
-
.min(1)
|
|
497
|
-
.max(65535)
|
|
498
|
-
.optional()
|
|
499
|
-
.describe("WS 服务端口(transport=ws 时可选,默认 31111)"),
|
|
500
|
-
wsWaitMs: z
|
|
501
|
-
.number()
|
|
502
|
-
.int()
|
|
503
|
-
.min(1)
|
|
504
|
-
.max(600000)
|
|
505
|
-
.optional()
|
|
506
|
-
.describe("WS 等待连接超时毫秒(transport=ws 时可选,默认 30000)"),
|
|
507
706
|
format: z
|
|
508
707
|
.enum(["file", "base64"])
|
|
509
708
|
.optional()
|
|
@@ -515,35 +714,12 @@ function registerRuntimeTools(server) {
|
|
|
515
714
|
.optional()
|
|
516
715
|
.describe("当 format=file 时可指定输出路径,不传则写入系统临时目录"),
|
|
517
716
|
},
|
|
518
|
-
}, async ({
|
|
519
|
-
const target = await
|
|
520
|
-
|
|
521
|
-
ip,
|
|
522
|
-
port,
|
|
523
|
-
wsPort,
|
|
524
|
-
wsWaitMs,
|
|
525
|
-
});
|
|
526
|
-
const requestOptions = target.transport === "ws"
|
|
527
|
-
? {
|
|
528
|
-
transport: "ws",
|
|
529
|
-
wsPort: target.wsPort,
|
|
530
|
-
wsWaitMs: target.wsWaitMs,
|
|
531
|
-
}
|
|
532
|
-
: {
|
|
533
|
-
ip: target.ip,
|
|
534
|
-
port: target.port,
|
|
535
|
-
transport: "http",
|
|
536
|
-
};
|
|
717
|
+
}, async ({ format, outputPath }) => {
|
|
718
|
+
const target = await resolveRuntimeHttpTarget();
|
|
719
|
+
const requestOptions = createRuntimeHttpRequestOptions(target);
|
|
537
720
|
if (format === "base64") {
|
|
538
721
|
const base64 = await (0, project_1.getScreenshotBase64OnDevice)(requestOptions);
|
|
539
|
-
return {
|
|
540
|
-
content: [
|
|
541
|
-
{
|
|
542
|
-
type: "text",
|
|
543
|
-
text: `截图成功: ${target.label}\nformat: base64\n${base64}`,
|
|
544
|
-
},
|
|
545
|
-
],
|
|
546
|
-
};
|
|
722
|
+
return createTextToolResult(`截图成功: ${target.label}\nformat: base64\n${base64}`);
|
|
547
723
|
}
|
|
548
724
|
const image = await (0, project_1.getScreenshotOnDevice)(requestOptions);
|
|
549
725
|
const targetPath = outputPath && outputPath.trim()
|
|
@@ -553,49 +729,12 @@ function registerRuntimeTools(server) {
|
|
|
553
729
|
.slice(2, 8)}.jpg`);
|
|
554
730
|
await fsExtra.ensureDir(path.dirname(targetPath));
|
|
555
731
|
await fsExtra.writeFile(targetPath, image);
|
|
556
|
-
return {
|
|
557
|
-
content: [
|
|
558
|
-
{
|
|
559
|
-
type: "text",
|
|
560
|
-
text: `截图成功: ${target.label}\nformat: file\npath: ${targetPath}\nsize: ${image.length} bytes`,
|
|
561
|
-
},
|
|
562
|
-
],
|
|
563
|
-
};
|
|
732
|
+
return createTextToolResult(`截图成功: ${target.label}\nformat: file\npath: ${targetPath}\nsize: ${image.length} bytes`);
|
|
564
733
|
});
|
|
565
734
|
server.registerTool("get_node_source", {
|
|
566
735
|
title: "Get Node Source",
|
|
567
|
-
description: "
|
|
736
|
+
description: "获取当前默认设备页面节点 XML(/api/source)。",
|
|
568
737
|
inputSchema: {
|
|
569
|
-
transport: z
|
|
570
|
-
.enum(["http", "ws"])
|
|
571
|
-
.optional()
|
|
572
|
-
.describe("传输方式:http|ws;不传时自动优先 ws(若已连接)"),
|
|
573
|
-
ip: z
|
|
574
|
-
.string()
|
|
575
|
-
.min(1)
|
|
576
|
-
.optional()
|
|
577
|
-
.describe("设备 IP 地址(transport=http 时可传;未传则使用 set_device 默认值)"),
|
|
578
|
-
port: z
|
|
579
|
-
.number()
|
|
580
|
-
.int()
|
|
581
|
-
.min(1)
|
|
582
|
-
.max(65535)
|
|
583
|
-
.optional()
|
|
584
|
-
.describe("设备端口,默认 9800"),
|
|
585
|
-
wsPort: z
|
|
586
|
-
.number()
|
|
587
|
-
.int()
|
|
588
|
-
.min(1)
|
|
589
|
-
.max(65535)
|
|
590
|
-
.optional()
|
|
591
|
-
.describe("WS 服务端口(transport=ws 时可选,默认 31111)"),
|
|
592
|
-
wsWaitMs: z
|
|
593
|
-
.number()
|
|
594
|
-
.int()
|
|
595
|
-
.min(1)
|
|
596
|
-
.max(600000)
|
|
597
|
-
.optional()
|
|
598
|
-
.describe("WS 等待连接超时毫秒(transport=ws 时可选,默认 30000)"),
|
|
599
738
|
maxDepth: z
|
|
600
739
|
.number()
|
|
601
740
|
.int()
|
|
@@ -613,113 +752,41 @@ function registerRuntimeTools(server) {
|
|
|
613
752
|
.default(120)
|
|
614
753
|
.describe("设备端节点抓取超时秒数,默认 120"),
|
|
615
754
|
},
|
|
616
|
-
}, async ({
|
|
617
|
-
const target = await
|
|
618
|
-
|
|
619
|
-
ip,
|
|
620
|
-
port,
|
|
621
|
-
wsPort,
|
|
622
|
-
wsWaitMs,
|
|
623
|
-
});
|
|
624
|
-
const requestOptions = target.transport === "ws"
|
|
625
|
-
? {
|
|
626
|
-
transport: "ws",
|
|
627
|
-
wsPort: target.wsPort,
|
|
628
|
-
wsWaitMs: target.wsWaitMs,
|
|
629
|
-
}
|
|
630
|
-
: {
|
|
631
|
-
ip: target.ip,
|
|
632
|
-
port: target.port,
|
|
633
|
-
transport: "http",
|
|
634
|
-
};
|
|
755
|
+
}, async ({ maxDepth, timeout }) => {
|
|
756
|
+
const target = await resolveRuntimeHttpTarget();
|
|
757
|
+
const requestOptions = createRuntimeHttpRequestOptions(target);
|
|
635
758
|
const source = await (0, project_1.getSourceOnDevice)(requestOptions, maxDepth, timeout);
|
|
636
|
-
return {
|
|
637
|
-
content: [
|
|
638
|
-
{
|
|
639
|
-
type: "text",
|
|
640
|
-
text: `节点获取成功: ${target.label}\nmaxDepth: ${maxDepth}\ntimeout: ${timeout}\n\n${source}`,
|
|
641
|
-
},
|
|
642
|
-
],
|
|
643
|
-
};
|
|
759
|
+
return createTextToolResult(`节点获取成功: ${target.label}\nmaxDepth: ${maxDepth}\ntimeout: ${timeout}\n\n${source}`);
|
|
644
760
|
});
|
|
645
|
-
server.registerTool("
|
|
646
|
-
title: "
|
|
647
|
-
description: "
|
|
761
|
+
server.registerTool("get_logs", {
|
|
762
|
+
title: "Get Device Logs",
|
|
763
|
+
description: "获取当前默认设备的日志缓存快照。调用 set_device 后会自动建立 SSE 后台订阅,此工具只返回当前已缓存的日志内容。",
|
|
648
764
|
inputSchema: {
|
|
649
|
-
|
|
650
|
-
.string()
|
|
651
|
-
.min(1)
|
|
652
|
-
.optional()
|
|
653
|
-
.describe("设备 IP 地址,未传则使用 set_device 保存的默认值"),
|
|
654
|
-
port: z
|
|
765
|
+
limit: z
|
|
655
766
|
.number()
|
|
656
767
|
.int()
|
|
657
768
|
.min(1)
|
|
658
|
-
.max(
|
|
769
|
+
.max(5000)
|
|
659
770
|
.optional()
|
|
660
|
-
.
|
|
661
|
-
|
|
771
|
+
.default(200)
|
|
772
|
+
.describe("返回最近日志条数,默认 200,最大 5000"),
|
|
773
|
+
runtimeStatusLimit: z
|
|
662
774
|
.number()
|
|
663
775
|
.int()
|
|
664
776
|
.min(0)
|
|
665
|
-
.max(
|
|
666
|
-
.optional()
|
|
667
|
-
.default(15)
|
|
668
|
-
.describe("监听时长(秒),默认 15 秒;传 0 表示持续监听直到连接断开"),
|
|
669
|
-
maxLogs: z
|
|
670
|
-
.number()
|
|
671
|
-
.int()
|
|
672
|
-
.min(10)
|
|
673
|
-
.max(5000)
|
|
674
|
-
.optional()
|
|
675
|
-
.default(200)
|
|
676
|
-
.describe("最多收集日志条数,默认 200"),
|
|
677
|
-
},
|
|
678
|
-
}, async ({ ip, port, durationSeconds, maxLogs }) => {
|
|
679
|
-
const device = await (0, device_config_1.resolveDeviceConfig)(ip, port);
|
|
680
|
-
const result = await (0, project_1.watchDeviceLogsBySse)(device.ip, device.port, durationSeconds * 1000, maxLogs);
|
|
681
|
-
const logsPreview = result.logs
|
|
682
|
-
.slice(-20)
|
|
683
|
-
.map((log) => `[${log.timestamp}] [${log.level.toUpperCase()}] ${log.message}`)
|
|
684
|
-
.join("\n");
|
|
685
|
-
return {
|
|
686
|
-
content: [
|
|
687
|
-
{
|
|
688
|
-
type: "text",
|
|
689
|
-
text: [
|
|
690
|
-
`监听目标: ${device.ip}:${device.port}`,
|
|
691
|
-
`监听模式: ${durationSeconds === 0 ? "持续直到连接断开" : `${durationSeconds}s`}`,
|
|
692
|
-
`结束原因: ${result.stopReason},收集日志: ${result.logs.length} 条,runtime_status: ${result.runtimeStatus.length} 条`,
|
|
693
|
-
"",
|
|
694
|
-
"最新日志(最多展示最后20条):",
|
|
695
|
-
logsPreview || "无日志",
|
|
696
|
-
].join("\n"),
|
|
697
|
-
},
|
|
698
|
-
],
|
|
699
|
-
};
|
|
700
|
-
});
|
|
701
|
-
server.registerTool("build_project", {
|
|
702
|
-
title: "Build Project",
|
|
703
|
-
description: "构建 KuaiJS 项目,支持开发模式与生产模式。",
|
|
704
|
-
inputSchema: {
|
|
705
|
-
workspacePath: z
|
|
706
|
-
.string()
|
|
707
|
-
.min(1)
|
|
708
|
-
.optional()
|
|
709
|
-
.describe("可选工作目录;不传时使用 set_workspace 记忆值"),
|
|
710
|
-
dev: z
|
|
711
|
-
.boolean()
|
|
777
|
+
.max(200)
|
|
712
778
|
.optional()
|
|
713
|
-
.
|
|
779
|
+
.default(20)
|
|
780
|
+
.describe("返回最近 runtime_status 条数,默认 20"),
|
|
714
781
|
},
|
|
715
|
-
}, async ({
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
782
|
+
}, async ({ limit, runtimeStatusLimit }) => {
|
|
783
|
+
if (!deviceLogSubscriptionState.target) {
|
|
784
|
+
const device = await (0, device_config_1.readDeviceConfig)();
|
|
785
|
+
if (device) {
|
|
786
|
+
ensureDeviceLogSubscription(device.ip, device.port);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
return createTextToolResult(buildDeviceLogSnapshotText(limit, runtimeStatusLimit));
|
|
723
790
|
});
|
|
724
791
|
server.registerTool("package_project", {
|
|
725
792
|
title: "Package Project",
|
|
@@ -734,271 +801,54 @@ function registerRuntimeTools(server) {
|
|
|
734
801
|
}, async ({ workspacePath }) => {
|
|
735
802
|
const workspace = await ensureWorkspacePath(workspacePath);
|
|
736
803
|
const encryptedPath = await (0, packager_1.packageProject)(workspace);
|
|
737
|
-
return {
|
|
738
|
-
content: [{ type: "text", text: `打包完成: ${encryptedPath}` }],
|
|
739
|
-
};
|
|
804
|
+
return createTextToolResult(`打包完成: ${encryptedPath}`);
|
|
740
805
|
});
|
|
741
806
|
server.registerTool("run_project", {
|
|
742
807
|
title: "Run Project",
|
|
743
|
-
description: "
|
|
808
|
+
description: "构建并同步到当前默认设备后运行项目,仅使用 HTTP 设备连接。",
|
|
744
809
|
inputSchema: {
|
|
745
|
-
transport: z
|
|
746
|
-
.enum(["http", "ws"])
|
|
747
|
-
.optional()
|
|
748
|
-
.describe("传输方式:http|ws;不传时自动优先 ws(若已连接)"),
|
|
749
|
-
ip: z
|
|
750
|
-
.string()
|
|
751
|
-
.min(1)
|
|
752
|
-
.optional()
|
|
753
|
-
.describe("设备 IP 地址(transport=http 时可传;未传则使用 set_device 默认值)"),
|
|
754
|
-
port: z
|
|
755
|
-
.number()
|
|
756
|
-
.int()
|
|
757
|
-
.min(1)
|
|
758
|
-
.max(65535)
|
|
759
|
-
.optional()
|
|
760
|
-
.describe("设备端口,默认 9800"),
|
|
761
|
-
wsPort: z
|
|
762
|
-
.number()
|
|
763
|
-
.int()
|
|
764
|
-
.min(1)
|
|
765
|
-
.max(65535)
|
|
766
|
-
.optional()
|
|
767
|
-
.describe("WS 服务端口(transport=ws 时可选,默认 31111)"),
|
|
768
|
-
wsWaitMs: z
|
|
769
|
-
.number()
|
|
770
|
-
.int()
|
|
771
|
-
.min(1)
|
|
772
|
-
.max(600000)
|
|
773
|
-
.optional()
|
|
774
|
-
.describe("WS 等待连接超时毫秒(transport=ws 时可选,默认 30000)"),
|
|
775
810
|
workspacePath: z
|
|
776
811
|
.string()
|
|
777
812
|
.min(1)
|
|
778
813
|
.optional()
|
|
779
814
|
.describe("可选工作目录;不传时使用 set_workspace 记忆值"),
|
|
780
|
-
watchLogs: z
|
|
781
|
-
.boolean()
|
|
782
|
-
.optional()
|
|
783
|
-
.default(false)
|
|
784
|
-
.describe("是否在运行前启动日志监听,默认 false"),
|
|
785
|
-
logMode: z
|
|
786
|
-
.enum(["single", "continuous"])
|
|
787
|
-
.optional()
|
|
788
|
-
.default("single")
|
|
789
|
-
.describe("日志模式:single=按 runtime_status 收敛,continuous=持续监听"),
|
|
790
|
-
logMaxLogs: z
|
|
791
|
-
.number()
|
|
792
|
-
.int()
|
|
793
|
-
.min(10)
|
|
794
|
-
.max(5000)
|
|
795
|
-
.optional()
|
|
796
|
-
.default(300)
|
|
797
|
-
.describe("日志最大收集条数,默认 300"),
|
|
798
815
|
},
|
|
799
|
-
}, async ({
|
|
816
|
+
}, async ({ workspacePath }) => {
|
|
800
817
|
const workspace = await ensureWorkspacePath(workspacePath);
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
let logModeNotice = "";
|
|
806
|
-
if (watchLogs) {
|
|
807
|
-
try {
|
|
808
|
-
const logDevice = await (0, device_config_1.resolveDeviceConfig)(ip, port);
|
|
809
|
-
// 先挂监听,再执行 run,确保拿到启动期实时日志
|
|
810
|
-
logDeviceIp = logDevice.ip;
|
|
811
|
-
logDevicePort = logDevice.port;
|
|
812
|
-
if (logMode === "continuous") {
|
|
813
|
-
logModeNotice = startBackgroundContinuousLogWatch(logDevice.ip, logDevice.port, logMaxLogs);
|
|
814
|
-
}
|
|
815
|
-
else {
|
|
816
|
-
logPromise = (0, project_1.watchDeviceLogsBySse)(logDevice.ip, logDevice.port, 0, logMaxLogs, true, true);
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
catch (error) {
|
|
820
|
-
logSetupWarning = `日志监听未启动: ${error instanceof Error ? error.message : String(error)}`;
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
const target = await resolvePreferredRuntimeTarget({
|
|
824
|
-
transport,
|
|
825
|
-
ip,
|
|
826
|
-
port,
|
|
827
|
-
wsPort,
|
|
828
|
-
wsWaitMs,
|
|
818
|
+
const target = await resolveRuntimeHttpTarget();
|
|
819
|
+
await (0, project_1.runOnDevice)({
|
|
820
|
+
...createRuntimeHttpRequestOptions(target),
|
|
821
|
+
workspacePath: workspace,
|
|
829
822
|
});
|
|
830
|
-
|
|
831
|
-
? {
|
|
832
|
-
transport: "ws",
|
|
833
|
-
wsPort: target.wsPort,
|
|
834
|
-
wsWaitMs: target.wsWaitMs,
|
|
835
|
-
workspacePath: workspace,
|
|
836
|
-
}
|
|
837
|
-
: {
|
|
838
|
-
ip: target.ip,
|
|
839
|
-
port: target.port,
|
|
840
|
-
transport: "http",
|
|
841
|
-
workspacePath: workspace,
|
|
842
|
-
});
|
|
843
|
-
let logSection = "";
|
|
844
|
-
if (logPromise) {
|
|
845
|
-
const result = await logPromise;
|
|
846
|
-
const sections = [
|
|
847
|
-
formatRunLogSection(logDeviceIp, logDevicePort, result),
|
|
848
|
-
];
|
|
849
|
-
if (result.stopReason === "runtime_continuous") {
|
|
850
|
-
sections.push(startBackgroundContinuousLogWatch(logDeviceIp, logDevicePort, logMaxLogs));
|
|
851
|
-
}
|
|
852
|
-
logSection = `\n\n${sections.join("\n")}`;
|
|
853
|
-
}
|
|
854
|
-
else if (logModeNotice) {
|
|
855
|
-
logSection = `\n\n${logModeNotice}`;
|
|
856
|
-
}
|
|
857
|
-
else if (logSetupWarning) {
|
|
858
|
-
logSection = `\n\n${logSetupWarning}`;
|
|
859
|
-
}
|
|
860
|
-
return {
|
|
861
|
-
content: [
|
|
862
|
-
{
|
|
863
|
-
type: "text",
|
|
864
|
-
text: `运行请求已发送到 ${target.label}${logSection}`,
|
|
865
|
-
},
|
|
866
|
-
],
|
|
867
|
-
};
|
|
823
|
+
return createTextToolResult(`运行请求已发送到 ${target.label}`);
|
|
868
824
|
});
|
|
869
825
|
server.registerTool("run_ui_project", {
|
|
870
826
|
title: "Run UI Project",
|
|
871
|
-
description: "
|
|
827
|
+
description: "构建并同步到当前默认设备后预览 UI,仅使用 HTTP 设备连接。",
|
|
872
828
|
inputSchema: {
|
|
873
|
-
transport: z
|
|
874
|
-
.enum(["http", "ws"])
|
|
875
|
-
.optional()
|
|
876
|
-
.describe("传输方式:http|ws;不传时自动优先 ws(若已连接)"),
|
|
877
|
-
ip: z
|
|
878
|
-
.string()
|
|
879
|
-
.min(1)
|
|
880
|
-
.optional()
|
|
881
|
-
.describe("设备 IP 地址(transport=http 时可传;未传则使用 set_device 默认值)"),
|
|
882
|
-
port: z
|
|
883
|
-
.number()
|
|
884
|
-
.int()
|
|
885
|
-
.min(1)
|
|
886
|
-
.max(65535)
|
|
887
|
-
.optional()
|
|
888
|
-
.describe("设备端口,默认 9800"),
|
|
889
|
-
wsPort: z
|
|
890
|
-
.number()
|
|
891
|
-
.int()
|
|
892
|
-
.min(1)
|
|
893
|
-
.max(65535)
|
|
894
|
-
.optional()
|
|
895
|
-
.describe("WS 服务端口(transport=ws 时可选,默认 31111)"),
|
|
896
|
-
wsWaitMs: z
|
|
897
|
-
.number()
|
|
898
|
-
.int()
|
|
899
|
-
.min(1)
|
|
900
|
-
.max(600000)
|
|
901
|
-
.optional()
|
|
902
|
-
.describe("WS 等待连接超时毫秒(transport=ws 时可选,默认 30000)"),
|
|
903
829
|
workspacePath: z
|
|
904
830
|
.string()
|
|
905
831
|
.min(1)
|
|
906
832
|
.optional()
|
|
907
833
|
.describe("可选工作目录;不传时使用 set_workspace 记忆值"),
|
|
908
834
|
},
|
|
909
|
-
}, async ({
|
|
835
|
+
}, async ({ workspacePath }) => {
|
|
910
836
|
const workspace = await ensureWorkspacePath(workspacePath);
|
|
911
|
-
const target = await
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
wsPort,
|
|
916
|
-
wsWaitMs,
|
|
837
|
+
const target = await resolveRuntimeHttpTarget();
|
|
838
|
+
await (0, project_1.runUIOnDevice)({
|
|
839
|
+
...createRuntimeHttpRequestOptions(target),
|
|
840
|
+
workspacePath: workspace,
|
|
917
841
|
});
|
|
918
|
-
|
|
919
|
-
? {
|
|
920
|
-
transport: "ws",
|
|
921
|
-
wsPort: target.wsPort,
|
|
922
|
-
wsWaitMs: target.wsWaitMs,
|
|
923
|
-
workspacePath: workspace,
|
|
924
|
-
}
|
|
925
|
-
: {
|
|
926
|
-
ip: target.ip,
|
|
927
|
-
port: target.port,
|
|
928
|
-
transport: "http",
|
|
929
|
-
workspacePath: workspace,
|
|
930
|
-
});
|
|
931
|
-
return {
|
|
932
|
-
content: [
|
|
933
|
-
{
|
|
934
|
-
type: "text",
|
|
935
|
-
text: `UI 预览请求已发送到 ${target.label}`,
|
|
936
|
-
},
|
|
937
|
-
],
|
|
938
|
-
};
|
|
842
|
+
return createTextToolResult(`UI 预览请求已发送到 ${target.label}`);
|
|
939
843
|
});
|
|
940
844
|
server.registerTool("stop_project", {
|
|
941
845
|
title: "Stop Project",
|
|
942
|
-
description: "
|
|
943
|
-
inputSchema: {
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
ip: z
|
|
949
|
-
.string()
|
|
950
|
-
.min(1)
|
|
951
|
-
.optional()
|
|
952
|
-
.describe("设备 IP 地址(transport=http 时可传;未传则使用 set_device 默认值)"),
|
|
953
|
-
port: z
|
|
954
|
-
.number()
|
|
955
|
-
.int()
|
|
956
|
-
.min(1)
|
|
957
|
-
.max(65535)
|
|
958
|
-
.optional()
|
|
959
|
-
.describe("设备端口,默认 9800"),
|
|
960
|
-
wsPort: z
|
|
961
|
-
.number()
|
|
962
|
-
.int()
|
|
963
|
-
.min(1)
|
|
964
|
-
.max(65535)
|
|
965
|
-
.optional()
|
|
966
|
-
.describe("WS 服务端口(transport=ws 时可选,默认 31111)"),
|
|
967
|
-
wsWaitMs: z
|
|
968
|
-
.number()
|
|
969
|
-
.int()
|
|
970
|
-
.min(1)
|
|
971
|
-
.max(600000)
|
|
972
|
-
.optional()
|
|
973
|
-
.describe("WS 等待连接超时毫秒(transport=ws 时可选,默认 30000)"),
|
|
974
|
-
},
|
|
975
|
-
}, async ({ transport, ip, port, wsPort, wsWaitMs }) => {
|
|
976
|
-
const target = await resolvePreferredRuntimeTarget({
|
|
977
|
-
transport,
|
|
978
|
-
ip,
|
|
979
|
-
port,
|
|
980
|
-
wsPort,
|
|
981
|
-
wsWaitMs,
|
|
982
|
-
});
|
|
983
|
-
await (0, project_1.stopOnDevice)(target.transport === "ws"
|
|
984
|
-
? {
|
|
985
|
-
transport: "ws",
|
|
986
|
-
wsPort: target.wsPort,
|
|
987
|
-
wsWaitMs: target.wsWaitMs,
|
|
988
|
-
}
|
|
989
|
-
: {
|
|
990
|
-
ip: target.ip,
|
|
991
|
-
port: target.port,
|
|
992
|
-
transport: "http",
|
|
993
|
-
});
|
|
994
|
-
return {
|
|
995
|
-
content: [
|
|
996
|
-
{
|
|
997
|
-
type: "text",
|
|
998
|
-
text: `停止请求已发送到 ${target.label}`,
|
|
999
|
-
},
|
|
1000
|
-
],
|
|
1001
|
-
};
|
|
846
|
+
description: "停止当前默认设备上的项目,仅使用 HTTP 设备连接。",
|
|
847
|
+
inputSchema: {},
|
|
848
|
+
}, async () => {
|
|
849
|
+
const target = await resolveRuntimeHttpTarget();
|
|
850
|
+
await (0, project_1.stopOnDevice)(createRuntimeHttpRequestOptions(target));
|
|
851
|
+
return createTextToolResult(`停止请求已发送到 ${target.label}`);
|
|
1002
852
|
});
|
|
1003
853
|
}
|
|
1004
854
|
/**
|