ms-vite-plugin 1.1.12 → 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 -502
- 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,56 +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
|
-
* 1) 若传入 ip/port,则走 http
|
|
357
|
-
* 2) 若当前已有 ws 设备连接,则优先走 ws
|
|
358
|
-
* 3) 否则回退到 http 默认设备配置
|
|
359
|
-
* @param options 工具调用参数
|
|
360
|
-
* @returns 返回标准化后的请求目标
|
|
647
|
+
* 解析当前默认 HTTP 设备
|
|
648
|
+
* - 当前 MCP 仅支持单设备模型
|
|
649
|
+
* - 所有设备工具默认复用 set_device 保存的连接信息
|
|
650
|
+
* @returns 返回标准化后的 HTTP 请求目标
|
|
361
651
|
* @example
|
|
362
|
-
* const target = await
|
|
652
|
+
* const target = await resolveRuntimeHttpTarget()
|
|
363
653
|
*/
|
|
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
|
-
};
|
|
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。");
|
|
379
661
|
}
|
|
380
|
-
const device = await (0, device_config_1.resolveDeviceConfig)(options.ip, options.port);
|
|
381
662
|
return {
|
|
382
|
-
transport: "http",
|
|
383
663
|
ip: device.ip,
|
|
384
664
|
port: (0, device_config_1.normalizePort)(device.port),
|
|
385
665
|
label: `${device.ip}:${device.port}`,
|
|
@@ -387,7 +667,7 @@ function registerRuntimeTools(server) {
|
|
|
387
667
|
}
|
|
388
668
|
server.registerTool("set_device", {
|
|
389
669
|
title: "Set Device",
|
|
390
|
-
description: "
|
|
670
|
+
description: "设置当前唯一设备连接信息。设置后其余设备工具均复用该设备。",
|
|
391
671
|
inputSchema: {
|
|
392
672
|
ip: z.string().min(1).describe("设备 IP 地址,例如 192.168.1.100"),
|
|
393
673
|
port: z
|
|
@@ -400,14 +680,13 @@ function registerRuntimeTools(server) {
|
|
|
400
680
|
},
|
|
401
681
|
}, async ({ ip, port }) => {
|
|
402
682
|
const config = await (0, device_config_1.setDeviceConfig)(ip, port);
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
};
|
|
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"));
|
|
411
690
|
});
|
|
412
691
|
server.registerTool("get_device", {
|
|
413
692
|
title: "Get Device",
|
|
@@ -416,52 +695,14 @@ function registerRuntimeTools(server) {
|
|
|
416
695
|
}, async () => {
|
|
417
696
|
const config = await (0, device_config_1.readDeviceConfig)();
|
|
418
697
|
if (!config) {
|
|
419
|
-
return
|
|
420
|
-
content: [
|
|
421
|
-
{ type: "text", text: "当前未设置默认设备,请先调用 set_device。" },
|
|
422
|
-
],
|
|
423
|
-
};
|
|
698
|
+
return createTextToolResult("当前未设置默认设备,请先调用 set_device。");
|
|
424
699
|
}
|
|
425
|
-
return {
|
|
426
|
-
content: [
|
|
427
|
-
{ type: "text", text: `当前默认设备: ${config.ip}:${config.port}` },
|
|
428
|
-
],
|
|
429
|
-
};
|
|
700
|
+
return createTextToolResult(`当前默认设备: ${config.ip}:${config.port}`);
|
|
430
701
|
});
|
|
431
702
|
server.registerTool("take_screenshot", {
|
|
432
703
|
title: "Take Screenshot",
|
|
433
|
-
description: "
|
|
704
|
+
description: "获取当前默认设备截图。可返回 base64,或落地到文件后返回文件路径(默认写入系统临时目录)。",
|
|
434
705
|
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
706
|
format: z
|
|
466
707
|
.enum(["file", "base64"])
|
|
467
708
|
.optional()
|
|
@@ -473,35 +714,12 @@ function registerRuntimeTools(server) {
|
|
|
473
714
|
.optional()
|
|
474
715
|
.describe("当 format=file 时可指定输出路径,不传则写入系统临时目录"),
|
|
475
716
|
},
|
|
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
|
-
};
|
|
717
|
+
}, async ({ format, outputPath }) => {
|
|
718
|
+
const target = await resolveRuntimeHttpTarget();
|
|
719
|
+
const requestOptions = createRuntimeHttpRequestOptions(target);
|
|
495
720
|
if (format === "base64") {
|
|
496
721
|
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
|
-
};
|
|
722
|
+
return createTextToolResult(`截图成功: ${target.label}\nformat: base64\n${base64}`);
|
|
505
723
|
}
|
|
506
724
|
const image = await (0, project_1.getScreenshotOnDevice)(requestOptions);
|
|
507
725
|
const targetPath = outputPath && outputPath.trim()
|
|
@@ -511,49 +729,12 @@ function registerRuntimeTools(server) {
|
|
|
511
729
|
.slice(2, 8)}.jpg`);
|
|
512
730
|
await fsExtra.ensureDir(path.dirname(targetPath));
|
|
513
731
|
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
|
-
};
|
|
732
|
+
return createTextToolResult(`截图成功: ${target.label}\nformat: file\npath: ${targetPath}\nsize: ${image.length} bytes`);
|
|
522
733
|
});
|
|
523
734
|
server.registerTool("get_node_source", {
|
|
524
735
|
title: "Get Node Source",
|
|
525
|
-
description: "
|
|
736
|
+
description: "获取当前默认设备页面节点 XML(/api/source)。",
|
|
526
737
|
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
738
|
maxDepth: z
|
|
558
739
|
.number()
|
|
559
740
|
.int()
|
|
@@ -571,113 +752,41 @@ function registerRuntimeTools(server) {
|
|
|
571
752
|
.default(120)
|
|
572
753
|
.describe("设备端节点抓取超时秒数,默认 120"),
|
|
573
754
|
},
|
|
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
|
-
};
|
|
755
|
+
}, async ({ maxDepth, timeout }) => {
|
|
756
|
+
const target = await resolveRuntimeHttpTarget();
|
|
757
|
+
const requestOptions = createRuntimeHttpRequestOptions(target);
|
|
593
758
|
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
|
-
};
|
|
759
|
+
return createTextToolResult(`节点获取成功: ${target.label}\nmaxDepth: ${maxDepth}\ntimeout: ${timeout}\n\n${source}`);
|
|
602
760
|
});
|
|
603
|
-
server.registerTool("
|
|
604
|
-
title: "
|
|
605
|
-
description: "
|
|
761
|
+
server.registerTool("get_logs", {
|
|
762
|
+
title: "Get Device Logs",
|
|
763
|
+
description: "获取当前默认设备的日志缓存快照。调用 set_device 后会自动建立 SSE 后台订阅,此工具只返回当前已缓存的日志内容。",
|
|
606
764
|
inputSchema: {
|
|
607
|
-
|
|
608
|
-
.string()
|
|
609
|
-
.min(1)
|
|
610
|
-
.optional()
|
|
611
|
-
.describe("设备 IP 地址,未传则使用 set_device 保存的默认值"),
|
|
612
|
-
port: z
|
|
765
|
+
limit: z
|
|
613
766
|
.number()
|
|
614
767
|
.int()
|
|
615
768
|
.min(1)
|
|
616
|
-
.max(
|
|
769
|
+
.max(5000)
|
|
617
770
|
.optional()
|
|
618
|
-
.
|
|
619
|
-
|
|
771
|
+
.default(200)
|
|
772
|
+
.describe("返回最近日志条数,默认 200,最大 5000"),
|
|
773
|
+
runtimeStatusLimit: z
|
|
620
774
|
.number()
|
|
621
775
|
.int()
|
|
622
776
|
.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()
|
|
777
|
+
.max(200)
|
|
670
778
|
.optional()
|
|
671
|
-
.
|
|
779
|
+
.default(20)
|
|
780
|
+
.describe("返回最近 runtime_status 条数,默认 20"),
|
|
672
781
|
},
|
|
673
|
-
}, async ({
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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));
|
|
681
790
|
});
|
|
682
791
|
server.registerTool("package_project", {
|
|
683
792
|
title: "Package Project",
|
|
@@ -692,214 +801,54 @@ function registerRuntimeTools(server) {
|
|
|
692
801
|
}, async ({ workspacePath }) => {
|
|
693
802
|
const workspace = await ensureWorkspacePath(workspacePath);
|
|
694
803
|
const encryptedPath = await (0, packager_1.packageProject)(workspace);
|
|
695
|
-
return {
|
|
696
|
-
content: [{ type: "text", text: `打包完成: ${encryptedPath}` }],
|
|
697
|
-
};
|
|
804
|
+
return createTextToolResult(`打包完成: ${encryptedPath}`);
|
|
698
805
|
});
|
|
699
806
|
server.registerTool("run_project", {
|
|
700
807
|
title: "Run Project",
|
|
701
|
-
description: "
|
|
808
|
+
description: "构建并同步到当前默认设备后运行项目,仅使用 HTTP 设备连接。",
|
|
702
809
|
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
810
|
workspacePath: z
|
|
734
811
|
.string()
|
|
735
812
|
.min(1)
|
|
736
813
|
.optional()
|
|
737
814
|
.describe("可选工作目录;不传时使用 set_workspace 记忆值"),
|
|
738
815
|
},
|
|
739
|
-
}, async ({
|
|
816
|
+
}, async ({ workspacePath }) => {
|
|
740
817
|
const workspace = await ensureWorkspacePath(workspacePath);
|
|
741
|
-
const target = await
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
wsPort,
|
|
746
|
-
wsWaitMs,
|
|
818
|
+
const target = await resolveRuntimeHttpTarget();
|
|
819
|
+
await (0, project_1.runOnDevice)({
|
|
820
|
+
...createRuntimeHttpRequestOptions(target),
|
|
821
|
+
workspacePath: workspace,
|
|
747
822
|
});
|
|
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
|
-
};
|
|
823
|
+
return createTextToolResult(`运行请求已发送到 ${target.label}`);
|
|
769
824
|
});
|
|
770
825
|
server.registerTool("run_ui_project", {
|
|
771
826
|
title: "Run UI Project",
|
|
772
|
-
description: "
|
|
827
|
+
description: "构建并同步到当前默认设备后预览 UI,仅使用 HTTP 设备连接。",
|
|
773
828
|
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
829
|
workspacePath: z
|
|
805
830
|
.string()
|
|
806
831
|
.min(1)
|
|
807
832
|
.optional()
|
|
808
833
|
.describe("可选工作目录;不传时使用 set_workspace 记忆值"),
|
|
809
834
|
},
|
|
810
|
-
}, async ({
|
|
835
|
+
}, async ({ workspacePath }) => {
|
|
811
836
|
const workspace = await ensureWorkspacePath(workspacePath);
|
|
812
|
-
const target = await
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
wsPort,
|
|
817
|
-
wsWaitMs,
|
|
837
|
+
const target = await resolveRuntimeHttpTarget();
|
|
838
|
+
await (0, project_1.runUIOnDevice)({
|
|
839
|
+
...createRuntimeHttpRequestOptions(target),
|
|
840
|
+
workspacePath: workspace,
|
|
818
841
|
});
|
|
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
|
-
};
|
|
842
|
+
return createTextToolResult(`UI 预览请求已发送到 ${target.label}`);
|
|
840
843
|
});
|
|
841
844
|
server.registerTool("stop_project", {
|
|
842
845
|
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
|
-
};
|
|
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}`);
|
|
903
852
|
});
|
|
904
853
|
}
|
|
905
854
|
/**
|