@bomon/nestjs-cct-explorer 0.1.0 → 1.1.0
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/cct-explorer.controller.d.ts +11 -2
- package/dist/cct-explorer.controller.js +35 -3
- package/dist/cct-explorer.module.js +2 -0
- package/dist/device-gateway.interface.d.ts +17 -0
- package/dist/device-queue-cache.service.d.ts +20 -0
- package/dist/device-queue-cache.service.js +72 -0
- package/dist/public/browser.html +75 -0
- package/dist/tokens.d.ts +5 -0
- package/package.json +1 -1
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import type { Response } from 'express';
|
|
2
2
|
import { DeviceGateway } from './device-gateway.interface';
|
|
3
3
|
import { CctExplorerModuleOptions } from './tokens';
|
|
4
|
+
import { DeviceQueueCacheService } from './device-queue-cache.service';
|
|
4
5
|
export declare class CctExplorerController {
|
|
5
6
|
private readonly gateway;
|
|
6
7
|
private readonly options;
|
|
7
|
-
|
|
8
|
+
private readonly queueCache;
|
|
9
|
+
constructor(gateway: DeviceGateway, options: CctExplorerModuleOptions, queueCache: DeviceQueueCacheService);
|
|
8
10
|
renderBrowser(res: Response): Promise<void>;
|
|
9
11
|
getDevices(): Promise<{
|
|
10
|
-
devices:
|
|
12
|
+
devices: {
|
|
13
|
+
scriptQueue: import("./device-gateway.interface").ScriptQueueSnapshot | null;
|
|
14
|
+
queueUpdatedAt: number | null;
|
|
15
|
+
queueError: string | null;
|
|
16
|
+
name: string;
|
|
17
|
+
connectedAt?: string;
|
|
18
|
+
}[];
|
|
11
19
|
}>;
|
|
12
20
|
readDir(device: string, virtualPath?: string): Promise<{
|
|
13
21
|
entries: import("./device-gateway.interface").DirEntry[];
|
|
@@ -21,4 +29,5 @@ export declare class CctExplorerController {
|
|
|
21
29
|
ok: boolean;
|
|
22
30
|
message: string;
|
|
23
31
|
}>;
|
|
32
|
+
getScriptQueue(device: string): Promise<import("./device-gateway.interface").ScriptQueueSnapshot>;
|
|
24
33
|
}
|
|
@@ -17,10 +17,12 @@ const common_1 = require("@nestjs/common");
|
|
|
17
17
|
const node_path_1 = require("node:path");
|
|
18
18
|
const tokens_1 = require("./tokens");
|
|
19
19
|
const path_utils_1 = require("./path-utils");
|
|
20
|
+
const device_queue_cache_service_1 = require("./device-queue-cache.service");
|
|
20
21
|
let CctExplorerController = class CctExplorerController {
|
|
21
|
-
constructor(gateway, options) {
|
|
22
|
+
constructor(gateway, options, queueCache) {
|
|
22
23
|
this.gateway = gateway;
|
|
23
24
|
this.options = options;
|
|
25
|
+
this.queueCache = queueCache;
|
|
24
26
|
}
|
|
25
27
|
async renderBrowser(res) {
|
|
26
28
|
const filePath = (0, node_path_1.join)(__dirname, 'public', 'browser.html');
|
|
@@ -28,7 +30,16 @@ let CctExplorerController = class CctExplorerController {
|
|
|
28
30
|
}
|
|
29
31
|
async getDevices() {
|
|
30
32
|
const devices = await this.gateway.listDevices();
|
|
31
|
-
|
|
33
|
+
const enriched = devices.map((d) => {
|
|
34
|
+
const state = this.queueCache.getState(d.name);
|
|
35
|
+
return {
|
|
36
|
+
...d,
|
|
37
|
+
scriptQueue: state?.snapshot ?? null,
|
|
38
|
+
queueUpdatedAt: state?.updatedAt ?? null,
|
|
39
|
+
queueError: state?.error ?? null,
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
return { devices: enriched };
|
|
32
43
|
}
|
|
33
44
|
async readDir(device, virtualPath = '') {
|
|
34
45
|
if (!device || !virtualPath) {
|
|
@@ -104,6 +115,20 @@ let CctExplorerController = class CctExplorerController {
|
|
|
104
115
|
await this.gateway.writeFile(device, parsed.devicePath, content ?? '');
|
|
105
116
|
return { ok: true, message: 'saved' };
|
|
106
117
|
}
|
|
118
|
+
async getScriptQueue(device) {
|
|
119
|
+
if (!this.gateway.getScriptQueue) {
|
|
120
|
+
throw new common_1.HttpException({ code: '400', message: '当前网关未实现队列查询能力' }, common_1.HttpStatus.BAD_REQUEST);
|
|
121
|
+
}
|
|
122
|
+
if (!device) {
|
|
123
|
+
throw new common_1.HttpException({ code: '400', message: '缺少 device' }, common_1.HttpStatus.BAD_REQUEST);
|
|
124
|
+
}
|
|
125
|
+
const cached = this.queueCache.getState(device);
|
|
126
|
+
if (cached?.snapshot && Date.now() - cached.updatedAt < 2000) {
|
|
127
|
+
return cached.snapshot;
|
|
128
|
+
}
|
|
129
|
+
const snapshot = await this.gateway.getScriptQueue(device);
|
|
130
|
+
return snapshot;
|
|
131
|
+
}
|
|
107
132
|
};
|
|
108
133
|
exports.CctExplorerController = CctExplorerController;
|
|
109
134
|
__decorate([
|
|
@@ -153,9 +178,16 @@ __decorate([
|
|
|
153
178
|
__metadata("design:paramtypes", [String, Object, String]),
|
|
154
179
|
__metadata("design:returntype", Promise)
|
|
155
180
|
], CctExplorerController.prototype, "writeFile", null);
|
|
181
|
+
__decorate([
|
|
182
|
+
(0, common_1.Get)('api/scripts/queue'),
|
|
183
|
+
__param(0, (0, common_1.Query)('device')),
|
|
184
|
+
__metadata("design:type", Function),
|
|
185
|
+
__metadata("design:paramtypes", [String]),
|
|
186
|
+
__metadata("design:returntype", Promise)
|
|
187
|
+
], CctExplorerController.prototype, "getScriptQueue", null);
|
|
156
188
|
exports.CctExplorerController = CctExplorerController = __decorate([
|
|
157
189
|
(0, common_1.Controller)(),
|
|
158
190
|
__param(0, (0, common_1.Inject)(tokens_1.DEVICE_GATEWAY_TOKEN)),
|
|
159
191
|
__param(1, (0, common_1.Inject)(tokens_1.CCT_EXPLORER_OPTIONS)),
|
|
160
|
-
__metadata("design:paramtypes", [Object, Object])
|
|
192
|
+
__metadata("design:paramtypes", [Object, Object, device_queue_cache_service_1.DeviceQueueCacheService])
|
|
161
193
|
], CctExplorerController);
|
|
@@ -10,6 +10,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
10
10
|
exports.CctExplorerModule = void 0;
|
|
11
11
|
const common_1 = require("@nestjs/common");
|
|
12
12
|
const cct_explorer_controller_1 = require("./cct-explorer.controller");
|
|
13
|
+
const device_queue_cache_service_1 = require("./device-queue-cache.service");
|
|
13
14
|
const tokens_1 = require("./tokens");
|
|
14
15
|
let CctExplorerModule = CctExplorerModule_1 = class CctExplorerModule {
|
|
15
16
|
static forRoot(options) {
|
|
@@ -27,6 +28,7 @@ let CctExplorerModule = CctExplorerModule_1 = class CctExplorerModule {
|
|
|
27
28
|
provide: tokens_1.DEVICE_GATEWAY_TOKEN,
|
|
28
29
|
useExisting: deviceGatewayClass,
|
|
29
30
|
},
|
|
31
|
+
device_queue_cache_service_1.DeviceQueueCacheService,
|
|
30
32
|
],
|
|
31
33
|
exports: [tokens_1.CCT_EXPLORER_OPTIONS, tokens_1.DEVICE_GATEWAY_TOKEN],
|
|
32
34
|
};
|
|
@@ -6,10 +6,27 @@ export interface DirEntry {
|
|
|
6
6
|
name: string;
|
|
7
7
|
type: 'file' | 'dir';
|
|
8
8
|
}
|
|
9
|
+
export interface ScriptJob {
|
|
10
|
+
id: string;
|
|
11
|
+
type: 'local' | 'remote';
|
|
12
|
+
filename?: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
createdAt: number;
|
|
15
|
+
startedAt?: number;
|
|
16
|
+
finishedAt?: number;
|
|
17
|
+
status: 'pending' | 'running' | 'success' | 'failed';
|
|
18
|
+
errorMessage?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ScriptQueueSnapshot {
|
|
21
|
+
running: ScriptJob | null;
|
|
22
|
+
pending: ScriptJob[];
|
|
23
|
+
capacity: number;
|
|
24
|
+
}
|
|
9
25
|
export interface DeviceGateway {
|
|
10
26
|
listDevices(): Promise<DeviceInfo[]>;
|
|
11
27
|
readDir(device: string, devicePath: string): Promise<DirEntry[]>;
|
|
12
28
|
readFile(device: string, devicePath: string): Promise<Buffer>;
|
|
13
29
|
removeFile(device: string, devicePath: string): Promise<void>;
|
|
14
30
|
writeFile?(device: string, devicePath: string, content: Buffer | string): Promise<void>;
|
|
31
|
+
getScriptQueue?(device: string): Promise<ScriptQueueSnapshot>;
|
|
15
32
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { OnModuleInit } from '@nestjs/common';
|
|
2
|
+
import type { ScriptQueueSnapshot } from './device-gateway.interface';
|
|
3
|
+
import type { DeviceGateway } from './device-gateway.interface';
|
|
4
|
+
import { type CctExplorerModuleOptions } from './tokens';
|
|
5
|
+
export interface DeviceQueueState {
|
|
6
|
+
snapshot: ScriptQueueSnapshot | null;
|
|
7
|
+
updatedAt: number;
|
|
8
|
+
error?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare class DeviceQueueCacheService implements OnModuleInit {
|
|
11
|
+
private readonly gateway;
|
|
12
|
+
private readonly options;
|
|
13
|
+
private readonly cache;
|
|
14
|
+
private intervalId;
|
|
15
|
+
private refreshing;
|
|
16
|
+
constructor(gateway: DeviceGateway, options: CctExplorerModuleOptions);
|
|
17
|
+
onModuleInit(): void;
|
|
18
|
+
getState(deviceName: string): DeviceQueueState | null;
|
|
19
|
+
private refresh;
|
|
20
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.DeviceQueueCacheService = void 0;
|
|
16
|
+
const common_1 = require("@nestjs/common");
|
|
17
|
+
const tokens_1 = require("./tokens");
|
|
18
|
+
const tokens_2 = require("./tokens");
|
|
19
|
+
let DeviceQueueCacheService = class DeviceQueueCacheService {
|
|
20
|
+
constructor(gateway, options) {
|
|
21
|
+
this.gateway = gateway;
|
|
22
|
+
this.options = options;
|
|
23
|
+
this.cache = new Map();
|
|
24
|
+
this.intervalId = null;
|
|
25
|
+
this.refreshing = false;
|
|
26
|
+
}
|
|
27
|
+
onModuleInit() {
|
|
28
|
+
const ms = this.options.scriptQueuePollIntervalMs ?? 1000;
|
|
29
|
+
if (ms <= 0 || typeof this.gateway.getScriptQueue !== 'function')
|
|
30
|
+
return;
|
|
31
|
+
this.refresh();
|
|
32
|
+
this.intervalId = setInterval(() => this.refresh(), ms);
|
|
33
|
+
}
|
|
34
|
+
getState(deviceName) {
|
|
35
|
+
return this.cache.get(deviceName) ?? null;
|
|
36
|
+
}
|
|
37
|
+
async refresh() {
|
|
38
|
+
if (this.refreshing)
|
|
39
|
+
return;
|
|
40
|
+
this.refreshing = true;
|
|
41
|
+
const now = Date.now();
|
|
42
|
+
try {
|
|
43
|
+
const devices = await this.gateway.listDevices();
|
|
44
|
+
await Promise.all(devices.map(async (d) => {
|
|
45
|
+
try {
|
|
46
|
+
const snapshot = await this.gateway.getScriptQueue(d.name);
|
|
47
|
+
this.cache.set(d.name, { snapshot, updatedAt: now });
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
this.cache.set(d.name, {
|
|
51
|
+
snapshot: null,
|
|
52
|
+
updatedAt: now,
|
|
53
|
+
error: err?.message ?? '获取队列失败',
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
// listDevices 失败时不覆盖已有缓存
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
this.refreshing = false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
exports.DeviceQueueCacheService = DeviceQueueCacheService;
|
|
67
|
+
exports.DeviceQueueCacheService = DeviceQueueCacheService = __decorate([
|
|
68
|
+
(0, common_1.Injectable)(),
|
|
69
|
+
__param(0, (0, common_1.Inject)(tokens_1.DEVICE_GATEWAY_TOKEN)),
|
|
70
|
+
__param(1, (0, common_1.Inject)(tokens_2.CCT_EXPLORER_OPTIONS)),
|
|
71
|
+
__metadata("design:paramtypes", [Object, Object])
|
|
72
|
+
], DeviceQueueCacheService);
|
package/dist/public/browser.html
CHANGED
|
@@ -556,6 +556,16 @@
|
|
|
556
556
|
</div>
|
|
557
557
|
</section>
|
|
558
558
|
|
|
559
|
+
<section class="panel" id="script-queue-panel" style="flex: 0 0 auto;">
|
|
560
|
+
<div class="panel-header">
|
|
561
|
+
<div class="panel-title">脚本队列</div>
|
|
562
|
+
<div class="panel-sub" id="script-queue-subtitle">选择设备后每秒刷新</div>
|
|
563
|
+
</div>
|
|
564
|
+
<div id="script-queue-content" style="font-size:12px;color:var(--text-muted);min-height:48px;">
|
|
565
|
+
未选择设备
|
|
566
|
+
</div>
|
|
567
|
+
</section>
|
|
568
|
+
|
|
559
569
|
<div class="status-bar">
|
|
560
570
|
<div class="status-badge">
|
|
561
571
|
<span class="status-dot" id="status-dot"></span>
|
|
@@ -594,6 +604,63 @@
|
|
|
594
604
|
currentPath: null,
|
|
595
605
|
};
|
|
596
606
|
|
|
607
|
+
function formatElapsed(startedAt) {
|
|
608
|
+
if (!startedAt) return '';
|
|
609
|
+
var s = Math.floor((Date.now() - startedAt) / 1000);
|
|
610
|
+
if (s < 60) return s + ' 秒';
|
|
611
|
+
var m = Math.floor(s / 60);
|
|
612
|
+
var r = s % 60;
|
|
613
|
+
return m + ' 分 ' + r + ' 秒';
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function jobLabel(job) {
|
|
617
|
+
return job.filename || job.description || job.id || job.type;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function loadScriptQueue() {
|
|
621
|
+
var device = state.currentDevice;
|
|
622
|
+
var el = document.getElementById('script-queue-content');
|
|
623
|
+
var sub = document.getElementById('script-queue-subtitle');
|
|
624
|
+
if (!device) {
|
|
625
|
+
el.textContent = '未选择设备';
|
|
626
|
+
sub.textContent = '选择设备后每秒刷新';
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
var meta = state.devices.find(function (d) { return d.name === device; });
|
|
630
|
+
if (!meta) {
|
|
631
|
+
el.textContent = '未找到设备信息';
|
|
632
|
+
sub.textContent = '';
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
var data = meta.scriptQueue;
|
|
636
|
+
if (!data) {
|
|
637
|
+
el.textContent = '暂无脚本队列信息(等待服务端刷新)';
|
|
638
|
+
sub.textContent = meta.queueError ? ('上次刷新失败:' + meta.queueError) : '选择设备后每秒刷新';
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
var cap = data.capacity != null ? data.capacity : 10;
|
|
642
|
+
var running = data.running || null;
|
|
643
|
+
var pending = data.pending || [];
|
|
644
|
+
var total = (running ? 1 : 0) + pending.length;
|
|
645
|
+
sub.textContent = total + ' / ' + cap + ' 占用';
|
|
646
|
+
var html = '';
|
|
647
|
+
if (running) {
|
|
648
|
+
var elapsed = formatElapsed(running.startedAt);
|
|
649
|
+
html += '<div style="margin-bottom:6px;"><span style="color:var(--accent);">▶ 运行中</span> ' + jobLabel(running) + (elapsed ? ' <span style="color:var(--text-muted);">已运行 ' + elapsed + '</span>' : '') + '</div>';
|
|
650
|
+
} else {
|
|
651
|
+
html += '<div style="margin-bottom:6px;color:var(--text-muted);">无运行中脚本</div>';
|
|
652
|
+
}
|
|
653
|
+
if (pending.length) {
|
|
654
|
+
html += '<div style="color:var(--text-muted);">排队中 (' + pending.length + '):</div>';
|
|
655
|
+
pending.forEach(function (j, i) {
|
|
656
|
+
html += '<div style="padding-left:8px;">' + (i + 1) + '. ' + jobLabel(j) + '</div>';
|
|
657
|
+
});
|
|
658
|
+
} else {
|
|
659
|
+
html += '<div style="color:var(--text-muted);">无排队任务</div>';
|
|
660
|
+
}
|
|
661
|
+
el.innerHTML = html;
|
|
662
|
+
}
|
|
663
|
+
|
|
597
664
|
function setStatus(text, level) {
|
|
598
665
|
level = level || 'info';
|
|
599
666
|
statusTextEl.textContent = text;
|
|
@@ -636,6 +703,7 @@
|
|
|
636
703
|
selectedDeviceLabelEl.textContent = '当前设备:' + d.name;
|
|
637
704
|
renderDevices();
|
|
638
705
|
renderRootForDevice();
|
|
706
|
+
loadScriptQueue();
|
|
639
707
|
setStatus('已选择设备 ' + d.name + ',请选择目录。');
|
|
640
708
|
});
|
|
641
709
|
deviceListEl.appendChild(item);
|
|
@@ -717,13 +785,17 @@
|
|
|
717
785
|
selectedDeviceLabelEl.textContent = '当前设备:' + state.currentDevice;
|
|
718
786
|
renderDevices();
|
|
719
787
|
renderRootForDevice();
|
|
788
|
+
loadScriptQueue();
|
|
720
789
|
setStatus('已加载设备列表,当前设备:' + state.currentDevice);
|
|
721
790
|
} else if (!state.devices.length) {
|
|
791
|
+
state.currentDevice = null;
|
|
722
792
|
selectedDeviceLabelEl.textContent = '未选择设备';
|
|
723
793
|
fileTbodyEl.innerHTML = '';
|
|
724
794
|
breadcrumbsEl.innerHTML = '<span style="font-size:12px;color:var(--text-muted)">暂无在线设备</span>';
|
|
795
|
+
loadScriptQueue();
|
|
725
796
|
setStatus('暂无在线设备。', 'warn');
|
|
726
797
|
} else {
|
|
798
|
+
loadScriptQueue();
|
|
727
799
|
setStatus('已刷新设备列表。');
|
|
728
800
|
}
|
|
729
801
|
} catch (err) {
|
|
@@ -798,7 +870,10 @@
|
|
|
798
870
|
btnRootScreenshots.addEventListener('click', () => { if (state.currentDevice) openDir('/' + state.currentDevice + '/screenshots'); else setStatus('请先选择设备。', 'warn'); });
|
|
799
871
|
btnRefreshDir.addEventListener('click', () => { if (state.currentPath) openDir(state.currentPath); else if (state.currentDevice) renderRootForDevice(); else setStatus('请先选择设备。', 'warn'); });
|
|
800
872
|
|
|
873
|
+
// 首次加载
|
|
801
874
|
loadDevices();
|
|
875
|
+
// 设备列表轮询(页面打开期间每秒拉一次)
|
|
876
|
+
setInterval(loadDevices, 1000);
|
|
802
877
|
})();
|
|
803
878
|
</script>
|
|
804
879
|
</body>
|
package/dist/tokens.d.ts
CHANGED
|
@@ -10,5 +10,10 @@ export interface CctExplorerModuleOptions {
|
|
|
10
10
|
* 空字符串表示挂载在根路径
|
|
11
11
|
*/
|
|
12
12
|
pathPrefix?: string;
|
|
13
|
+
/**
|
|
14
|
+
* 脚本队列轮询间隔(毫秒)。模块内会定时向所有设备拉取 script_queue 并缓存,GET api/devices 会带上 scriptQueue/queueUpdatedAt/queueError。
|
|
15
|
+
* 默认 1000。设为 0 则关闭轮询,由接入方自行实现。
|
|
16
|
+
*/
|
|
17
|
+
scriptQueuePollIntervalMs?: number;
|
|
13
18
|
}
|
|
14
19
|
export declare const CCT_EXPLORER_OPTIONS: unique symbol;
|