@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.
@@ -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
- constructor(gateway: DeviceGateway, options: CctExplorerModuleOptions);
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: import("./device-gateway.interface").DeviceInfo[];
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
- return { devices };
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);
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bomon/nestjs-cct-explorer",
3
- "version": "0.1.0",
3
+ "version": "1.1.0",
4
4
  "description": "NestJS 模块:封装 CDP client 的设备资源浏览器 HTTP 接口与 /browser 页面",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",