ms-vite-plugin 1.1.6 → 1.1.8
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 +133 -36
- package/dist/project.d.ts +6 -3
- package/dist/project.js +40 -14
- package/package.json +1 -1
package/dist/mcp/tools.js
CHANGED
|
@@ -303,23 +303,63 @@ function registerRuntimeTools(server) {
|
|
|
303
303
|
return workspace;
|
|
304
304
|
}
|
|
305
305
|
/**
|
|
306
|
-
*
|
|
306
|
+
* 格式化运行期日志结果(供 run/run_ui 直接回传)
|
|
307
|
+
* @param ip 设备 IP
|
|
308
|
+
* @param port 设备端口
|
|
309
|
+
* @param durationSeconds 监听秒数
|
|
307
310
|
* @param result 日志监听结果
|
|
308
|
-
* @returns
|
|
311
|
+
* @returns 可拼接到 tool 响应的文本
|
|
309
312
|
* @example
|
|
310
|
-
* const text =
|
|
313
|
+
* const text = formatRunLogSection("192.168.1.10", 9800, 10, result)
|
|
311
314
|
*/
|
|
312
|
-
function
|
|
315
|
+
function formatRunLogSection(ip, port, durationSeconds, result) {
|
|
313
316
|
const logsPreview = result.logs
|
|
314
|
-
.slice(-
|
|
317
|
+
.slice(-20)
|
|
315
318
|
.map((log) => `[${log.timestamp}] [${log.level.toUpperCase()}] ${log.message}`)
|
|
316
319
|
.join("\n");
|
|
317
320
|
return [
|
|
318
|
-
|
|
319
|
-
|
|
321
|
+
`启动期日志窗口: ${durationSeconds}s(${ip}:${port})`,
|
|
322
|
+
`结束原因: ${result.stopReason},日志: ${result.logs.length} 条,runtime_status: ${result.runtimeStatus.length} 条`,
|
|
323
|
+
"最新日志(最多展示最后20条):",
|
|
320
324
|
logsPreview || "无日志",
|
|
321
325
|
].join("\n");
|
|
322
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* 提取最后一次 runtime_status 的 isRunning 值
|
|
329
|
+
* @param result 日志监听结果
|
|
330
|
+
* @returns 返回 true/false,无法判断时返回 undefined
|
|
331
|
+
* @example
|
|
332
|
+
* const running = getLatestIsRunning(result)
|
|
333
|
+
*/
|
|
334
|
+
function getLatestIsRunning(result) {
|
|
335
|
+
for (let i = result.runtimeStatus.length - 1; i >= 0; i -= 1) {
|
|
336
|
+
const raw = result.runtimeStatus[i].raw;
|
|
337
|
+
if (typeof raw.isRunning === "boolean") {
|
|
338
|
+
return raw.isRunning;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return undefined;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* 启动持续日志监听(后台,不阻塞当前 tool 返回)
|
|
345
|
+
* @param ip 设备 IP
|
|
346
|
+
* @param port 设备端口
|
|
347
|
+
* @param maxLogs 日志环形缓冲上限
|
|
348
|
+
* @returns 返回提示文本
|
|
349
|
+
* @example
|
|
350
|
+
* const tip = startBackgroundContinuousLogWatch("192.168.1.10", 9800, 300)
|
|
351
|
+
*/
|
|
352
|
+
function startBackgroundContinuousLogWatch(ip, port, maxLogs) {
|
|
353
|
+
void (0, project_1.watchDeviceLogsBySse)(ip, port, 0, maxLogs, false)
|
|
354
|
+
.then((result) => {
|
|
355
|
+
console.log(`[mcp] continuous logs ${ip}:${port} ended, reason=${result.stopReason}, logs=${result.logs.length}, runtime_status=${result.runtimeStatus.length}`);
|
|
356
|
+
})
|
|
357
|
+
.catch((error) => {
|
|
358
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
359
|
+
console.warn(`[mcp] continuous logs ${ip}:${port} failed: ${message}`);
|
|
360
|
+
});
|
|
361
|
+
return `已切换为持续日志监听: ${ip}:${port}(直到连接断开,max ${maxLogs})`;
|
|
362
|
+
}
|
|
323
363
|
/**
|
|
324
364
|
* 解析运行目标:
|
|
325
365
|
* - 显式 transport=ws: 强制走 ws
|
|
@@ -631,11 +671,11 @@ function registerRuntimeTools(server) {
|
|
|
631
671
|
durationSeconds: z
|
|
632
672
|
.number()
|
|
633
673
|
.int()
|
|
634
|
-
.min(
|
|
674
|
+
.min(0)
|
|
635
675
|
.max(300)
|
|
636
676
|
.optional()
|
|
637
677
|
.default(15)
|
|
638
|
-
.describe("监听时长(秒),默认 15
|
|
678
|
+
.describe("监听时长(秒),默认 15 秒;传 0 表示持续监听直到连接断开"),
|
|
639
679
|
maxLogs: z
|
|
640
680
|
.number()
|
|
641
681
|
.int()
|
|
@@ -659,7 +699,8 @@ function registerRuntimeTools(server) {
|
|
|
659
699
|
type: "text",
|
|
660
700
|
text: [
|
|
661
701
|
`监听目标: ${device.ip}:${device.port}`,
|
|
662
|
-
|
|
702
|
+
`监听模式: ${durationSeconds === 0 ? "持续直到连接断开" : `${durationSeconds}s`}`,
|
|
703
|
+
`结束原因: ${result.stopReason},收集日志: ${result.logs.length} 条,runtime_status: ${result.runtimeStatus.length} 条`,
|
|
663
704
|
"",
|
|
664
705
|
"最新日志(最多展示最后20条):",
|
|
665
706
|
logsPreview || "无日志",
|
|
@@ -752,14 +793,19 @@ function registerRuntimeTools(server) {
|
|
|
752
793
|
.optional()
|
|
753
794
|
.default(true)
|
|
754
795
|
.describe("是否在运行前启动实时日志监听,默认 true"),
|
|
796
|
+
logMode: z
|
|
797
|
+
.enum(["auto", "single", "continuous"])
|
|
798
|
+
.optional()
|
|
799
|
+
.default("auto")
|
|
800
|
+
.describe("日志模式:auto=单次优先并自动判长任务,single=按 runtime_status 收敛,continuous=持续监听"),
|
|
755
801
|
logDurationSeconds: z
|
|
756
802
|
.number()
|
|
757
803
|
.int()
|
|
758
804
|
.min(1)
|
|
759
805
|
.max(300)
|
|
760
806
|
.optional()
|
|
761
|
-
.default(
|
|
762
|
-
.describe("日志监听时长(秒),默认
|
|
807
|
+
.default(10)
|
|
808
|
+
.describe("日志监听时长(秒),默认 10,用于回传 run 启动期实时输出"),
|
|
763
809
|
logMaxLogs: z
|
|
764
810
|
.number()
|
|
765
811
|
.int()
|
|
@@ -769,17 +815,25 @@ function registerRuntimeTools(server) {
|
|
|
769
815
|
.default(300)
|
|
770
816
|
.describe("日志最大收集条数,默认 300"),
|
|
771
817
|
},
|
|
772
|
-
}, async ({ transport, ip, port, wsPort, wsWaitMs, workspacePath, watchLogs, logDurationSeconds, logMaxLogs, }) => {
|
|
818
|
+
}, async ({ transport, ip, port, wsPort, wsWaitMs, workspacePath, watchLogs, logMode, logDurationSeconds, logMaxLogs, }) => {
|
|
773
819
|
const workspace = await ensureWorkspacePath(workspacePath);
|
|
774
|
-
let
|
|
775
|
-
let
|
|
820
|
+
let logPromise;
|
|
821
|
+
let logDeviceIp = "";
|
|
822
|
+
let logDevicePort = 0;
|
|
776
823
|
let logSetupWarning = "";
|
|
824
|
+
let logModeNotice = "";
|
|
777
825
|
if (watchLogs) {
|
|
778
826
|
try {
|
|
779
827
|
const logDevice = await (0, device_config_1.resolveDeviceConfig)(ip, port);
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
828
|
+
// 先挂监听,再执行 run,确保拿到启动期实时日志
|
|
829
|
+
logDeviceIp = logDevice.ip;
|
|
830
|
+
logDevicePort = logDevice.port;
|
|
831
|
+
if (logMode === "continuous") {
|
|
832
|
+
logModeNotice = startBackgroundContinuousLogWatch(logDevice.ip, logDevice.port, logMaxLogs);
|
|
833
|
+
}
|
|
834
|
+
else {
|
|
835
|
+
logPromise = (0, project_1.watchDeviceLogsBySse)(logDevice.ip, logDevice.port, logDurationSeconds * 1000, logMaxLogs, logMode === "single" || logMode === "auto");
|
|
836
|
+
}
|
|
783
837
|
}
|
|
784
838
|
catch (error) {
|
|
785
839
|
logSetupWarning = `日志监听未启动: ${error instanceof Error ? error.message : String(error)}`;
|
|
@@ -805,11 +859,26 @@ function registerRuntimeTools(server) {
|
|
|
805
859
|
transport: "http",
|
|
806
860
|
workspacePath: workspace,
|
|
807
861
|
});
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
862
|
+
let logSection = "";
|
|
863
|
+
if (logPromise) {
|
|
864
|
+
const result = await logPromise;
|
|
865
|
+
const sections = [
|
|
866
|
+
formatRunLogSection(logDeviceIp, logDevicePort, logDurationSeconds, result),
|
|
867
|
+
];
|
|
868
|
+
if (logMode === "auto") {
|
|
869
|
+
const latestIsRunning = getLatestIsRunning(result);
|
|
870
|
+
if (result.stopReason === "timeout" && latestIsRunning === true) {
|
|
871
|
+
sections.push(startBackgroundContinuousLogWatch(logDeviceIp, logDevicePort, logMaxLogs));
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
logSection = `\n\n${sections.join("\n")}`;
|
|
875
|
+
}
|
|
876
|
+
else if (logModeNotice) {
|
|
877
|
+
logSection = `\n\n${logModeNotice}`;
|
|
878
|
+
}
|
|
879
|
+
else if (logSetupWarning) {
|
|
880
|
+
logSection = `\n\n${logSetupWarning}`;
|
|
881
|
+
}
|
|
813
882
|
return {
|
|
814
883
|
content: [
|
|
815
884
|
{
|
|
@@ -863,14 +932,19 @@ function registerRuntimeTools(server) {
|
|
|
863
932
|
.optional()
|
|
864
933
|
.default(true)
|
|
865
934
|
.describe("是否在预览前启动实时日志监听,默认 true"),
|
|
935
|
+
logMode: z
|
|
936
|
+
.enum(["auto", "single", "continuous"])
|
|
937
|
+
.optional()
|
|
938
|
+
.default("auto")
|
|
939
|
+
.describe("日志模式:auto=单次优先并自动判长任务,single=按 runtime_status 收敛,continuous=持续监听"),
|
|
866
940
|
logDurationSeconds: z
|
|
867
941
|
.number()
|
|
868
942
|
.int()
|
|
869
943
|
.min(1)
|
|
870
944
|
.max(300)
|
|
871
945
|
.optional()
|
|
872
|
-
.default(
|
|
873
|
-
.describe("日志监听时长(秒),默认
|
|
946
|
+
.default(10)
|
|
947
|
+
.describe("日志监听时长(秒),默认 10,用于回传 run-ui 启动期实时输出"),
|
|
874
948
|
logMaxLogs: z
|
|
875
949
|
.number()
|
|
876
950
|
.int()
|
|
@@ -880,17 +954,25 @@ function registerRuntimeTools(server) {
|
|
|
880
954
|
.default(300)
|
|
881
955
|
.describe("日志最大收集条数,默认 300"),
|
|
882
956
|
},
|
|
883
|
-
}, async ({ transport, ip, port, wsPort, wsWaitMs, workspacePath, watchLogs, logDurationSeconds, logMaxLogs, }) => {
|
|
957
|
+
}, async ({ transport, ip, port, wsPort, wsWaitMs, workspacePath, watchLogs, logMode, logDurationSeconds, logMaxLogs, }) => {
|
|
884
958
|
const workspace = await ensureWorkspacePath(workspacePath);
|
|
885
|
-
let
|
|
886
|
-
let
|
|
959
|
+
let logPromise;
|
|
960
|
+
let logDeviceIp = "";
|
|
961
|
+
let logDevicePort = 0;
|
|
887
962
|
let logSetupWarning = "";
|
|
963
|
+
let logModeNotice = "";
|
|
888
964
|
if (watchLogs) {
|
|
889
965
|
try {
|
|
890
966
|
const logDevice = await (0, device_config_1.resolveDeviceConfig)(ip, port);
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
967
|
+
// 先挂监听,再执行 run-ui,确保拿到启动期实时日志
|
|
968
|
+
logDeviceIp = logDevice.ip;
|
|
969
|
+
logDevicePort = logDevice.port;
|
|
970
|
+
if (logMode === "continuous") {
|
|
971
|
+
logModeNotice = startBackgroundContinuousLogWatch(logDevice.ip, logDevice.port, logMaxLogs);
|
|
972
|
+
}
|
|
973
|
+
else {
|
|
974
|
+
logPromise = (0, project_1.watchDeviceLogsBySse)(logDevice.ip, logDevice.port, logDurationSeconds * 1000, logMaxLogs, logMode === "single" || logMode === "auto");
|
|
975
|
+
}
|
|
894
976
|
}
|
|
895
977
|
catch (error) {
|
|
896
978
|
logSetupWarning = `日志监听未启动: ${error instanceof Error ? error.message : String(error)}`;
|
|
@@ -916,11 +998,26 @@ function registerRuntimeTools(server) {
|
|
|
916
998
|
transport: "http",
|
|
917
999
|
workspacePath: workspace,
|
|
918
1000
|
});
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
1001
|
+
let logSection = "";
|
|
1002
|
+
if (logPromise) {
|
|
1003
|
+
const result = await logPromise;
|
|
1004
|
+
const sections = [
|
|
1005
|
+
formatRunLogSection(logDeviceIp, logDevicePort, logDurationSeconds, result),
|
|
1006
|
+
];
|
|
1007
|
+
if (logMode === "auto") {
|
|
1008
|
+
const latestIsRunning = getLatestIsRunning(result);
|
|
1009
|
+
if (result.stopReason === "timeout" && latestIsRunning === true) {
|
|
1010
|
+
sections.push(startBackgroundContinuousLogWatch(logDeviceIp, logDevicePort, logMaxLogs));
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
logSection = `\n\n${sections.join("\n")}`;
|
|
1014
|
+
}
|
|
1015
|
+
else if (logModeNotice) {
|
|
1016
|
+
logSection = `\n\n${logModeNotice}`;
|
|
1017
|
+
}
|
|
1018
|
+
else if (logSetupWarning) {
|
|
1019
|
+
logSection = `\n\n${logSetupWarning}`;
|
|
1020
|
+
}
|
|
924
1021
|
return {
|
|
925
1022
|
content: [
|
|
926
1023
|
{
|
package/dist/project.d.ts
CHANGED
|
@@ -37,6 +37,8 @@ export interface DeviceLogWatchResult {
|
|
|
37
37
|
logs: DeviceLogEntry[];
|
|
38
38
|
/** 收集到的 runtime_status 条目 */
|
|
39
39
|
runtimeStatus: DeviceRuntimeStatusEntry[];
|
|
40
|
+
/** 监听结束原因 */
|
|
41
|
+
stopReason: "timeout" | "runtime_stopped" | "stream_closed" | "manual_abort" | "unknown";
|
|
40
42
|
}
|
|
41
43
|
/**
|
|
42
44
|
* 在设备上获取截图 JPG 二进制数据(HTTP)
|
|
@@ -68,13 +70,14 @@ export declare function getSourceOnDevice(options: DeviceCliOptions, maxDepth?:
|
|
|
68
70
|
* 监听设备 SSE 日志并返回原始结果
|
|
69
71
|
* @param ip 设备 IP 地址
|
|
70
72
|
* @param port 设备端口
|
|
71
|
-
* @param durationMs 监听时长(毫秒),默认 15000
|
|
73
|
+
* @param durationMs 监听时长(毫秒),默认 15000;传 0 表示不设超时,持续监听
|
|
72
74
|
* @param maxLogs 最多收集日志条数,默认 200
|
|
75
|
+
* @param stopOnRuntimeStopped 若为 true,则在运行状态从 true 回落到 false 后结束监听
|
|
73
76
|
* @returns 返回日志与运行状态
|
|
74
77
|
* @example
|
|
75
|
-
* const result = await watchDeviceLogsBySse("192.168.1.10", 9800,
|
|
78
|
+
* const result = await watchDeviceLogsBySse("192.168.1.10", 9800, 30000, 300, true)
|
|
76
79
|
*/
|
|
77
|
-
export declare function watchDeviceLogsBySse(ip: string, port: number, durationMs?: number, maxLogs?: number): Promise<DeviceLogWatchResult>;
|
|
80
|
+
export declare function watchDeviceLogsBySse(ip: string, port: number, durationMs?: number, maxLogs?: number, stopOnRuntimeStopped?: boolean): Promise<DeviceLogWatchResult>;
|
|
78
81
|
/**
|
|
79
82
|
* 在设备上运行项目(先同步后运行)
|
|
80
83
|
* @param options 命令选项
|
package/dist/project.js
CHANGED
|
@@ -513,20 +513,21 @@ function normalizeLogEntry(payload) {
|
|
|
513
513
|
* 监听设备 SSE 日志并返回原始结果
|
|
514
514
|
* @param ip 设备 IP 地址
|
|
515
515
|
* @param port 设备端口
|
|
516
|
-
* @param durationMs 监听时长(毫秒),默认 15000
|
|
516
|
+
* @param durationMs 监听时长(毫秒),默认 15000;传 0 表示不设超时,持续监听
|
|
517
517
|
* @param maxLogs 最多收集日志条数,默认 200
|
|
518
|
+
* @param stopOnRuntimeStopped 若为 true,则在运行状态从 true 回落到 false 后结束监听
|
|
518
519
|
* @returns 返回日志与运行状态
|
|
519
520
|
* @example
|
|
520
|
-
* const result = await watchDeviceLogsBySse("192.168.1.10", 9800,
|
|
521
|
+
* const result = await watchDeviceLogsBySse("192.168.1.10", 9800, 30000, 300, true)
|
|
521
522
|
*/
|
|
522
|
-
async function watchDeviceLogsBySse(ip, port, durationMs = 15000, maxLogs = 200) {
|
|
523
|
+
async function watchDeviceLogsBySse(ip, port, durationMs = 15000, maxLogs = 200, stopOnRuntimeStopped = false) {
|
|
523
524
|
if (!ip.trim()) {
|
|
524
525
|
throw new Error("监听日志失败: 设备 IP 不能为空");
|
|
525
526
|
}
|
|
526
527
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
527
528
|
throw new Error(`监听日志失败: 无效端口 ${port}`);
|
|
528
529
|
}
|
|
529
|
-
if (!Number.isInteger(durationMs) || durationMs <
|
|
530
|
+
if (!Number.isInteger(durationMs) || durationMs < 0) {
|
|
530
531
|
throw new Error(`监听日志失败: 无效 durationMs ${durationMs}`);
|
|
531
532
|
}
|
|
532
533
|
if (!Number.isInteger(maxLogs) || maxLogs < 1) {
|
|
@@ -536,9 +537,14 @@ async function watchDeviceLogsBySse(ip, port, durationMs = 15000, maxLogs = 200)
|
|
|
536
537
|
const logs = [];
|
|
537
538
|
const runtimeStatus = [];
|
|
538
539
|
const controller = new AbortController();
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
540
|
+
let stopReason = "unknown";
|
|
541
|
+
let seenRunningStatus = false;
|
|
542
|
+
const timeoutHandle = durationMs > 0
|
|
543
|
+
? setTimeout(() => {
|
|
544
|
+
stopReason = "timeout";
|
|
545
|
+
controller.abort();
|
|
546
|
+
}, durationMs)
|
|
547
|
+
: undefined;
|
|
542
548
|
try {
|
|
543
549
|
const response = await fetch(url, {
|
|
544
550
|
method: "GET",
|
|
@@ -556,6 +562,9 @@ async function watchDeviceLogsBySse(ip, port, durationMs = 15000, maxLogs = 200)
|
|
|
556
562
|
while (true) {
|
|
557
563
|
const { value, done } = await reader.read();
|
|
558
564
|
if (done) {
|
|
565
|
+
if (stopReason === "unknown") {
|
|
566
|
+
stopReason = "stream_closed";
|
|
567
|
+
}
|
|
559
568
|
break;
|
|
560
569
|
}
|
|
561
570
|
buffer += decoder.decode(value, { stream: true });
|
|
@@ -569,25 +578,36 @@ async function watchDeviceLogsBySse(ip, port, durationMs = 15000, maxLogs = 200)
|
|
|
569
578
|
try {
|
|
570
579
|
const parsed = JSON.parse(event.data);
|
|
571
580
|
if (event.event === "log") {
|
|
572
|
-
|
|
573
|
-
|
|
581
|
+
// 日志缓冲使用环形窗口,持续监听时不会无限增长
|
|
582
|
+
if (logs.length >= maxLogs) {
|
|
583
|
+
logs.shift();
|
|
574
584
|
}
|
|
585
|
+
logs.push(normalizeLogEntry(parsed));
|
|
575
586
|
}
|
|
576
587
|
else if (event.event === "runtime_status") {
|
|
588
|
+
if (runtimeStatus.length >= maxLogs) {
|
|
589
|
+
runtimeStatus.shift();
|
|
590
|
+
}
|
|
577
591
|
runtimeStatus.push({
|
|
578
592
|
raw: parsed,
|
|
579
593
|
timestamp: new Date().toISOString(),
|
|
580
594
|
});
|
|
595
|
+
if (parsed.isRunning === true) {
|
|
596
|
+
seenRunningStatus = true;
|
|
597
|
+
}
|
|
598
|
+
if (stopOnRuntimeStopped &&
|
|
599
|
+
seenRunningStatus &&
|
|
600
|
+
parsed.isRunning === false) {
|
|
601
|
+
stopReason = "runtime_stopped";
|
|
602
|
+
controller.abort();
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
581
605
|
}
|
|
582
606
|
}
|
|
583
607
|
catch {
|
|
584
608
|
// 兼容非 JSON 数据:不抛错,继续监听后续事件
|
|
585
609
|
}
|
|
586
610
|
}
|
|
587
|
-
if (logs.length >= maxLogs) {
|
|
588
|
-
controller.abort();
|
|
589
|
-
break;
|
|
590
|
-
}
|
|
591
611
|
splitIndex = buffer.search(/\r?\n\r?\n/);
|
|
592
612
|
}
|
|
593
613
|
}
|
|
@@ -599,13 +619,19 @@ async function watchDeviceLogsBySse(ip, port, durationMs = 15000, maxLogs = 200)
|
|
|
599
619
|
error.message === "The operation was aborted."))) {
|
|
600
620
|
throw error;
|
|
601
621
|
}
|
|
622
|
+
if (stopReason === "unknown") {
|
|
623
|
+
stopReason = "manual_abort";
|
|
624
|
+
}
|
|
602
625
|
}
|
|
603
626
|
finally {
|
|
604
|
-
|
|
627
|
+
if (timeoutHandle) {
|
|
628
|
+
clearTimeout(timeoutHandle);
|
|
629
|
+
}
|
|
605
630
|
}
|
|
606
631
|
return {
|
|
607
632
|
logs,
|
|
608
633
|
runtimeStatus,
|
|
634
|
+
stopReason,
|
|
609
635
|
};
|
|
610
636
|
}
|
|
611
637
|
/**
|