ms-vite-plugin 1.4.13 → 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.
@@ -1,25 +1,10 @@
1
+ import type { RuntimeHttpTarget } from "./tool-utils";
1
2
  /**
2
- * 设备日志内存缓存上限
3
- * - 采用环形窗口思路,仅保留最新 5000 条,避免 MCP 进程长期运行时内存无限增长
4
- */
5
- export declare const DEVICE_LOG_MEMORY_LIMIT = 5000;
6
- /**
7
- * 确保后台日志订阅处于目标设备上
8
- * - 若目标未变化且订阅仍在运行,则直接复用已有连接
9
- * - 若目标发生变化,则重置缓存并重新建立订阅
10
- * @param ip 设备 IP
11
- * @param port 设备端口
12
- * @returns 返回是否复用了现有订阅
13
- * @example
14
- * const reused = ensureDeviceLogSubscription("192.168.1.10", 9800)
15
- */
16
- export declare function ensureDeviceLogSubscription(ip: string, port: number): boolean;
17
- /**
18
- * 生成日志缓存快照文本
19
- * @param limit 需要返回的日志条数
20
- * @param runtimeStatusLimit 需要返回的 runtime_status 条数
21
- * @returns 返回用于 MCP 文本响应的快照内容
3
+ * 构建当前设备日志文本快照
4
+ * @param target 当前默认 HTTP 设备
5
+ * @param limit 需要返回的最新日志行数
6
+ * @returns 返回可直接作为 MCP 文本响应的日志内容
22
7
  * @example
23
- * const text = buildDeviceLogSnapshotText(100, 20)
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(limit: number, runtimeStatusLimit: number): string;
10
+ export declare function buildDeviceLogSnapshotText(target: RuntimeHttpTarget, limit: number): Promise<string>;
@@ -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
- * - 采用环形窗口思路,仅保留最新 5000 条,避免 MCP 进程长期运行时内存无限增长
9
- */
10
- exports.DEVICE_LOG_MEMORY_LIMIT = 5000;
11
- /**
12
- * SSE 重连等待时间(毫秒)
13
- * - 当设备日志流异常断开时,后台会自动尝试重连
14
- */
15
- const DEVICE_LOG_RECONNECT_DELAY_MS = 1500;
16
- /**
17
- * 视为“仍在工作中”的日志订阅状态集合
18
- * - 用于判断同一设备是否可以直接复用已有后台连接
19
- */
20
- const ACTIVE_DEVICE_LOG_SUBSCRIPTION_STATUSES = new Set([
21
- "connecting",
22
- "connected",
23
- "reconnecting",
24
- ]);
25
- /**
26
- * 进程级设备日志订阅状态
27
- * - 当前 MCP 仅维护单设备日志缓存
28
- * - MCP Tool 处理函数会复用同一份内存缓存
29
- */
30
- const deviceLogSubscriptionState = {
31
- target: null,
32
- status: "idle",
33
- lastError: null,
34
- lastEventAt: null,
35
- startedAt: null,
36
- logs: [],
37
- runtimeStatus: [],
38
- generation: 0,
39
- controller: null,
40
- reconnectTimer: null,
41
- };
42
- /**
43
- * 向环形缓冲区追加数据
44
- * @param buffer 目标数组
45
- * @param entry 待写入的数据
46
- * @param limit 数组最大长度
47
- * @returns 无返回值
48
- * @example
49
- * pushRingBuffer([1, 2], 3, 2)
50
- */
51
- function pushRingBuffer(buffer, entry, limit) {
52
- if (buffer.length >= limit) {
53
- buffer.splice(0, buffer.length - limit + 1);
54
- }
55
- buffer.push(entry);
56
- }
57
- /**
58
- * 重置当前日志缓存快照
59
- * @returns 无返回值
60
- * @example
61
- * resetDeviceLogSnapshotState()
62
- */
63
- function resetDeviceLogSnapshotState() {
64
- deviceLogSubscriptionState.lastError = null;
65
- deviceLogSubscriptionState.lastEventAt = null;
66
- deviceLogSubscriptionState.startedAt = new Date().toISOString();
67
- deviceLogSubscriptionState.logs = [];
68
- deviceLogSubscriptionState.runtimeStatus = [];
69
- }
70
- /**
71
- * 判断日志订阅是否仍然活跃
72
- * @returns 活跃返回 true,否则返回 false
73
- * @example
74
- * const active = isDeviceLogSubscriptionActive()
75
- */
76
- function isDeviceLogSubscriptionActive() {
77
- return (ACTIVE_DEVICE_LOG_SUBSCRIPTION_STATUSES.has(deviceLogSubscriptionState.status) || deviceLogSubscriptionState.reconnectTimer !== null);
78
- }
79
- /**
80
- * 解析单个 SSE 文本块
81
- * @param rawBlock 原始 SSE 文本块
82
- * @returns 返回事件名与 data 字符串
83
- * @example
84
- * parseSseEventBlock("event: log\ndata: {\"message\":\"ok\"}")
85
- */
86
- function parseSseEventBlock(rawBlock) {
87
- const lines = rawBlock.split(/\r?\n/);
88
- let eventName = "message";
89
- const dataLines = [];
90
- for (const line of lines) {
91
- if (line.startsWith("event:")) {
92
- eventName = line.slice(6).trim() || "message";
93
- continue;
94
- }
95
- if (line.startsWith("data:")) {
96
- dataLines.push(line.slice(5).trimStart());
97
- }
98
- }
99
- return {
100
- event: eventName,
101
- data: dataLines.join("\n"),
102
- };
103
- }
104
- /**
105
- * 将原始日志对象标准化为统一结构
106
- * @param payload 日志原始对象
107
- * @returns 返回标准化后的日志
108
- * @example
109
- * normalizeBufferedLogEntry({ level: "info", message: "started" })
110
- */
111
- function normalizeBufferedLogEntry(payload) {
112
- const levelText = String(payload.level ?? "info").toLowerCase();
113
- const messageText = String(payload.message ?? "");
114
- const timestampText = typeof payload.timestamp === "string" && payload.timestamp
115
- ? payload.timestamp
116
- : new Date().toISOString();
117
- return {
118
- level: levelText,
119
- message: messageText,
120
- timestamp: timestampText,
121
- };
122
- }
123
- /**
124
- * 清理当前后台日志订阅
125
- * @returns 无返回值
126
- * @example
127
- * stopDeviceLogSubscription()
128
- */
129
- function stopDeviceLogSubscription() {
130
- if (deviceLogSubscriptionState.reconnectTimer) {
131
- clearTimeout(deviceLogSubscriptionState.reconnectTimer);
132
- deviceLogSubscriptionState.reconnectTimer = null;
133
- }
134
- if (deviceLogSubscriptionState.controller) {
135
- deviceLogSubscriptionState.controller.abort();
136
- deviceLogSubscriptionState.controller = null;
137
- }
138
- }
139
- /**
140
- * 安排后台重连
141
- * @param generation 当前订阅代次
142
- * @returns 无返回值
143
- * @example
144
- * scheduleDeviceLogReconnect(1)
145
- */
146
- function scheduleDeviceLogReconnect(generation) {
147
- if (generation !== deviceLogSubscriptionState.generation ||
148
- !deviceLogSubscriptionState.target) {
149
- return;
150
- }
151
- if (deviceLogSubscriptionState.reconnectTimer) {
152
- return;
153
- }
154
- deviceLogSubscriptionState.status = "reconnecting";
155
- deviceLogSubscriptionState.reconnectTimer = setTimeout(() => {
156
- deviceLogSubscriptionState.reconnectTimer = null;
157
- void startDeviceLogSubscriptionLoop(generation);
158
- }, DEVICE_LOG_RECONNECT_DELAY_MS);
159
- }
160
- /**
161
- * 启动一次 SSE 读取循环
162
- * - 该函数仅负责单次连接;若连接中断,会由外层自动调度重连
163
- * @param generation 当前订阅代次
164
- * @returns 读取流程结束后返回 Promise<void>
165
- * @example
166
- * await startDeviceLogSubscriptionLoop(1)
167
- */
168
- async function startDeviceLogSubscriptionLoop(generation) {
169
- if (generation !== deviceLogSubscriptionState.generation ||
170
- !deviceLogSubscriptionState.target) {
171
- return;
172
- }
173
- const { ip, port } = deviceLogSubscriptionState.target;
174
- const controller = new AbortController();
175
- const url = `http://${ip}:${port}/logger/sse`;
176
- deviceLogSubscriptionState.controller = controller;
177
- deviceLogSubscriptionState.status =
178
- deviceLogSubscriptionState.lastEventAt === null
179
- ? "connecting"
180
- : "reconnecting";
181
- try {
182
- const response = await fetch(url, {
183
- method: "GET",
184
- signal: controller.signal,
185
- headers: {
186
- Accept: "text/event-stream",
187
- },
188
- });
189
- if (!response.ok || !response.body) {
190
- throw new Error(`连接日志流失败: ${response.status} ${response.statusText || "unknown"}`);
191
- }
192
- deviceLogSubscriptionState.status = "connected";
193
- deviceLogSubscriptionState.lastError = null;
194
- const reader = response.body.getReader();
195
- const decoder = new TextDecoder("utf-8");
196
- let buffer = "";
197
- while (generation === deviceLogSubscriptionState.generation) {
198
- const { value, done } = await reader.read();
199
- if (done) {
200
- break;
201
- }
202
- buffer += decoder.decode(value, { stream: true });
203
- let splitIndex = buffer.search(/\r?\n\r?\n/);
204
- while (splitIndex >= 0) {
205
- const rawBlock = buffer.slice(0, splitIndex);
206
- buffer = buffer.slice(splitIndex + (buffer[splitIndex] === "\r" ? 4 : 2));
207
- const event = parseSseEventBlock(rawBlock);
208
- if (event.data) {
209
- try {
210
- const parsed = JSON.parse(event.data);
211
- deviceLogSubscriptionState.lastEventAt = new Date().toISOString();
212
- if (event.event === "log") {
213
- pushRingBuffer(deviceLogSubscriptionState.logs, normalizeBufferedLogEntry(parsed), exports.DEVICE_LOG_MEMORY_LIMIT);
214
- }
215
- else if (event.event === "runtime_status") {
216
- pushRingBuffer(deviceLogSubscriptionState.runtimeStatus, {
217
- raw: parsed,
218
- timestamp: new Date().toISOString(),
219
- }, exports.DEVICE_LOG_MEMORY_LIMIT);
220
- }
221
- }
222
- catch {
223
- // 忽略单条非法 JSON,避免因为异常日志阻塞整个后台连接
224
- }
225
- }
226
- splitIndex = buffer.search(/\r?\n\r?\n/);
227
- }
228
- }
229
- if (generation === deviceLogSubscriptionState.generation) {
230
- scheduleDeviceLogReconnect(generation);
231
- }
232
- }
233
- catch (error) {
234
- if (error instanceof Error &&
235
- (error.name === "AbortError" ||
236
- error.message === "The operation was aborted.")) {
237
- return;
238
- }
239
- if (generation === deviceLogSubscriptionState.generation) {
240
- deviceLogSubscriptionState.status = "error";
241
- deviceLogSubscriptionState.lastError =
242
- error instanceof Error ? error.message : String(error);
243
- scheduleDeviceLogReconnect(generation);
244
- }
245
- }
246
- finally {
247
- if (deviceLogSubscriptionState.controller === controller) {
248
- deviceLogSubscriptionState.controller = null;
249
- }
250
- }
251
- }
252
- /**
253
- * 确保后台日志订阅处于目标设备上
254
- * - 若目标未变化且订阅仍在运行,则直接复用已有连接
255
- * - 若目标发生变化,则重置缓存并重新建立订阅
256
- * @param ip 设备 IP
257
- * @param port 设备端口
258
- * @returns 返回是否复用了现有订阅
259
- * @example
260
- * const reused = ensureDeviceLogSubscription("192.168.1.10", 9800)
261
- */
262
- function ensureDeviceLogSubscription(ip, port) {
263
- const sameTarget = deviceLogSubscriptionState.target?.ip === ip &&
264
- deviceLogSubscriptionState.target?.port === port;
265
- const isActive = isDeviceLogSubscriptionActive();
266
- if (sameTarget && isActive) {
267
- return true;
268
- }
269
- stopDeviceLogSubscription();
270
- deviceLogSubscriptionState.generation += 1;
271
- deviceLogSubscriptionState.target = { ip, port };
272
- deviceLogSubscriptionState.status = "connecting";
273
- resetDeviceLogSnapshotState();
274
- void startDeviceLogSubscriptionLoop(deviceLogSubscriptionState.generation);
275
- return false;
276
- }
277
- /**
278
- * 按日志页面风格格式化时间
279
- * @param value ISO 时间字符串
280
- * @returns 返回 `yyyy/mm/dd hh:mm:ss.xxx` 风格文本,异常时回退原值
281
- * @example
282
- * formatDisplayTimestamp("2026-05-07T11:34:48.776Z")
283
- */
284
- function formatDisplayTimestamp(value) {
285
- const date = new Date(value);
286
- if (Number.isNaN(date.getTime())) {
287
- return value;
288
- }
289
- const pad = (number, length = 2) => String(number).padStart(length, "0");
290
- return `${date.getFullYear()}/${pad(date.getMonth() + 1)}/${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds(), 3)}`;
291
- }
292
- /**
293
- * 将日志订阅连接状态转为中文文本
294
- * @param status 原始连接状态
295
- * @returns 返回中文状态说明
296
- * @example
297
- * formatDeviceLogSubscriptionStatus("connected")
298
- */
299
- function formatDeviceLogSubscriptionStatus(status) {
300
- switch (status) {
301
- case "idle":
302
- return "未订阅";
303
- case "connecting":
304
- return "连接中";
305
- case "connected":
306
- return "已连接";
307
- case "reconnecting":
308
- return "重连中";
309
- case "error":
310
- return "连接异常";
311
- default:
312
- return status;
313
- }
314
- }
315
- /**
316
- * 将 runtime_status 中的内存数值格式化为可读文本
317
- * @param value 原始数值
318
- * @param suffix 数值单位后缀
319
- * @returns 数值存在时返回格式化结果,否则返回未知
320
- * @example
321
- * formatRuntimeMetricText(12.3456, " MB")
322
- */
323
- function formatRuntimeMetricText(value, suffix = "") {
324
- if (typeof value === "number" && Number.isFinite(value)) {
325
- return `${value.toFixed(2)}${suffix}`;
326
- }
327
- return "未知";
328
- }
329
- /**
330
- * 将单条 runtime_status 事件格式化为中文运行状态摘要
331
- * @param entry runtime_status 条目
332
- * @returns 返回更易读的状态文本
333
- * @example
334
- * formatRuntimeStatusEntry({ raw: { isRunning: false }, timestamp: "2026-01-01T00:00:00.000Z" })
335
- */
336
- function formatRuntimeStatusEntry(entry) {
337
- const raw = entry.raw;
338
- const memory = raw.memory && typeof raw.memory === "object"
339
- ? raw.memory
340
- : {};
341
- return [
342
- `[${formatDisplayTimestamp(entry.timestamp)}]`,
343
- `UI: ${typeof raw.isUIShowing === "boolean"
344
- ? raw.isUIShowing
345
- ? "显示中"
346
- : "未显示"
347
- : "未知"}`,
348
- `脚本: ${typeof raw.isRunning === "boolean"
349
- ? raw.isRunning
350
- ? "运行中"
351
- : "未运行"
352
- : "未知"}`,
353
- `系统: ${formatRuntimeMetricText(memory.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
- `设备: ${deviceLogSubscriptionState.target
375
- ? `${deviceLogSubscriptionState.target.ip}:${deviceLogSubscriptionState.target.port}`
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
- `最新运行状态(最多 ${runtimeStatusLimit} 条):`,
386
- recentRuntimeStatus.length > 0
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
- const reusedSubscription = (0, device_log_1.ensureDeviceLogSubscription)(config.ip, config.port);
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: "获取当前默认设备的日志缓存快照。调用 set_device 后会自动建立 SSE 后台订阅,此工具只返回当前已缓存的日志内容。",
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, runtimeStatusLimit }) => {
234
+ }, async ({ limit }) => {
249
235
  const target = await (0, tool_utils_1.resolveRuntimeHttpTarget)();
250
- (0, device_log_1.ensureDeviceLogSubscription)(target.ip, Number.parseInt(target.port, 10));
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 DeviceLogEntry {
16
- /** 日志级别,例如 debug/info/warn/error */
17
- level: string;
18
- /** 日志文本 */
19
- message: string;
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
- * 监听设备 SSE 日志并返回原始结果
71
- * @param ip 设备 IP 地址
72
- * @param port 设备端口
73
- * @param durationMs 监听时长(毫秒),默认 15000;传 0 表示不设超时,持续监听
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 watchDeviceLogsBySse("192.168.1.10", 9800, 30000, 300, true, true)
53
+ * const result = await getCurrentLogLinesOnDevice({ ip: "192.168.1.10", port: "9800" }, 200)
80
54
  */
81
- export declare function watchDeviceLogsBySse(ip: string, port: number, durationMs?: number, maxLogs?: number, stopOnRuntimeStopped?: boolean, stopOnRuntimeContinuous?: boolean): Promise<DeviceLogWatchResult>;
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.watchDeviceLogsBySse = watchDeviceLogsBySse;
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
- * 将 SSE 文本块解析为事件字段结构
467
- * @param rawBlock 单个 SSE 事件文本块(以空行分隔)
468
- * @returns 返回事件名与数据字符串
466
+ * 在设备上获取当前日志文件最新行
467
+ * @param options 命令选项(仅支持 http 传输)
468
+ * @param count 需要读取的最新日志行数,默认 200,最大 5000
469
+ * @returns 返回最新日志行和是否还有更早日志
469
470
  * @example
470
- * parseSseEventBlock("event: log\ndata: {\"level\":\"info\"}")
471
+ * const result = await getCurrentLogLinesOnDevice({ ip: "192.168.1.10", port: "9800" }, 200)
471
472
  */
472
- function parseSseEventBlock(rawBlock) {
473
- const lines = rawBlock.split(/\r?\n/);
474
- let eventName = "message";
475
- const dataLines = [];
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(port) || port < 1 || port > 65535) {
529
- throw new Error(`监听日志失败: 无效端口 ${port}`);
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
- catch (error) {
637
- // 监听超时是预期行为,忽略 AbortError 并正常输出已采集日志
638
- if (!(error instanceof Error &&
639
- (error.name === "AbortError" ||
640
- error.message === "The operation was aborted."))) {
641
- throw error;
642
- }
643
- if (stopReason === "unknown") {
644
- stopReason = "manual_abort";
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
- finally {
648
- if (timeoutHandle) {
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
- logs,
654
- runtimeStatus,
655
- stopReason,
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
@@ -112,8 +112,7 @@ MCP 可用时:
112
112
 
113
113
  1. 使用 `get_device` 查看当前默认设备。
114
114
  2. 如果未设置设备,使用 `set_device` 设置设备 IP 和端口。
115
- 3. 设置设备后,日志 SSE 后台订阅会自动启动。
116
- 4. 使用 `get_logs` 获取日志缓存快照。
115
+ 3. 使用 `get_logs` 获取当前日志最新行。
117
116
 
118
117
  命令 fallback 时:
119
118
 
package/docs/api/pip.md CHANGED
@@ -8,6 +8,7 @@
8
8
  - **关闭控制**:支持主动关闭悬浮窗
9
9
  - **状态查询**:判断悬浮窗是否正在显示
10
10
  - **尺寸设置**:日志模式和自定义模式支持设置内容宽高,并按模式分别持久化
11
+ - **日志显示**:日志模式支持设置日志文字大小
11
12
  - **自定义文本**:自定义模式支持文本、换行、文字颜色、背景色和文字大小
12
13
 
13
14
  ## API 方法
@@ -54,6 +55,7 @@ function switchToLogMode(): boolean;
54
55
  ```typescript
55
56
  takeMeToFront();
56
57
  const success = pip.switchToLogMode();
58
+ pip.setLogFontSize(10);
57
59
  ```
58
60
 
59
61
  ### switchToKeepAliveMode - 切换到保活模式
@@ -152,6 +154,33 @@ pip.switchToCustomMode();
152
154
  pip.setContentSize(120, 180);
153
155
  ```
154
156
 
157
+ ### setLogFontSize - 设置日志文字大小
158
+
159
+ ```typescript
160
+ function setLogFontSize(fontSize: number): boolean;
161
+ ```
162
+
163
+ 设置日志模式显示文本的文字大小。该方法只更新日志字号,不会自动切换到日志模式。
164
+
165
+ **参数:**
166
+
167
+ | 参数 | 类型 | 必填 | 描述 |
168
+ | ---------- | -------- | ---- | ------------------------ |
169
+ | `fontSize` | `number` | 是 | 日志字号,必须是有效数字 |
170
+
171
+ **返回值:**
172
+
173
+ | 类型 | 描述 |
174
+ | --------- | ---------------------------------------------------------- |
175
+ | `boolean` | `true` 代表日志文字大小已提交;`false` 代表字号无效 |
176
+
177
+ **示例:**
178
+
179
+ ```typescript
180
+ pip.setLogFontSize(10);
181
+ pip.switchToLogMode();
182
+ ```
183
+
155
184
  ### setCustomText - 设置自定义文本
156
185
 
157
186
  ```typescript
@@ -267,6 +296,7 @@ takeMeToFront();
267
296
 
268
297
  pip.switchToLogMode();
269
298
  pip.setContentSize(280, 120);
299
+ pip.setLogFontSize(10);
270
300
 
271
301
  pip.setCustomText("任务运行中\n1/3");
272
302
  pip.setCustomTextColor("#34c759");
@@ -282,7 +312,8 @@ pip.setContentSize(120, 180);
282
312
  2. **设备兼容性**:仅支持 iOS 15+ 且具备画中画功能的设备
283
313
  3. **系统限制**:悬浮窗位置和实际显示尺寸由 iOS 管理
284
314
  4. **尺寸持久化**:日志模式和自定义模式分别保存尺寸,互不共用;保活模式不接受脚本尺寸
285
- 5. **竖向文本**:自定义窗口高度大于宽度时,文本会按逐字符换行方式竖向显示
315
+ 5. **日志字号**:日志字号只影响日志模式,保活模式和自定义模式不受影响
316
+ 6. **竖向文本**:自定义窗口高度大于宽度时,文本会按逐字符换行方式竖向显示
286
317
 
287
318
  ## 故障排除
288
319
 
@@ -396,6 +396,30 @@ const result = tomatoOcr.findTapPoints(“确定”);
396
396
  logi(result);
397
397
  ```
398
398
 
399
+ ### 资源释放方法
400
+
401
+ #### `release`
402
+
403
+ 释放 TomatoOCR SDK 已加载的 OCR 模型和内部资源。
404
+
405
+ ```typescript
406
+ function release(): void;
407
+ ```
408
+
409
+ **参数:**
410
+
411
+
412
+
413
+ **返回值:**
414
+
415
+
416
+
417
+ **示例:**
418
+
419
+ ```typescript
420
+ tomatoOcr.release();
421
+ ```
422
+
399
423
  ## 完整示例
400
424
 
401
425
  ```typescript
@@ -417,6 +441,9 @@ logi("识别结果:" + ocrResult);
417
441
  // 查找点击点
418
442
  const tapResult = tomatoOcr.findTapPoint("登录");
419
443
  logi("点击点坐标:" + tapResult);
444
+
445
+ // 释放 TomatoOCR 资源
446
+ tomatoOcr.release();
420
447
  ```
421
448
 
422
449
  ## 注意事项
package/docs/apicn/pip.md CHANGED
@@ -8,6 +8,7 @@
8
8
  - **关闭控制**:支持主动关闭悬浮窗
9
9
  - **状态查询**:判断悬浮窗是否正在显示
10
10
  - **尺寸设置**:日志模式和自定义模式支持设置内容宽高,并按模式分别持久化
11
+ - **日志显示**:日志模式支持设置日志文字大小
11
12
  - **自定义文本**:自定义模式支持文本、换行、文字颜色、背景色和文字大小
12
13
 
13
14
  ## API 方法
@@ -54,6 +55,7 @@ function 切换到日志模式(): 布尔值;
54
55
  ```typescript
55
56
  $APP切入前台();
56
57
  const 是否成功 = $悬浮窗.切换到日志模式();
58
+ $悬浮窗.设置日志文字大小(10);
57
59
  ```
58
60
 
59
61
  ### 切换到保活模式
@@ -152,6 +154,33 @@ $悬浮窗.切换到自定义模式();
152
154
  $悬浮窗.设置内容尺寸(120, 180);
153
155
  ```
154
156
 
157
+ ### 设置日志文字大小
158
+
159
+ ```typescript
160
+ function 设置日志文字大小(字号: 数字): 布尔值;
161
+ ```
162
+
163
+ 设置日志模式显示文本的文字大小。该方法只更新日志字号,不会自动切换到日志模式。
164
+
165
+ **参数:**
166
+
167
+ | 参数 | 类型 | 必填 | 描述 |
168
+ | ------ | ------ | ---- | ------------------------ |
169
+ | `字号` | `数字` | 是 | 日志字号,必须是有效数字 |
170
+
171
+ **返回值:**
172
+
173
+ | 类型 | 描述 |
174
+ | -------- | -------------------------------------------------------- |
175
+ | `布尔值` | `true` 代表日志文字大小已提交;`false` 代表字号无效 |
176
+
177
+ **示例:**
178
+
179
+ ```typescript
180
+ $悬浮窗.设置日志文字大小(10);
181
+ $悬浮窗.切换到日志模式();
182
+ ```
183
+
155
184
  ### 设置自定义文本
156
185
 
157
186
  ```typescript
@@ -267,6 +296,7 @@ $APP切入前台();
267
296
 
268
297
  $悬浮窗.切换到日志模式();
269
298
  $悬浮窗.设置内容尺寸(280, 120);
299
+ $悬浮窗.设置日志文字大小(10);
270
300
 
271
301
  $悬浮窗.设置自定义文本("任务运行中\n1/3");
272
302
  $悬浮窗.设置自定义文字颜色("#34c759");
@@ -282,7 +312,8 @@ $悬浮窗.设置内容尺寸(120, 180);
282
312
  2. **设备兼容性**:仅支持 iOS 15+ 且具备画中画功能的设备
283
313
  3. **系统限制**:悬浮窗位置和实际显示尺寸由 iOS 管理
284
314
  4. **尺寸持久化**:日志模式和自定义模式分别保存尺寸,互不共用;保活模式不接受脚本尺寸
285
- 5. **竖向文本**:自定义窗口高度大于宽度时,文本会按逐字符换行方式竖向显示
315
+ 5. **日志字号**:日志字号只影响日志模式,保活模式和自定义模式不受影响
316
+ 6. **竖向文本**:自定义窗口高度大于宽度时,文本会按逐字符换行方式竖向显示
286
317
 
287
318
  ## 故障排除
288
319
 
@@ -8,6 +8,7 @@
8
8
  - **关闭控制**:支持主动关闭悬浮窗
9
9
  - **状态查询**:判断悬浮窗是否正在显示
10
10
  - **尺寸设置**:日志模式和自定义模式支持设置内容宽高,并按模式分别持久化
11
+ - **日志显示**:日志模式支持设置日志文字大小
11
12
  - **自定义文本**:自定义模式支持文本、换行、文字颜色、背景色和文字大小
12
13
 
13
14
  ## API 参考
@@ -56,6 +57,7 @@ from kuaijs import pip, utils
56
57
 
57
58
  utils.takeMeToFront()
58
59
  success = pip.switchToLogMode()
60
+ pip.setLogFontSize(10)
59
61
  ```
60
62
 
61
63
  ### switchToKeepAliveMode - 切换到保活模式
@@ -162,6 +164,35 @@ pip.switchToCustomMode()
162
164
  pip.setContentSize(120, 180)
163
165
  ```
164
166
 
167
+ ### setLogFontSize - 设置日志文字大小
168
+
169
+ 设置日志模式显示文本的文字大小。该方法只更新日志字号,不会自动切换到日志模式。
170
+
171
+ ```python
172
+ def setLogFontSize(fontSize: float) -> bool
173
+ ```
174
+
175
+ **参数:**
176
+
177
+ | 参数 | 类型 | 必填 | 描述 |
178
+ | ---------- | ------- | ---- | ------------------------ |
179
+ | `fontSize` | `float` | 是 | 日志字号,必须是有效数字 |
180
+
181
+ **返回值:**
182
+
183
+ | 类型 | 描述 |
184
+ | ------ | ------------------------------------------------------- |
185
+ | `bool` | `True` 代表日志文字大小已提交;`False` 代表字号无效 |
186
+
187
+ **示例:**
188
+
189
+ ```python
190
+ from kuaijs import pip
191
+
192
+ pip.setLogFontSize(10)
193
+ pip.switchToLogMode()
194
+ ```
195
+
165
196
  ### setCustomText - 设置自定义文本
166
197
 
167
198
  设置自定义模式显示的纯文本内容。该方法只更新自定义文本数据,不会自动切换到自定义模式。
@@ -287,6 +318,7 @@ utils.takeMeToFront()
287
318
 
288
319
  pip.switchToLogMode()
289
320
  pip.setContentSize(280, 120)
321
+ pip.setLogFontSize(10)
290
322
 
291
323
  pip.setCustomText("任务运行中\n1/3")
292
324
  pip.setCustomTextColor("#34c759")
@@ -302,7 +334,8 @@ pip.setContentSize(120, 180)
302
334
  2. **设备兼容性**:仅支持 iOS 15+ 且具备画中画功能的设备
303
335
  3. **系统限制**:悬浮窗位置和实际显示尺寸由 iOS 管理
304
336
  4. **尺寸持久化**:日志模式和自定义模式分别保存尺寸,互不共用;保活模式不接受脚本尺寸
305
- 5. **竖向文本**:自定义窗口高度大于宽度时,文本会按逐字符换行方式竖向显示
337
+ 5. **日志字号**:日志字号只影响日志模式,保活模式和自定义模式不受影响
338
+ 6. **竖向文本**:自定义窗口高度大于宽度时,文本会按逐字符换行方式竖向显示
306
339
 
307
340
  ## 故障排除
308
341
 
@@ -413,6 +413,31 @@ result = tomatoocr.findTapPoints("确定")
413
413
  print(result)
414
414
  ```
415
415
 
416
+ ### 资源释放方法
417
+
418
+ #### `release`
419
+
420
+ 释放 TomatoOCR SDK 已加载的 OCR 模型和内部资源。
421
+
422
+ ```python
423
+ def release() -> None
424
+ ```
425
+
426
+ **参数:**
427
+
428
+
429
+
430
+ **返回值:**
431
+
432
+
433
+
434
+ **示例:**
435
+
436
+ ```python
437
+ from kuaijs import tomatoocr
438
+ tomatoocr.release()
439
+ ```
440
+
416
441
  ## 完整示例
417
442
 
418
443
  ```python
@@ -436,6 +461,9 @@ print("识别结果:" + ocr_result)
436
461
  # 查找点击点
437
462
  tap_result = tomatoocr.findTapPoint("登录")
438
463
  print("点击点坐标:" + tap_result)
464
+
465
+ # 释放 TomatoOCR 资源
466
+ tomatoocr.release()
439
467
  ```
440
468
 
441
469
  ## 注意事项
@@ -137,8 +137,7 @@ MCP 可用时:
137
137
 
138
138
  - 使用 `get_device` 查看当前默认设备。
139
139
  - 使用 `set_device` 设置设备 IP 和端口。
140
- - 设备设置后会自动启动日志 SSE 后台订阅。
141
- - 使用 `get_logs` 查看日志缓存快照。
140
+ - 使用 `get_logs` 查看当前日志最新行。
142
141
 
143
142
  UI 预览发起后不需要长时间等待结果,可以通过 `take_screenshot` 查看当前界面效果。
144
143
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ms-vite-plugin",
3
- "version": "1.4.13",
3
+ "version": "1.4.15",
4
4
  "type": "commonjs",
5
5
  "license": "MIT",
6
6
  "publishConfig": {