ms-vite-plugin 1.4.14 → 1.4.15
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/device-log.d.ts +7 -22
- package/dist/mcp/device-log.js +20 -383
- package/dist/mcp/runtime-tools.js +4 -19
- package/dist/project.d.ts +12 -38
- package/dist/project.js +28 -184
- package/docs/SKILL.md +1 -2
- package/docs/mcp-agent-description.md +1 -2
- package/package.json +1 -1
package/dist/mcp/device-log.d.ts
CHANGED
|
@@ -1,25 +1,10 @@
|
|
|
1
|
+
import type { RuntimeHttpTarget } from "./tool-utils";
|
|
1
2
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* 确保后台日志订阅处于目标设备上
|
|
8
|
-
* - 若目标未变化且订阅仍在运行,则直接复用已有连接
|
|
9
|
-
* - 若目标发生变化,则重置缓存并重新建立订阅
|
|
10
|
-
* @param ip 设备 IP
|
|
11
|
-
* @param port 设备端口
|
|
12
|
-
* @returns 返回是否复用了现有订阅
|
|
13
|
-
* @example
|
|
14
|
-
* const reused = ensureDeviceLogSubscription("192.168.1.10", 9800)
|
|
15
|
-
*/
|
|
16
|
-
export declare function ensureDeviceLogSubscription(ip: string, port: number): boolean;
|
|
17
|
-
/**
|
|
18
|
-
* 生成日志缓存快照文本
|
|
19
|
-
* @param limit 需要返回的日志条数
|
|
20
|
-
* @param runtimeStatusLimit 需要返回的 runtime_status 条数
|
|
21
|
-
* @returns 返回用于 MCP 文本响应的快照内容
|
|
3
|
+
* 构建当前设备日志文本快照
|
|
4
|
+
* @param target 当前默认 HTTP 设备
|
|
5
|
+
* @param limit 需要返回的最新日志行数
|
|
6
|
+
* @returns 返回可直接作为 MCP 文本响应的日志内容
|
|
22
7
|
* @example
|
|
23
|
-
* const text = buildDeviceLogSnapshotText(
|
|
8
|
+
* const text = await buildDeviceLogSnapshotText({ ip: "192.168.1.10", port: "9800", label: "192.168.1.10:9800" }, 200)
|
|
24
9
|
*/
|
|
25
|
-
export declare function buildDeviceLogSnapshotText(
|
|
10
|
+
export declare function buildDeviceLogSnapshotText(target: RuntimeHttpTarget, limit: number): Promise<string>;
|
package/dist/mcp/device-log.js
CHANGED
|
@@ -1,390 +1,27 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DEVICE_LOG_MEMORY_LIMIT = void 0;
|
|
4
|
-
exports.ensureDeviceLogSubscription = ensureDeviceLogSubscription;
|
|
5
3
|
exports.buildDeviceLogSnapshotText = buildDeviceLogSnapshotText;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
*
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
"connected",
|
|
23
|
-
"reconnecting",
|
|
24
|
-
]);
|
|
25
|
-
/**
|
|
26
|
-
* 进程级设备日志订阅状态
|
|
27
|
-
* - 当前 MCP 仅维护单设备日志缓存
|
|
28
|
-
* - MCP Tool 处理函数会复用同一份内存缓存
|
|
29
|
-
*/
|
|
30
|
-
const deviceLogSubscriptionState = {
|
|
31
|
-
target: null,
|
|
32
|
-
status: "idle",
|
|
33
|
-
lastError: null,
|
|
34
|
-
lastEventAt: null,
|
|
35
|
-
startedAt: null,
|
|
36
|
-
logs: [],
|
|
37
|
-
runtimeStatus: [],
|
|
38
|
-
generation: 0,
|
|
39
|
-
controller: null,
|
|
40
|
-
reconnectTimer: null,
|
|
41
|
-
};
|
|
42
|
-
/**
|
|
43
|
-
* 向环形缓冲区追加数据
|
|
44
|
-
* @param buffer 目标数组
|
|
45
|
-
* @param entry 待写入的数据
|
|
46
|
-
* @param limit 数组最大长度
|
|
47
|
-
* @returns 无返回值
|
|
48
|
-
* @example
|
|
49
|
-
* pushRingBuffer([1, 2], 3, 2)
|
|
50
|
-
*/
|
|
51
|
-
function pushRingBuffer(buffer, entry, limit) {
|
|
52
|
-
if (buffer.length >= limit) {
|
|
53
|
-
buffer.splice(0, buffer.length - limit + 1);
|
|
54
|
-
}
|
|
55
|
-
buffer.push(entry);
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* 重置当前日志缓存快照
|
|
59
|
-
* @returns 无返回值
|
|
60
|
-
* @example
|
|
61
|
-
* resetDeviceLogSnapshotState()
|
|
62
|
-
*/
|
|
63
|
-
function resetDeviceLogSnapshotState() {
|
|
64
|
-
deviceLogSubscriptionState.lastError = null;
|
|
65
|
-
deviceLogSubscriptionState.lastEventAt = null;
|
|
66
|
-
deviceLogSubscriptionState.startedAt = new Date().toISOString();
|
|
67
|
-
deviceLogSubscriptionState.logs = [];
|
|
68
|
-
deviceLogSubscriptionState.runtimeStatus = [];
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* 判断日志订阅是否仍然活跃
|
|
72
|
-
* @returns 活跃返回 true,否则返回 false
|
|
73
|
-
* @example
|
|
74
|
-
* const active = isDeviceLogSubscriptionActive()
|
|
75
|
-
*/
|
|
76
|
-
function isDeviceLogSubscriptionActive() {
|
|
77
|
-
return (ACTIVE_DEVICE_LOG_SUBSCRIPTION_STATUSES.has(deviceLogSubscriptionState.status) || deviceLogSubscriptionState.reconnectTimer !== null);
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* 解析单个 SSE 文本块
|
|
81
|
-
* @param rawBlock 原始 SSE 文本块
|
|
82
|
-
* @returns 返回事件名与 data 字符串
|
|
83
|
-
* @example
|
|
84
|
-
* parseSseEventBlock("event: log\ndata: {\"message\":\"ok\"}")
|
|
85
|
-
*/
|
|
86
|
-
function parseSseEventBlock(rawBlock) {
|
|
87
|
-
const lines = rawBlock.split(/\r?\n/);
|
|
88
|
-
let eventName = "message";
|
|
89
|
-
const dataLines = [];
|
|
90
|
-
for (const line of lines) {
|
|
91
|
-
if (line.startsWith("event:")) {
|
|
92
|
-
eventName = line.slice(6).trim() || "message";
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
if (line.startsWith("data:")) {
|
|
96
|
-
dataLines.push(line.slice(5).trimStart());
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return {
|
|
100
|
-
event: eventName,
|
|
101
|
-
data: dataLines.join("\n"),
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* 将原始日志对象标准化为统一结构
|
|
106
|
-
* @param payload 日志原始对象
|
|
107
|
-
* @returns 返回标准化后的日志
|
|
108
|
-
* @example
|
|
109
|
-
* normalizeBufferedLogEntry({ level: "info", message: "started" })
|
|
110
|
-
*/
|
|
111
|
-
function normalizeBufferedLogEntry(payload) {
|
|
112
|
-
const levelText = String(payload.level ?? "info").toLowerCase();
|
|
113
|
-
const messageText = String(payload.message ?? "");
|
|
114
|
-
const timestampText = typeof payload.timestamp === "string" && payload.timestamp
|
|
115
|
-
? payload.timestamp
|
|
116
|
-
: new Date().toISOString();
|
|
117
|
-
return {
|
|
118
|
-
level: levelText,
|
|
119
|
-
message: messageText,
|
|
120
|
-
timestamp: timestampText,
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* 清理当前后台日志订阅
|
|
125
|
-
* @returns 无返回值
|
|
126
|
-
* @example
|
|
127
|
-
* stopDeviceLogSubscription()
|
|
128
|
-
*/
|
|
129
|
-
function stopDeviceLogSubscription() {
|
|
130
|
-
if (deviceLogSubscriptionState.reconnectTimer) {
|
|
131
|
-
clearTimeout(deviceLogSubscriptionState.reconnectTimer);
|
|
132
|
-
deviceLogSubscriptionState.reconnectTimer = null;
|
|
133
|
-
}
|
|
134
|
-
if (deviceLogSubscriptionState.controller) {
|
|
135
|
-
deviceLogSubscriptionState.controller.abort();
|
|
136
|
-
deviceLogSubscriptionState.controller = null;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* 安排后台重连
|
|
141
|
-
* @param generation 当前订阅代次
|
|
142
|
-
* @returns 无返回值
|
|
143
|
-
* @example
|
|
144
|
-
* scheduleDeviceLogReconnect(1)
|
|
145
|
-
*/
|
|
146
|
-
function scheduleDeviceLogReconnect(generation) {
|
|
147
|
-
if (generation !== deviceLogSubscriptionState.generation ||
|
|
148
|
-
!deviceLogSubscriptionState.target) {
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
if (deviceLogSubscriptionState.reconnectTimer) {
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
deviceLogSubscriptionState.status = "reconnecting";
|
|
155
|
-
deviceLogSubscriptionState.reconnectTimer = setTimeout(() => {
|
|
156
|
-
deviceLogSubscriptionState.reconnectTimer = null;
|
|
157
|
-
void startDeviceLogSubscriptionLoop(generation);
|
|
158
|
-
}, DEVICE_LOG_RECONNECT_DELAY_MS);
|
|
159
|
-
}
|
|
160
|
-
/**
|
|
161
|
-
* 启动一次 SSE 读取循环
|
|
162
|
-
* - 该函数仅负责单次连接;若连接中断,会由外层自动调度重连
|
|
163
|
-
* @param generation 当前订阅代次
|
|
164
|
-
* @returns 读取流程结束后返回 Promise<void>
|
|
165
|
-
* @example
|
|
166
|
-
* await startDeviceLogSubscriptionLoop(1)
|
|
167
|
-
*/
|
|
168
|
-
async function startDeviceLogSubscriptionLoop(generation) {
|
|
169
|
-
if (generation !== deviceLogSubscriptionState.generation ||
|
|
170
|
-
!deviceLogSubscriptionState.target) {
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
const { ip, port } = deviceLogSubscriptionState.target;
|
|
174
|
-
const controller = new AbortController();
|
|
175
|
-
const url = `http://${ip}:${port}/logger/sse`;
|
|
176
|
-
deviceLogSubscriptionState.controller = controller;
|
|
177
|
-
deviceLogSubscriptionState.status =
|
|
178
|
-
deviceLogSubscriptionState.lastEventAt === null
|
|
179
|
-
? "connecting"
|
|
180
|
-
: "reconnecting";
|
|
181
|
-
try {
|
|
182
|
-
const response = await fetch(url, {
|
|
183
|
-
method: "GET",
|
|
184
|
-
signal: controller.signal,
|
|
185
|
-
headers: {
|
|
186
|
-
Accept: "text/event-stream",
|
|
187
|
-
},
|
|
188
|
-
});
|
|
189
|
-
if (!response.ok || !response.body) {
|
|
190
|
-
throw new Error(`连接日志流失败: ${response.status} ${response.statusText || "unknown"}`);
|
|
191
|
-
}
|
|
192
|
-
deviceLogSubscriptionState.status = "connected";
|
|
193
|
-
deviceLogSubscriptionState.lastError = null;
|
|
194
|
-
const reader = response.body.getReader();
|
|
195
|
-
const decoder = new TextDecoder("utf-8");
|
|
196
|
-
let buffer = "";
|
|
197
|
-
while (generation === deviceLogSubscriptionState.generation) {
|
|
198
|
-
const { value, done } = await reader.read();
|
|
199
|
-
if (done) {
|
|
200
|
-
break;
|
|
201
|
-
}
|
|
202
|
-
buffer += decoder.decode(value, { stream: true });
|
|
203
|
-
let splitIndex = buffer.search(/\r?\n\r?\n/);
|
|
204
|
-
while (splitIndex >= 0) {
|
|
205
|
-
const rawBlock = buffer.slice(0, splitIndex);
|
|
206
|
-
buffer = buffer.slice(splitIndex + (buffer[splitIndex] === "\r" ? 4 : 2));
|
|
207
|
-
const event = parseSseEventBlock(rawBlock);
|
|
208
|
-
if (event.data) {
|
|
209
|
-
try {
|
|
210
|
-
const parsed = JSON.parse(event.data);
|
|
211
|
-
deviceLogSubscriptionState.lastEventAt = new Date().toISOString();
|
|
212
|
-
if (event.event === "log") {
|
|
213
|
-
pushRingBuffer(deviceLogSubscriptionState.logs, normalizeBufferedLogEntry(parsed), exports.DEVICE_LOG_MEMORY_LIMIT);
|
|
214
|
-
}
|
|
215
|
-
else if (event.event === "runtime_status") {
|
|
216
|
-
pushRingBuffer(deviceLogSubscriptionState.runtimeStatus, {
|
|
217
|
-
raw: parsed,
|
|
218
|
-
timestamp: new Date().toISOString(),
|
|
219
|
-
}, exports.DEVICE_LOG_MEMORY_LIMIT);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
catch {
|
|
223
|
-
// 忽略单条非法 JSON,避免因为异常日志阻塞整个后台连接
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
splitIndex = buffer.search(/\r?\n\r?\n/);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
if (generation === deviceLogSubscriptionState.generation) {
|
|
230
|
-
scheduleDeviceLogReconnect(generation);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
catch (error) {
|
|
234
|
-
if (error instanceof Error &&
|
|
235
|
-
(error.name === "AbortError" ||
|
|
236
|
-
error.message === "The operation was aborted.")) {
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
if (generation === deviceLogSubscriptionState.generation) {
|
|
240
|
-
deviceLogSubscriptionState.status = "error";
|
|
241
|
-
deviceLogSubscriptionState.lastError =
|
|
242
|
-
error instanceof Error ? error.message : String(error);
|
|
243
|
-
scheduleDeviceLogReconnect(generation);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
finally {
|
|
247
|
-
if (deviceLogSubscriptionState.controller === controller) {
|
|
248
|
-
deviceLogSubscriptionState.controller = null;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
/**
|
|
253
|
-
* 确保后台日志订阅处于目标设备上
|
|
254
|
-
* - 若目标未变化且订阅仍在运行,则直接复用已有连接
|
|
255
|
-
* - 若目标发生变化,则重置缓存并重新建立订阅
|
|
256
|
-
* @param ip 设备 IP
|
|
257
|
-
* @param port 设备端口
|
|
258
|
-
* @returns 返回是否复用了现有订阅
|
|
259
|
-
* @example
|
|
260
|
-
* const reused = ensureDeviceLogSubscription("192.168.1.10", 9800)
|
|
261
|
-
*/
|
|
262
|
-
function ensureDeviceLogSubscription(ip, port) {
|
|
263
|
-
const sameTarget = deviceLogSubscriptionState.target?.ip === ip &&
|
|
264
|
-
deviceLogSubscriptionState.target?.port === port;
|
|
265
|
-
const isActive = isDeviceLogSubscriptionActive();
|
|
266
|
-
if (sameTarget && isActive) {
|
|
267
|
-
return true;
|
|
268
|
-
}
|
|
269
|
-
stopDeviceLogSubscription();
|
|
270
|
-
deviceLogSubscriptionState.generation += 1;
|
|
271
|
-
deviceLogSubscriptionState.target = { ip, port };
|
|
272
|
-
deviceLogSubscriptionState.status = "connecting";
|
|
273
|
-
resetDeviceLogSnapshotState();
|
|
274
|
-
void startDeviceLogSubscriptionLoop(deviceLogSubscriptionState.generation);
|
|
275
|
-
return false;
|
|
276
|
-
}
|
|
277
|
-
/**
|
|
278
|
-
* 按日志页面风格格式化时间
|
|
279
|
-
* @param value ISO 时间字符串
|
|
280
|
-
* @returns 返回 `yyyy/mm/dd hh:mm:ss.xxx` 风格文本,异常时回退原值
|
|
281
|
-
* @example
|
|
282
|
-
* formatDisplayTimestamp("2026-05-07T11:34:48.776Z")
|
|
283
|
-
*/
|
|
284
|
-
function formatDisplayTimestamp(value) {
|
|
285
|
-
const date = new Date(value);
|
|
286
|
-
if (Number.isNaN(date.getTime())) {
|
|
287
|
-
return value;
|
|
288
|
-
}
|
|
289
|
-
const pad = (number, length = 2) => String(number).padStart(length, "0");
|
|
290
|
-
return `${date.getFullYear()}/${pad(date.getMonth() + 1)}/${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds(), 3)}`;
|
|
291
|
-
}
|
|
292
|
-
/**
|
|
293
|
-
* 将日志订阅连接状态转为中文文本
|
|
294
|
-
* @param status 原始连接状态
|
|
295
|
-
* @returns 返回中文状态说明
|
|
296
|
-
* @example
|
|
297
|
-
* formatDeviceLogSubscriptionStatus("connected")
|
|
298
|
-
*/
|
|
299
|
-
function formatDeviceLogSubscriptionStatus(status) {
|
|
300
|
-
switch (status) {
|
|
301
|
-
case "idle":
|
|
302
|
-
return "未订阅";
|
|
303
|
-
case "connecting":
|
|
304
|
-
return "连接中";
|
|
305
|
-
case "connected":
|
|
306
|
-
return "已连接";
|
|
307
|
-
case "reconnecting":
|
|
308
|
-
return "重连中";
|
|
309
|
-
case "error":
|
|
310
|
-
return "连接异常";
|
|
311
|
-
default:
|
|
312
|
-
return status;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
/**
|
|
316
|
-
* 将 runtime_status 中的内存数值格式化为可读文本
|
|
317
|
-
* @param value 原始数值
|
|
318
|
-
* @param suffix 数值单位后缀
|
|
319
|
-
* @returns 数值存在时返回格式化结果,否则返回未知
|
|
320
|
-
* @example
|
|
321
|
-
* formatRuntimeMetricText(12.3456, " MB")
|
|
322
|
-
*/
|
|
323
|
-
function formatRuntimeMetricText(value, suffix = "") {
|
|
324
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
325
|
-
return `${value.toFixed(2)}${suffix}`;
|
|
326
|
-
}
|
|
327
|
-
return "未知";
|
|
328
|
-
}
|
|
329
|
-
/**
|
|
330
|
-
* 将单条 runtime_status 事件格式化为中文运行状态摘要
|
|
331
|
-
* @param entry runtime_status 条目
|
|
332
|
-
* @returns 返回更易读的状态文本
|
|
333
|
-
* @example
|
|
334
|
-
* formatRuntimeStatusEntry({ raw: { isRunning: false }, timestamp: "2026-01-01T00:00:00.000Z" })
|
|
335
|
-
*/
|
|
336
|
-
function formatRuntimeStatusEntry(entry) {
|
|
337
|
-
const raw = entry.raw;
|
|
338
|
-
const memory = raw.memory && typeof raw.memory === "object"
|
|
339
|
-
? raw.memory
|
|
340
|
-
: {};
|
|
341
|
-
return [
|
|
342
|
-
`[${formatDisplayTimestamp(entry.timestamp)}]`,
|
|
343
|
-
`UI: ${typeof raw.isUIShowing === "boolean"
|
|
344
|
-
? raw.isUIShowing
|
|
345
|
-
? "显示中"
|
|
346
|
-
: "未显示"
|
|
347
|
-
: "未知"}`,
|
|
348
|
-
`脚本: ${typeof raw.isRunning === "boolean"
|
|
349
|
-
? raw.isRunning
|
|
350
|
-
? "运行中"
|
|
351
|
-
: "未运行"
|
|
352
|
-
: "未知"}`,
|
|
353
|
-
`系统: ${formatRuntimeMetricText(memory.systemUsed, " MB")}/${formatRuntimeMetricText(memory.total, " MB")}`,
|
|
354
|
-
`系统占用: ${formatRuntimeMetricText(memory.usagePercentage, "%")}`,
|
|
355
|
-
`应用: ${formatRuntimeMetricText(memory.used, " MB")}`,
|
|
356
|
-
].join(" | ");
|
|
357
|
-
}
|
|
358
|
-
/**
|
|
359
|
-
* 生成日志缓存快照文本
|
|
360
|
-
* @param limit 需要返回的日志条数
|
|
361
|
-
* @param runtimeStatusLimit 需要返回的 runtime_status 条数
|
|
362
|
-
* @returns 返回用于 MCP 文本响应的快照内容
|
|
363
|
-
* @example
|
|
364
|
-
* const text = buildDeviceLogSnapshotText(100, 20)
|
|
365
|
-
*/
|
|
366
|
-
function buildDeviceLogSnapshotText(limit, runtimeStatusLimit) {
|
|
367
|
-
const recentLogs = deviceLogSubscriptionState.logs
|
|
368
|
-
.slice(-limit)
|
|
369
|
-
.map((log) => `[${formatDisplayTimestamp(log.timestamp)}] [${log.level.toUpperCase()}] ${log.message}`);
|
|
370
|
-
const recentRuntimeStatus = deviceLogSubscriptionState.runtimeStatus
|
|
371
|
-
.slice(-runtimeStatusLimit)
|
|
372
|
-
.map((entry) => formatRuntimeStatusEntry(entry));
|
|
4
|
+
const project_1 = require("../project");
|
|
5
|
+
/**
|
|
6
|
+
* 构建当前设备日志文本快照
|
|
7
|
+
* @param target 当前默认 HTTP 设备
|
|
8
|
+
* @param limit 需要返回的最新日志行数
|
|
9
|
+
* @returns 返回可直接作为 MCP 文本响应的日志内容
|
|
10
|
+
* @example
|
|
11
|
+
* const text = await buildDeviceLogSnapshotText({ ip: "192.168.1.10", port: "9800", label: "192.168.1.10:9800" }, 200)
|
|
12
|
+
*/
|
|
13
|
+
async function buildDeviceLogSnapshotText(target, limit) {
|
|
14
|
+
const result = await (0, project_1.getCurrentLogLinesOnDevice)({
|
|
15
|
+
ip: target.ip,
|
|
16
|
+
port: target.port,
|
|
17
|
+
transport: "http",
|
|
18
|
+
}, limit);
|
|
19
|
+
const lines = result.lines.length > 0 ? result.lines.join("\n") : "无日志";
|
|
373
20
|
return [
|
|
374
|
-
`设备: ${
|
|
375
|
-
|
|
376
|
-
: "未订阅"}`,
|
|
377
|
-
`状态: ${formatDeviceLogSubscriptionStatus(deviceLogSubscriptionState.status)}`,
|
|
378
|
-
...(deviceLogSubscriptionState.lastError
|
|
379
|
-
? [`最近错误: ${deviceLogSubscriptionState.lastError}`]
|
|
380
|
-
: []),
|
|
381
|
-
"",
|
|
382
|
-
`最新日志(最多 ${limit} 条):`,
|
|
383
|
-
recentLogs.length > 0 ? recentLogs.join("\n") : "无日志",
|
|
21
|
+
`设备: ${target.label}`,
|
|
22
|
+
`还有更早日志: ${result.hasMore ? "是" : "否"}`,
|
|
384
23
|
"",
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
? recentRuntimeStatus.join("\n")
|
|
388
|
-
: "无运行状态",
|
|
24
|
+
`最新日志(最多 ${limit} 行):`,
|
|
25
|
+
lines,
|
|
389
26
|
].join("\n");
|
|
390
27
|
}
|
|
@@ -142,13 +142,7 @@ function registerRuntimeTools(server) {
|
|
|
142
142
|
},
|
|
143
143
|
}, async ({ ip, port }) => {
|
|
144
144
|
const config = await (0, device_config_1.setDeviceConfig)(ip, port);
|
|
145
|
-
|
|
146
|
-
return (0, tool_utils_1.createTextToolResult)([
|
|
147
|
-
`默认设备已设置为 ${config.ip}:${config.port}`,
|
|
148
|
-
reusedSubscription
|
|
149
|
-
? "SSE 日志后台订阅已复用现有连接"
|
|
150
|
-
: `SSE 日志后台订阅已启动,内存缓存上限 ${device_log_1.DEVICE_LOG_MEMORY_LIMIT} 条`,
|
|
151
|
-
].join("\n"));
|
|
145
|
+
return (0, tool_utils_1.createTextToolResult)(`默认设备已设置为 ${config.ip}:${config.port}`);
|
|
152
146
|
});
|
|
153
147
|
server.registerTool("get_device", {
|
|
154
148
|
title: "Get Device",
|
|
@@ -226,7 +220,7 @@ function registerRuntimeTools(server) {
|
|
|
226
220
|
});
|
|
227
221
|
server.registerTool("get_logs", {
|
|
228
222
|
title: "Get Device Logs",
|
|
229
|
-
description: "
|
|
223
|
+
description: "获取当前默认设备的最新日志行。",
|
|
230
224
|
inputSchema: {
|
|
231
225
|
limit: z
|
|
232
226
|
.number()
|
|
@@ -236,19 +230,10 @@ function registerRuntimeTools(server) {
|
|
|
236
230
|
.optional()
|
|
237
231
|
.default(200)
|
|
238
232
|
.describe("返回最近日志条数,默认 200,最大 5000"),
|
|
239
|
-
runtimeStatusLimit: z
|
|
240
|
-
.number()
|
|
241
|
-
.int()
|
|
242
|
-
.min(0)
|
|
243
|
-
.max(200)
|
|
244
|
-
.optional()
|
|
245
|
-
.default(20)
|
|
246
|
-
.describe("返回最近 runtime_status 条数,默认 20"),
|
|
247
233
|
},
|
|
248
|
-
}, async ({ limit
|
|
234
|
+
}, async ({ limit }) => {
|
|
249
235
|
const target = await (0, tool_utils_1.resolveRuntimeHttpTarget)();
|
|
250
|
-
(0,
|
|
251
|
-
return (0, tool_utils_1.createTextToolResult)((0, device_log_1.buildDeviceLogSnapshotText)(limit, runtimeStatusLimit));
|
|
236
|
+
return (0, tool_utils_1.createTextToolResult)(await (0, device_log_1.buildDeviceLogSnapshotText)(target, limit));
|
|
252
237
|
});
|
|
253
238
|
server.registerTool("package_project", {
|
|
254
239
|
title: "Package Project",
|
package/dist/project.d.ts
CHANGED
|
@@ -10,35 +10,13 @@ export interface DeviceCliOptions {
|
|
|
10
10
|
workspacePath?: string;
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
13
|
+
* 当前日志最新行接口响应
|
|
14
14
|
*/
|
|
15
|
-
export interface
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
/** 日志时间戳(ISO 字符串) */
|
|
21
|
-
timestamp: string;
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* 运行时状态条目(来自 SSE runtime_status 事件)
|
|
25
|
-
*/
|
|
26
|
-
export interface DeviceRuntimeStatusEntry {
|
|
27
|
-
/** 原始 runtime_status 数据 */
|
|
28
|
-
raw: Record<string, unknown>;
|
|
29
|
-
/** 事件时间戳(本地接收时间) */
|
|
30
|
-
timestamp: string;
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* SSE 日志监听结果
|
|
34
|
-
*/
|
|
35
|
-
export interface DeviceLogWatchResult {
|
|
36
|
-
/** 收集到的日志条目 */
|
|
37
|
-
logs: DeviceLogEntry[];
|
|
38
|
-
/** 收集到的 runtime_status 条目 */
|
|
39
|
-
runtimeStatus: DeviceRuntimeStatusEntry[];
|
|
40
|
-
/** 监听结束原因 */
|
|
41
|
-
stopReason: "timeout" | "runtime_continuous" | "runtime_stopped" | "stream_closed" | "manual_abort" | "unknown";
|
|
15
|
+
export interface CurrentLogLinesResponse {
|
|
16
|
+
/** 当前日志文件最新行,按日志原始输出顺序排列 */
|
|
17
|
+
lines: string[];
|
|
18
|
+
/** 本次返回范围前是否还有更早日志 */
|
|
19
|
+
hasMore: boolean;
|
|
42
20
|
}
|
|
43
21
|
/**
|
|
44
22
|
* 在设备上获取截图 JPG 二进制数据(HTTP)
|
|
@@ -67,18 +45,14 @@ export declare function getScreenshotBase64OnDevice(options: DeviceCliOptions):
|
|
|
67
45
|
*/
|
|
68
46
|
export declare function getSourceOnDevice(options: DeviceCliOptions, maxDepth?: number, timeout?: number): Promise<string>;
|
|
69
47
|
/**
|
|
70
|
-
*
|
|
71
|
-
* @param
|
|
72
|
-
* @param
|
|
73
|
-
* @
|
|
74
|
-
* @param maxLogs 最多收集日志条数,默认 200
|
|
75
|
-
* @param stopOnRuntimeStopped 若为 true,则在运行状态从 true 回落到 false 后结束监听
|
|
76
|
-
* @param stopOnRuntimeContinuous 若为 true,则在确认持续运行态后结束监听(用于 single 模式快速返回)
|
|
77
|
-
* @returns 返回日志与运行状态
|
|
48
|
+
* 在设备上获取当前日志文件最新行
|
|
49
|
+
* @param options 命令选项(仅支持 http 传输)
|
|
50
|
+
* @param count 需要读取的最新日志行数,默认 200,最大 5000
|
|
51
|
+
* @returns 返回最新日志行和是否还有更早日志
|
|
78
52
|
* @example
|
|
79
|
-
* const result = await
|
|
53
|
+
* const result = await getCurrentLogLinesOnDevice({ ip: "192.168.1.10", port: "9800" }, 200)
|
|
80
54
|
*/
|
|
81
|
-
export declare function
|
|
55
|
+
export declare function getCurrentLogLinesOnDevice(options: DeviceCliOptions, count?: number): Promise<CurrentLogLinesResponse>;
|
|
82
56
|
/**
|
|
83
57
|
* 在设备上运行项目(先同步后运行)
|
|
84
58
|
* @param options 命令选项
|
package/dist/project.js
CHANGED
|
@@ -39,7 +39,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
39
39
|
exports.getScreenshotOnDevice = getScreenshotOnDevice;
|
|
40
40
|
exports.getScreenshotBase64OnDevice = getScreenshotBase64OnDevice;
|
|
41
41
|
exports.getSourceOnDevice = getSourceOnDevice;
|
|
42
|
-
exports.
|
|
42
|
+
exports.getCurrentLogLinesOnDevice = getCurrentLogLinesOnDevice;
|
|
43
43
|
exports.runOnDevice = runOnDevice;
|
|
44
44
|
exports.runUIOnDevice = runUIOnDevice;
|
|
45
45
|
exports.stopOnDevice = stopOnDevice;
|
|
@@ -463,196 +463,40 @@ async function getSourceOnDevice(options, maxDepth = 50, timeout = 120) {
|
|
|
463
463
|
return response.text();
|
|
464
464
|
}
|
|
465
465
|
/**
|
|
466
|
-
*
|
|
467
|
-
* @param
|
|
468
|
-
* @
|
|
466
|
+
* 在设备上获取当前日志文件最新行
|
|
467
|
+
* @param options 命令选项(仅支持 http 传输)
|
|
468
|
+
* @param count 需要读取的最新日志行数,默认 200,最大 5000
|
|
469
|
+
* @returns 返回最新日志行和是否还有更早日志
|
|
469
470
|
* @example
|
|
470
|
-
*
|
|
471
|
+
* const result = await getCurrentLogLinesOnDevice({ ip: "192.168.1.10", port: "9800" }, 200)
|
|
471
472
|
*/
|
|
472
|
-
function
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
for (const line of lines) {
|
|
477
|
-
if (!line || line.startsWith(":")) {
|
|
478
|
-
continue;
|
|
479
|
-
}
|
|
480
|
-
if (line.startsWith("event:")) {
|
|
481
|
-
eventName = line.slice("event:".length).trim() || "message";
|
|
482
|
-
continue;
|
|
483
|
-
}
|
|
484
|
-
if (line.startsWith("data:")) {
|
|
485
|
-
dataLines.push(line.slice("data:".length).trimStart());
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
return {
|
|
489
|
-
event: eventName,
|
|
490
|
-
data: dataLines.join("\n"),
|
|
491
|
-
};
|
|
492
|
-
}
|
|
493
|
-
/**
|
|
494
|
-
* 将原始日志对象标准化为 DeviceLogEntry
|
|
495
|
-
* @param payload 日志原始对象
|
|
496
|
-
* @returns 返回标准化日志对象
|
|
497
|
-
* @example
|
|
498
|
-
* normalizeLogEntry({ level: "info", message: "ok", timestamp: "2026-01-01T00:00:00Z" })
|
|
499
|
-
*/
|
|
500
|
-
function normalizeLogEntry(payload) {
|
|
501
|
-
const levelText = String(payload.level ?? "info").toLowerCase();
|
|
502
|
-
const messageText = String(payload.message ?? "");
|
|
503
|
-
const timestampText = typeof payload.timestamp === "string" && payload.timestamp
|
|
504
|
-
? payload.timestamp
|
|
505
|
-
: new Date().toISOString();
|
|
506
|
-
return {
|
|
507
|
-
level: levelText,
|
|
508
|
-
message: messageText,
|
|
509
|
-
timestamp: timestampText,
|
|
510
|
-
};
|
|
511
|
-
}
|
|
512
|
-
/**
|
|
513
|
-
* 监听设备 SSE 日志并返回原始结果
|
|
514
|
-
* @param ip 设备 IP 地址
|
|
515
|
-
* @param port 设备端口
|
|
516
|
-
* @param durationMs 监听时长(毫秒),默认 15000;传 0 表示不设超时,持续监听
|
|
517
|
-
* @param maxLogs 最多收集日志条数,默认 200
|
|
518
|
-
* @param stopOnRuntimeStopped 若为 true,则在运行状态从 true 回落到 false 后结束监听
|
|
519
|
-
* @param stopOnRuntimeContinuous 若为 true,则在确认持续运行态后结束监听(用于 single 模式快速返回)
|
|
520
|
-
* @returns 返回日志与运行状态
|
|
521
|
-
* @example
|
|
522
|
-
* const result = await watchDeviceLogsBySse("192.168.1.10", 9800, 30000, 300, true, true)
|
|
523
|
-
*/
|
|
524
|
-
async function watchDeviceLogsBySse(ip, port, durationMs = 15000, maxLogs = 200, stopOnRuntimeStopped = false, stopOnRuntimeContinuous = false) {
|
|
525
|
-
if (!ip.trim()) {
|
|
526
|
-
throw new Error("监听日志失败: 设备 IP 不能为空");
|
|
473
|
+
async function getCurrentLogLinesOnDevice(options, count = 200) {
|
|
474
|
+
const transport = parseTransport(options);
|
|
475
|
+
if (transport !== "http") {
|
|
476
|
+
throw new Error("获取日志失败: 当前日志行接口仅支持 http 传输");
|
|
527
477
|
}
|
|
528
|
-
if (!Number.isInteger(
|
|
529
|
-
throw new Error(
|
|
530
|
-
}
|
|
531
|
-
if (!Number.isInteger(durationMs) || durationMs < 0) {
|
|
532
|
-
throw new Error(`监听日志失败: 无效 durationMs ${durationMs}`);
|
|
533
|
-
}
|
|
534
|
-
if (!Number.isInteger(maxLogs) || maxLogs < 1) {
|
|
535
|
-
throw new Error(`监听日志失败: 无效 maxLogs ${maxLogs}`);
|
|
536
|
-
}
|
|
537
|
-
const url = `http://${ip}:${port}/logger/sse`;
|
|
538
|
-
const logs = [];
|
|
539
|
-
const runtimeStatus = [];
|
|
540
|
-
const controller = new AbortController();
|
|
541
|
-
let stopReason = "unknown";
|
|
542
|
-
let seenRunningStatus = false;
|
|
543
|
-
let runningStatusCount = 0;
|
|
544
|
-
let shouldStopReading = false;
|
|
545
|
-
const timeoutHandle = durationMs > 0
|
|
546
|
-
? setTimeout(() => {
|
|
547
|
-
stopReason = "timeout";
|
|
548
|
-
controller.abort();
|
|
549
|
-
}, durationMs)
|
|
550
|
-
: undefined;
|
|
551
|
-
try {
|
|
552
|
-
const response = await fetch(url, {
|
|
553
|
-
method: "GET",
|
|
554
|
-
signal: controller.signal,
|
|
555
|
-
headers: {
|
|
556
|
-
Accept: "text/event-stream",
|
|
557
|
-
},
|
|
558
|
-
});
|
|
559
|
-
if (!response.ok || !response.body) {
|
|
560
|
-
throw new Error(`监听日志失败: ${response.status} ${response.statusText || "unknown"}`);
|
|
561
|
-
}
|
|
562
|
-
const reader = response.body.getReader();
|
|
563
|
-
const decoder = new TextDecoder("utf-8");
|
|
564
|
-
let buffer = "";
|
|
565
|
-
while (true) {
|
|
566
|
-
if (shouldStopReading) {
|
|
567
|
-
break;
|
|
568
|
-
}
|
|
569
|
-
const { value, done } = await reader.read();
|
|
570
|
-
if (done) {
|
|
571
|
-
if (stopReason === "unknown") {
|
|
572
|
-
stopReason = "stream_closed";
|
|
573
|
-
}
|
|
574
|
-
break;
|
|
575
|
-
}
|
|
576
|
-
buffer += decoder.decode(value, { stream: true });
|
|
577
|
-
// SSE 事件以双换行分隔,这里逐块解析,避免半包解析错误
|
|
578
|
-
let splitIndex = buffer.search(/\r?\n\r?\n/);
|
|
579
|
-
while (splitIndex >= 0) {
|
|
580
|
-
const rawBlock = buffer.slice(0, splitIndex);
|
|
581
|
-
buffer = buffer.slice(splitIndex + (buffer[splitIndex] === "\r" ? 4 : 2));
|
|
582
|
-
const event = parseSseEventBlock(rawBlock);
|
|
583
|
-
if (event.data) {
|
|
584
|
-
try {
|
|
585
|
-
const parsed = JSON.parse(event.data);
|
|
586
|
-
if (event.event === "log") {
|
|
587
|
-
// 日志缓冲使用环形窗口,持续监听时不会无限增长
|
|
588
|
-
if (logs.length >= maxLogs) {
|
|
589
|
-
logs.shift();
|
|
590
|
-
}
|
|
591
|
-
logs.push(normalizeLogEntry(parsed));
|
|
592
|
-
}
|
|
593
|
-
else if (event.event === "runtime_status") {
|
|
594
|
-
if (runtimeStatus.length >= maxLogs) {
|
|
595
|
-
runtimeStatus.shift();
|
|
596
|
-
}
|
|
597
|
-
runtimeStatus.push({
|
|
598
|
-
raw: parsed,
|
|
599
|
-
timestamp: new Date().toISOString(),
|
|
600
|
-
});
|
|
601
|
-
if (parsed.isRunning === true) {
|
|
602
|
-
seenRunningStatus = true;
|
|
603
|
-
runningStatusCount += 1;
|
|
604
|
-
// 连续收到运行态,视为长期任务,single 模式可立即返回并转持续监听
|
|
605
|
-
if (stopOnRuntimeContinuous && runningStatusCount >= 2) {
|
|
606
|
-
stopReason = "runtime_continuous";
|
|
607
|
-
controller.abort();
|
|
608
|
-
shouldStopReading = true;
|
|
609
|
-
break;
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
else if (parsed.isRunning === false) {
|
|
613
|
-
runningStatusCount = 0;
|
|
614
|
-
}
|
|
615
|
-
if (stopOnRuntimeStopped &&
|
|
616
|
-
seenRunningStatus &&
|
|
617
|
-
parsed.isRunning === false) {
|
|
618
|
-
stopReason = "runtime_stopped";
|
|
619
|
-
controller.abort();
|
|
620
|
-
shouldStopReading = true;
|
|
621
|
-
break;
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
catch {
|
|
626
|
-
// 兼容非 JSON 数据:不抛错,继续监听后续事件
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
splitIndex = buffer.search(/\r?\n\r?\n/);
|
|
630
|
-
}
|
|
631
|
-
if (shouldStopReading) {
|
|
632
|
-
break;
|
|
633
|
-
}
|
|
634
|
-
}
|
|
478
|
+
if (!Number.isInteger(count) || count < 1 || count > 5000) {
|
|
479
|
+
throw new Error(`获取日志失败: 无效 count ${count}`);
|
|
635
480
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
}
|
|
481
|
+
const { ip, port } = parseHttpOptions(options);
|
|
482
|
+
const url = new URL(`http://${ip}:${port}/logger/current/lines`);
|
|
483
|
+
url.searchParams.set("count", String(count));
|
|
484
|
+
const response = await fetch(url.toString(), { method: "GET" });
|
|
485
|
+
if (response.status === 404) {
|
|
486
|
+
return {
|
|
487
|
+
lines: [],
|
|
488
|
+
hasMore: false,
|
|
489
|
+
};
|
|
646
490
|
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
clearTimeout(timeoutHandle);
|
|
650
|
-
}
|
|
491
|
+
if (!response.ok) {
|
|
492
|
+
throw new Error(`获取日志失败,状态码: ${response.status}`);
|
|
651
493
|
}
|
|
494
|
+
const payload = (await response.json());
|
|
652
495
|
return {
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
496
|
+
lines: Array.isArray(payload.lines)
|
|
497
|
+
? payload.lines.map((line) => String(line))
|
|
498
|
+
: [],
|
|
499
|
+
hasMore: payload.hasMore === true,
|
|
656
500
|
};
|
|
657
501
|
}
|
|
658
502
|
/**
|
package/docs/SKILL.md
CHANGED