@camstack/core 0.1.13 → 0.1.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/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +220 -0
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js.map +1 -0
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +9 -0
- package/dist/builtins/addon-pages-aggregator/index.js +222 -0
- package/dist/builtins/addon-pages-aggregator/index.js.map +1 -0
- package/dist/builtins/addon-pages-aggregator/index.mjs +9 -0
- package/dist/builtins/addon-pages-aggregator/index.mjs.map +1 -0
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +200 -0
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js.map +1 -0
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +9 -0
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs.map +1 -0
- package/dist/builtins/addon-widgets-aggregator/index.js +202 -0
- package/dist/builtins/addon-widgets-aggregator/index.js.map +1 -0
- package/dist/builtins/addon-widgets-aggregator/index.mjs +9 -0
- package/dist/builtins/addon-widgets-aggregator/index.mjs.map +1 -0
- package/dist/builtins/alerts/alerts.addon.js +443 -0
- package/dist/builtins/alerts/alerts.addon.js.map +1 -0
- package/dist/builtins/alerts/alerts.addon.mjs +9 -0
- package/dist/builtins/alerts/alerts.addon.mjs.map +1 -0
- package/dist/builtins/alerts/index.js +443 -0
- package/dist/builtins/alerts/index.js.map +1 -0
- package/dist/builtins/alerts/index.mjs +8 -0
- package/dist/builtins/alerts/index.mjs.map +1 -0
- package/dist/builtins/console-logging/index.js +242 -0
- package/dist/builtins/console-logging/index.js.map +1 -0
- package/dist/builtins/console-logging/index.mjs +11 -0
- package/dist/builtins/console-logging/index.mjs.map +1 -0
- package/dist/builtins/device-manager/device-manager.addon.js +2155 -0
- package/dist/builtins/device-manager/device-manager.addon.js.map +1 -0
- package/dist/builtins/device-manager/device-manager.addon.mjs +9 -0
- package/dist/builtins/device-manager/device-manager.addon.mjs.map +1 -0
- package/dist/builtins/device-manager/index.js +2157 -0
- package/dist/builtins/device-manager/index.js.map +1 -0
- package/dist/builtins/device-manager/index.mjs +10 -0
- package/dist/builtins/device-manager/index.mjs.map +1 -0
- package/dist/builtins/hub-forwarder/index.js +297 -0
- package/dist/builtins/hub-forwarder/index.js.map +1 -0
- package/dist/builtins/hub-forwarder/index.mjs +11 -0
- package/dist/builtins/hub-forwarder/index.mjs.map +1 -0
- package/dist/builtins/local-auth/index.js +623 -0
- package/dist/builtins/local-auth/index.js.map +1 -0
- package/dist/builtins/local-auth/index.mjs +8 -0
- package/dist/builtins/local-auth/index.mjs.map +1 -0
- package/dist/builtins/local-auth/local-auth.addon.js +623 -0
- package/dist/builtins/local-auth/local-auth.addon.js.map +1 -0
- package/dist/builtins/local-auth/local-auth.addon.mjs +9 -0
- package/dist/builtins/local-auth/local-auth.addon.mjs.map +1 -0
- package/dist/builtins/local-backup/index.js +53 -68
- package/dist/builtins/local-backup/index.js.map +1 -1
- package/dist/builtins/local-backup/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +898 -0
- package/dist/builtins/native-metrics/native-metrics.addon.js.map +1 -0
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +7 -0
- package/dist/builtins/native-metrics/native-metrics.addon.mjs.map +1 -0
- package/dist/builtins/snapshot/index.js +504 -0
- package/dist/builtins/snapshot/index.js.map +1 -0
- package/dist/builtins/snapshot/index.mjs +477 -0
- package/dist/builtins/snapshot/index.mjs.map +1 -0
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +16 -166
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js.map +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/index.js +554 -621
- package/dist/builtins/sqlite-storage/index.js.map +1 -1
- package/dist/builtins/sqlite-storage/index.mjs +9 -11
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +368 -130
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js.map +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +1 -1
- package/dist/builtins/system-config/index.js +189 -0
- package/dist/builtins/system-config/index.js.map +1 -0
- package/dist/builtins/system-config/index.mjs +10 -0
- package/dist/builtins/system-config/index.mjs.map +1 -0
- package/dist/builtins/system-config/system-config.addon.js +187 -0
- package/dist/builtins/system-config/system-config.addon.js.map +1 -0
- package/dist/builtins/system-config/system-config.addon.mjs +9 -0
- package/dist/builtins/system-config/system-config.addon.mjs.map +1 -0
- package/dist/builtins/winston-logging/index.js +185 -65
- package/dist/builtins/winston-logging/index.js.map +1 -1
- package/dist/builtins/winston-logging/index.mjs +2 -1
- package/dist/chunk-2CIYKDRN.mjs +1 -0
- package/dist/chunk-2CIYKDRN.mjs.map +1 -0
- package/dist/chunk-2F76X6NL.mjs +136 -0
- package/dist/chunk-2F76X6NL.mjs.map +1 -0
- package/dist/chunk-2QUFBZ7M.mjs +1 -0
- package/dist/chunk-2QUFBZ7M.mjs.map +1 -0
- package/dist/chunk-3BK2Y7GY.mjs +593 -0
- package/dist/chunk-3BK2Y7GY.mjs.map +1 -0
- package/dist/chunk-4OOHFJHT.mjs +421 -0
- package/dist/chunk-4OOHFJHT.mjs.map +1 -0
- package/dist/chunk-4XHB7IHT.mjs +809 -0
- package/dist/chunk-4XHB7IHT.mjs.map +1 -0
- package/dist/{chunk-2F3XZYRW.mjs → chunk-6M2HSSTQ.mjs} +16 -7
- package/dist/chunk-6M2HSSTQ.mjs.map +1 -0
- package/dist/{chunk-SO4LROOT.mjs → chunk-7FI7SQS7.mjs} +54 -69
- package/dist/chunk-7FI7SQS7.mjs.map +1 -0
- package/dist/chunk-ED57RCQE.mjs +171 -0
- package/dist/chunk-ED57RCQE.mjs.map +1 -0
- package/dist/chunk-FZN56HGQ.mjs +626 -0
- package/dist/chunk-FZN56HGQ.mjs.map +1 -0
- package/dist/chunk-GL4OOB25.mjs +51 -0
- package/dist/chunk-GL4OOB25.mjs.map +1 -0
- package/dist/chunk-KDG2NTDB.mjs +137 -0
- package/dist/chunk-KDG2NTDB.mjs.map +1 -0
- package/dist/chunk-NRBQWBDM.mjs +191 -0
- package/dist/chunk-NRBQWBDM.mjs.map +1 -0
- package/dist/chunk-O4V246GG.mjs +2137 -0
- package/dist/chunk-O4V246GG.mjs.map +1 -0
- package/dist/chunk-QT57H266.mjs +163 -0
- package/dist/chunk-QT57H266.mjs.map +1 -0
- package/dist/chunk-QX4RH25I.mjs +141 -0
- package/dist/chunk-QX4RH25I.mjs.map +1 -0
- package/dist/chunk-TB562PZX.mjs +86 -0
- package/dist/chunk-TB562PZX.mjs.map +1 -0
- package/dist/chunk-TDYPZXK5.mjs +1 -0
- package/dist/chunk-TDYPZXK5.mjs.map +1 -0
- package/dist/chunk-UJI4LN5P.mjs +36 -0
- package/dist/chunk-UJI4LN5P.mjs.map +1 -0
- package/dist/chunk-W6RTHQGP.mjs +1 -0
- package/dist/chunk-W6RTHQGP.mjs.map +1 -0
- package/dist/chunk-ZELBCPDC.mjs +369 -0
- package/dist/chunk-ZELBCPDC.mjs.map +1 -0
- package/dist/index.d.mts +1103 -544
- package/dist/index.d.ts +1103 -544
- package/dist/index.js +7032 -6033
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +568 -2226
- package/dist/index.mjs.map +1 -1
- package/dist/resource-monitor-UZUGPIAU.mjs +9 -0
- package/dist/resource-monitor-UZUGPIAU.mjs.map +1 -0
- package/dist/storage-location-manager-HFNB3PCS.mjs +7 -0
- package/dist/storage-location-manager-HFNB3PCS.mjs.map +1 -0
- package/package.json +123 -2
- package/dist/builtins/local-backup/index.d.mts +0 -42
- package/dist/builtins/local-backup/index.d.ts +0 -42
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.d.mts +0 -2
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.d.ts +0 -2
- package/dist/builtins/sqlite-storage/index.d.mts +0 -4
- package/dist/builtins/sqlite-storage/index.d.ts +0 -4
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.d.mts +0 -2
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.d.ts +0 -2
- package/dist/builtins/winston-logging/index.d.mts +0 -30
- package/dist/builtins/winston-logging/index.d.ts +0 -30
- package/dist/chunk-2F3XZYRW.mjs.map +0 -1
- package/dist/chunk-LQFPAEQF.mjs +0 -147
- package/dist/chunk-LQFPAEQF.mjs.map +0 -1
- package/dist/chunk-R3DIIBBX.mjs +0 -532
- package/dist/chunk-R3DIIBBX.mjs.map +0 -1
- package/dist/chunk-SMNR44VG.mjs +0 -386
- package/dist/chunk-SMNR44VG.mjs.map +0 -1
- package/dist/chunk-SO4LROOT.mjs.map +0 -1
- package/dist/chunk-SPA4JBKN.mjs +0 -175
- package/dist/chunk-SPA4JBKN.mjs.map +0 -1
- package/dist/dist-3BY63UQ5.mjs +0 -2151
- package/dist/dist-3BY63UQ5.mjs.map +0 -1
- package/dist/filesystem-storage.addon-C42r589X.d.mts +0 -57
- package/dist/filesystem-storage.addon-C42r589X.d.ts +0 -57
- package/dist/sql-schema-CKz78rId.d.mts +0 -97
- package/dist/sql-schema-CKz78rId.d.ts +0 -97
- package/dist/sqlite-settings.addon-KwG-uKMP.d.mts +0 -79
- package/dist/sqlite-settings.addon-KwG-uKMP.d.ts +0 -79
- package/dist/storage-location-manager-KKDQNAKA.mjs +0 -7
- /package/dist/{storage-location-manager-KKDQNAKA.mjs.map → builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs.map} +0 -0
package/dist/index.mjs
CHANGED
|
@@ -1,259 +1,85 @@
|
|
|
1
|
+
import "./chunk-2QUFBZ7M.mjs";
|
|
2
|
+
import {
|
|
3
|
+
DeviceManagerAddon
|
|
4
|
+
} from "./chunk-O4V246GG.mjs";
|
|
5
|
+
import "./chunk-TDYPZXK5.mjs";
|
|
6
|
+
import {
|
|
7
|
+
ApiKeyManager,
|
|
8
|
+
AuthManager,
|
|
9
|
+
LocalAuthAddon,
|
|
10
|
+
ScopedTokenManager,
|
|
11
|
+
UserManager
|
|
12
|
+
} from "./chunk-3BK2Y7GY.mjs";
|
|
13
|
+
import {
|
|
14
|
+
getPidStats,
|
|
15
|
+
getSinglePidStats
|
|
16
|
+
} from "./chunk-GL4OOB25.mjs";
|
|
17
|
+
import {
|
|
18
|
+
FsStorageBackend,
|
|
19
|
+
StorageLocationManager
|
|
20
|
+
} from "./chunk-6M2HSSTQ.mjs";
|
|
21
|
+
import {
|
|
22
|
+
NativeMetricsAddon,
|
|
23
|
+
NativeMetricsProvider
|
|
24
|
+
} from "./chunk-4XHB7IHT.mjs";
|
|
25
|
+
import "./chunk-2CIYKDRN.mjs";
|
|
26
|
+
import {
|
|
27
|
+
AlertCenterAddon
|
|
28
|
+
} from "./chunk-4OOHFJHT.mjs";
|
|
29
|
+
import "./chunk-W6RTHQGP.mjs";
|
|
30
|
+
import {
|
|
31
|
+
SystemConfigAddon
|
|
32
|
+
} from "./chunk-QT57H266.mjs";
|
|
1
33
|
import {
|
|
2
34
|
CORE_TABLE_DDL,
|
|
3
|
-
|
|
35
|
+
ConfigStore,
|
|
36
|
+
DeviceStore,
|
|
37
|
+
FilesystemStorageProvider,
|
|
4
38
|
SettingsStore,
|
|
5
|
-
SqliteStorageAddon,
|
|
6
|
-
SqliteStorageProvider,
|
|
7
39
|
addonTableToDdl
|
|
8
|
-
} from "./chunk-
|
|
9
|
-
import
|
|
10
|
-
|
|
40
|
+
} from "./chunk-ZELBCPDC.mjs";
|
|
41
|
+
import {
|
|
42
|
+
FilesystemStorageAddon
|
|
43
|
+
} from "./chunk-UJI4LN5P.mjs";
|
|
44
|
+
import {
|
|
45
|
+
SqliteSettingsAddon,
|
|
46
|
+
SqliteSettingsBackend
|
|
47
|
+
} from "./chunk-FZN56HGQ.mjs";
|
|
11
48
|
import {
|
|
12
49
|
WinstonDestination,
|
|
13
50
|
WinstonLoggingAddon
|
|
14
|
-
} from "./chunk-
|
|
51
|
+
} from "./chunk-KDG2NTDB.mjs";
|
|
52
|
+
import {
|
|
53
|
+
ConsoleDestination,
|
|
54
|
+
ConsoleLoggingAddon
|
|
55
|
+
} from "./chunk-TB562PZX.mjs";
|
|
56
|
+
import {
|
|
57
|
+
HubForwarderAddon,
|
|
58
|
+
HubForwarderDestination
|
|
59
|
+
} from "./chunk-QX4RH25I.mjs";
|
|
60
|
+
import {
|
|
61
|
+
formatLogLine
|
|
62
|
+
} from "./chunk-2F76X6NL.mjs";
|
|
15
63
|
import {
|
|
16
64
|
LocalBackupAddon,
|
|
17
65
|
LocalBackupService
|
|
18
|
-
} from "./chunk-
|
|
19
|
-
import {
|
|
20
|
-
FsStorageBackend,
|
|
21
|
-
StorageLocationManager
|
|
22
|
-
} from "./chunk-2F3XZYRW.mjs";
|
|
23
|
-
|
|
24
|
-
// src/deps/binary-downloader.ts
|
|
25
|
-
import { existsSync, mkdirSync, chmodSync, createWriteStream, unlinkSync } from "fs";
|
|
26
|
-
import { join } from "path";
|
|
27
|
-
import { pipeline } from "stream/promises";
|
|
28
|
-
import { Readable } from "stream";
|
|
29
|
-
import { execFileSync } from "child_process";
|
|
30
|
-
function getPlatformInfo() {
|
|
31
|
-
return {
|
|
32
|
-
platform: process.platform,
|
|
33
|
-
arch: process.arch
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
function buildBinaryPath(dataDir, name, platform2) {
|
|
37
|
-
const ext = (platform2 ?? process.platform) === "win32" ? ".exe" : "";
|
|
38
|
-
return join(dataDir, "deps", `${name}${ext}`);
|
|
39
|
-
}
|
|
40
|
-
function findInPath(name) {
|
|
41
|
-
try {
|
|
42
|
-
execFileSync(name, ["--version"], { stdio: "pipe", timeout: 5e3 });
|
|
43
|
-
return name;
|
|
44
|
-
} catch {
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
async function downloadBinary(opts) {
|
|
49
|
-
const { name, url, targetDir, targetName, logger, isArchive, archiveFormat, archiveInnerPath } = opts;
|
|
50
|
-
const targetPath = join(targetDir, targetName);
|
|
51
|
-
if (existsSync(targetPath)) {
|
|
52
|
-
logger.debug(`${name} binary already exists at ${targetPath}`);
|
|
53
|
-
return targetPath;
|
|
54
|
-
}
|
|
55
|
-
mkdirSync(targetDir, { recursive: true });
|
|
56
|
-
logger.info(`Downloading ${name} from ${url}`);
|
|
57
|
-
const response = await fetch(url, { redirect: "follow" });
|
|
58
|
-
if (!response.ok || !response.body) {
|
|
59
|
-
throw new Error(`Failed to download ${name}: ${response.status} ${response.statusText}`);
|
|
60
|
-
}
|
|
61
|
-
if (isArchive) {
|
|
62
|
-
const ext = archiveFormat ?? "tar.gz";
|
|
63
|
-
const tmpArchive = join(targetDir, `${name}-download.${ext}`);
|
|
64
|
-
const nodeStream = Readable.fromWeb(response.body);
|
|
65
|
-
await pipeline(nodeStream, createWriteStream(tmpArchive));
|
|
66
|
-
const tmpExtractDir = join(targetDir, `${name}-extract`);
|
|
67
|
-
mkdirSync(tmpExtractDir, { recursive: true });
|
|
68
|
-
if (ext === "zip") {
|
|
69
|
-
try {
|
|
70
|
-
execFileSync("unzip", ["-o", "-q", tmpArchive, "-d", tmpExtractDir], { stdio: "pipe" });
|
|
71
|
-
} catch {
|
|
72
|
-
execFileSync("tar", ["-xf", tmpArchive, "-C", tmpExtractDir], { stdio: "pipe" });
|
|
73
|
-
}
|
|
74
|
-
} else if (ext === "tar.xz") {
|
|
75
|
-
execFileSync("tar", ["-xJf", tmpArchive, "-C", tmpExtractDir], { stdio: "pipe" });
|
|
76
|
-
} else {
|
|
77
|
-
execFileSync("tar", ["-xzf", tmpArchive, "-C", tmpExtractDir], { stdio: "pipe" });
|
|
78
|
-
}
|
|
79
|
-
if (archiveInnerPath) {
|
|
80
|
-
const { copyFileSync } = await import("fs");
|
|
81
|
-
const sourcePath = join(tmpExtractDir, archiveInnerPath);
|
|
82
|
-
if (!existsSync(sourcePath)) {
|
|
83
|
-
throw new Error(`Binary not found in archive at ${archiveInnerPath}`);
|
|
84
|
-
}
|
|
85
|
-
copyFileSync(sourcePath, targetPath);
|
|
86
|
-
}
|
|
87
|
-
unlinkSync(tmpArchive);
|
|
88
|
-
const { rmSync: rmSync2 } = await import("fs");
|
|
89
|
-
rmSync2(tmpExtractDir, { recursive: true, force: true });
|
|
90
|
-
} else {
|
|
91
|
-
const nodeStream = Readable.fromWeb(response.body);
|
|
92
|
-
await pipeline(nodeStream, createWriteStream(targetPath));
|
|
93
|
-
}
|
|
94
|
-
chmodSync(targetPath, 493);
|
|
95
|
-
logger.info(`${name} downloaded to ${targetPath}`);
|
|
96
|
-
return targetPath;
|
|
97
|
-
}
|
|
98
|
-
async function ensureBinary(opts) {
|
|
99
|
-
const ext = process.platform === "win32" ? ".exe" : "";
|
|
100
|
-
const targetName = `${opts.name}${ext}`;
|
|
101
|
-
const targetPath = join(opts.targetDir, targetName);
|
|
102
|
-
if (existsSync(targetPath)) {
|
|
103
|
-
opts.logger.debug(`${opts.name} found at ${targetPath}`);
|
|
104
|
-
return targetPath;
|
|
105
|
-
}
|
|
106
|
-
const inPath = findInPath(opts.name);
|
|
107
|
-
if (inPath) {
|
|
108
|
-
opts.logger.info(`${opts.name} found in system PATH`);
|
|
109
|
-
return inPath;
|
|
110
|
-
}
|
|
111
|
-
return downloadBinary({
|
|
112
|
-
name: opts.name,
|
|
113
|
-
url: opts.downloadUrl,
|
|
114
|
-
targetDir: opts.targetDir,
|
|
115
|
-
targetName,
|
|
116
|
-
logger: opts.logger,
|
|
117
|
-
isArchive: opts.isArchive,
|
|
118
|
-
archiveFormat: opts.archiveFormat,
|
|
119
|
-
archiveInnerPath: opts.archiveInnerPath
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// src/deps/ffmpeg-downloader.ts
|
|
124
|
-
import { join as join2 } from "path";
|
|
125
|
-
var FFMPEG_VERSION = "7.1";
|
|
126
|
-
function getFfmpegDownloadUrl(platform2, arch2) {
|
|
127
|
-
switch (platform2) {
|
|
128
|
-
case "linux": {
|
|
129
|
-
const archMap = { x64: "amd64", arm64: "arm64" };
|
|
130
|
-
const a = archMap[arch2];
|
|
131
|
-
if (!a) throw new Error(`Unsupported Linux architecture: ${arch2}`);
|
|
132
|
-
return `https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-${a}-static.tar.xz`;
|
|
133
|
-
}
|
|
134
|
-
case "darwin": {
|
|
135
|
-
const archMap = { arm64: "arm64", x64: "amd64" };
|
|
136
|
-
const a = archMap[arch2];
|
|
137
|
-
if (!a) throw new Error(`Unsupported macOS architecture: ${arch2}`);
|
|
138
|
-
return `https://www.osxexperts.net/ffmpeg${FFMPEG_VERSION.replace(".", "")}arm.zip`;
|
|
139
|
-
}
|
|
140
|
-
default:
|
|
141
|
-
throw new Error(`Unsupported platform: ${platform2}`);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
function getFfmpegArchiveInfo(platform2) {
|
|
145
|
-
switch (platform2) {
|
|
146
|
-
case "linux":
|
|
147
|
-
return {
|
|
148
|
-
isArchive: true,
|
|
149
|
-
archiveFormat: "tar.xz",
|
|
150
|
-
archiveInnerPath: `ffmpeg-${FFMPEG_VERSION}-amd64-static/ffmpeg`
|
|
151
|
-
};
|
|
152
|
-
case "darwin":
|
|
153
|
-
return {
|
|
154
|
-
isArchive: true,
|
|
155
|
-
archiveFormat: "zip",
|
|
156
|
-
archiveInnerPath: "ffmpeg"
|
|
157
|
-
};
|
|
158
|
-
default:
|
|
159
|
-
throw new Error(`Unsupported platform: ${platform2}`);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
async function ensureFfmpeg(dataDir, logger) {
|
|
163
|
-
const depsDir = join2(dataDir, "deps");
|
|
164
|
-
const platform2 = process.platform;
|
|
165
|
-
const arch2 = process.arch;
|
|
166
|
-
const archiveInfo = getFfmpegArchiveInfo(platform2);
|
|
167
|
-
return ensureBinary({
|
|
168
|
-
name: "ffmpeg",
|
|
169
|
-
targetDir: depsDir,
|
|
170
|
-
downloadUrl: getFfmpegDownloadUrl(platform2, arch2),
|
|
171
|
-
logger,
|
|
172
|
-
...archiveInfo
|
|
173
|
-
});
|
|
174
|
-
}
|
|
66
|
+
} from "./chunk-7FI7SQS7.mjs";
|
|
175
67
|
|
|
176
|
-
// src/
|
|
177
|
-
import {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
return `${PP_BASE}/python-headless-${PYTHON_VERSION}-linux-${a}.zip`;
|
|
192
|
-
}
|
|
193
|
-
case "darwin": {
|
|
194
|
-
return `${PP_BASE}/python-headless-${PYTHON_VERSION}-darwin-universal2.zip`;
|
|
195
|
-
}
|
|
196
|
-
default:
|
|
197
|
-
throw new Error(`Unsupported platform for portable Python: ${platform2}`);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
async function ensurePython(dataDir, logger) {
|
|
201
|
-
const pythonDir = join3(dataDir, "deps", "python");
|
|
202
|
-
const pythonBin = join3(pythonDir, "bin", "python3");
|
|
203
|
-
if (existsSync2(pythonBin)) {
|
|
204
|
-
logger.debug(`Portable Python found at ${pythonBin}`);
|
|
205
|
-
return pythonBin;
|
|
206
|
-
}
|
|
207
|
-
for (const name of ["python3", "python"]) {
|
|
208
|
-
const inPath = findInPath(name);
|
|
209
|
-
if (inPath) {
|
|
210
|
-
logger.info(`Python found in system PATH: ${name}`);
|
|
211
|
-
return inPath;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
logger.info(`Downloading portable Python ${PYTHON_VERSION} (headless)...`);
|
|
215
|
-
const depsDir = join3(dataDir, "deps");
|
|
216
|
-
mkdirSync2(depsDir, { recursive: true });
|
|
217
|
-
const url = getPythonDownloadUrl(process.platform, process.arch);
|
|
218
|
-
const tmpArchive = join3(depsDir, "python-download.zip");
|
|
219
|
-
logger.info(` Source: ${url}`);
|
|
220
|
-
const response = await fetch(url, { redirect: "follow" });
|
|
221
|
-
if (!response.ok || !response.body) {
|
|
222
|
-
logger.warn(`Failed to download Python: ${response.status} \u2014 ML detection will not be available`);
|
|
223
|
-
return null;
|
|
224
|
-
}
|
|
225
|
-
const nodeStream = Readable2.fromWeb(response.body);
|
|
226
|
-
await pipeline2(nodeStream, createWriteStream2(tmpArchive));
|
|
227
|
-
try {
|
|
228
|
-
execFileSync2("unzip", ["-o", "-q", tmpArchive, "-d", depsDir], { stdio: "pipe" });
|
|
229
|
-
} catch {
|
|
230
|
-
execFileSync2("python3", ["-m", "zipfile", "-e", tmpArchive, depsDir], { stdio: "pipe" });
|
|
231
|
-
}
|
|
232
|
-
const { unlinkSync: unlinkSync3 } = await import("fs");
|
|
233
|
-
unlinkSync3(tmpArchive);
|
|
234
|
-
if (!existsSync2(pythonBin)) {
|
|
235
|
-
logger.warn("Python extraction succeeded but binary not found at expected path");
|
|
236
|
-
return null;
|
|
237
|
-
}
|
|
238
|
-
chmodSync2(pythonBin, 493);
|
|
239
|
-
logger.info(`Portable Python ${PYTHON_VERSION} installed at ${pythonBin}`);
|
|
240
|
-
return pythonBin;
|
|
241
|
-
}
|
|
242
|
-
async function installPythonPackages(pythonPath, packages, logger) {
|
|
243
|
-
if (packages.length === 0) return;
|
|
244
|
-
logger.info(`Installing Python packages: ${packages.join(", ")}`);
|
|
245
|
-
try {
|
|
246
|
-
execFileSync2(pythonPath, ["-m", "pip", "install", "--quiet", ...packages], {
|
|
247
|
-
stdio: "pipe",
|
|
248
|
-
timeout: 3e5
|
|
249
|
-
// 5 minutes for large packages like torch
|
|
250
|
-
});
|
|
251
|
-
logger.info("Python packages installed successfully");
|
|
252
|
-
} catch (err) {
|
|
253
|
-
logger.error(`Failed to install Python packages: ${err instanceof Error ? err.message : err}`);
|
|
254
|
-
throw err;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
68
|
+
// src/index.ts
|
|
69
|
+
import {
|
|
70
|
+
ensureBinary,
|
|
71
|
+
downloadBinary,
|
|
72
|
+
findInPath,
|
|
73
|
+
getPlatformInfo,
|
|
74
|
+
buildBinaryPath,
|
|
75
|
+
ensureFfmpeg,
|
|
76
|
+
getFfmpegDownloadUrl,
|
|
77
|
+
ensurePython,
|
|
78
|
+
installPythonPackages,
|
|
79
|
+
installPythonRequirements,
|
|
80
|
+
getPythonDownloadUrl,
|
|
81
|
+
PYTHON_VERSION
|
|
82
|
+
} from "@camstack/types/node";
|
|
257
83
|
|
|
258
84
|
// src/events/event-bus.ts
|
|
259
85
|
var EventBus = class {
|
|
@@ -324,7 +150,7 @@ async function downloadFile(url, destPath, onProgress) {
|
|
|
324
150
|
try {
|
|
325
151
|
for (; ; ) {
|
|
326
152
|
const { done, value } = await reader.read();
|
|
327
|
-
if (done) break;
|
|
153
|
+
if (done || !value) break;
|
|
328
154
|
fileStream.write(value);
|
|
329
155
|
downloaded += value.length;
|
|
330
156
|
onProgress?.(downloaded, total);
|
|
@@ -370,10 +196,10 @@ async function downloadModel(options) {
|
|
|
370
196
|
try {
|
|
371
197
|
await downloadFile(tryUrl, destPath, onProgress);
|
|
372
198
|
if (expectedSha256) {
|
|
373
|
-
const
|
|
374
|
-
if (
|
|
199
|
+
const hash = await computeSha256(destPath);
|
|
200
|
+
if (hash !== expectedSha256) {
|
|
375
201
|
fs.unlinkSync(destPath);
|
|
376
|
-
throw new Error(`SHA256 mismatch: expected ${expectedSha256}, got ${
|
|
202
|
+
throw new Error(`SHA256 mismatch: expected ${expectedSha256}, got ${hash}`);
|
|
377
203
|
}
|
|
378
204
|
}
|
|
379
205
|
const stat = fs.statSync(destPath);
|
|
@@ -387,13 +213,102 @@ async function downloadModel(options) {
|
|
|
387
213
|
}
|
|
388
214
|
async function computeSha256(filePath) {
|
|
389
215
|
return new Promise((resolve, reject) => {
|
|
390
|
-
const
|
|
216
|
+
const hash = createHash("sha256");
|
|
391
217
|
const stream = fs.createReadStream(filePath);
|
|
392
|
-
stream.on("data", (chunk) =>
|
|
393
|
-
stream.on("end", () => resolve(
|
|
218
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
219
|
+
stream.on("end", () => resolve(hash.digest("hex")));
|
|
394
220
|
stream.on("error", reject);
|
|
395
221
|
});
|
|
396
222
|
}
|
|
223
|
+
async function downloadDirectory(url, destDir, knownFiles, onProgress) {
|
|
224
|
+
const match = url.match(/huggingface\.co\/([^/]+\/[^/]+)\/resolve\/main\/(.+)/);
|
|
225
|
+
if (!match) throw new Error(`Cannot parse HuggingFace URL: ${url}`);
|
|
226
|
+
const [, repo, dirPath] = match;
|
|
227
|
+
const files = (knownFiles ?? []).map((f) => ({
|
|
228
|
+
relativePath: f,
|
|
229
|
+
fileUrl: `https://huggingface.co/${repo}/resolve/main/${dirPath}/${f}`
|
|
230
|
+
}));
|
|
231
|
+
if (files.length === 0) {
|
|
232
|
+
throw new Error(`Directory bundle requires explicit \`files\` list (got none for ${url})`);
|
|
233
|
+
}
|
|
234
|
+
const tmpDir = destDir + ".downloading";
|
|
235
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
236
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
237
|
+
let totalDownloaded = 0;
|
|
238
|
+
try {
|
|
239
|
+
for (const file of files) {
|
|
240
|
+
const destPath = path.join(tmpDir, file.relativePath);
|
|
241
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
242
|
+
await downloadFile(file.fileUrl, destPath, (downloaded, _total) => {
|
|
243
|
+
onProgress?.(totalDownloaded + downloaded, void 0);
|
|
244
|
+
});
|
|
245
|
+
totalDownloaded += fs.statSync(destPath).size;
|
|
246
|
+
}
|
|
247
|
+
fs.rmSync(destDir, { recursive: true, force: true });
|
|
248
|
+
fs.renameSync(tmpDir, destDir);
|
|
249
|
+
} catch (err) {
|
|
250
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
251
|
+
throw err;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
async function ensureModel(modelsDir, entry, format, onProgress) {
|
|
255
|
+
const formatEntry = entry.formats[format];
|
|
256
|
+
if (!formatEntry) {
|
|
257
|
+
throw new Error(
|
|
258
|
+
`Model "${entry.id}" has no ${format} format. Available: ${Object.keys(entry.formats).join(", ")}`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
if (entry.extraFiles) {
|
|
262
|
+
for (const extra of entry.extraFiles) {
|
|
263
|
+
await downloadFile(extra.url, path.join(modelsDir, extra.filename));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const filename = formatEntry.url.split("/").pop() ?? `${entry.id}.${format}`;
|
|
267
|
+
const modelPath = path.join(modelsDir, filename);
|
|
268
|
+
if (fs.existsSync(modelPath)) {
|
|
269
|
+
if (formatEntry.isDirectory && !fs.existsSync(path.join(modelPath, "Manifest.json"))) {
|
|
270
|
+
fs.rmSync(modelPath, { recursive: true, force: true });
|
|
271
|
+
} else {
|
|
272
|
+
return modelPath;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
fs.mkdirSync(modelsDir, { recursive: true });
|
|
276
|
+
if (formatEntry.isDirectory) {
|
|
277
|
+
await downloadDirectory(formatEntry.url, modelPath, formatEntry.files, onProgress);
|
|
278
|
+
} else {
|
|
279
|
+
await downloadFile(
|
|
280
|
+
formatEntry.url,
|
|
281
|
+
modelPath,
|
|
282
|
+
(downloaded, total) => onProgress?.(downloaded, total === 0 ? void 0 : total)
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
return modelPath;
|
|
286
|
+
}
|
|
287
|
+
function getModelFilePath(modelsDir, entry, format) {
|
|
288
|
+
const formatEntry = entry.formats[format];
|
|
289
|
+
if (!formatEntry) return null;
|
|
290
|
+
const filename = formatEntry.url.split("/").pop() ?? `${entry.id}.${format}`;
|
|
291
|
+
return path.join(modelsDir, filename);
|
|
292
|
+
}
|
|
293
|
+
function isModelDownloaded(modelsDir, entry, format) {
|
|
294
|
+
const formatEntry = entry.formats[format];
|
|
295
|
+
if (!formatEntry) return false;
|
|
296
|
+
const modelPath = getModelFilePath(modelsDir, entry, format);
|
|
297
|
+
if (!modelPath || !fs.existsSync(modelPath)) return false;
|
|
298
|
+
if (formatEntry.isDirectory) return fs.existsSync(path.join(modelPath, "Manifest.json"));
|
|
299
|
+
return fs.statSync(modelPath).size > 0;
|
|
300
|
+
}
|
|
301
|
+
function deleteModelFromDisk(modelsDir, entry, format) {
|
|
302
|
+
const modelPath = getModelFilePath(modelsDir, entry, format);
|
|
303
|
+
if (!modelPath || !fs.existsSync(modelPath)) return false;
|
|
304
|
+
const formatEntry = entry.formats[format];
|
|
305
|
+
if (formatEntry?.isDirectory) {
|
|
306
|
+
fs.rmSync(modelPath, { recursive: true, force: true });
|
|
307
|
+
} else {
|
|
308
|
+
fs.unlinkSync(modelPath);
|
|
309
|
+
}
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
397
312
|
|
|
398
313
|
// src/download/model-download-service.ts
|
|
399
314
|
import * as fs2 from "fs";
|
|
@@ -408,6 +323,8 @@ var ModelDownloadService = class {
|
|
|
408
323
|
}
|
|
409
324
|
this.catalog = map;
|
|
410
325
|
}
|
|
326
|
+
modelsDir;
|
|
327
|
+
onProgress;
|
|
411
328
|
catalog;
|
|
412
329
|
/**
|
|
413
330
|
* Ensure a model (and its extra files) is downloaded.
|
|
@@ -492,13 +409,6 @@ var ModelDownloadService = class {
|
|
|
492
409
|
}
|
|
493
410
|
return fs2.statSync(modelPath).size > 0;
|
|
494
411
|
}
|
|
495
|
-
/**
|
|
496
|
-
* Legacy API: download a model by ID (delegates to ensure with default format).
|
|
497
|
-
* Required by IAddonModelManager interface.
|
|
498
|
-
*/
|
|
499
|
-
async downloadModel(id) {
|
|
500
|
-
return this.ensure(id);
|
|
501
|
-
}
|
|
502
412
|
/** Get the catalog entry for a model by ID. */
|
|
503
413
|
getEntry(modelId) {
|
|
504
414
|
return this.catalog.get(modelId);
|
|
@@ -528,7 +438,7 @@ var ModelDownloadService = class {
|
|
|
528
438
|
* Download a directory bundle (e.g., .mlpackage) from HuggingFace.
|
|
529
439
|
* ATOMIC: downloads to temp dir, renames only on complete success.
|
|
530
440
|
*/
|
|
531
|
-
async downloadDirectory(url, destDir, knownFiles,
|
|
441
|
+
async downloadDirectory(url, destDir, knownFiles, _modelId) {
|
|
532
442
|
const match = url.match(/huggingface\.co\/([^/]+\/[^/]+)\/resolve\/main\/(.+)/);
|
|
533
443
|
if (!match) throw new Error(`Cannot parse HuggingFace URL: ${url}`);
|
|
534
444
|
const [, repo, dirPath] = match;
|
|
@@ -588,12 +498,11 @@ import * as fs3 from "fs";
|
|
|
588
498
|
import * as path3 from "path";
|
|
589
499
|
var execFileAsync = promisify(execFile);
|
|
590
500
|
var PythonEnvManager = class {
|
|
501
|
+
venvPath;
|
|
502
|
+
cachedProbe = null;
|
|
591
503
|
constructor(dataDir) {
|
|
592
|
-
this.dataDir = dataDir;
|
|
593
504
|
this.venvPath = path3.join(dataDir, ".venv");
|
|
594
505
|
}
|
|
595
|
-
venvPath;
|
|
596
|
-
cachedProbe = null;
|
|
597
506
|
async probe() {
|
|
598
507
|
if (this.cachedProbe) return this.cachedProbe;
|
|
599
508
|
for (const cmd of ["python3", "python"]) {
|
|
@@ -648,17 +557,18 @@ var PythonEnvManager = class {
|
|
|
648
557
|
|
|
649
558
|
// src/pipeline/pipeline-validator.ts
|
|
650
559
|
var PipelineValidator = class {
|
|
651
|
-
constructor(
|
|
652
|
-
this.
|
|
560
|
+
constructor(stepExists) {
|
|
561
|
+
this.stepExists = stepExists;
|
|
653
562
|
}
|
|
563
|
+
stepExists;
|
|
654
564
|
validate(config) {
|
|
655
565
|
const errors = [];
|
|
656
566
|
const warnings = [];
|
|
657
567
|
for (const node of config.video) {
|
|
658
|
-
this.validateNode(node, errors, warnings
|
|
568
|
+
this.validateNode(node, errors, warnings);
|
|
659
569
|
}
|
|
660
570
|
if (config.audio) {
|
|
661
|
-
this.validateNode(config.audio, errors, warnings
|
|
571
|
+
this.validateNode(config.audio, errors, warnings);
|
|
662
572
|
}
|
|
663
573
|
return {
|
|
664
574
|
valid: errors.length === 0,
|
|
@@ -666,24 +576,19 @@ var PipelineValidator = class {
|
|
|
666
576
|
warnings
|
|
667
577
|
};
|
|
668
578
|
}
|
|
669
|
-
validateNode(node, errors, warnings
|
|
670
|
-
if (!this.
|
|
579
|
+
validateNode(node, errors, warnings) {
|
|
580
|
+
if (!this.stepExists(node.addon)) {
|
|
671
581
|
errors.push({
|
|
672
582
|
step: node.step,
|
|
673
583
|
addon: node.addon,
|
|
674
|
-
message: `
|
|
584
|
+
message: `Step "${node.addon}" has no provider \u2014 the addon that provided it may have been removed`,
|
|
675
585
|
severity: "error"
|
|
676
586
|
});
|
|
677
587
|
return;
|
|
678
588
|
}
|
|
679
|
-
const registered = this.loader.getAddon(node.addon);
|
|
680
|
-
if (registered) {
|
|
681
|
-
const manifest = registered.declaration;
|
|
682
|
-
void manifest;
|
|
683
|
-
}
|
|
684
589
|
if (node.children) {
|
|
685
590
|
for (const child of node.children) {
|
|
686
|
-
this.validateNode(child, errors, warnings
|
|
591
|
+
this.validateNode(child, errors, warnings);
|
|
687
592
|
}
|
|
688
593
|
}
|
|
689
594
|
}
|
|
@@ -691,11 +596,12 @@ var PipelineValidator = class {
|
|
|
691
596
|
|
|
692
597
|
// src/pipeline/pipeline-runner.ts
|
|
693
598
|
import { randomUUID } from "crypto";
|
|
599
|
+
import { errMsg } from "@camstack/types";
|
|
694
600
|
var PipelineRunner = class {
|
|
695
|
-
constructor(
|
|
696
|
-
this.
|
|
697
|
-
this.addonConfigs = addonConfigs;
|
|
601
|
+
constructor(addonResolver) {
|
|
602
|
+
this.addonResolver = addonResolver;
|
|
698
603
|
}
|
|
604
|
+
addonResolver;
|
|
699
605
|
async run(frame, config) {
|
|
700
606
|
const startTime = performance.now();
|
|
701
607
|
const results = [];
|
|
@@ -713,74 +619,12 @@ var PipelineRunner = class {
|
|
|
713
619
|
frameTimestamp: frame.timestamp
|
|
714
620
|
};
|
|
715
621
|
}
|
|
716
|
-
/**
|
|
717
|
-
* Run only the audio classification node on an audio chunk.
|
|
718
|
-
* Used by the audio path in DetectionWiringService (separate from video pipeline).
|
|
719
|
-
*/
|
|
720
|
-
async runAudioNode(chunk, audioNode) {
|
|
721
|
-
const startTime = performance.now();
|
|
722
|
-
const results = [];
|
|
723
|
-
const timings = {};
|
|
724
|
-
const resultId = randomUUID();
|
|
725
|
-
const stepStart = performance.now();
|
|
726
|
-
try {
|
|
727
|
-
const globalConfig = this.addonConfigs.get(audioNode.addon) ?? {};
|
|
728
|
-
const engine = await this.engineManager.getOrCreateEngine(
|
|
729
|
-
audioNode.addon,
|
|
730
|
-
globalConfig,
|
|
731
|
-
audioNode.configOverride
|
|
732
|
-
);
|
|
733
|
-
if (!("classifyAudio" in engine) || typeof engine["classifyAudio"] !== "function") {
|
|
734
|
-
throw new Error(`Addon "${audioNode.addon}" has no classifyAudio method`);
|
|
735
|
-
}
|
|
736
|
-
const output = await engine.classifyAudio(chunk);
|
|
737
|
-
const stepMs = performance.now() - stepStart;
|
|
738
|
-
const stepResult = {
|
|
739
|
-
addon: audioNode.addon,
|
|
740
|
-
slot: "classifier",
|
|
741
|
-
output,
|
|
742
|
-
resultId,
|
|
743
|
-
inferenceMs: output.inferenceMs,
|
|
744
|
-
preprocessMs: 0,
|
|
745
|
-
totalMs: stepMs
|
|
746
|
-
};
|
|
747
|
-
results.push(stepResult);
|
|
748
|
-
timings[audioNode.step] = stepMs;
|
|
749
|
-
} catch (error) {
|
|
750
|
-
const stepMs = performance.now() - stepStart;
|
|
751
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
752
|
-
results.push({
|
|
753
|
-
addon: audioNode.addon,
|
|
754
|
-
slot: "classifier",
|
|
755
|
-
output: { classifications: [], inferenceMs: 0, modelId: "" },
|
|
756
|
-
resultId,
|
|
757
|
-
inferenceMs: 0,
|
|
758
|
-
preprocessMs: 0,
|
|
759
|
-
totalMs: stepMs,
|
|
760
|
-
error: {
|
|
761
|
-
code: "ADDON_ERROR",
|
|
762
|
-
message,
|
|
763
|
-
childrenSkipped: true
|
|
764
|
-
}
|
|
765
|
-
});
|
|
766
|
-
}
|
|
767
|
-
return {
|
|
768
|
-
results,
|
|
769
|
-
totalMs: performance.now() - startTime,
|
|
770
|
-
timings,
|
|
771
|
-
frameTimestamp: chunk.timestamp
|
|
772
|
-
};
|
|
773
|
-
}
|
|
774
622
|
async executeNode(node, frame, parentDetection, results, timings) {
|
|
775
623
|
const resultId = randomUUID();
|
|
776
624
|
const stepStart = performance.now();
|
|
777
625
|
try {
|
|
778
|
-
const
|
|
779
|
-
const
|
|
780
|
-
node.addon,
|
|
781
|
-
globalConfig,
|
|
782
|
-
node.configOverride
|
|
783
|
-
);
|
|
626
|
+
const effectiveConfig = { ...node.configOverride };
|
|
627
|
+
const caller = await this.addonResolver.resolve(node.addon, effectiveConfig);
|
|
784
628
|
let output;
|
|
785
629
|
if (parentDetection) {
|
|
786
630
|
const cropInput = {
|
|
@@ -788,28 +632,9 @@ var PipelineRunner = class {
|
|
|
788
632
|
roi: parentDetection.det.bbox,
|
|
789
633
|
parentDetection: parentDetection.det
|
|
790
634
|
};
|
|
791
|
-
|
|
792
|
-
output = await engine.crop(cropInput);
|
|
793
|
-
} else if ("classify" in engine && typeof engine["classify"] === "function") {
|
|
794
|
-
output = await engine.classify(cropInput);
|
|
795
|
-
} else if ("detect" in engine && typeof engine["detect"] === "function") {
|
|
796
|
-
output = await engine.detect(frame);
|
|
797
|
-
} else {
|
|
798
|
-
throw new Error(`Addon "${node.addon}" has no detect/crop/classify method`);
|
|
799
|
-
}
|
|
635
|
+
output = await caller.process(cropInput);
|
|
800
636
|
} else {
|
|
801
|
-
|
|
802
|
-
output = await engine.detect(frame);
|
|
803
|
-
} else if ("classify" in engine && typeof engine["classify"] === "function") {
|
|
804
|
-
const rootCropInput = {
|
|
805
|
-
frame,
|
|
806
|
-
roi: { x: 0, y: 0, w: frame.width, h: frame.height },
|
|
807
|
-
parentDetection: { class: "", originalClass: "", score: 1, bbox: { x: 0, y: 0, w: frame.width, h: frame.height } }
|
|
808
|
-
};
|
|
809
|
-
output = await engine.classify(rootCropInput);
|
|
810
|
-
} else {
|
|
811
|
-
throw new Error(`Addon "${node.addon}" has no detect/classify method`);
|
|
812
|
-
}
|
|
637
|
+
output = await caller.process(frame);
|
|
813
638
|
}
|
|
814
639
|
const stepMs = performance.now() - stepStart;
|
|
815
640
|
const slot = "detections" in output ? "detector" : "crops" in output ? "cropper" : "classifier";
|
|
@@ -831,7 +656,7 @@ var PipelineRunner = class {
|
|
|
831
656
|
}
|
|
832
657
|
} catch (error) {
|
|
833
658
|
const stepMs = performance.now() - stepStart;
|
|
834
|
-
const message =
|
|
659
|
+
const message = errMsg(error);
|
|
835
660
|
results.push({
|
|
836
661
|
addon: node.addon,
|
|
837
662
|
slot: "detector",
|
|
@@ -852,6 +677,7 @@ var PipelineRunner = class {
|
|
|
852
677
|
async executeChildren(children, frame, parentDetections, parentResultId, results, timings) {
|
|
853
678
|
const promises2 = [];
|
|
854
679
|
for (const detection of parentDetections) {
|
|
680
|
+
if (detection.bbox.w < 1 || detection.bbox.h < 1) continue;
|
|
855
681
|
for (const child of children) {
|
|
856
682
|
promises2.push(
|
|
857
683
|
this.executeNode(child, frame, { det: detection, resultId: parentResultId }, results, timings)
|
|
@@ -862,207 +688,25 @@ var PipelineRunner = class {
|
|
|
862
688
|
}
|
|
863
689
|
};
|
|
864
690
|
|
|
865
|
-
// src/
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
var ManagedProcess = class {
|
|
871
|
-
constructor(config, events, logger) {
|
|
872
|
-
this.config = config;
|
|
873
|
-
this.events = events;
|
|
874
|
-
this.logger = logger;
|
|
875
|
-
}
|
|
876
|
-
childProcess = null;
|
|
877
|
-
_state = "stopped";
|
|
878
|
-
_startedAt;
|
|
879
|
-
restartTimer;
|
|
880
|
-
_restartCount = 0;
|
|
881
|
-
_lastCrashAt;
|
|
882
|
-
_lastCrashError;
|
|
883
|
-
get state() {
|
|
884
|
-
return this._state;
|
|
885
|
-
}
|
|
886
|
-
async start() {
|
|
887
|
-
this._state = "starting";
|
|
888
|
-
try {
|
|
889
|
-
if (this.config.modulePath) {
|
|
890
|
-
this.childProcess = fork(this.config.modulePath, this.config.args ?? [], {
|
|
891
|
-
env: { ...process.env, ...this.config.env },
|
|
892
|
-
stdio: ["pipe", "pipe", "pipe", "ipc"]
|
|
893
|
-
});
|
|
894
|
-
} else if (this.config.command) {
|
|
895
|
-
this.childProcess = spawn2(this.config.command, this.config.args ?? [], {
|
|
896
|
-
env: { ...process.env, ...this.config.env },
|
|
897
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
898
|
-
});
|
|
899
|
-
} else {
|
|
900
|
-
throw new Error("No command or modulePath specified");
|
|
901
|
-
}
|
|
902
|
-
this.childProcess.stdout?.on("data", (data) => {
|
|
903
|
-
this.logger.debug(data.toString().trim());
|
|
904
|
-
});
|
|
905
|
-
this.childProcess.stderr?.on("data", (data) => {
|
|
906
|
-
this.logger.warn(data.toString().trim());
|
|
907
|
-
});
|
|
908
|
-
this.childProcess.on("exit", (code, signal) => {
|
|
909
|
-
const msg = `Process exited: code=${code}, signal=${signal}`;
|
|
910
|
-
if (code === 0) {
|
|
911
|
-
this.logger.info(msg);
|
|
912
|
-
this._state = "stopped";
|
|
913
|
-
} else {
|
|
914
|
-
this._lastCrashAt = Date.now();
|
|
915
|
-
this._lastCrashError = msg;
|
|
916
|
-
this.logger.error(msg);
|
|
917
|
-
this._state = "error";
|
|
918
|
-
this.events.emitProcessCrashed(
|
|
919
|
-
this.config.id,
|
|
920
|
-
code,
|
|
921
|
-
signal,
|
|
922
|
-
this._restartCount
|
|
923
|
-
);
|
|
924
|
-
const maxRestarts = this.config.maxRestarts ?? DEFAULT_MAX_RESTARTS;
|
|
925
|
-
if (this.config.autoRestart && this._restartCount < maxRestarts) {
|
|
926
|
-
this.scheduleRestart();
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
this.childProcess = null;
|
|
930
|
-
});
|
|
931
|
-
this.childProcess.on("error", (err) => {
|
|
932
|
-
this.logger.error(`Process error: ${err.message}`);
|
|
933
|
-
this._lastCrashError = err.message;
|
|
934
|
-
this._state = "error";
|
|
935
|
-
});
|
|
936
|
-
this._state = "running";
|
|
937
|
-
this._startedAt = Date.now();
|
|
938
|
-
} catch (err) {
|
|
939
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
940
|
-
this._state = "error";
|
|
941
|
-
this._lastCrashError = msg;
|
|
942
|
-
throw err;
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
async stop() {
|
|
946
|
-
if (this.restartTimer) {
|
|
947
|
-
clearTimeout(this.restartTimer);
|
|
948
|
-
this.restartTimer = void 0;
|
|
949
|
-
}
|
|
950
|
-
if (this._state === "stopped") {
|
|
951
|
-
return;
|
|
952
|
-
}
|
|
953
|
-
this._state = "stopping";
|
|
954
|
-
if (this.childProcess && !this.childProcess.killed) {
|
|
955
|
-
this.childProcess.kill("SIGTERM");
|
|
956
|
-
await new Promise((resolve) => {
|
|
957
|
-
const timeout = setTimeout(() => {
|
|
958
|
-
if (this.childProcess && !this.childProcess.killed) {
|
|
959
|
-
this.childProcess.kill("SIGKILL");
|
|
960
|
-
}
|
|
961
|
-
resolve();
|
|
962
|
-
}, GRACEFUL_SHUTDOWN_MS);
|
|
963
|
-
this.childProcess?.on("exit", () => {
|
|
964
|
-
clearTimeout(timeout);
|
|
965
|
-
resolve();
|
|
966
|
-
});
|
|
967
|
-
});
|
|
968
|
-
}
|
|
969
|
-
const currentState = this._state;
|
|
970
|
-
if (currentState !== "stopped") {
|
|
971
|
-
this._state = "stopped";
|
|
972
|
-
}
|
|
691
|
+
// src/pipeline/engine-manager-resolver.ts
|
|
692
|
+
var EngineManagerResolver = class {
|
|
693
|
+
constructor(engineManager, addonConfigs) {
|
|
694
|
+
this.engineManager = engineManager;
|
|
695
|
+
this.addonConfigs = addonConfigs;
|
|
973
696
|
}
|
|
974
|
-
|
|
697
|
+
engineManager;
|
|
698
|
+
addonConfigs;
|
|
699
|
+
async resolve(addonId, config) {
|
|
700
|
+
const globalConfig = this.addonConfigs.get(addonId) ?? {};
|
|
701
|
+
const engine = await this.engineManager.getOrCreateEngine(addonId, globalConfig, config);
|
|
975
702
|
return {
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
state: this._state,
|
|
979
|
-
pid: this.childProcess?.pid,
|
|
980
|
-
stats: this.childProcess?.pid ? this.getStats() : void 0,
|
|
981
|
-
lastCrashAt: this._lastCrashAt,
|
|
982
|
-
lastCrashError: this._lastCrashError,
|
|
983
|
-
restartCount: this._restartCount,
|
|
984
|
-
nextRestartAt: this.restartTimer ? this.getNextRestartTime() : void 0
|
|
985
|
-
};
|
|
986
|
-
}
|
|
987
|
-
scheduleRestart() {
|
|
988
|
-
this._restartCount++;
|
|
989
|
-
const delayMs = Math.min(1e3 * Math.pow(2, this._restartCount - 1), MAX_BACKOFF_MS);
|
|
990
|
-
this.logger.info(`Scheduling restart #${this._restartCount} in ${delayMs}ms`);
|
|
991
|
-
this.events.emitProcessRestartScheduled(
|
|
992
|
-
this.config.id,
|
|
993
|
-
this._restartCount,
|
|
994
|
-
delayMs
|
|
995
|
-
);
|
|
996
|
-
this.restartTimer = setTimeout(async () => {
|
|
997
|
-
this.restartTimer = void 0;
|
|
998
|
-
try {
|
|
999
|
-
await this.start();
|
|
1000
|
-
this.events.emitProcessRestarted(this.config.id, this._restartCount);
|
|
1001
|
-
} catch (err) {
|
|
1002
|
-
this.logger.error(`Restart failed: ${err}`);
|
|
703
|
+
process: async (input) => {
|
|
704
|
+
return engine.process(input);
|
|
1003
705
|
}
|
|
1004
|
-
}, delayMs);
|
|
1005
|
-
}
|
|
1006
|
-
getStats() {
|
|
1007
|
-
if (!this.childProcess?.pid) return void 0;
|
|
1008
|
-
const uptime = this._startedAt ? Date.now() - this._startedAt : 0;
|
|
1009
|
-
return {
|
|
1010
|
-
pid: this.childProcess.pid,
|
|
1011
|
-
cpu: 0,
|
|
1012
|
-
memory: 0,
|
|
1013
|
-
uptime,
|
|
1014
|
-
restartCount: this._restartCount
|
|
1015
706
|
};
|
|
1016
707
|
}
|
|
1017
|
-
getNextRestartTime() {
|
|
1018
|
-
if (!this._lastCrashAt) return void 0;
|
|
1019
|
-
const delayMs = Math.min(1e3 * Math.pow(2, this._restartCount - 1), MAX_BACKOFF_MS);
|
|
1020
|
-
return this._lastCrashAt + delayMs;
|
|
1021
|
-
}
|
|
1022
|
-
};
|
|
1023
|
-
|
|
1024
|
-
// src/process/process-manager.ts
|
|
1025
|
-
var ProcessManager = class {
|
|
1026
|
-
constructor(events, loggerFactory) {
|
|
1027
|
-
this.events = events;
|
|
1028
|
-
this.loggerFactory = loggerFactory;
|
|
1029
|
-
}
|
|
1030
|
-
processes = /* @__PURE__ */ new Map();
|
|
1031
|
-
register(config) {
|
|
1032
|
-
if (this.processes.has(config.id)) {
|
|
1033
|
-
throw new Error(`Process already registered: ${config.id}`);
|
|
1034
|
-
}
|
|
1035
|
-
const logger = this.loggerFactory.createLogger(`process:${config.id}`);
|
|
1036
|
-
const managed = new ManagedProcess(config, this.events, logger);
|
|
1037
|
-
this.processes.set(config.id, managed);
|
|
1038
|
-
return managed;
|
|
1039
|
-
}
|
|
1040
|
-
async start(id) {
|
|
1041
|
-
const p = this.get(id);
|
|
1042
|
-
await p.start();
|
|
1043
|
-
}
|
|
1044
|
-
async stop(id) {
|
|
1045
|
-
const p = this.get(id);
|
|
1046
|
-
await p.stop();
|
|
1047
|
-
}
|
|
1048
|
-
async restart(id) {
|
|
1049
|
-
const p = this.get(id);
|
|
1050
|
-
await p.stop();
|
|
1051
|
-
await p.start();
|
|
1052
|
-
}
|
|
1053
|
-
get(id) {
|
|
1054
|
-
const p = this.processes.get(id);
|
|
1055
|
-
if (!p) throw new Error(`Process not found: ${id}`);
|
|
1056
|
-
return p;
|
|
1057
|
-
}
|
|
1058
|
-
listAll() {
|
|
1059
|
-
return [...this.processes.values()].map((p) => p.getStatus());
|
|
1060
|
-
}
|
|
1061
708
|
async shutdownAll() {
|
|
1062
|
-
|
|
1063
|
-
(p) => p.getStatus().state === "running"
|
|
1064
|
-
);
|
|
1065
|
-
await Promise.all(running.map((p) => p.stop()));
|
|
709
|
+
await this.engineManager.shutdownAll();
|
|
1066
710
|
}
|
|
1067
711
|
};
|
|
1068
712
|
|
|
@@ -1134,14 +778,15 @@ var ReplEngine = class {
|
|
|
1134
778
|
constructor(contextProvider) {
|
|
1135
779
|
this.contextProvider = contextProvider;
|
|
1136
780
|
}
|
|
781
|
+
contextProvider;
|
|
1137
782
|
async execute(code, context) {
|
|
1138
783
|
const start = Date.now();
|
|
1139
|
-
const sandbox = this.buildSandbox(context);
|
|
784
|
+
const sandbox = await this.buildSandbox(context);
|
|
1140
785
|
try {
|
|
1141
786
|
const vmContext = vm.createContext(sandbox);
|
|
1142
787
|
const script = new vm.Script(code);
|
|
1143
788
|
let result = script.runInContext(vmContext, { timeout: EXECUTION_TIMEOUT_MS });
|
|
1144
|
-
if (result && typeof result.then === "function") {
|
|
789
|
+
if (result && typeof result === "object" && "then" in result && typeof result.then === "function") {
|
|
1145
790
|
result = await Promise.race([
|
|
1146
791
|
result,
|
|
1147
792
|
new Promise(
|
|
@@ -1173,7 +818,7 @@ var ReplEngine = class {
|
|
|
1173
818
|
}
|
|
1174
819
|
}
|
|
1175
820
|
async getCompletions(partial, context) {
|
|
1176
|
-
const sandbox = this.buildSandbox(context);
|
|
821
|
+
const sandbox = await this.buildSandbox(context);
|
|
1177
822
|
const keys = Object.keys(sandbox);
|
|
1178
823
|
if (!partial) return keys;
|
|
1179
824
|
const lastDot = partial.lastIndexOf(".");
|
|
@@ -1194,7 +839,7 @@ var ReplEngine = class {
|
|
|
1194
839
|
}
|
|
1195
840
|
return [];
|
|
1196
841
|
}
|
|
1197
|
-
buildSandbox(context) {
|
|
842
|
+
async buildSandbox(context) {
|
|
1198
843
|
const base = {
|
|
1199
844
|
console: {
|
|
1200
845
|
log: (...args) => util.inspect(args),
|
|
@@ -1223,583 +868,40 @@ var ReplEngine = class {
|
|
|
1223
868
|
};
|
|
1224
869
|
switch (context.scope.type) {
|
|
1225
870
|
case "system":
|
|
1226
|
-
return { ...base, ...this.contextProvider.getSystemSandbox() };
|
|
871
|
+
return { ...base, ...await this.contextProvider.getSystemSandbox() };
|
|
1227
872
|
case "device":
|
|
1228
|
-
return { ...base, ...this.contextProvider.getDeviceSandbox(context.scope.deviceId) };
|
|
873
|
+
return { ...base, ...await this.contextProvider.getDeviceSandbox(context.scope.deviceId) };
|
|
1229
874
|
case "provider":
|
|
1230
|
-
return { ...base, ...this.contextProvider.getProviderSandbox(context.scope.providerId) };
|
|
875
|
+
return { ...base, ...await this.contextProvider.getProviderSandbox(context.scope.providerId) };
|
|
1231
876
|
case "addon":
|
|
1232
|
-
return { ...base, ...this.contextProvider.getAddonSandbox(context.scope.addonId) };
|
|
877
|
+
return { ...base, ...await this.contextProvider.getAddonSandbox(context.scope.addonId) };
|
|
1233
878
|
default:
|
|
1234
879
|
return base;
|
|
1235
880
|
}
|
|
1236
881
|
}
|
|
1237
882
|
};
|
|
1238
883
|
|
|
1239
|
-
// src/
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
/** Register a new agent */
|
|
1249
|
-
registerAgent(info) {
|
|
1250
|
-
const entry = {
|
|
1251
|
-
info,
|
|
1252
|
-
state: "online",
|
|
1253
|
-
connectedSince: Date.now(),
|
|
1254
|
-
lastHeartbeat: Date.now(),
|
|
1255
|
-
activeTaskCount: 0,
|
|
1256
|
-
completedTaskCount: 0,
|
|
1257
|
-
failedTaskCount: 0
|
|
1258
|
-
};
|
|
1259
|
-
this.agents.set(info.id, entry);
|
|
1260
|
-
this.startHeartbeatCheck(info.id);
|
|
1261
|
-
this.events.emitAgentRegistered(info.id, [...info.capabilities]);
|
|
1262
|
-
}
|
|
1263
|
-
/** Remove agent */
|
|
1264
|
-
unregisterAgent(id) {
|
|
1265
|
-
const interval = this.heartbeatIntervals.get(id);
|
|
1266
|
-
if (interval) {
|
|
1267
|
-
clearInterval(interval);
|
|
1268
|
-
this.heartbeatIntervals.delete(id);
|
|
1269
|
-
}
|
|
1270
|
-
this.agents.delete(id);
|
|
1271
|
-
this.events.emitAgentUnregistered(id);
|
|
1272
|
-
}
|
|
1273
|
-
/** Update heartbeat timestamp */
|
|
1274
|
-
heartbeat(id) {
|
|
1275
|
-
const entry = this.agents.get(id);
|
|
1276
|
-
if (!entry) return;
|
|
1277
|
-
entry.lastHeartbeat = Date.now();
|
|
1278
|
-
if (entry.state === "offline") {
|
|
1279
|
-
entry.state = "online";
|
|
1280
|
-
this.events.emitAgentOnline(id);
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
/** Find agents with a specific capability */
|
|
1284
|
-
getAgentsWithCapability(capability) {
|
|
1285
|
-
return [...this.agents.values()].filter(
|
|
1286
|
-
(a) => a.state === "online" && a.info.capabilities.includes(capability)
|
|
1287
|
-
);
|
|
1288
|
-
}
|
|
1289
|
-
/** Get best agent for a task (least loaded) */
|
|
1290
|
-
selectAgent(capability, preferredId) {
|
|
1291
|
-
const capable = this.getAgentsWithCapability(capability);
|
|
1292
|
-
if (capable.length === 0) return null;
|
|
1293
|
-
if (preferredId) {
|
|
1294
|
-
const preferred = capable.find((a) => a.info.id === preferredId);
|
|
1295
|
-
if (preferred) return preferred;
|
|
1296
|
-
}
|
|
1297
|
-
const sorted = [...capable].sort((a, b) => a.activeTaskCount - b.activeTaskCount);
|
|
1298
|
-
return sorted[0] ?? null;
|
|
1299
|
-
}
|
|
1300
|
-
/** List all agents with status */
|
|
1301
|
-
listAgents() {
|
|
1302
|
-
return [...this.agents.values()].map((entry) => ({
|
|
1303
|
-
id: entry.info.id,
|
|
1304
|
-
name: entry.info.name,
|
|
1305
|
-
state: entry.state,
|
|
1306
|
-
capabilities: [...entry.info.capabilities],
|
|
1307
|
-
lastHeartbeat: entry.lastHeartbeat,
|
|
1308
|
-
connectedSince: entry.connectedSince,
|
|
1309
|
-
resources: entry.info.resources,
|
|
1310
|
-
activeTaskCount: entry.activeTaskCount,
|
|
1311
|
-
completedTaskCount: entry.completedTaskCount,
|
|
1312
|
-
failedTaskCount: entry.failedTaskCount
|
|
1313
|
-
}));
|
|
1314
|
-
}
|
|
1315
|
-
/** Get single agent */
|
|
1316
|
-
getAgent(id) {
|
|
1317
|
-
return this.agents.get(id) ?? null;
|
|
1318
|
-
}
|
|
1319
|
-
/** Destroy all heartbeat intervals */
|
|
1320
|
-
destroy() {
|
|
1321
|
-
for (const interval of this.heartbeatIntervals.values()) {
|
|
1322
|
-
clearInterval(interval);
|
|
1323
|
-
}
|
|
1324
|
-
this.heartbeatIntervals.clear();
|
|
1325
|
-
}
|
|
1326
|
-
startHeartbeatCheck(id) {
|
|
1327
|
-
const interval = setInterval(() => {
|
|
1328
|
-
const entry = this.agents.get(id);
|
|
1329
|
-
if (!entry) return;
|
|
1330
|
-
const elapsed = Date.now() - entry.lastHeartbeat;
|
|
1331
|
-
if (elapsed > this.heartbeatTimeoutMs && entry.state === "online") {
|
|
1332
|
-
entry.state = "offline";
|
|
1333
|
-
this.events.emitAgentOffline(id);
|
|
1334
|
-
}
|
|
1335
|
-
}, this.heartbeatCheckIntervalMs);
|
|
1336
|
-
this.heartbeatIntervals.set(id, interval);
|
|
1337
|
-
}
|
|
884
|
+
// src/lifecycle/lifecycle-state-machine.ts
|
|
885
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
886
|
+
var VALID_TRANSITIONS = {
|
|
887
|
+
stopped: ["starting", "disabled"],
|
|
888
|
+
starting: ["running", "error", "stopping"],
|
|
889
|
+
running: ["stopping", "error"],
|
|
890
|
+
stopping: ["stopped", "error"],
|
|
891
|
+
error: ["starting", "stopped", "disabled"],
|
|
892
|
+
disabled: ["stopped"]
|
|
1338
893
|
};
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
this.
|
|
1344
|
-
this.
|
|
1345
|
-
}
|
|
1346
|
-
pendingTasks = /* @__PURE__ */ new Map();
|
|
1347
|
-
localExecutors = /* @__PURE__ */ new Map();
|
|
1348
|
-
/** Dispatch a task to the best available agent */
|
|
1349
|
-
async dispatch(task, options) {
|
|
1350
|
-
const capability = options?.capability ?? task.capability;
|
|
1351
|
-
const agent = this.agentRegistry.selectAgent(capability, options?.preferredAgent);
|
|
1352
|
-
if (!agent && !options?.remoteOnly) {
|
|
1353
|
-
return this.executeLocally(task);
|
|
1354
|
-
}
|
|
1355
|
-
if (!agent) {
|
|
1356
|
-
return {
|
|
1357
|
-
taskId: task.id,
|
|
1358
|
-
agentId: "none",
|
|
1359
|
-
status: "error",
|
|
1360
|
-
error: "No agent available",
|
|
1361
|
-
durationMs: 0
|
|
1362
|
-
};
|
|
1363
|
-
}
|
|
1364
|
-
return this.sendToAgent(task, agent);
|
|
1365
|
-
}
|
|
1366
|
-
/** Register a local executor for a capability (in-process fallback) */
|
|
1367
|
-
registerLocalExecutor(capability, executor) {
|
|
1368
|
-
this.localExecutors.set(capability, executor);
|
|
1369
|
-
}
|
|
1370
|
-
/** Called when an agent returns a result */
|
|
1371
|
-
handleTaskResult(result) {
|
|
1372
|
-
const pending = this.pendingTasks.get(result.taskId);
|
|
1373
|
-
if (pending) {
|
|
1374
|
-
clearTimeout(pending.timeout);
|
|
1375
|
-
this.pendingTasks.delete(result.taskId);
|
|
1376
|
-
pending.resolve(result);
|
|
1377
|
-
}
|
|
1378
|
-
const agent = this.agentRegistry.getAgent(result.agentId);
|
|
1379
|
-
if (agent) {
|
|
1380
|
-
agent.activeTaskCount--;
|
|
1381
|
-
if (result.status === "success") {
|
|
1382
|
-
agent.completedTaskCount++;
|
|
1383
|
-
} else {
|
|
1384
|
-
agent.failedTaskCount++;
|
|
1385
|
-
}
|
|
1386
|
-
}
|
|
1387
|
-
}
|
|
1388
|
-
async executeLocally(task) {
|
|
1389
|
-
const executor = this.localExecutors.get(task.capability);
|
|
1390
|
-
if (!executor) {
|
|
1391
|
-
return {
|
|
1392
|
-
taskId: task.id,
|
|
1393
|
-
agentId: "local",
|
|
1394
|
-
status: "error",
|
|
1395
|
-
error: `No local executor for ${task.capability}`,
|
|
1396
|
-
durationMs: 0
|
|
1397
|
-
};
|
|
1398
|
-
}
|
|
1399
|
-
const start = Date.now();
|
|
1400
|
-
try {
|
|
1401
|
-
const output = await executor(task.input);
|
|
1402
|
-
return {
|
|
1403
|
-
taskId: task.id,
|
|
1404
|
-
agentId: "local",
|
|
1405
|
-
status: "success",
|
|
1406
|
-
output,
|
|
1407
|
-
durationMs: Date.now() - start
|
|
1408
|
-
};
|
|
1409
|
-
} catch (err) {
|
|
1410
|
-
return {
|
|
1411
|
-
taskId: task.id,
|
|
1412
|
-
agentId: "local",
|
|
1413
|
-
status: "error",
|
|
1414
|
-
error: String(err),
|
|
1415
|
-
durationMs: Date.now() - start
|
|
1416
|
-
};
|
|
1417
|
-
}
|
|
894
|
+
var LifecycleStateMachine = class {
|
|
895
|
+
constructor(elementId, elementType, eventBus, logger) {
|
|
896
|
+
this.elementId = elementId;
|
|
897
|
+
this.elementType = elementType;
|
|
898
|
+
this.eventBus = eventBus;
|
|
899
|
+
this.logger = logger;
|
|
1418
900
|
}
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
const timeout = setTimeout(() => {
|
|
1423
|
-
this.pendingTasks.delete(task.id);
|
|
1424
|
-
agent.activeTaskCount--;
|
|
1425
|
-
agent.failedTaskCount++;
|
|
1426
|
-
resolve({
|
|
1427
|
-
taskId: task.id,
|
|
1428
|
-
agentId: agent.info.id,
|
|
1429
|
-
status: "timeout",
|
|
1430
|
-
durationMs: task.timeout,
|
|
1431
|
-
error: "Task timed out"
|
|
1432
|
-
});
|
|
1433
|
-
}, task.timeout);
|
|
1434
|
-
this.pendingTasks.set(task.id, { resolve, reject: () => {
|
|
1435
|
-
}, timeout });
|
|
1436
|
-
this.events.emitTaskDispatched(task.id, agent.info.id, task.capability);
|
|
1437
|
-
});
|
|
1438
|
-
}
|
|
1439
|
-
};
|
|
1440
|
-
|
|
1441
|
-
// src/agent/agent-client.ts
|
|
1442
|
-
var RECONNECT_BASE_MS = 1e3;
|
|
1443
|
-
var RECONNECT_MAX_MS = 3e4;
|
|
1444
|
-
var HEARTBEAT_INTERVAL_MS = 1e4;
|
|
1445
|
-
var AgentClient = class {
|
|
1446
|
-
ws = null;
|
|
1447
|
-
reconnectAttempt = 0;
|
|
1448
|
-
reconnectTimer = null;
|
|
1449
|
-
heartbeatTimer = null;
|
|
1450
|
-
destroyed = false;
|
|
1451
|
-
messageHandlers = [];
|
|
1452
|
-
binaryHandlers = [];
|
|
1453
|
-
connectHandlers = [];
|
|
1454
|
-
disconnectHandlers = [];
|
|
901
|
+
elementId;
|
|
902
|
+
elementType;
|
|
903
|
+
eventBus;
|
|
1455
904
|
logger;
|
|
1456
|
-
hubUrl;
|
|
1457
|
-
token;
|
|
1458
|
-
registrationInfo;
|
|
1459
|
-
runtimeStatus = {
|
|
1460
|
-
activeCameras: 0,
|
|
1461
|
-
cpuPercent: 0,
|
|
1462
|
-
memoryPercent: 0,
|
|
1463
|
-
fps: {},
|
|
1464
|
-
errors: []
|
|
1465
|
-
};
|
|
1466
|
-
constructor(config) {
|
|
1467
|
-
this.hubUrl = config.hubUrl;
|
|
1468
|
-
this.token = config.token;
|
|
1469
|
-
this.logger = config.logger;
|
|
1470
|
-
this.registrationInfo = config.registrationInfo;
|
|
1471
|
-
}
|
|
1472
|
-
/** Connect to the hub WebSocket */
|
|
1473
|
-
async connect() {
|
|
1474
|
-
return new Promise((resolve, reject) => {
|
|
1475
|
-
this.destroyed = false;
|
|
1476
|
-
this.doConnect(resolve, reject);
|
|
1477
|
-
});
|
|
1478
|
-
}
|
|
1479
|
-
/** Disconnect and stop reconnecting */
|
|
1480
|
-
disconnect() {
|
|
1481
|
-
this.destroyed = true;
|
|
1482
|
-
this.clearTimers();
|
|
1483
|
-
if (this.ws) {
|
|
1484
|
-
this.ws.close();
|
|
1485
|
-
this.ws = null;
|
|
1486
|
-
}
|
|
1487
|
-
}
|
|
1488
|
-
/** Send a JSON control message to the hub */
|
|
1489
|
-
send(msg) {
|
|
1490
|
-
if (!this.ws || this.ws.readyState !== 1) {
|
|
1491
|
-
this.logger.warn("send() called while not connected");
|
|
1492
|
-
return;
|
|
1493
|
-
}
|
|
1494
|
-
this.ws.send(JSON.stringify(msg));
|
|
1495
|
-
}
|
|
1496
|
-
/** Send a binary frame to the hub */
|
|
1497
|
-
sendBinary(data) {
|
|
1498
|
-
if (!this.ws || this.ws.readyState !== 1) {
|
|
1499
|
-
return;
|
|
1500
|
-
}
|
|
1501
|
-
this.ws.send(data);
|
|
1502
|
-
}
|
|
1503
|
-
/** Register a handler for JSON messages from hub */
|
|
1504
|
-
onMessage(handler) {
|
|
1505
|
-
this.messageHandlers.push(handler);
|
|
1506
|
-
}
|
|
1507
|
-
/** Register a handler for binary frames from hub */
|
|
1508
|
-
onBinaryFrame(handler) {
|
|
1509
|
-
this.binaryHandlers.push(handler);
|
|
1510
|
-
}
|
|
1511
|
-
/** Register a handler for successful connection */
|
|
1512
|
-
onConnect(handler) {
|
|
1513
|
-
this.connectHandlers.push(handler);
|
|
1514
|
-
}
|
|
1515
|
-
/** Register a handler for disconnection */
|
|
1516
|
-
onDisconnect(handler) {
|
|
1517
|
-
this.disconnectHandlers.push(handler);
|
|
1518
|
-
}
|
|
1519
|
-
/** Update the runtime status (used in heartbeat) */
|
|
1520
|
-
updateStatus(status) {
|
|
1521
|
-
this.runtimeStatus = status;
|
|
1522
|
-
}
|
|
1523
|
-
/** Whether currently connected */
|
|
1524
|
-
get connected() {
|
|
1525
|
-
return this.ws?.readyState === 1;
|
|
1526
|
-
}
|
|
1527
|
-
doConnect(onConnect, onError) {
|
|
1528
|
-
if (this.destroyed) return;
|
|
1529
|
-
import("ws").then(({ default: WebSocket }) => {
|
|
1530
|
-
if (this.destroyed) return;
|
|
1531
|
-
const url = this.token ? `${this.hubUrl}?token=${encodeURIComponent(this.token)}` : this.hubUrl;
|
|
1532
|
-
const wsOptions = this.hubUrl.startsWith("wss://") ? { rejectUnauthorized: false } : {};
|
|
1533
|
-
try {
|
|
1534
|
-
this.ws = new WebSocket(url, wsOptions);
|
|
1535
|
-
} catch (err) {
|
|
1536
|
-
this.scheduleReconnect();
|
|
1537
|
-
if (onError) {
|
|
1538
|
-
onError(err instanceof Error ? err : new Error(String(err)));
|
|
1539
|
-
onError = void 0;
|
|
1540
|
-
}
|
|
1541
|
-
return;
|
|
1542
|
-
}
|
|
1543
|
-
this.ws.once("open", () => {
|
|
1544
|
-
this.reconnectAttempt = 0;
|
|
1545
|
-
this.logger.info(`Connected to hub: ${this.hubUrl}`);
|
|
1546
|
-
this.send({ type: "register", info: this.registrationInfo });
|
|
1547
|
-
this.startHeartbeat();
|
|
1548
|
-
for (const h of this.connectHandlers) h();
|
|
1549
|
-
if (onConnect) {
|
|
1550
|
-
onConnect();
|
|
1551
|
-
onConnect = void 0;
|
|
1552
|
-
}
|
|
1553
|
-
});
|
|
1554
|
-
this.ws.on("message", (data, isBinary) => {
|
|
1555
|
-
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
1556
|
-
if (isBinary) {
|
|
1557
|
-
for (const h of this.binaryHandlers) h(buf);
|
|
1558
|
-
} else {
|
|
1559
|
-
try {
|
|
1560
|
-
const msg = JSON.parse(buf.toString());
|
|
1561
|
-
this.handleBuiltinMessage(msg);
|
|
1562
|
-
for (const h of this.messageHandlers) h(msg);
|
|
1563
|
-
} catch {
|
|
1564
|
-
this.logger.warn("Failed to parse message from hub");
|
|
1565
|
-
}
|
|
1566
|
-
}
|
|
1567
|
-
});
|
|
1568
|
-
this.ws.once("close", () => {
|
|
1569
|
-
this.stopHeartbeat();
|
|
1570
|
-
for (const h of this.disconnectHandlers) h();
|
|
1571
|
-
if (!this.destroyed) {
|
|
1572
|
-
this.logger.warn("Disconnected from hub, scheduling reconnect");
|
|
1573
|
-
this.scheduleReconnect();
|
|
1574
|
-
}
|
|
1575
|
-
});
|
|
1576
|
-
this.ws.once("error", (err) => {
|
|
1577
|
-
this.logger.error("WebSocket error", { message: err.message });
|
|
1578
|
-
if (onError) {
|
|
1579
|
-
onError(err);
|
|
1580
|
-
onError = void 0;
|
|
1581
|
-
}
|
|
1582
|
-
});
|
|
1583
|
-
}).catch((err) => {
|
|
1584
|
-
this.logger.error("Failed to import ws module", { message: String(err) });
|
|
1585
|
-
if (onError) {
|
|
1586
|
-
onError(err instanceof Error ? err : new Error(String(err)));
|
|
1587
|
-
}
|
|
1588
|
-
});
|
|
1589
|
-
}
|
|
1590
|
-
handleBuiltinMessage(msg) {
|
|
1591
|
-
if (msg.type === "ping") {
|
|
1592
|
-
this.send({ type: "pong" });
|
|
1593
|
-
}
|
|
1594
|
-
}
|
|
1595
|
-
scheduleReconnect() {
|
|
1596
|
-
if (this.destroyed) return;
|
|
1597
|
-
const delay = Math.min(
|
|
1598
|
-
RECONNECT_BASE_MS * Math.pow(2, this.reconnectAttempt),
|
|
1599
|
-
RECONNECT_MAX_MS
|
|
1600
|
-
);
|
|
1601
|
-
this.reconnectAttempt++;
|
|
1602
|
-
this.logger.info(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempt})`);
|
|
1603
|
-
this.reconnectTimer = setTimeout(() => {
|
|
1604
|
-
this.doConnect();
|
|
1605
|
-
}, delay);
|
|
1606
|
-
}
|
|
1607
|
-
startHeartbeat() {
|
|
1608
|
-
this.stopHeartbeat();
|
|
1609
|
-
this.heartbeatTimer = setInterval(() => {
|
|
1610
|
-
this.send({ type: "heartbeat", status: this.runtimeStatus });
|
|
1611
|
-
}, HEARTBEAT_INTERVAL_MS);
|
|
1612
|
-
}
|
|
1613
|
-
stopHeartbeat() {
|
|
1614
|
-
if (this.heartbeatTimer) {
|
|
1615
|
-
clearInterval(this.heartbeatTimer);
|
|
1616
|
-
this.heartbeatTimer = null;
|
|
1617
|
-
}
|
|
1618
|
-
}
|
|
1619
|
-
clearTimers() {
|
|
1620
|
-
this.stopHeartbeat();
|
|
1621
|
-
if (this.reconnectTimer) {
|
|
1622
|
-
clearTimeout(this.reconnectTimer);
|
|
1623
|
-
this.reconnectTimer = null;
|
|
1624
|
-
}
|
|
1625
|
-
}
|
|
1626
|
-
};
|
|
1627
|
-
|
|
1628
|
-
// src/agent/agent-task-runner.ts
|
|
1629
|
-
var AgentTaskRunner = class {
|
|
1630
|
-
constructor(agentId, client, logger) {
|
|
1631
|
-
this.agentId = agentId;
|
|
1632
|
-
this.client = client;
|
|
1633
|
-
this.logger = logger.child("TaskRunner");
|
|
1634
|
-
}
|
|
1635
|
-
handlers = /* @__PURE__ */ new Map();
|
|
1636
|
-
runningTasks = /* @__PURE__ */ new Map();
|
|
1637
|
-
logger;
|
|
1638
|
-
/** Register a task handler for a given task type */
|
|
1639
|
-
registerHandler(handler) {
|
|
1640
|
-
if (this.handlers.has(handler.taskType)) {
|
|
1641
|
-
this.logger.warn(`Overwriting handler for task type: ${handler.taskType}`);
|
|
1642
|
-
}
|
|
1643
|
-
this.handlers.set(handler.taskType, handler);
|
|
1644
|
-
this.logger.debug(`Registered handler: ${handler.taskType}`);
|
|
1645
|
-
}
|
|
1646
|
-
/** Unregister a task handler */
|
|
1647
|
-
unregisterHandler(taskType) {
|
|
1648
|
-
this.handlers.delete(taskType);
|
|
1649
|
-
}
|
|
1650
|
-
/** Get all registered task types */
|
|
1651
|
-
getTaskTypes() {
|
|
1652
|
-
return [...this.handlers.keys()];
|
|
1653
|
-
}
|
|
1654
|
-
/** Get a handler by task type */
|
|
1655
|
-
getHandler(taskType) {
|
|
1656
|
-
return this.handlers.get(taskType);
|
|
1657
|
-
}
|
|
1658
|
-
/**
|
|
1659
|
-
* Execute a task dispatched from the hub.
|
|
1660
|
-
* Sends result or error back to hub via the AgentClient.
|
|
1661
|
-
*/
|
|
1662
|
-
async executeTask(taskId, taskType, payload) {
|
|
1663
|
-
const handler = this.handlers.get(taskType);
|
|
1664
|
-
if (!handler) {
|
|
1665
|
-
this.client.send({
|
|
1666
|
-
type: "task.result",
|
|
1667
|
-
taskId,
|
|
1668
|
-
success: false,
|
|
1669
|
-
error: `No handler registered for task type: ${taskType}`
|
|
1670
|
-
});
|
|
1671
|
-
return;
|
|
1672
|
-
}
|
|
1673
|
-
const runningTask = { taskId, taskType, cancelled: false };
|
|
1674
|
-
this.runningTasks.set(taskId, runningTask);
|
|
1675
|
-
const context = {
|
|
1676
|
-
taskId,
|
|
1677
|
-
agentId: this.agentId,
|
|
1678
|
-
logger: this.logger.child(taskType),
|
|
1679
|
-
reportProgress: (progress) => {
|
|
1680
|
-
this.client.send({ type: "task.progress", taskId, progress });
|
|
1681
|
-
},
|
|
1682
|
-
isCancelled: () => runningTask.cancelled
|
|
1683
|
-
};
|
|
1684
|
-
try {
|
|
1685
|
-
const result = await handler.handle(payload, context);
|
|
1686
|
-
this.runningTasks.delete(taskId);
|
|
1687
|
-
if (!runningTask.cancelled) {
|
|
1688
|
-
this.client.send({
|
|
1689
|
-
type: "task.result",
|
|
1690
|
-
taskId,
|
|
1691
|
-
success: true,
|
|
1692
|
-
result
|
|
1693
|
-
});
|
|
1694
|
-
}
|
|
1695
|
-
} catch (err) {
|
|
1696
|
-
this.runningTasks.delete(taskId);
|
|
1697
|
-
this.logger.error(`Task ${taskType} (${taskId}) failed`, {
|
|
1698
|
-
message: err instanceof Error ? err.message : String(err)
|
|
1699
|
-
});
|
|
1700
|
-
this.client.send({
|
|
1701
|
-
type: "task.result",
|
|
1702
|
-
taskId,
|
|
1703
|
-
success: false,
|
|
1704
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1705
|
-
});
|
|
1706
|
-
}
|
|
1707
|
-
}
|
|
1708
|
-
/** Cancel a running task */
|
|
1709
|
-
async cancelTask(taskId) {
|
|
1710
|
-
const running = this.runningTasks.get(taskId);
|
|
1711
|
-
if (!running) return;
|
|
1712
|
-
running.cancelled = true;
|
|
1713
|
-
const handler = this.handlers.get(running.taskType);
|
|
1714
|
-
if (handler?.cancel) {
|
|
1715
|
-
try {
|
|
1716
|
-
await handler.cancel();
|
|
1717
|
-
} catch (err) {
|
|
1718
|
-
this.logger.error(`Error cancelling task ${taskId}`, {
|
|
1719
|
-
message: err instanceof Error ? err.message : String(err)
|
|
1720
|
-
});
|
|
1721
|
-
}
|
|
1722
|
-
}
|
|
1723
|
-
this.runningTasks.delete(taskId);
|
|
1724
|
-
}
|
|
1725
|
-
/** Number of currently running tasks */
|
|
1726
|
-
get activeTaskCount() {
|
|
1727
|
-
return this.runningTasks.size;
|
|
1728
|
-
}
|
|
1729
|
-
/** Destroy: cancel all running tasks */
|
|
1730
|
-
async destroy() {
|
|
1731
|
-
const taskIds = [...this.runningTasks.keys()];
|
|
1732
|
-
for (const taskId of taskIds) {
|
|
1733
|
-
await this.cancelTask(taskId);
|
|
1734
|
-
}
|
|
1735
|
-
this.handlers.clear();
|
|
1736
|
-
}
|
|
1737
|
-
};
|
|
1738
|
-
|
|
1739
|
-
// src/agent/pipeline-task-handlers.ts
|
|
1740
|
-
var DecodeTaskHandler = class {
|
|
1741
|
-
taskType = "pipeline.decode";
|
|
1742
|
-
description = "Decode an RTSP stream and produce JPEG frames";
|
|
1743
|
-
async handle(payload, context) {
|
|
1744
|
-
const { logger } = context;
|
|
1745
|
-
const config = payload;
|
|
1746
|
-
if (!config.cameraId || !config.rtspUrl) {
|
|
1747
|
-
throw new Error("pipeline.decode requires cameraId and rtspUrl in payload");
|
|
1748
|
-
}
|
|
1749
|
-
logger.info(`Decode task started: camera=${config.cameraId} url=${config.rtspUrl} fps=${config.fps ?? 1}`);
|
|
1750
|
-
return { status: "started", cameraId: config.cameraId };
|
|
1751
|
-
}
|
|
1752
|
-
async cancel() {
|
|
1753
|
-
}
|
|
1754
|
-
};
|
|
1755
|
-
var DetectTaskHandler = class {
|
|
1756
|
-
taskType = "pipeline.detect";
|
|
1757
|
-
description = "Run object detection on received frames";
|
|
1758
|
-
async handle(payload, context) {
|
|
1759
|
-
const { logger } = context;
|
|
1760
|
-
const config = payload;
|
|
1761
|
-
if (!config.cameraId) {
|
|
1762
|
-
throw new Error("pipeline.detect requires cameraId in payload");
|
|
1763
|
-
}
|
|
1764
|
-
logger.info(`Detect task started: camera=${config.cameraId} model=${config.modelId ?? "default"} runtime=${config.runtime ?? "auto"}`);
|
|
1765
|
-
return { status: "started", cameraId: config.cameraId };
|
|
1766
|
-
}
|
|
1767
|
-
async cancel() {
|
|
1768
|
-
}
|
|
1769
|
-
};
|
|
1770
|
-
var RecordTaskHandler = class {
|
|
1771
|
-
taskType = "pipeline.record";
|
|
1772
|
-
description = "Record an RTSP stream to disk segments";
|
|
1773
|
-
async handle(payload, context) {
|
|
1774
|
-
const { logger } = context;
|
|
1775
|
-
const config = payload;
|
|
1776
|
-
if (!config.cameraId || !config.rtspUrl) {
|
|
1777
|
-
throw new Error("pipeline.record requires cameraId and rtspUrl in payload");
|
|
1778
|
-
}
|
|
1779
|
-
logger.info(`Record task started: camera=${config.cameraId} url=${config.rtspUrl}`);
|
|
1780
|
-
return { status: "started", cameraId: config.cameraId };
|
|
1781
|
-
}
|
|
1782
|
-
async cancel() {
|
|
1783
|
-
}
|
|
1784
|
-
};
|
|
1785
|
-
|
|
1786
|
-
// src/lifecycle/lifecycle-state-machine.ts
|
|
1787
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
1788
|
-
var VALID_TRANSITIONS = {
|
|
1789
|
-
stopped: ["starting", "disabled"],
|
|
1790
|
-
starting: ["running", "error", "stopping"],
|
|
1791
|
-
running: ["stopping", "error"],
|
|
1792
|
-
stopping: ["stopped", "error"],
|
|
1793
|
-
error: ["starting", "stopped", "disabled"],
|
|
1794
|
-
disabled: ["stopped"]
|
|
1795
|
-
};
|
|
1796
|
-
var LifecycleStateMachine = class {
|
|
1797
|
-
constructor(elementId, elementType, eventBus, logger) {
|
|
1798
|
-
this.elementId = elementId;
|
|
1799
|
-
this.elementType = elementType;
|
|
1800
|
-
this.eventBus = eventBus;
|
|
1801
|
-
this.logger = logger;
|
|
1802
|
-
}
|
|
1803
905
|
_state = "stopped";
|
|
1804
906
|
_error;
|
|
1805
907
|
_startedAt;
|
|
@@ -1822,7 +924,7 @@ var LifecycleStateMachine = class {
|
|
|
1822
924
|
transition(to, error) {
|
|
1823
925
|
const from = this._state;
|
|
1824
926
|
if (!this.isValidTransition(from, to)) {
|
|
1825
|
-
this.logger.warn(
|
|
927
|
+
this.logger.warn("Invalid state transition", { meta: { from, to } });
|
|
1826
928
|
return false;
|
|
1827
929
|
}
|
|
1828
930
|
this._state = to;
|
|
@@ -1838,7 +940,7 @@ var LifecycleStateMachine = class {
|
|
|
1838
940
|
if (to === "stopped" || to === "error" || to === "disabled") {
|
|
1839
941
|
this._stoppedAt = Date.now();
|
|
1840
942
|
}
|
|
1841
|
-
this.logger.info(
|
|
943
|
+
this.logger.info("State transition", { meta: { from, to, error } });
|
|
1842
944
|
this.eventBus.emit({
|
|
1843
945
|
id: randomUUID2(),
|
|
1844
946
|
timestamp: /* @__PURE__ */ new Date(),
|
|
@@ -1861,6 +963,7 @@ var FeatureManager = class {
|
|
|
1861
963
|
constructor(configReader) {
|
|
1862
964
|
this.configReader = configReader;
|
|
1863
965
|
}
|
|
966
|
+
configReader;
|
|
1864
967
|
isEnabled(flag) {
|
|
1865
968
|
return this.configReader.features[flag];
|
|
1866
969
|
}
|
|
@@ -1873,8 +976,9 @@ var FeatureManager = class {
|
|
|
1873
976
|
var EventRingBuffer = class {
|
|
1874
977
|
constructor(capacity) {
|
|
1875
978
|
this.capacity = capacity;
|
|
1876
|
-
this.buffer =
|
|
979
|
+
this.buffer = Array.from({ length: capacity });
|
|
1877
980
|
}
|
|
981
|
+
capacity;
|
|
1878
982
|
buffer;
|
|
1879
983
|
head = 0;
|
|
1880
984
|
count = 0;
|
|
@@ -1912,14 +1016,36 @@ function matchesCategory(eventCategory, filterCategory) {
|
|
|
1912
1016
|
return eventCategory === filterCategory;
|
|
1913
1017
|
}
|
|
1914
1018
|
function matchesFilter(event, filter) {
|
|
1915
|
-
if (filter.category
|
|
1916
|
-
|
|
1019
|
+
if (filter.category) {
|
|
1020
|
+
const raw = filter.category;
|
|
1021
|
+
const categories = Array.isArray(raw) ? raw.map(String) : [String(raw)];
|
|
1022
|
+
if (!categories.some((cat) => matchesCategory(event.category, cat))) {
|
|
1023
|
+
return false;
|
|
1024
|
+
}
|
|
1917
1025
|
}
|
|
1918
1026
|
if (filter.source) {
|
|
1919
1027
|
if (event.source.type !== filter.source.type || event.source.id !== filter.source.id) {
|
|
1920
1028
|
return false;
|
|
1921
1029
|
}
|
|
1922
1030
|
}
|
|
1031
|
+
if (filter.agentId) {
|
|
1032
|
+
const eventNodeId = event.source.nodeId;
|
|
1033
|
+
if (!eventNodeId || !eventNodeId.startsWith(filter.agentId)) {
|
|
1034
|
+
return false;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
if (filter.addonId) {
|
|
1038
|
+
const eventAddonId = event.source.addonId ?? (event.source.type === "addon" ? String(event.source.id) : void 0);
|
|
1039
|
+
if (eventAddonId !== filter.addonId) {
|
|
1040
|
+
return false;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
if (filter.deviceId !== void 0) {
|
|
1044
|
+
const eventDeviceId = event.source.deviceId ?? (event.source.type === "device" ? Number(event.source.id) : void 0);
|
|
1045
|
+
if (eventDeviceId !== filter.deviceId) {
|
|
1046
|
+
return false;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1923
1049
|
if (filter.since && event.timestamp < filter.since) {
|
|
1924
1050
|
return false;
|
|
1925
1051
|
}
|
|
@@ -1945,6 +1071,10 @@ var SystemEventBus = class {
|
|
|
1945
1071
|
}
|
|
1946
1072
|
}
|
|
1947
1073
|
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Subscribe to events matching a filter.
|
|
1076
|
+
* When called with a typed category filter, the handler receives typed event data.
|
|
1077
|
+
*/
|
|
1948
1078
|
subscribe(filter, handler) {
|
|
1949
1079
|
const subscriber = { filter, callback: handler };
|
|
1950
1080
|
this.subscribers.push(subscriber);
|
|
@@ -1961,11 +1091,32 @@ var SystemEventBus = class {
|
|
|
1961
1091
|
};
|
|
1962
1092
|
|
|
1963
1093
|
// src/logging/log-ring-buffer.ts
|
|
1094
|
+
function matchesLogTags(entryTags, filterTags) {
|
|
1095
|
+
if (!entryTags) return false;
|
|
1096
|
+
for (const [key, value] of Object.entries(filterTags)) {
|
|
1097
|
+
if (value === void 0) continue;
|
|
1098
|
+
const entryValue = entryTags[key];
|
|
1099
|
+
if (key === "addonId") {
|
|
1100
|
+
const bare = typeof entryValue === "string" ? entryValue.split("@")[0] : entryValue;
|
|
1101
|
+
if (bare !== value) return false;
|
|
1102
|
+
} else if (key === "agentId") {
|
|
1103
|
+
const nodeId = entryTags["nodeId"];
|
|
1104
|
+
const matchEmitter = entryValue === value;
|
|
1105
|
+
const matchExactNode = nodeId === value;
|
|
1106
|
+
const matchWorker = typeof nodeId === "string" && nodeId.startsWith(`${value}/`);
|
|
1107
|
+
if (!matchEmitter && !matchExactNode && !matchWorker) return false;
|
|
1108
|
+
} else {
|
|
1109
|
+
if (entryValue !== value) return false;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
return true;
|
|
1113
|
+
}
|
|
1964
1114
|
var LogRingBuffer = class {
|
|
1965
1115
|
constructor(capacity = 1e4) {
|
|
1966
1116
|
this.capacity = capacity;
|
|
1967
|
-
this.buffer =
|
|
1117
|
+
this.buffer = Array.from({ length: capacity });
|
|
1968
1118
|
}
|
|
1119
|
+
capacity;
|
|
1969
1120
|
buffer;
|
|
1970
1121
|
head = 0;
|
|
1971
1122
|
count = 0;
|
|
@@ -1987,14 +1138,6 @@ var LogRingBuffer = class {
|
|
|
1987
1138
|
query(filter) {
|
|
1988
1139
|
const all = this.getAll();
|
|
1989
1140
|
let result = all;
|
|
1990
|
-
if (filter.scope && filter.scope.length > 0) {
|
|
1991
|
-
result = result.filter((entry) => {
|
|
1992
|
-
for (let i = 0; i < filter.scope.length; i++) {
|
|
1993
|
-
if (entry.scope[i] !== filter.scope[i]) return false;
|
|
1994
|
-
}
|
|
1995
|
-
return true;
|
|
1996
|
-
});
|
|
1997
|
-
}
|
|
1998
1141
|
if (filter.level) {
|
|
1999
1142
|
result = result.filter((entry) => entry.level === filter.level);
|
|
2000
1143
|
}
|
|
@@ -2004,44 +1147,98 @@ var LogRingBuffer = class {
|
|
|
2004
1147
|
if (filter.until) {
|
|
2005
1148
|
result = result.filter((entry) => entry.timestamp <= filter.until);
|
|
2006
1149
|
}
|
|
1150
|
+
if (filter.tags) {
|
|
1151
|
+
const filterTags = filter.tags;
|
|
1152
|
+
result = result.filter((entry) => matchesLogTags(entry.tags, filterTags));
|
|
1153
|
+
}
|
|
2007
1154
|
if (filter.limit !== void 0 && filter.limit > 0) {
|
|
2008
1155
|
result = result.slice(0, filter.limit);
|
|
2009
1156
|
}
|
|
2010
1157
|
return result;
|
|
2011
1158
|
}
|
|
1159
|
+
/**
|
|
1160
|
+
* Drop entries that match `filter`. Returns the number of entries
|
|
1161
|
+
* removed. Triggered by the UI's "Clear logs" button so the operator
|
|
1162
|
+
* can wipe the historical buffer for a specific scope (per-device,
|
|
1163
|
+
* per-addon, per-agent) — without it, closing and reopening the
|
|
1164
|
+
* panel re-populates the cleared rows from the server's ring buffer.
|
|
1165
|
+
*
|
|
1166
|
+
* Filter semantics mirror `query` (same level / since / until / tags
|
|
1167
|
+
* handling) — whatever you'd see listed by `query(filter)` is what
|
|
1168
|
+
* gets removed.
|
|
1169
|
+
*/
|
|
1170
|
+
clear(filter = {}) {
|
|
1171
|
+
if (this.count === 0) return 0;
|
|
1172
|
+
const survivors = [];
|
|
1173
|
+
for (let i = 0; i < this.count; i++) {
|
|
1174
|
+
const index = (this.head - 1 - i + this.capacity) % this.capacity;
|
|
1175
|
+
const entry = this.buffer[index];
|
|
1176
|
+
let matches = true;
|
|
1177
|
+
if (filter.level !== void 0 && entry.level !== filter.level) matches = false;
|
|
1178
|
+
if (matches && filter.since !== void 0 && entry.timestamp < filter.since) matches = false;
|
|
1179
|
+
if (matches && filter.until !== void 0 && entry.timestamp > filter.until) matches = false;
|
|
1180
|
+
if (matches && filter.tags !== void 0 && !matchesLogTags(entry.tags, filter.tags)) matches = false;
|
|
1181
|
+
if (!matches) survivors.push(entry);
|
|
1182
|
+
}
|
|
1183
|
+
const removed = this.count - survivors.length;
|
|
1184
|
+
if (removed === 0) return 0;
|
|
1185
|
+
this.buffer.fill(void 0);
|
|
1186
|
+
this.head = 0;
|
|
1187
|
+
this.count = 0;
|
|
1188
|
+
for (let i = survivors.length - 1; i >= 0; i--) {
|
|
1189
|
+
this.push(survivors[i]);
|
|
1190
|
+
}
|
|
1191
|
+
return removed;
|
|
1192
|
+
}
|
|
2012
1193
|
};
|
|
2013
1194
|
|
|
2014
1195
|
// src/logging/scoped-logger.ts
|
|
2015
1196
|
var ScopedLogger = class _ScopedLogger {
|
|
2016
|
-
constructor(scope, writeFn) {
|
|
1197
|
+
constructor(scope, writeFn, tags) {
|
|
2017
1198
|
this.scope = scope;
|
|
2018
1199
|
this.writeFn = writeFn;
|
|
1200
|
+
this.tags = tags;
|
|
2019
1201
|
}
|
|
2020
|
-
|
|
2021
|
-
|
|
1202
|
+
scope;
|
|
1203
|
+
writeFn;
|
|
1204
|
+
tags;
|
|
1205
|
+
debug(message, extras) {
|
|
1206
|
+
this.write("debug", message, extras);
|
|
2022
1207
|
}
|
|
2023
|
-
info(message,
|
|
2024
|
-
this.write("info", message,
|
|
1208
|
+
info(message, extras) {
|
|
1209
|
+
this.write("info", message, extras);
|
|
2025
1210
|
}
|
|
2026
|
-
warn(message,
|
|
2027
|
-
this.write("warn", message,
|
|
1211
|
+
warn(message, extras) {
|
|
1212
|
+
this.write("warn", message, extras);
|
|
2028
1213
|
}
|
|
2029
|
-
error(message,
|
|
2030
|
-
this.write("error", message,
|
|
1214
|
+
error(message, extras) {
|
|
1215
|
+
this.write("error", message, extras);
|
|
2031
1216
|
}
|
|
2032
1217
|
child(childScope) {
|
|
2033
|
-
return new _ScopedLogger(
|
|
1218
|
+
return new _ScopedLogger(childScope, this.writeFn, this.tags);
|
|
1219
|
+
}
|
|
1220
|
+
withTags(tags) {
|
|
1221
|
+
const merged = this.tags ? { ...this.tags, ...tags } : tags;
|
|
1222
|
+
return new _ScopedLogger(this.scope, this.writeFn, merged);
|
|
2034
1223
|
}
|
|
2035
|
-
write(level, message,
|
|
1224
|
+
write(level, message, extras) {
|
|
1225
|
+
const mergedTags = this.mergeTags(extras?.tags);
|
|
2036
1226
|
const entry = {
|
|
2037
1227
|
timestamp: /* @__PURE__ */ new Date(),
|
|
2038
1228
|
level,
|
|
2039
|
-
scope: this.scope,
|
|
2040
1229
|
message,
|
|
2041
|
-
...
|
|
1230
|
+
...this.scope !== void 0 ? { scope: this.scope } : {},
|
|
1231
|
+
...extras?.meta !== void 0 ? { meta: extras.meta } : {},
|
|
1232
|
+
...mergedTags ? { tags: mergedTags } : {}
|
|
2042
1233
|
};
|
|
2043
1234
|
this.writeFn(entry);
|
|
2044
1235
|
}
|
|
1236
|
+
mergeTags(extraTags) {
|
|
1237
|
+
if (!this.tags && !extraTags) return void 0;
|
|
1238
|
+
if (!this.tags) return extraTags;
|
|
1239
|
+
if (!extraTags) return this.tags;
|
|
1240
|
+
return { ...this.tags, ...extraTags };
|
|
1241
|
+
}
|
|
2045
1242
|
};
|
|
2046
1243
|
|
|
2047
1244
|
// src/logging/log-manager.ts
|
|
@@ -2049,25 +1246,50 @@ var LogManager = class {
|
|
|
2049
1246
|
ringBuffer;
|
|
2050
1247
|
destinations = [];
|
|
2051
1248
|
subscribers = /* @__PURE__ */ new Map();
|
|
1249
|
+
deviceNameLookup = null;
|
|
2052
1250
|
constructor(bufferSize = 1e4) {
|
|
2053
1251
|
this.ringBuffer = new LogRingBuffer(bufferSize);
|
|
2054
1252
|
}
|
|
1253
|
+
/**
|
|
1254
|
+
* Register a callback that resolves `deviceId → deviceName`. Called
|
|
1255
|
+
* for every emitted entry that carries `tags.deviceId` but no
|
|
1256
|
+
* explicit `tags.deviceName` — the LogManager injects the resolved
|
|
1257
|
+
* name directly into the entry's tags BEFORE ring-buffer / destination
|
|
1258
|
+
* write, so every destination (local console, file, remote forwarder,
|
|
1259
|
+
* addon-bundled copies of core) sees the enriched shape regardless of
|
|
1260
|
+
* which module instance the destination was built from.
|
|
1261
|
+
*/
|
|
1262
|
+
setDeviceNameLookup(lookup) {
|
|
1263
|
+
this.deviceNameLookup = lookup;
|
|
1264
|
+
}
|
|
2055
1265
|
createLogger(scope) {
|
|
2056
|
-
return new ScopedLogger(
|
|
2057
|
-
this.
|
|
1266
|
+
return new ScopedLogger(scope, (entry) => {
|
|
1267
|
+
const enriched = this.enrichDeviceName(entry);
|
|
1268
|
+
this.ringBuffer.push(enriched);
|
|
2058
1269
|
for (const dest of this.destinations) {
|
|
2059
|
-
dest.write(
|
|
1270
|
+
dest.write(enriched);
|
|
2060
1271
|
}
|
|
2061
1272
|
for (const [, sub] of this.subscribers) {
|
|
2062
|
-
if (this.matchesFilter(
|
|
1273
|
+
if (this.matchesFilter(enriched, sub.filter)) {
|
|
2063
1274
|
try {
|
|
2064
|
-
sub.callback(
|
|
1275
|
+
sub.callback(enriched);
|
|
2065
1276
|
} catch {
|
|
2066
1277
|
}
|
|
2067
1278
|
}
|
|
2068
1279
|
}
|
|
2069
1280
|
});
|
|
2070
1281
|
}
|
|
1282
|
+
enrichDeviceName(entry) {
|
|
1283
|
+
if (!this.deviceNameLookup) return entry;
|
|
1284
|
+
const tags = entry.tags;
|
|
1285
|
+
if (!tags) return entry;
|
|
1286
|
+
const deviceId = tags.deviceId;
|
|
1287
|
+
if (typeof deviceId !== "number" || !Number.isFinite(deviceId)) return entry;
|
|
1288
|
+
if (typeof tags.deviceName === "string") return entry;
|
|
1289
|
+
const name = this.deviceNameLookup(deviceId);
|
|
1290
|
+
if (!name) return entry;
|
|
1291
|
+
return { ...entry, tags: { ...tags, deviceName: name } };
|
|
1292
|
+
}
|
|
2071
1293
|
/** Subscribe to live logs matching a filter. Returns an unsubscribe function. */
|
|
2072
1294
|
subscribe(filter, callback) {
|
|
2073
1295
|
const id = Math.random().toString(36).slice(2);
|
|
@@ -2077,20 +1299,36 @@ var LogManager = class {
|
|
|
2077
1299
|
};
|
|
2078
1300
|
}
|
|
2079
1301
|
matchesFilter(entry, filter) {
|
|
2080
|
-
if (filter.scope && filter.scope.length > 0) {
|
|
2081
|
-
const entryScope = entry.scope?.[0] ?? "";
|
|
2082
|
-
if (!filter.scope.some((s) => entryScope === s || entryScope.startsWith(s))) return false;
|
|
2083
|
-
}
|
|
2084
1302
|
if (filter.level) {
|
|
2085
1303
|
const levels = ["debug", "info", "warn", "error"];
|
|
2086
1304
|
const minIdx = levels.indexOf(filter.level);
|
|
2087
1305
|
const entryIdx = levels.indexOf(entry.level);
|
|
2088
1306
|
if (entryIdx < minIdx) return false;
|
|
2089
1307
|
}
|
|
1308
|
+
if (filter.tags) {
|
|
1309
|
+
if (!matchesLogTags(entry.tags, filter.tags)) return false;
|
|
1310
|
+
}
|
|
2090
1311
|
return true;
|
|
2091
1312
|
}
|
|
2092
|
-
|
|
1313
|
+
/**
|
|
1314
|
+
* Register a log destination.
|
|
1315
|
+
*
|
|
1316
|
+
* @param opts.replay If true (default), replay every entry currently in
|
|
1317
|
+
* the ring buffer to the new destination before live
|
|
1318
|
+
* traffic starts. Lets a destination added mid-boot
|
|
1319
|
+
* still receive boot-time log entries.
|
|
1320
|
+
*/
|
|
1321
|
+
addDestination(dest, opts = {}) {
|
|
1322
|
+
const replay = opts.replay ?? true;
|
|
2093
1323
|
this.destinations.push(dest);
|
|
1324
|
+
if (replay) {
|
|
1325
|
+
for (const entry of this.ringBuffer.query({})) {
|
|
1326
|
+
try {
|
|
1327
|
+
dest.write(entry);
|
|
1328
|
+
} catch {
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
2094
1332
|
}
|
|
2095
1333
|
removeDestination(dest) {
|
|
2096
1334
|
const index = this.destinations.indexOf(dest);
|
|
@@ -2101,6 +1339,16 @@ var LogManager = class {
|
|
|
2101
1339
|
query(filter) {
|
|
2102
1340
|
return this.ringBuffer.query(filter);
|
|
2103
1341
|
}
|
|
1342
|
+
/**
|
|
1343
|
+
* Drop ring-buffer entries matching `filter`. Returns the count
|
|
1344
|
+
* removed. Backs the UI's "Clear logs" admin action so an operator
|
|
1345
|
+
* can wipe the historical window for a scope (per-device, per-addon,
|
|
1346
|
+
* agent-level) without restarting the server. External destinations
|
|
1347
|
+
* (file, forwarder) are NOT touched — only the in-memory ring.
|
|
1348
|
+
*/
|
|
1349
|
+
clear(filter = {}) {
|
|
1350
|
+
return this.ringBuffer.clear(filter);
|
|
1351
|
+
}
|
|
2104
1352
|
};
|
|
2105
1353
|
|
|
2106
1354
|
// src/storage/storage-manager.ts
|
|
@@ -2113,7 +1361,7 @@ var StorageManager = class {
|
|
|
2113
1361
|
settingsBackend = null;
|
|
2114
1362
|
/** @deprecated Set by legacy capability consumer — use setNewStorageProvider instead */
|
|
2115
1363
|
setProvider(provider) {
|
|
2116
|
-
if ("getLocation" in provider
|
|
1364
|
+
if ("getLocation" in provider) {
|
|
2117
1365
|
this.legacyProvider = provider;
|
|
2118
1366
|
} else {
|
|
2119
1367
|
this.newStorageProvider = provider;
|
|
@@ -2125,6 +1373,12 @@ var StorageManager = class {
|
|
|
2125
1373
|
setSettingsBackend(backend) {
|
|
2126
1374
|
this.settingsBackend = backend;
|
|
2127
1375
|
}
|
|
1376
|
+
getSettingsBackend() {
|
|
1377
|
+
if (!this.settingsBackend) {
|
|
1378
|
+
throw new Error("SettingsBackend not initialized \u2014 storage addon not yet loaded");
|
|
1379
|
+
}
|
|
1380
|
+
return this.settingsBackend;
|
|
1381
|
+
}
|
|
2128
1382
|
getProvider() {
|
|
2129
1383
|
if (this.legacyProvider) return this.legacyProvider;
|
|
2130
1384
|
if (!this.newStorageProvider && !this.settingsBackend && !this.locationManager) {
|
|
@@ -2142,7 +1396,7 @@ var StorageManager = class {
|
|
|
2142
1396
|
return this.locationManager;
|
|
2143
1397
|
}
|
|
2144
1398
|
async initializeLocations(dataPath) {
|
|
2145
|
-
const { StorageLocationManager: StorageLocationManager2 } = await import("./storage-location-manager-
|
|
1399
|
+
const { StorageLocationManager: StorageLocationManager2 } = await import("./storage-location-manager-HFNB3PCS.mjs");
|
|
2146
1400
|
const manager = new StorageLocationManager2(dataPath);
|
|
2147
1401
|
await manager.initializeDefaults();
|
|
2148
1402
|
this.locationManager = manager;
|
|
@@ -2161,7 +1415,8 @@ var StorageManager = class {
|
|
|
2161
1415
|
if (this.legacyProvider) {
|
|
2162
1416
|
try {
|
|
2163
1417
|
const LEGACY_MAP = { config: "data", events: "data", addon: "data" };
|
|
2164
|
-
const
|
|
1418
|
+
const mapped = name in LEGACY_MAP ? LEGACY_MAP[name] : name;
|
|
1419
|
+
const mappedName = mapped;
|
|
2165
1420
|
const location2 = this.legacyProvider.getLocation(mappedName);
|
|
2166
1421
|
return namespace ? this.createNamespacedLocation(location2, namespace) : location2;
|
|
2167
1422
|
} catch {
|
|
@@ -2172,29 +1427,29 @@ var StorageManager = class {
|
|
|
2172
1427
|
const backend = this.settingsBackend;
|
|
2173
1428
|
location.structured = {
|
|
2174
1429
|
async query(collection, filter) {
|
|
2175
|
-
const results = await backend.query(collection, filter);
|
|
1430
|
+
const results = await backend.query({ collection, filter });
|
|
2176
1431
|
return results.map((r) => ({ collection, id: r.id, data: r.data }));
|
|
2177
1432
|
},
|
|
2178
1433
|
async insert(record) {
|
|
2179
1434
|
const id = record.id || (await import("crypto")).randomUUID();
|
|
2180
|
-
await backend.insert(record.collection, { id, data: record.data });
|
|
1435
|
+
await backend.insert({ collection: record.collection, record: { id, data: record.data } });
|
|
2181
1436
|
return { ...record, id };
|
|
2182
1437
|
},
|
|
2183
1438
|
async update(collection, id, data) {
|
|
2184
|
-
await backend.update(collection, id, data);
|
|
1439
|
+
await backend.update({ collection, id, data });
|
|
2185
1440
|
return { collection, id, data };
|
|
2186
1441
|
},
|
|
2187
1442
|
async delete(collection, id) {
|
|
2188
|
-
await backend.delete(collection, id);
|
|
1443
|
+
await backend.delete({ collection, key: id });
|
|
2189
1444
|
},
|
|
2190
1445
|
async count(collection, filter) {
|
|
2191
|
-
return backend.count(collection, filter);
|
|
1446
|
+
return backend.count({ collection, filter });
|
|
2192
1447
|
}
|
|
2193
1448
|
};
|
|
2194
1449
|
}
|
|
2195
1450
|
if (this.locationManager) {
|
|
2196
1451
|
const LEGACY_MAP = { config: "data", events: "data", addon: "data" };
|
|
2197
|
-
const mappedName = LEGACY_MAP[name]
|
|
1452
|
+
const mappedName = name in LEGACY_MAP ? LEGACY_MAP[name] : name;
|
|
2198
1453
|
try {
|
|
2199
1454
|
const basePath = this.locationManager.getBackend(mappedName).basePath;
|
|
2200
1455
|
location.files = {
|
|
@@ -2290,387 +1545,13 @@ var StorageManager = class {
|
|
|
2290
1545
|
}
|
|
2291
1546
|
};
|
|
2292
1547
|
|
|
2293
|
-
// src/auth/auth-manager.ts
|
|
2294
|
-
import * as jwt from "jsonwebtoken";
|
|
2295
|
-
import * as bcrypt from "bcryptjs";
|
|
2296
|
-
import * as crypto from "crypto";
|
|
2297
|
-
var AuthManager = class {
|
|
2298
|
-
constructor(config) {
|
|
2299
|
-
this.config = config;
|
|
2300
|
-
const configured = this.config.get("auth.jwtSecret");
|
|
2301
|
-
if (configured) {
|
|
2302
|
-
this.jwtSecret = configured;
|
|
2303
|
-
} else {
|
|
2304
|
-
const secret = crypto.randomBytes(32).toString("hex");
|
|
2305
|
-
this.config.update("auth", { jwtSecret: secret });
|
|
2306
|
-
console.log("[AuthManager] Generated JWT secret and saved to config.yaml (auth.jwtSecret)");
|
|
2307
|
-
this.jwtSecret = secret;
|
|
2308
|
-
}
|
|
2309
|
-
}
|
|
2310
|
-
jwtSecret;
|
|
2311
|
-
scopedTokenManager = null;
|
|
2312
|
-
signToken(payload) {
|
|
2313
|
-
return jwt.sign({ ...payload }, this.jwtSecret, { expiresIn: "24h" });
|
|
2314
|
-
}
|
|
2315
|
-
verifyToken(token) {
|
|
2316
|
-
return jwt.verify(token, this.jwtSecret);
|
|
2317
|
-
}
|
|
2318
|
-
async hashPassword(password) {
|
|
2319
|
-
return bcrypt.hash(password, 10);
|
|
2320
|
-
}
|
|
2321
|
-
async comparePassword(password, hash2) {
|
|
2322
|
-
return bcrypt.compare(password, hash2);
|
|
2323
|
-
}
|
|
2324
|
-
generateApiKey() {
|
|
2325
|
-
const token = crypto.randomBytes(32).toString("hex");
|
|
2326
|
-
const hash2 = crypto.createHash("sha256").update(token).digest("hex");
|
|
2327
|
-
const prefix = token.slice(0, 8);
|
|
2328
|
-
return { token, hash: hash2, prefix };
|
|
2329
|
-
}
|
|
2330
|
-
validateApiKey(token, storedHash) {
|
|
2331
|
-
const hash2 = crypto.createHash("sha256").update(token).digest("hex");
|
|
2332
|
-
return hash2 === storedHash;
|
|
2333
|
-
}
|
|
2334
|
-
/**
|
|
2335
|
-
* Create a service token for agent/worker authentication.
|
|
2336
|
-
* Used when forking workers or when agents register.
|
|
2337
|
-
*/
|
|
2338
|
-
createServiceToken(opts) {
|
|
2339
|
-
const payload = {
|
|
2340
|
-
userId: opts.agentId,
|
|
2341
|
-
username: opts.agentId,
|
|
2342
|
-
role: opts.role ?? "agent",
|
|
2343
|
-
type: "service",
|
|
2344
|
-
agentId: opts.agentId,
|
|
2345
|
-
allowedProviders: "*",
|
|
2346
|
-
allowedDevices: {}
|
|
2347
|
-
};
|
|
2348
|
-
const expiresIn = opts.expiresIn ?? "24h";
|
|
2349
|
-
return jwt.sign(payload, this.jwtSecret, { expiresIn });
|
|
2350
|
-
}
|
|
2351
|
-
/**
|
|
2352
|
-
* Set the scoped token manager for the auth chain.
|
|
2353
|
-
*/
|
|
2354
|
-
setScopedTokenManager(manager) {
|
|
2355
|
-
this.scopedTokenManager = manager;
|
|
2356
|
-
}
|
|
2357
|
-
/**
|
|
2358
|
-
* Validate a scoped token string.
|
|
2359
|
-
* Returns the token record if valid, null otherwise.
|
|
2360
|
-
*/
|
|
2361
|
-
async validateScopedToken(rawToken) {
|
|
2362
|
-
if (!this.scopedTokenManager) {
|
|
2363
|
-
return null;
|
|
2364
|
-
}
|
|
2365
|
-
return this.scopedTokenManager.validate(rawToken);
|
|
2366
|
-
}
|
|
2367
|
-
/**
|
|
2368
|
-
* Check whether a scoped token grants access to a given addon/route/capability.
|
|
2369
|
-
*/
|
|
2370
|
-
matchesScopedTokenScope(token, addonId, routePath, capability) {
|
|
2371
|
-
if (!this.scopedTokenManager) {
|
|
2372
|
-
return false;
|
|
2373
|
-
}
|
|
2374
|
-
return this.scopedTokenManager.matchesScope(token, addonId, routePath, capability);
|
|
2375
|
-
}
|
|
2376
|
-
};
|
|
2377
|
-
|
|
2378
|
-
// src/auth/api-key-manager.ts
|
|
2379
|
-
import * as crypto2 from "crypto";
|
|
2380
|
-
var API_KEYS_COLLECTION = "api_keys";
|
|
2381
|
-
var ApiKeyManager = class {
|
|
2382
|
-
constructor(storageAccess, auth) {
|
|
2383
|
-
this.storageAccess = storageAccess;
|
|
2384
|
-
this.auth = auth;
|
|
2385
|
-
}
|
|
2386
|
-
get structured() {
|
|
2387
|
-
return this.storageAccess.getStructuredStorage();
|
|
2388
|
-
}
|
|
2389
|
-
async create(input) {
|
|
2390
|
-
const { token, hash: hash2, prefix } = this.auth.generateApiKey();
|
|
2391
|
-
const now = Date.now();
|
|
2392
|
-
const record = {
|
|
2393
|
-
id: crypto2.randomUUID(),
|
|
2394
|
-
label: input.label,
|
|
2395
|
-
role: input.role,
|
|
2396
|
-
allowedProviders: input.allowedProviders ?? "*",
|
|
2397
|
-
allowedDevices: input.allowedDevices ?? {},
|
|
2398
|
-
tokenHash: hash2,
|
|
2399
|
-
tokenPrefix: prefix,
|
|
2400
|
-
createdAt: now
|
|
2401
|
-
};
|
|
2402
|
-
await this.structured.insert({
|
|
2403
|
-
collection: API_KEYS_COLLECTION,
|
|
2404
|
-
id: record.id,
|
|
2405
|
-
data: record
|
|
2406
|
-
});
|
|
2407
|
-
return { record, token };
|
|
2408
|
-
}
|
|
2409
|
-
async validateToken(token) {
|
|
2410
|
-
const allKeys = await this.structured.query(API_KEYS_COLLECTION);
|
|
2411
|
-
for (const entry of allKeys) {
|
|
2412
|
-
const record = entry.data;
|
|
2413
|
-
if (this.auth.validateApiKey(token, record.tokenHash)) {
|
|
2414
|
-
const updatedData = {
|
|
2415
|
-
...record,
|
|
2416
|
-
lastUsedAt: Date.now()
|
|
2417
|
-
};
|
|
2418
|
-
await this.structured.update(API_KEYS_COLLECTION, record.id, updatedData);
|
|
2419
|
-
return { ...record, lastUsedAt: updatedData.lastUsedAt };
|
|
2420
|
-
}
|
|
2421
|
-
}
|
|
2422
|
-
return null;
|
|
2423
|
-
}
|
|
2424
|
-
async listAll() {
|
|
2425
|
-
const results = await this.structured.query(API_KEYS_COLLECTION);
|
|
2426
|
-
return results.map((r) => {
|
|
2427
|
-
const { tokenHash, ...rest } = r.data;
|
|
2428
|
-
return rest;
|
|
2429
|
-
});
|
|
2430
|
-
}
|
|
2431
|
-
async revoke(id) {
|
|
2432
|
-
await this.structured.delete(API_KEYS_COLLECTION, id);
|
|
2433
|
-
}
|
|
2434
|
-
async findById(id) {
|
|
2435
|
-
const results = await this.structured.query(API_KEYS_COLLECTION, {
|
|
2436
|
-
where: { id }
|
|
2437
|
-
});
|
|
2438
|
-
if (results.length === 0) {
|
|
2439
|
-
return null;
|
|
2440
|
-
}
|
|
2441
|
-
return results[0].data;
|
|
2442
|
-
}
|
|
2443
|
-
};
|
|
2444
|
-
|
|
2445
|
-
// src/auth/user-manager.ts
|
|
2446
|
-
import * as crypto3 from "crypto";
|
|
2447
|
-
var USERS_COLLECTION = "users";
|
|
2448
|
-
var UserManager = class {
|
|
2449
|
-
constructor(storageAccess, auth, config) {
|
|
2450
|
-
this.storageAccess = storageAccess;
|
|
2451
|
-
this.auth = auth;
|
|
2452
|
-
this.config = config;
|
|
2453
|
-
}
|
|
2454
|
-
get structured() {
|
|
2455
|
-
return this.storageAccess.getStructuredStorage();
|
|
2456
|
-
}
|
|
2457
|
-
async create(input) {
|
|
2458
|
-
const existing = await this.findByUsername(input.username);
|
|
2459
|
-
if (existing) {
|
|
2460
|
-
throw new Error(`User with username "${input.username}" already exists`);
|
|
2461
|
-
}
|
|
2462
|
-
const passwordHash = await this.auth.hashPassword(input.password);
|
|
2463
|
-
const now = Date.now();
|
|
2464
|
-
const record = {
|
|
2465
|
-
id: crypto3.randomUUID(),
|
|
2466
|
-
username: input.username,
|
|
2467
|
-
passwordHash,
|
|
2468
|
-
role: input.role,
|
|
2469
|
-
allowedProviders: input.allowedProviders ?? "*",
|
|
2470
|
-
allowedDevices: input.allowedDevices ?? {},
|
|
2471
|
-
createdAt: now,
|
|
2472
|
-
updatedAt: now
|
|
2473
|
-
};
|
|
2474
|
-
await this.structured.insert({
|
|
2475
|
-
collection: USERS_COLLECTION,
|
|
2476
|
-
id: record.id,
|
|
2477
|
-
data: record
|
|
2478
|
-
});
|
|
2479
|
-
return record;
|
|
2480
|
-
}
|
|
2481
|
-
async findByUsername(username) {
|
|
2482
|
-
const results = await this.structured.query(USERS_COLLECTION, {
|
|
2483
|
-
where: { username }
|
|
2484
|
-
});
|
|
2485
|
-
if (results.length === 0) {
|
|
2486
|
-
return null;
|
|
2487
|
-
}
|
|
2488
|
-
return results[0].data;
|
|
2489
|
-
}
|
|
2490
|
-
async findById(id) {
|
|
2491
|
-
const results = await this.structured.query(USERS_COLLECTION, {
|
|
2492
|
-
where: { id }
|
|
2493
|
-
});
|
|
2494
|
-
if (results.length === 0) {
|
|
2495
|
-
return null;
|
|
2496
|
-
}
|
|
2497
|
-
return results[0].data;
|
|
2498
|
-
}
|
|
2499
|
-
async validateCredentials(username, password) {
|
|
2500
|
-
const user = await this.findByUsername(username);
|
|
2501
|
-
if (!user) {
|
|
2502
|
-
return null;
|
|
2503
|
-
}
|
|
2504
|
-
const valid = await this.auth.comparePassword(password, user.passwordHash);
|
|
2505
|
-
return valid ? user : null;
|
|
2506
|
-
}
|
|
2507
|
-
async listAll() {
|
|
2508
|
-
const results = await this.structured.query(USERS_COLLECTION);
|
|
2509
|
-
return results.map((r) => {
|
|
2510
|
-
const { passwordHash, ...rest } = r.data;
|
|
2511
|
-
return rest;
|
|
2512
|
-
});
|
|
2513
|
-
}
|
|
2514
|
-
async update(id, data) {
|
|
2515
|
-
const existing = await this.findById(id);
|
|
2516
|
-
if (!existing) {
|
|
2517
|
-
throw new Error(`User with id "${id}" not found`);
|
|
2518
|
-
}
|
|
2519
|
-
const updatedData = {
|
|
2520
|
-
...existing,
|
|
2521
|
-
...data,
|
|
2522
|
-
updatedAt: Date.now()
|
|
2523
|
-
};
|
|
2524
|
-
await this.structured.update(USERS_COLLECTION, id, updatedData);
|
|
2525
|
-
}
|
|
2526
|
-
async delete(id) {
|
|
2527
|
-
await this.structured.delete(USERS_COLLECTION, id);
|
|
2528
|
-
}
|
|
2529
|
-
async resetPassword(id, newPassword) {
|
|
2530
|
-
const existing = await this.findById(id);
|
|
2531
|
-
if (!existing) {
|
|
2532
|
-
throw new Error(`User with id "${id}" not found`);
|
|
2533
|
-
}
|
|
2534
|
-
const passwordHash = await this.auth.hashPassword(newPassword);
|
|
2535
|
-
const updatedData = {
|
|
2536
|
-
...existing,
|
|
2537
|
-
passwordHash,
|
|
2538
|
-
updatedAt: Date.now()
|
|
2539
|
-
};
|
|
2540
|
-
await this.structured.update(USERS_COLLECTION, id, updatedData);
|
|
2541
|
-
}
|
|
2542
|
-
async ensureAdminExists() {
|
|
2543
|
-
const adminUsername = this.config.get("auth.adminUsername");
|
|
2544
|
-
const adminPassword = this.config.get("auth.adminPassword");
|
|
2545
|
-
if (!adminUsername || !adminPassword) {
|
|
2546
|
-
return;
|
|
2547
|
-
}
|
|
2548
|
-
const existing = await this.findByUsername(adminUsername);
|
|
2549
|
-
if (existing) {
|
|
2550
|
-
return;
|
|
2551
|
-
}
|
|
2552
|
-
await this.create({
|
|
2553
|
-
username: adminUsername,
|
|
2554
|
-
password: adminPassword,
|
|
2555
|
-
role: "super_admin",
|
|
2556
|
-
allowedProviders: "*",
|
|
2557
|
-
allowedDevices: {}
|
|
2558
|
-
});
|
|
2559
|
-
}
|
|
2560
|
-
};
|
|
2561
|
-
|
|
2562
|
-
// src/auth/scoped-token-manager.ts
|
|
2563
|
-
import * as crypto4 from "crypto";
|
|
2564
|
-
var TOKENS_COLLECTION = "scoped_tokens";
|
|
2565
|
-
var TOKEN_PREFIX = "cst_";
|
|
2566
|
-
var ScopedTokenManager = class {
|
|
2567
|
-
constructor(storage) {
|
|
2568
|
-
this.storage = storage;
|
|
2569
|
-
}
|
|
2570
|
-
/**
|
|
2571
|
-
* Create a new scoped token. Returns the raw token string (shown once)
|
|
2572
|
-
* and the stored record (with hash, not the raw token).
|
|
2573
|
-
*/
|
|
2574
|
-
async create(userId, name, scopes, expiresAt) {
|
|
2575
|
-
const rawHex = crypto4.randomBytes(32).toString("hex");
|
|
2576
|
-
const rawToken = `${TOKEN_PREFIX}${rawHex}`;
|
|
2577
|
-
const tokenHash = crypto4.createHash("sha256").update(rawToken).digest("hex");
|
|
2578
|
-
const tokenPrefix = rawToken.slice(0, 12);
|
|
2579
|
-
const record = {
|
|
2580
|
-
id: crypto4.randomUUID(),
|
|
2581
|
-
userId,
|
|
2582
|
-
name,
|
|
2583
|
-
tokenHash,
|
|
2584
|
-
tokenPrefix,
|
|
2585
|
-
scopes: scopes.map((s) => ({ ...s })),
|
|
2586
|
-
expiresAt,
|
|
2587
|
-
lastUsedAt: void 0,
|
|
2588
|
-
createdAt: Date.now()
|
|
2589
|
-
};
|
|
2590
|
-
await this.storage.insert({
|
|
2591
|
-
collection: TOKENS_COLLECTION,
|
|
2592
|
-
id: record.id,
|
|
2593
|
-
data: record
|
|
2594
|
-
});
|
|
2595
|
-
return { token: rawToken, record };
|
|
2596
|
-
}
|
|
2597
|
-
/**
|
|
2598
|
-
* Validate a raw token string. Returns the token record if valid, null otherwise.
|
|
2599
|
-
*/
|
|
2600
|
-
async validate(rawToken) {
|
|
2601
|
-
if (!rawToken.startsWith(TOKEN_PREFIX)) {
|
|
2602
|
-
return null;
|
|
2603
|
-
}
|
|
2604
|
-
const tokenHash = crypto4.createHash("sha256").update(rawToken).digest("hex");
|
|
2605
|
-
const results = await this.storage.query(TOKENS_COLLECTION, {
|
|
2606
|
-
where: { tokenHash }
|
|
2607
|
-
});
|
|
2608
|
-
if (results.length === 0) {
|
|
2609
|
-
return null;
|
|
2610
|
-
}
|
|
2611
|
-
const record = results[0].data;
|
|
2612
|
-
if (record.expiresAt !== void 0 && record.expiresAt !== null && Date.now() > record.expiresAt) {
|
|
2613
|
-
return null;
|
|
2614
|
-
}
|
|
2615
|
-
this.updateLastUsed(record.id).catch(() => {
|
|
2616
|
-
});
|
|
2617
|
-
return record;
|
|
2618
|
-
}
|
|
2619
|
-
/**
|
|
2620
|
-
* Check whether a token's scopes grant access to the given addon, route, or capability.
|
|
2621
|
-
*/
|
|
2622
|
-
matchesScope(token, addonId, routePath, capability) {
|
|
2623
|
-
for (const scope of token.scopes) {
|
|
2624
|
-
switch (scope.type) {
|
|
2625
|
-
case "addon":
|
|
2626
|
-
if (addonId && scope.target === addonId) return true;
|
|
2627
|
-
break;
|
|
2628
|
-
case "route-prefix":
|
|
2629
|
-
if (routePath && routePath.startsWith(scope.target)) return true;
|
|
2630
|
-
break;
|
|
2631
|
-
case "capability":
|
|
2632
|
-
if (capability && scope.target === capability) return true;
|
|
2633
|
-
break;
|
|
2634
|
-
}
|
|
2635
|
-
}
|
|
2636
|
-
return false;
|
|
2637
|
-
}
|
|
2638
|
-
/**
|
|
2639
|
-
* Revoke a token by ID.
|
|
2640
|
-
*/
|
|
2641
|
-
async revoke(tokenId) {
|
|
2642
|
-
await this.storage.delete(TOKENS_COLLECTION, tokenId);
|
|
2643
|
-
}
|
|
2644
|
-
/**
|
|
2645
|
-
* List all tokens for a user (without exposing the raw token).
|
|
2646
|
-
*/
|
|
2647
|
-
async listForUser(userId) {
|
|
2648
|
-
const results = await this.storage.query(TOKENS_COLLECTION, {
|
|
2649
|
-
where: { userId }
|
|
2650
|
-
});
|
|
2651
|
-
return results.map((r) => r.data);
|
|
2652
|
-
}
|
|
2653
|
-
/**
|
|
2654
|
-
* Update the lastUsedAt timestamp for a token.
|
|
2655
|
-
*/
|
|
2656
|
-
async updateLastUsed(tokenId) {
|
|
2657
|
-
const results = await this.storage.query(TOKENS_COLLECTION, {
|
|
2658
|
-
where: { id: tokenId }
|
|
2659
|
-
});
|
|
2660
|
-
if (results.length === 0) return;
|
|
2661
|
-
const existing = results[0].data;
|
|
2662
|
-
await this.storage.update(TOKENS_COLLECTION, tokenId, {
|
|
2663
|
-
...existing,
|
|
2664
|
-
lastUsedAt: Date.now()
|
|
2665
|
-
});
|
|
2666
|
-
}
|
|
2667
|
-
};
|
|
2668
|
-
|
|
2669
1548
|
// src/notification/notification-service.ts
|
|
1549
|
+
import { errMsg as errMsg2 } from "@camstack/types";
|
|
2670
1550
|
var NotificationService = class {
|
|
2671
1551
|
constructor(logger) {
|
|
2672
1552
|
this.logger = logger;
|
|
2673
1553
|
}
|
|
1554
|
+
logger;
|
|
2674
1555
|
localOutputs = /* @__PURE__ */ new Map();
|
|
2675
1556
|
routing = /* @__PURE__ */ new Map();
|
|
2676
1557
|
rateLimits = /* @__PURE__ */ new Map();
|
|
@@ -2692,15 +1573,15 @@ var NotificationService = class {
|
|
|
2692
1573
|
}
|
|
2693
1574
|
return this.localOutputs;
|
|
2694
1575
|
}
|
|
2695
|
-
/**
|
|
2696
|
-
|
|
1576
|
+
/** Register an output in the local fallback map (used when no registry is set). */
|
|
1577
|
+
registerLocalOutput(output) {
|
|
2697
1578
|
this.localOutputs.set(output.id, output);
|
|
2698
|
-
this.logger.info(
|
|
1579
|
+
this.logger.info("Notification output added", { meta: { name: output.name, outputId: output.id } });
|
|
2699
1580
|
}
|
|
2700
|
-
/**
|
|
2701
|
-
|
|
1581
|
+
/** Remove an output from the local fallback map. */
|
|
1582
|
+
unregisterLocalOutput(id) {
|
|
2702
1583
|
this.localOutputs.delete(id);
|
|
2703
|
-
this.logger.info(
|
|
1584
|
+
this.logger.info("Notification output removed", { meta: { outputId: id } });
|
|
2704
1585
|
}
|
|
2705
1586
|
setRouting(category, outputIds) {
|
|
2706
1587
|
this.routing.set(category, [...outputIds]);
|
|
@@ -2715,13 +1596,13 @@ var NotificationService = class {
|
|
|
2715
1596
|
if (minInterval > 0) {
|
|
2716
1597
|
const last = this.lastSent.get(rateLimitKey) ?? 0;
|
|
2717
1598
|
if (notification.timestamp - last < minInterval) {
|
|
2718
|
-
this.logger.debug(
|
|
1599
|
+
this.logger.debug("Rate limited", { meta: { rateLimitKey } });
|
|
2719
1600
|
return;
|
|
2720
1601
|
}
|
|
2721
1602
|
}
|
|
2722
1603
|
const targetIds = this.routing.get(category) ?? this.routing.get("*") ?? [];
|
|
2723
1604
|
if (targetIds.length === 0) {
|
|
2724
|
-
this.logger.debug(
|
|
1605
|
+
this.logger.debug("No routing configured for category", { meta: { category } });
|
|
2725
1606
|
return;
|
|
2726
1607
|
}
|
|
2727
1608
|
const currentOutputs = this.outputs;
|
|
@@ -2731,8 +1612,8 @@ var NotificationService = class {
|
|
|
2731
1612
|
try {
|
|
2732
1613
|
await output.send(notification);
|
|
2733
1614
|
} catch (err) {
|
|
2734
|
-
const msg =
|
|
2735
|
-
this.logger.error(
|
|
1615
|
+
const msg = errMsg2(err);
|
|
1616
|
+
this.logger.error("Notification output failed", { meta: { outputId: output.id, error: msg } });
|
|
2736
1617
|
}
|
|
2737
1618
|
})
|
|
2738
1619
|
);
|
|
@@ -2865,142 +1746,19 @@ function matchPath(pattern, path5) {
|
|
|
2865
1746
|
return params;
|
|
2866
1747
|
}
|
|
2867
1748
|
|
|
2868
|
-
// src/device/device-registry.ts
|
|
2869
|
-
import { randomUUID as randomUUID6 } from "crypto";
|
|
2870
|
-
var DeviceRegistry = class {
|
|
2871
|
-
constructor(eventBus, loggingService) {
|
|
2872
|
-
this.eventBus = eventBus;
|
|
2873
|
-
this.logger = loggingService.createLogger("device-registry");
|
|
2874
|
-
}
|
|
2875
|
-
devices = /* @__PURE__ */ new Map();
|
|
2876
|
-
logger;
|
|
2877
|
-
registerDevice(device) {
|
|
2878
|
-
this.devices.set(device.id, device);
|
|
2879
|
-
this.logger.info(`Device registered: ${device.id} (${device.name})`);
|
|
2880
|
-
this.eventBus.emit({
|
|
2881
|
-
id: randomUUID6(),
|
|
2882
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
2883
|
-
source: { type: "core", id: "device-registry" },
|
|
2884
|
-
category: "device.registered",
|
|
2885
|
-
data: { deviceId: device.id, name: device.name, providerId: device.providerId }
|
|
2886
|
-
});
|
|
2887
|
-
}
|
|
2888
|
-
unregisterDevice(id) {
|
|
2889
|
-
const device = this.devices.get(id);
|
|
2890
|
-
if (!device) {
|
|
2891
|
-
return;
|
|
2892
|
-
}
|
|
2893
|
-
this.devices.delete(id);
|
|
2894
|
-
this.logger.info(`Device unregistered: ${id}`);
|
|
2895
|
-
this.eventBus.emit({
|
|
2896
|
-
id: randomUUID6(),
|
|
2897
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
2898
|
-
source: { type: "core", id: "device-registry" },
|
|
2899
|
-
category: "device.unregistered",
|
|
2900
|
-
data: { deviceId: id }
|
|
2901
|
-
});
|
|
2902
|
-
}
|
|
2903
|
-
getDevice(id) {
|
|
2904
|
-
return this.devices.get(id) ?? null;
|
|
2905
|
-
}
|
|
2906
|
-
listDevices() {
|
|
2907
|
-
return Array.from(this.devices.values());
|
|
2908
|
-
}
|
|
2909
|
-
getDevicesByProvider(providerId) {
|
|
2910
|
-
return Array.from(this.devices.values()).filter(
|
|
2911
|
-
(device) => device.providerId === providerId
|
|
2912
|
-
);
|
|
2913
|
-
}
|
|
2914
|
-
getDevicesWithCapability(cap) {
|
|
2915
|
-
return Array.from(this.devices.values()).filter(
|
|
2916
|
-
(device) => device.capabilities.includes(cap)
|
|
2917
|
-
);
|
|
2918
|
-
}
|
|
2919
|
-
registerProviderDevices(providerId, devices) {
|
|
2920
|
-
for (const device of devices) {
|
|
2921
|
-
this.registerDevice(device);
|
|
2922
|
-
}
|
|
2923
|
-
this.logger.info(`Bulk registered ${devices.length} devices for provider ${providerId}`);
|
|
2924
|
-
}
|
|
2925
|
-
unregisterProviderDevices(providerId) {
|
|
2926
|
-
const providerDevices = this.getDevicesByProvider(providerId);
|
|
2927
|
-
for (const device of providerDevices) {
|
|
2928
|
-
this.unregisterDevice(device.id);
|
|
2929
|
-
}
|
|
2930
|
-
this.logger.info(`Bulk unregistered ${providerDevices.length} devices for provider ${providerId}`);
|
|
2931
|
-
}
|
|
2932
|
-
};
|
|
2933
|
-
|
|
2934
|
-
// src/device/capability-resolver.ts
|
|
2935
|
-
var CapabilityResolver = class {
|
|
2936
|
-
constructor(addonRegistry) {
|
|
2937
|
-
this.addonRegistry = addonRegistry;
|
|
2938
|
-
}
|
|
2939
|
-
bindings = /* @__PURE__ */ new Map();
|
|
2940
|
-
resolve(device, cap) {
|
|
2941
|
-
const deviceBindings = this.bindings.get(device.id);
|
|
2942
|
-
const binding = deviceBindings?.[cap];
|
|
2943
|
-
if (binding) {
|
|
2944
|
-
if (binding.source === "disabled") {
|
|
2945
|
-
return null;
|
|
2946
|
-
}
|
|
2947
|
-
if (binding.source === "addon" && binding.addonId) {
|
|
2948
|
-
const addon = this.addonRegistry.getAddon(binding.addonId);
|
|
2949
|
-
if (addon && typeof addon.getCapabilityForDevice === "function") {
|
|
2950
|
-
return addon.getCapabilityForDevice(device, cap, binding.config) ?? null;
|
|
2951
|
-
}
|
|
2952
|
-
return null;
|
|
2953
|
-
}
|
|
2954
|
-
}
|
|
2955
|
-
return device.getCapability(cap);
|
|
2956
|
-
}
|
|
2957
|
-
setBinding(deviceId, cap, binding) {
|
|
2958
|
-
const existing = this.bindings.get(deviceId) ?? {};
|
|
2959
|
-
this.bindings.set(deviceId, { ...existing, [cap]: binding });
|
|
2960
|
-
}
|
|
2961
|
-
removeBinding(deviceId, cap) {
|
|
2962
|
-
const existing = this.bindings.get(deviceId);
|
|
2963
|
-
if (!existing) {
|
|
2964
|
-
return;
|
|
2965
|
-
}
|
|
2966
|
-
const updated = { ...existing };
|
|
2967
|
-
delete updated[cap];
|
|
2968
|
-
this.bindings.set(deviceId, updated);
|
|
2969
|
-
}
|
|
2970
|
-
getBindings(deviceId) {
|
|
2971
|
-
return this.bindings.get(deviceId) ?? {};
|
|
2972
|
-
}
|
|
2973
|
-
getEffectiveCapabilities(device) {
|
|
2974
|
-
const deviceBindings = this.bindings.get(device.id) ?? {};
|
|
2975
|
-
const result = [];
|
|
2976
|
-
for (const cap of device.capabilities) {
|
|
2977
|
-
const binding = deviceBindings[cap];
|
|
2978
|
-
if (!binding || binding.source !== "disabled") {
|
|
2979
|
-
result.push(cap);
|
|
2980
|
-
}
|
|
2981
|
-
}
|
|
2982
|
-
for (const [cap, binding] of Object.entries(deviceBindings)) {
|
|
2983
|
-
if (binding && binding.source === "addon" && !device.capabilities.includes(cap)) {
|
|
2984
|
-
result.push(cap);
|
|
2985
|
-
}
|
|
2986
|
-
}
|
|
2987
|
-
return result;
|
|
2988
|
-
}
|
|
2989
|
-
};
|
|
2990
|
-
|
|
2991
1749
|
// src/tls/cert-manager.ts
|
|
2992
1750
|
import { X509Certificate } from "crypto";
|
|
2993
1751
|
import { execFile as execFile2 } from "child_process";
|
|
2994
1752
|
import { promisify as promisify2 } from "util";
|
|
2995
|
-
import { existsSync as
|
|
2996
|
-
import { join as
|
|
1753
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync } from "fs";
|
|
1754
|
+
import { join as join5 } from "path";
|
|
2997
1755
|
import * as os from "os";
|
|
2998
1756
|
var execFileAsync2 = promisify2(execFile2);
|
|
2999
1757
|
async function ensureTlsCert(dataDir, options) {
|
|
3000
|
-
const tlsDir =
|
|
3001
|
-
const certPath =
|
|
3002
|
-
const keyPath =
|
|
3003
|
-
if (
|
|
1758
|
+
const tlsDir = join5(dataDir, "tls");
|
|
1759
|
+
const certPath = join5(tlsDir, "camstack.crt");
|
|
1760
|
+
const keyPath = join5(tlsDir, "camstack.key");
|
|
1761
|
+
if (existsSync4(certPath) && existsSync4(keyPath)) {
|
|
3004
1762
|
try {
|
|
3005
1763
|
const certPem = readFileSync(certPath);
|
|
3006
1764
|
const x509 = new X509Certificate(certPem);
|
|
@@ -3011,7 +1769,7 @@ async function ensureTlsCert(dataDir, options) {
|
|
|
3011
1769
|
} catch {
|
|
3012
1770
|
}
|
|
3013
1771
|
}
|
|
3014
|
-
|
|
1772
|
+
mkdirSync3(tlsDir, { recursive: true });
|
|
3015
1773
|
const cn = options?.commonName ?? "camstack.local";
|
|
3016
1774
|
const validDays = options?.validDays ?? 825;
|
|
3017
1775
|
const sanDns = /* @__PURE__ */ new Set(["localhost", cn, os.hostname()]);
|
|
@@ -3069,316 +1827,52 @@ function loadTlsCert(certPath, keyPath) {
|
|
|
3069
1827
|
}
|
|
3070
1828
|
|
|
3071
1829
|
// src/addon/addon-api-factory.ts
|
|
1830
|
+
function wrapCallerAsClient(caller) {
|
|
1831
|
+
return new Proxy(/* @__PURE__ */ Object.create(null), {
|
|
1832
|
+
get(_target, prop) {
|
|
1833
|
+
if (typeof prop !== "string") return void 0;
|
|
1834
|
+
if (prop === "query" || prop === "mutate") {
|
|
1835
|
+
if (typeof caller !== "function") {
|
|
1836
|
+
throw new Error(
|
|
1837
|
+
`Cannot call .${prop}() \u2014 this tRPC node is not a procedure`
|
|
1838
|
+
);
|
|
1839
|
+
}
|
|
1840
|
+
const fn = caller;
|
|
1841
|
+
return (input) => fn(input);
|
|
1842
|
+
}
|
|
1843
|
+
if (prop === "subscribe") {
|
|
1844
|
+
return () => {
|
|
1845
|
+
throw new Error(
|
|
1846
|
+
`subscribe() is not supported on direct caller \u2014 use the WSS client for subscriptions`
|
|
1847
|
+
);
|
|
1848
|
+
};
|
|
1849
|
+
}
|
|
1850
|
+
const child = caller?.[prop];
|
|
1851
|
+
if (child === void 0 || child === null) return void 0;
|
|
1852
|
+
return wrapCallerAsClient(child);
|
|
1853
|
+
}
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
3072
1856
|
var AddonApiFactory = class {
|
|
3073
|
-
/**
|
|
3074
|
-
* Build a WSS URL from host and port.
|
|
3075
|
-
*/
|
|
3076
|
-
buildWssUrl(host, port) {
|
|
3077
|
-
return `wss://${host}:${port}/trpc`;
|
|
3078
|
-
}
|
|
3079
1857
|
/**
|
|
3080
1858
|
* Create a direct caller -- calls tRPC procedures directly in-process.
|
|
3081
1859
|
* Zero network overhead. Used for in-process addons and dev mode.
|
|
3082
1860
|
*
|
|
3083
|
-
*
|
|
3084
|
-
*
|
|
3085
|
-
*
|
|
1861
|
+
* Returns `AddonApi` — the same tRPC client surface that broker-routed
|
|
1862
|
+
* callers get. Callers treat it uniformly as `context.api` regardless
|
|
1863
|
+
* of the underlying transport.
|
|
3086
1864
|
*/
|
|
3087
1865
|
async createDirectCaller(options) {
|
|
3088
1866
|
const { initTRPC } = await import("@trpc/server");
|
|
3089
1867
|
const t = initTRPC.create();
|
|
3090
1868
|
const callerFactory = t.createCallerFactory(options.router);
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
/**
|
|
3094
|
-
* Create a WSS tRPC client -- connects to the server via WebSocket.
|
|
3095
|
-
* Used for forked workers and remote agents.
|
|
3096
|
-
*
|
|
3097
|
-
* @param options.url - WSS URL (e.g., wss://localhost:4443/trpc)
|
|
3098
|
-
* @param options.token - Bearer token for authentication
|
|
3099
|
-
* @returns A tRPC client that can be used as context.api
|
|
3100
|
-
*/
|
|
3101
|
-
async createWssClient(options) {
|
|
3102
|
-
const { createTRPCClient, createWSClient, wsLink } = await import("./dist-3BY63UQ5.mjs");
|
|
3103
|
-
const wsClient = createWSClient({
|
|
3104
|
-
url: options.url,
|
|
3105
|
-
connectionParams: () => ({ token: options.token })
|
|
3106
|
-
});
|
|
3107
|
-
const client = createTRPCClient({
|
|
3108
|
-
links: [wsLink({ client: wsClient })]
|
|
3109
|
-
});
|
|
3110
|
-
return {
|
|
3111
|
-
client,
|
|
3112
|
-
close: () => wsClient.close()
|
|
3113
|
-
};
|
|
3114
|
-
}
|
|
3115
|
-
};
|
|
3116
|
-
|
|
3117
|
-
// src/platform/platform-scorer.ts
|
|
3118
|
-
import * as os2 from "os";
|
|
3119
|
-
import { execFileSync as execFileSync3 } from "child_process";
|
|
3120
|
-
async function getAvailableRAM_MB() {
|
|
3121
|
-
try {
|
|
3122
|
-
const si = await import("systeminformation");
|
|
3123
|
-
const mem = await si.mem();
|
|
3124
|
-
return Math.round(mem.available / 1024 / 1024);
|
|
3125
|
-
} catch {
|
|
3126
|
-
const platform2 = os2.platform();
|
|
3127
|
-
try {
|
|
3128
|
-
if (platform2 === "darwin") {
|
|
3129
|
-
const output = execFileSync3("vm_stat", { encoding: "utf8", timeout: 3e3 });
|
|
3130
|
-
const pageSize = parseInt(output.match(/page size of (\d+)/)?.[1] ?? "16384");
|
|
3131
|
-
const free = parseInt(output.match(/Pages free:\s+(\d+)/)?.[1] ?? "0");
|
|
3132
|
-
const inactive = parseInt(output.match(/Pages inactive:\s+(\d+)/)?.[1] ?? "0");
|
|
3133
|
-
const purgeable = parseInt(output.match(/Pages purgeable:\s+(\d+)/)?.[1] ?? "0");
|
|
3134
|
-
return Math.round((free + inactive + purgeable) * pageSize / 1024 / 1024);
|
|
3135
|
-
} else if (platform2 === "linux") {
|
|
3136
|
-
const { readFileSync: readFileSync2 } = await import("fs");
|
|
3137
|
-
const meminfo = readFileSync2("/proc/meminfo", "utf8");
|
|
3138
|
-
const match = meminfo.match(/MemAvailable:\s+(\d+)\s+kB/);
|
|
3139
|
-
if (match) return Math.round(parseInt(match[1]) / 1024);
|
|
3140
|
-
}
|
|
3141
|
-
} catch {
|
|
3142
|
-
}
|
|
3143
|
-
return Math.round(os2.totalmem() / 1024 / 1024);
|
|
3144
|
-
}
|
|
3145
|
-
}
|
|
3146
|
-
var PlatformScorer = class {
|
|
3147
|
-
cached = null;
|
|
3148
|
-
/** Probe hardware + runtimes and score all backend combos. Cached after first call. */
|
|
3149
|
-
async probe() {
|
|
3150
|
-
if (this.cached) return this.cached;
|
|
3151
|
-
const start = Date.now();
|
|
3152
|
-
console.log("[PlatformScorer] Probing hardware...");
|
|
3153
|
-
const hardware = await this.probeHardware();
|
|
3154
|
-
console.log(`[PlatformScorer] Hardware: ${hardware.platform}/${hardware.arch}, ${hardware.cpuModel} (${hardware.cpuCores} cores), RAM ${Math.round(hardware.totalRAM_MB / 1024)}GB`);
|
|
3155
|
-
if (hardware.gpu) console.log(`[PlatformScorer] GPU: ${hardware.gpu.name}`);
|
|
3156
|
-
if (hardware.npu) console.log(`[PlatformScorer] NPU: ${hardware.npu.type}`);
|
|
3157
|
-
console.log("[PlatformScorer] Probing Node.js backends...");
|
|
3158
|
-
const nodeBackends = await this.probeNodeBackends();
|
|
3159
|
-
console.log(`[PlatformScorer] Node backends: ${nodeBackends.map((b) => b.id).join(", ")}`);
|
|
3160
|
-
console.log("[PlatformScorer] Probing Python backends...");
|
|
3161
|
-
const pythonInfo = this.probePythonBackends();
|
|
3162
|
-
if (pythonInfo.pythonPath) {
|
|
3163
|
-
console.log(`[PlatformScorer] Python: ${pythonInfo.pythonPath} \u2192 ${pythonInfo.backends.filter((b) => b.available).map((b) => b.id).join(", ") || "no backends"}`);
|
|
3164
|
-
} else {
|
|
3165
|
-
console.log("[PlatformScorer] Python: not found");
|
|
3166
|
-
}
|
|
3167
|
-
const scores = this.scoreBackends(hardware, nodeBackends, pythonInfo.backends);
|
|
3168
|
-
const bestScore = scores.find((s) => s.available) ?? scores[scores.length - 1];
|
|
3169
|
-
const elapsed = Date.now() - start;
|
|
3170
|
-
console.log(`[PlatformScorer] Scoring complete in ${elapsed}ms \u2014 ${scores.length} combos`);
|
|
3171
|
-
console.log(`[PlatformScorer] Best: ${bestScore.runtime}/${bestScore.backend} (${bestScore.format}) \u2014 ${bestScore.reason} [score: ${bestScore.score}]`);
|
|
3172
|
-
for (const s of scores) {
|
|
3173
|
-
console.log(`[PlatformScorer] ${s.available ? "\u2713" : "\u2717"} ${s.runtime}/${s.backend} (${s.format}) \u2014 score ${s.score} \u2014 ${s.reason}`);
|
|
3174
|
-
}
|
|
3175
|
-
this.cached = {
|
|
3176
|
-
hardware,
|
|
3177
|
-
scores,
|
|
3178
|
-
bestScore,
|
|
3179
|
-
pythonPath: pythonInfo.pythonPath
|
|
3180
|
-
};
|
|
3181
|
-
return this.cached;
|
|
3182
|
-
}
|
|
3183
|
-
async probeHardware() {
|
|
3184
|
-
const platform2 = os2.platform();
|
|
3185
|
-
const arch2 = os2.arch();
|
|
3186
|
-
const cpus2 = os2.cpus();
|
|
3187
|
-
const cpuModel = cpus2[0]?.model ?? "unknown";
|
|
3188
|
-
const cpuCores = cpus2.length;
|
|
3189
|
-
const totalRAM_MB = Math.round(os2.totalmem() / 1024 / 1024);
|
|
3190
|
-
const availableRAM_MB = await getAvailableRAM_MB();
|
|
3191
|
-
console.log(`[PlatformScorer] RAM: total=${totalRAM_MB}MB, available=${availableRAM_MB}MB (via systeminformation)`);
|
|
3192
|
-
let gpu = null;
|
|
3193
|
-
let npu = null;
|
|
3194
|
-
if (platform2 === "darwin" && arch2 === "arm64") {
|
|
3195
|
-
gpu = { type: "apple", name: "Apple Silicon GPU" };
|
|
3196
|
-
npu = { type: "apple-ane" };
|
|
3197
|
-
}
|
|
3198
|
-
if (platform2 === "linux") {
|
|
3199
|
-
try {
|
|
3200
|
-
const output = execFileSync3("nvidia-smi", ["--query-gpu=name,memory.total", "--format=csv,noheader"], {
|
|
3201
|
-
encoding: "utf8",
|
|
3202
|
-
timeout: 5e3,
|
|
3203
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
3204
|
-
}).trim();
|
|
3205
|
-
if (output) {
|
|
3206
|
-
const [name, mem] = output.split(",").map((s) => s.trim());
|
|
3207
|
-
gpu = {
|
|
3208
|
-
type: "nvidia",
|
|
3209
|
-
name: name ?? "NVIDIA GPU",
|
|
3210
|
-
memoryMB: parseInt(mem ?? "0")
|
|
3211
|
-
};
|
|
3212
|
-
}
|
|
3213
|
-
} catch {
|
|
3214
|
-
}
|
|
3215
|
-
}
|
|
3216
|
-
return { platform: platform2, arch: arch2, cpuModel, cpuCores, totalRAM_MB, availableRAM_MB, gpu, npu };
|
|
3217
|
-
}
|
|
3218
|
-
async probeNodeBackends() {
|
|
3219
|
-
const backends = [
|
|
3220
|
-
{ id: "cpu", available: true }
|
|
3221
|
-
];
|
|
3222
|
-
try {
|
|
3223
|
-
const ort = await import("onnxruntime-node");
|
|
3224
|
-
const providers = ort.InferenceSession?.getAvailableProviders?.() ?? [];
|
|
3225
|
-
for (const p of providers) {
|
|
3226
|
-
const n = p.toLowerCase().replace("executionprovider", "");
|
|
3227
|
-
if (n === "coreml") backends.push({ id: "coreml", available: true });
|
|
3228
|
-
else if (n === "cuda") backends.push({ id: "cuda", available: true });
|
|
3229
|
-
else if (n === "tensorrt") backends.push({ id: "tensorrt", available: true });
|
|
3230
|
-
}
|
|
3231
|
-
} catch {
|
|
3232
|
-
}
|
|
3233
|
-
if (os2.platform() === "darwin" && !backends.some((b) => b.id === "coreml")) {
|
|
3234
|
-
backends.push({ id: "coreml", available: true });
|
|
3235
|
-
}
|
|
3236
|
-
return backends;
|
|
3237
|
-
}
|
|
3238
|
-
probePythonBackends() {
|
|
3239
|
-
let pythonPath = null;
|
|
3240
|
-
for (const cmd of ["python3", "python"]) {
|
|
3241
|
-
try {
|
|
3242
|
-
execFileSync3(cmd, ["--version"], { timeout: 5e3, stdio: "pipe" });
|
|
3243
|
-
pythonPath = cmd;
|
|
3244
|
-
break;
|
|
3245
|
-
} catch {
|
|
3246
|
-
}
|
|
3247
|
-
}
|
|
3248
|
-
if (!pythonPath) return { pythonPath: null, backends: [] };
|
|
3249
|
-
const checks = [
|
|
3250
|
-
// [pythonModule, backendId, modelFormat]
|
|
3251
|
-
["coremltools", "coreml", "coreml"],
|
|
3252
|
-
["openvino.runtime", "openvino", "openvino"],
|
|
3253
|
-
["torch", "pytorch", "onnx"],
|
|
3254
|
-
["onnxruntime", "onnx-py", "onnx"]
|
|
3255
|
-
];
|
|
3256
|
-
const backends = [];
|
|
3257
|
-
for (const [mod, id, format] of checks) {
|
|
3258
|
-
let available = false;
|
|
3259
|
-
const probeStart = Date.now();
|
|
3260
|
-
try {
|
|
3261
|
-
execFileSync3(pythonPath, ["-c", `import ${mod}`], { timeout: 3e4, stdio: "ignore" });
|
|
3262
|
-
available = true;
|
|
3263
|
-
} catch {
|
|
3264
|
-
}
|
|
3265
|
-
const probeMs = Date.now() - probeStart;
|
|
3266
|
-
console.log(`[PlatformScorer] Python ${mod}: ${available ? "\u2713" : "\u2717"} (${probeMs}ms)`);
|
|
3267
|
-
if (id === "coreml" && os2.platform() === "darwin") {
|
|
3268
|
-
backends.push({ id, format, available });
|
|
3269
|
-
} else if (available) {
|
|
3270
|
-
backends.push({ id, format, available });
|
|
3271
|
-
}
|
|
3272
|
-
}
|
|
3273
|
-
return { pythonPath, backends };
|
|
3274
|
-
}
|
|
3275
|
-
scoreBackends(hardware, nodeBackends, pythonBackends) {
|
|
3276
|
-
const scores = [];
|
|
3277
|
-
if (hardware.platform === "darwin" && hardware.arch === "arm64") {
|
|
3278
|
-
const pyCoreMl = pythonBackends.find((b) => b.id === "coreml");
|
|
3279
|
-
if (pyCoreMl) {
|
|
3280
|
-
scores.push({ runtime: "python", backend: "coreml", format: "coreml", score: 95, reason: "Apple Neural Engine (Python CoreML)", available: pyCoreMl.available });
|
|
3281
|
-
}
|
|
3282
|
-
const nodeCoreMl = nodeBackends.find((b) => b.id === "coreml");
|
|
3283
|
-
if (nodeCoreMl) {
|
|
3284
|
-
scores.push({ runtime: "node", backend: "coreml", format: "onnx", score: 90, reason: "CoreML via ONNX Runtime", available: nodeCoreMl.available });
|
|
3285
|
-
}
|
|
3286
|
-
}
|
|
3287
|
-
if (hardware.gpu?.type === "nvidia") {
|
|
3288
|
-
const tensorrt = nodeBackends.find((b) => b.id === "tensorrt");
|
|
3289
|
-
if (tensorrt) scores.push({ runtime: "node", backend: "tensorrt", format: "onnx", score: 95, reason: "NVIDIA TensorRT", available: true });
|
|
3290
|
-
const cuda = nodeBackends.find((b) => b.id === "cuda");
|
|
3291
|
-
if (cuda) scores.push({ runtime: "node", backend: "cuda", format: "onnx", score: 85, reason: "NVIDIA CUDA", available: true });
|
|
3292
|
-
}
|
|
3293
|
-
const openvino = pythonBackends.find((b) => b.id === "openvino");
|
|
3294
|
-
if (openvino) {
|
|
3295
|
-
const score = hardware.npu?.type === "intel-npu" ? 90 : 80;
|
|
3296
|
-
scores.push({ runtime: "python", backend: "openvino", format: "openvino", score, reason: "Intel OpenVINO", available: openvino.available });
|
|
3297
|
-
}
|
|
3298
|
-
scores.push({ runtime: "node", backend: "cpu", format: "onnx", score: 50, reason: "CPU (ONNX Runtime Node)", available: true });
|
|
3299
|
-
const pyOnnx = pythonBackends.find((b) => b.id === "onnx-py");
|
|
3300
|
-
if (pyOnnx) {
|
|
3301
|
-
scores.push({ runtime: "python", backend: "cpu", format: "onnx", score: 45, reason: "CPU (Python ONNX)", available: pyOnnx.available });
|
|
3302
|
-
}
|
|
3303
|
-
return scores.sort((a, b) => b.score - a.score);
|
|
3304
|
-
}
|
|
3305
|
-
};
|
|
3306
|
-
|
|
3307
|
-
// src/platform/inference-config-resolver.ts
|
|
3308
|
-
var InferenceConfigResolver = class {
|
|
3309
|
-
constructor(scores, hardware) {
|
|
3310
|
-
this.scores = scores;
|
|
3311
|
-
this.hardware = hardware;
|
|
3312
|
-
}
|
|
3313
|
-
/**
|
|
3314
|
-
* Compute accuracy/backend weights based on available system RAM.
|
|
3315
|
-
* availableRAM_MB is now sourced from systeminformation (reliable cross-platform),
|
|
3316
|
-
* not os.freemem() which is broken on macOS.
|
|
3317
|
-
*
|
|
3318
|
-
* - > 16 GB available: prefer larger, more accurate models (accuracy 0.6, backend 0.4)
|
|
3319
|
-
* - > 8 GB available: balanced (accuracy 0.5, backend 0.5)
|
|
3320
|
-
* - <= 8 GB available: prefer speed (accuracy 0.4, backend 0.6)
|
|
3321
|
-
*/
|
|
3322
|
-
getWeights() {
|
|
3323
|
-
const ramMB = this.hardware.availableRAM_MB;
|
|
3324
|
-
if (ramMB > 16384) return { accuracyWeight: 0.6, backendWeight: 0.4 };
|
|
3325
|
-
if (ramMB > 8192) return { accuracyWeight: 0.5, backendWeight: 0.5 };
|
|
3326
|
-
return { accuracyWeight: 0.4, backendWeight: 0.6 };
|
|
3327
|
-
}
|
|
3328
|
-
/**
|
|
3329
|
-
* Given an addon's model requirements, pick the best model + runtime + backend.
|
|
3330
|
-
*
|
|
3331
|
-
* Algorithm:
|
|
3332
|
-
* 1. Filter models by available RAM (minRAM_MB < 25% of available RAM)
|
|
3333
|
-
* 2. For each remaining model, find the best platform score whose format
|
|
3334
|
-
* is available in the model's formats
|
|
3335
|
-
* 3. Pick the model with the highest combined score using RAM-adaptive weights:
|
|
3336
|
-
* - High RAM (>16 GB): accuracy × 0.6 + backend × 0.4 (prefer accuracy)
|
|
3337
|
-
* - Mid RAM (>8 GB): accuracy × 0.5 + backend × 0.5 (balanced)
|
|
3338
|
-
* - Low RAM (<=8 GB): accuracy × 0.4 + backend × 0.6 (prefer speed)
|
|
3339
|
-
*/
|
|
3340
|
-
resolve(requirements) {
|
|
3341
|
-
if (requirements.length === 0) {
|
|
3342
|
-
return { modelId: "", runtime: "node", backend: "cpu", format: "onnx", reason: "No models declared" };
|
|
3343
|
-
}
|
|
3344
|
-
const ramBudget = this.hardware.availableRAM_MB * 0.25;
|
|
3345
|
-
const { accuracyWeight, backendWeight } = this.getWeights();
|
|
3346
|
-
console.log(`[InferenceConfigResolver] availableRAM: ${this.hardware.availableRAM_MB}MB, budget: ${Math.round(ramBudget)}MB, weights: accuracy=${accuracyWeight}, backend=${backendWeight}`);
|
|
3347
|
-
const fits = requirements.filter((m) => m.minRAM_MB < ramBudget);
|
|
3348
|
-
const candidates = fits.length > 0 ? fits : [requirements[0]];
|
|
3349
|
-
console.log(`[InferenceConfigResolver] ${candidates.length}/${requirements.length} models fit RAM budget`);
|
|
3350
|
-
let bestCombo = null;
|
|
3351
|
-
for (const model of candidates) {
|
|
3352
|
-
for (const score of this.scores) {
|
|
3353
|
-
if (!score.available) continue;
|
|
3354
|
-
if (!model.formats.includes(score.format)) continue;
|
|
3355
|
-
const combined = model.accuracyScore * accuracyWeight + score.score * backendWeight;
|
|
3356
|
-
if (!bestCombo || combined > bestCombo.combined) {
|
|
3357
|
-
console.log(`[InferenceConfigResolver] New best: ${model.modelId} (accuracy=${model.accuracyScore}) + ${score.backend}/${score.format} (score=${score.score}) \u2192 combined=${Math.round(combined)}`);
|
|
3358
|
-
bestCombo = { model, score, combined };
|
|
3359
|
-
}
|
|
3360
|
-
}
|
|
3361
|
-
}
|
|
3362
|
-
if (!bestCombo) {
|
|
3363
|
-
return {
|
|
3364
|
-
modelId: candidates[0].modelId,
|
|
3365
|
-
runtime: "node",
|
|
3366
|
-
backend: "cpu",
|
|
3367
|
-
format: "onnx",
|
|
3368
|
-
reason: "No compatible backend \u2014 CPU fallback"
|
|
3369
|
-
};
|
|
3370
|
-
}
|
|
3371
|
-
return {
|
|
3372
|
-
modelId: bestCombo.model.modelId,
|
|
3373
|
-
runtime: bestCombo.score.runtime,
|
|
3374
|
-
backend: bestCombo.score.backend,
|
|
3375
|
-
format: bestCombo.score.format,
|
|
3376
|
-
reason: `${bestCombo.model.name} on ${bestCombo.score.reason} (score: ${Math.round(bestCombo.combined)})`
|
|
3377
|
-
};
|
|
1869
|
+
const caller = callerFactory(options.context);
|
|
1870
|
+
return wrapCallerAsClient(caller);
|
|
3378
1871
|
}
|
|
3379
1872
|
};
|
|
3380
1873
|
|
|
3381
1874
|
// src/builtins/sqlite-storage/integration-registry.ts
|
|
1875
|
+
import { parseJsonObject } from "@camstack/types";
|
|
3382
1876
|
function serializeSetting(value) {
|
|
3383
1877
|
if (typeof value === "number") return { value: String(value), valueType: "number" };
|
|
3384
1878
|
if (typeof value === "boolean") return { value: String(value), valueType: "boolean" };
|
|
@@ -3479,10 +1973,10 @@ var IntegrationRegistry = class {
|
|
|
3479
1973
|
}
|
|
3480
1974
|
}
|
|
3481
1975
|
// --- Integrations ---
|
|
3482
|
-
createIntegration(input) {
|
|
1976
|
+
async createIntegration(input) {
|
|
3483
1977
|
const id = nextIntegrationId();
|
|
3484
1978
|
const now = Math.floor(Date.now() / 1e3);
|
|
3485
|
-
|
|
1979
|
+
await this.backend.tableInsert?.("integrations", {
|
|
3486
1980
|
id,
|
|
3487
1981
|
addon_id: input.addonId,
|
|
3488
1982
|
name: input.name,
|
|
@@ -3492,7 +1986,7 @@ var IntegrationRegistry = class {
|
|
|
3492
1986
|
updated_at: now
|
|
3493
1987
|
});
|
|
3494
1988
|
if (input.settings) {
|
|
3495
|
-
this.setIntegrationSettings(id, input.settings);
|
|
1989
|
+
await this.setIntegrationSettings(id, input.settings);
|
|
3496
1990
|
}
|
|
3497
1991
|
return {
|
|
3498
1992
|
id,
|
|
@@ -3504,75 +1998,65 @@ var IntegrationRegistry = class {
|
|
|
3504
1998
|
updatedAt: now
|
|
3505
1999
|
};
|
|
3506
2000
|
}
|
|
3507
|
-
getIntegration(id) {
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
if (row) result = this.mapIntegration(row);
|
|
3511
|
-
});
|
|
3512
|
-
return result;
|
|
2001
|
+
async getIntegration(id) {
|
|
2002
|
+
const row = await this.backend.tableGet?.("integrations", { id });
|
|
2003
|
+
return row ? this.mapIntegration(row) : null;
|
|
3513
2004
|
}
|
|
3514
|
-
getIntegrationByAddonId(addonId) {
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
if (row) result = this.mapIntegration(row);
|
|
3518
|
-
});
|
|
3519
|
-
return result;
|
|
2005
|
+
async getIntegrationByAddonId(addonId) {
|
|
2006
|
+
const row = await this.backend.tableGet?.("integrations", { addon_id: addonId });
|
|
2007
|
+
return row ? this.mapIntegration(row) : null;
|
|
3520
2008
|
}
|
|
3521
|
-
listIntegrations() {
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
result = rows.map((r) => this.mapIntegration(r));
|
|
3525
|
-
});
|
|
3526
|
-
return result;
|
|
2009
|
+
async listIntegrations() {
|
|
2010
|
+
const rows = await this.backend.tableQuery?.("integrations", { orderBy: { field: "created_at", direction: "asc" } }) ?? [];
|
|
2011
|
+
return rows.map((r) => this.mapIntegration(r));
|
|
3527
2012
|
}
|
|
3528
|
-
updateIntegration(id, updates) {
|
|
2013
|
+
async updateIntegration(id, updates) {
|
|
3529
2014
|
const updateRow = { updated_at: Math.floor(Date.now() / 1e3) };
|
|
3530
2015
|
if (updates.name !== void 0) updateRow["name"] = updates.name;
|
|
3531
2016
|
if (updates.enabled !== void 0) updateRow["enabled"] = updates.enabled ? 1 : 0;
|
|
3532
2017
|
if (updates.info !== void 0) updateRow["info"] = JSON.stringify(updates.info);
|
|
3533
|
-
|
|
2018
|
+
await this.backend.tableUpdate?.("integrations", { id }, updateRow);
|
|
3534
2019
|
return this.getIntegration(id);
|
|
3535
2020
|
}
|
|
3536
|
-
deleteIntegration(id) {
|
|
3537
|
-
const devices = this.listDevices(id);
|
|
2021
|
+
async deleteIntegration(id) {
|
|
2022
|
+
const devices = await this.listDevices(id);
|
|
3538
2023
|
for (const d of devices) {
|
|
3539
|
-
|
|
2024
|
+
await this.backend.tableDelete?.("device_settings_kv", { device_id: d.id });
|
|
3540
2025
|
}
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
2026
|
+
await this.backend.tableDelete?.("devices", { integration_id: id });
|
|
2027
|
+
await this.backend.tableDelete?.("integration_settings", { integration_id: id });
|
|
2028
|
+
await this.backend.tableDelete?.("integrations", { id });
|
|
3544
2029
|
return true;
|
|
3545
2030
|
}
|
|
3546
2031
|
// --- Integration Settings ---
|
|
3547
|
-
getIntegrationSettings(integrationId) {
|
|
2032
|
+
async getIntegrationSettings(integrationId) {
|
|
3548
2033
|
const result = {};
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
});
|
|
2034
|
+
const rows = await this.backend.tableQuery?.("integration_settings", { where: { integration_id: integrationId } }) ?? [];
|
|
2035
|
+
for (const row of rows) {
|
|
2036
|
+
result[String(row["key"])] = deserializeSetting(String(row["value"]), String(row["value_type"]));
|
|
2037
|
+
}
|
|
3554
2038
|
return result;
|
|
3555
2039
|
}
|
|
3556
|
-
setIntegrationSetting(integrationId, key, value) {
|
|
2040
|
+
async setIntegrationSetting(integrationId, key, value) {
|
|
3557
2041
|
const s = serializeSetting(value);
|
|
3558
|
-
|
|
3559
|
-
|
|
2042
|
+
await this.backend.tableDelete?.("integration_settings", { integration_id: integrationId, key });
|
|
2043
|
+
await this.backend.tableInsert?.("integration_settings", {
|
|
3560
2044
|
integration_id: integrationId,
|
|
3561
2045
|
key,
|
|
3562
2046
|
value: s.value,
|
|
3563
2047
|
value_type: s.valueType
|
|
3564
2048
|
});
|
|
3565
2049
|
}
|
|
3566
|
-
setIntegrationSettings(integrationId, settings) {
|
|
2050
|
+
async setIntegrationSettings(integrationId, settings) {
|
|
3567
2051
|
for (const [key, value] of Object.entries(settings)) {
|
|
3568
|
-
this.setIntegrationSetting(integrationId, key, value);
|
|
2052
|
+
await this.setIntegrationSetting(integrationId, key, value);
|
|
3569
2053
|
}
|
|
3570
2054
|
}
|
|
3571
2055
|
// --- Devices ---
|
|
3572
|
-
createDevice(input) {
|
|
2056
|
+
async createDevice(input) {
|
|
3573
2057
|
const id = nextDeviceId();
|
|
3574
2058
|
const now = Math.floor(Date.now() / 1e3);
|
|
3575
|
-
|
|
2059
|
+
await this.backend.tableInsert?.("devices", {
|
|
3576
2060
|
id,
|
|
3577
2061
|
integration_id: input.integrationId,
|
|
3578
2062
|
stable_id: input.stableId,
|
|
@@ -3584,7 +2068,7 @@ var IntegrationRegistry = class {
|
|
|
3584
2068
|
updated_at: now
|
|
3585
2069
|
});
|
|
3586
2070
|
if (input.settings) {
|
|
3587
|
-
this.setDeviceSettings(id, input.settings);
|
|
2071
|
+
await this.setDeviceSettings(id, input.settings);
|
|
3588
2072
|
}
|
|
3589
2073
|
return {
|
|
3590
2074
|
id,
|
|
@@ -3598,71 +2082,58 @@ var IntegrationRegistry = class {
|
|
|
3598
2082
|
updatedAt: now
|
|
3599
2083
|
};
|
|
3600
2084
|
}
|
|
3601
|
-
getDevice(id) {
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
if (row) result = this.mapDevice(row);
|
|
3605
|
-
});
|
|
3606
|
-
return result;
|
|
2085
|
+
async getDevice(id) {
|
|
2086
|
+
const row = await this.backend.tableGet?.("devices", { id });
|
|
2087
|
+
return row ? this.mapDevice(row) : null;
|
|
3607
2088
|
}
|
|
3608
|
-
getDeviceByStableId(stableId) {
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
if (row) result = this.mapDevice(row);
|
|
3612
|
-
});
|
|
3613
|
-
return result;
|
|
2089
|
+
async getDeviceByStableId(stableId) {
|
|
2090
|
+
const row = await this.backend.tableGet?.("devices", { stable_id: stableId });
|
|
2091
|
+
return row ? this.mapDevice(row) : null;
|
|
3614
2092
|
}
|
|
3615
|
-
listDevices(integrationId) {
|
|
3616
|
-
let result = [];
|
|
2093
|
+
async listDevices(integrationId) {
|
|
3617
2094
|
const options = integrationId ? { where: { integration_id: integrationId }, orderBy: { field: "created_at", direction: "asc" } } : { orderBy: { field: "created_at", direction: "asc" } };
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
});
|
|
3621
|
-
return result;
|
|
2095
|
+
const rows = await this.backend.tableQuery?.("devices", options) ?? [];
|
|
2096
|
+
return rows.map((r) => this.mapDevice(r));
|
|
3622
2097
|
}
|
|
3623
|
-
listCameras() {
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
result = rows.map((r) => this.mapDevice(r));
|
|
3627
|
-
});
|
|
3628
|
-
return result;
|
|
2098
|
+
async listCameras() {
|
|
2099
|
+
const rows = await this.backend.tableQuery?.("devices", { where: { type: "camera" }, orderBy: { field: "created_at", direction: "asc" } }) ?? [];
|
|
2100
|
+
return rows.map((r) => this.mapDevice(r));
|
|
3629
2101
|
}
|
|
3630
|
-
updateDevice(id, updates) {
|
|
2102
|
+
async updateDevice(id, updates) {
|
|
3631
2103
|
const updateRow = { updated_at: Math.floor(Date.now() / 1e3) };
|
|
3632
2104
|
if (updates.name !== void 0) updateRow["name"] = updates.name;
|
|
3633
2105
|
if (updates.enabled !== void 0) updateRow["enabled"] = updates.enabled ? 1 : 0;
|
|
3634
2106
|
if (updates.info !== void 0) updateRow["info"] = JSON.stringify(updates.info);
|
|
3635
|
-
|
|
2107
|
+
await this.backend.tableUpdate?.("devices", { id }, updateRow);
|
|
3636
2108
|
return this.getDevice(id);
|
|
3637
2109
|
}
|
|
3638
|
-
deleteDevice(id) {
|
|
3639
|
-
|
|
3640
|
-
|
|
2110
|
+
async deleteDevice(id) {
|
|
2111
|
+
await this.backend.tableDelete?.("device_settings_kv", { device_id: id });
|
|
2112
|
+
await this.backend.tableDelete?.("devices", { id });
|
|
3641
2113
|
return true;
|
|
3642
2114
|
}
|
|
3643
2115
|
// --- Device Settings ---
|
|
3644
|
-
getDeviceSettings(deviceId) {
|
|
2116
|
+
async getDeviceSettings(deviceId) {
|
|
3645
2117
|
const result = {};
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
});
|
|
2118
|
+
const rows = await this.backend.tableQuery?.("device_settings_kv", { where: { device_id: deviceId } }) ?? [];
|
|
2119
|
+
for (const row of rows) {
|
|
2120
|
+
result[String(row["key"])] = deserializeSetting(String(row["value"]), String(row["value_type"]));
|
|
2121
|
+
}
|
|
3651
2122
|
return result;
|
|
3652
2123
|
}
|
|
3653
|
-
setDeviceSetting(deviceId, key, value) {
|
|
2124
|
+
async setDeviceSetting(deviceId, key, value) {
|
|
3654
2125
|
const s = serializeSetting(value);
|
|
3655
|
-
|
|
3656
|
-
|
|
2126
|
+
await this.backend.tableDelete?.("device_settings_kv", { device_id: deviceId, key });
|
|
2127
|
+
await this.backend.tableInsert?.("device_settings_kv", {
|
|
3657
2128
|
device_id: deviceId,
|
|
3658
2129
|
key,
|
|
3659
2130
|
value: s.value,
|
|
3660
2131
|
value_type: s.valueType
|
|
3661
2132
|
});
|
|
3662
2133
|
}
|
|
3663
|
-
setDeviceSettings(deviceId, settings) {
|
|
2134
|
+
async setDeviceSettings(deviceId, settings) {
|
|
3664
2135
|
for (const [key, value] of Object.entries(settings)) {
|
|
3665
|
-
this.setDeviceSetting(deviceId, key, value);
|
|
2136
|
+
await this.setDeviceSetting(deviceId, key, value);
|
|
3666
2137
|
}
|
|
3667
2138
|
}
|
|
3668
2139
|
// --- Mappers ---
|
|
@@ -3672,7 +2143,7 @@ var IntegrationRegistry = class {
|
|
|
3672
2143
|
addonId: String(row["addon_id"]),
|
|
3673
2144
|
name: String(row["name"]),
|
|
3674
2145
|
enabled: row["enabled"] === 1,
|
|
3675
|
-
info: typeof row["info"] === "string" ?
|
|
2146
|
+
info: typeof row["info"] === "string" ? parseJsonObject(row["info"]) ?? {} : {},
|
|
3676
2147
|
createdAt: Number(row["created_at"]),
|
|
3677
2148
|
updatedAt: Number(row["updated_at"])
|
|
3678
2149
|
};
|
|
@@ -3685,214 +2156,85 @@ var IntegrationRegistry = class {
|
|
|
3685
2156
|
type: String(row["type"]),
|
|
3686
2157
|
name: String(row["name"]),
|
|
3687
2158
|
enabled: row["enabled"] === 1,
|
|
3688
|
-
info: typeof row["info"] === "string" ?
|
|
2159
|
+
info: typeof row["info"] === "string" ? parseJsonObject(row["info"]) ?? {} : {},
|
|
3689
2160
|
createdAt: Number(row["created_at"]),
|
|
3690
2161
|
updatedAt: Number(row["updated_at"])
|
|
3691
2162
|
};
|
|
3692
2163
|
}
|
|
3693
2164
|
};
|
|
3694
|
-
|
|
3695
|
-
// src/provider/provider-manager.ts
|
|
3696
|
-
import { randomUUID as randomUUID7 } from "crypto";
|
|
3697
|
-
var ProviderManager = class {
|
|
3698
|
-
constructor(deviceRegistry, eventBus, loggingService) {
|
|
3699
|
-
this.deviceRegistry = deviceRegistry;
|
|
3700
|
-
this.eventBus = eventBus;
|
|
3701
|
-
this.loggingService = loggingService;
|
|
3702
|
-
this.logger = loggingService.createLogger("provider-manager");
|
|
3703
|
-
}
|
|
3704
|
-
providers = /* @__PURE__ */ new Map();
|
|
3705
|
-
logger;
|
|
3706
|
-
registerProvider(provider) {
|
|
3707
|
-
const providerLogger = this.loggingService.createLogger(`provider:${provider.id}`);
|
|
3708
|
-
const lifecycle = new LifecycleStateMachine(
|
|
3709
|
-
provider.id,
|
|
3710
|
-
"provider",
|
|
3711
|
-
this.eventBus,
|
|
3712
|
-
providerLogger
|
|
3713
|
-
);
|
|
3714
|
-
this.providers.set(provider.id, { provider, lifecycle, started: false });
|
|
3715
|
-
this.logger.info(`Provider registered: ${provider.id} (${provider.name})`);
|
|
3716
|
-
}
|
|
3717
|
-
async startProvider(id) {
|
|
3718
|
-
const entry = this.providers.get(id);
|
|
3719
|
-
if (!entry) {
|
|
3720
|
-
throw new Error(`Provider "${id}" is not registered`);
|
|
3721
|
-
}
|
|
3722
|
-
if (entry.lifecycle.state === "disabled") {
|
|
3723
|
-
throw new Error(`Provider "${id}" is disabled`);
|
|
3724
|
-
}
|
|
3725
|
-
entry.lifecycle.transition("starting");
|
|
3726
|
-
try {
|
|
3727
|
-
await entry.provider.start();
|
|
3728
|
-
} catch (err) {
|
|
3729
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
3730
|
-
entry.lifecycle.transition("error", message);
|
|
3731
|
-
throw err;
|
|
3732
|
-
}
|
|
3733
|
-
entry.lifecycle.transition("running");
|
|
3734
|
-
const devices = entry.provider.getDevices();
|
|
3735
|
-
this.deviceRegistry.registerProviderDevices(id, devices);
|
|
3736
|
-
const unsubscribe = entry.provider.subscribeLiveEvents((event) => {
|
|
3737
|
-
this.eventBus.emit({
|
|
3738
|
-
id: randomUUID7(),
|
|
3739
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
3740
|
-
source: { type: "provider", id },
|
|
3741
|
-
category: `provider.${event.type}`,
|
|
3742
|
-
data: event.data
|
|
3743
|
-
});
|
|
3744
|
-
});
|
|
3745
|
-
entry.started = true;
|
|
3746
|
-
entry.unsubscribe = unsubscribe;
|
|
3747
|
-
this.eventBus.emit({
|
|
3748
|
-
id: randomUUID7(),
|
|
3749
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
3750
|
-
source: { type: "core", id: "provider-manager" },
|
|
3751
|
-
category: "provider.started",
|
|
3752
|
-
data: { providerId: id }
|
|
3753
|
-
});
|
|
3754
|
-
this.logger.info(`Provider started: ${id}`);
|
|
3755
|
-
}
|
|
3756
|
-
async stopProvider(id) {
|
|
3757
|
-
const entry = this.providers.get(id);
|
|
3758
|
-
if (!entry) {
|
|
3759
|
-
throw new Error(`Provider "${id}" is not registered`);
|
|
3760
|
-
}
|
|
3761
|
-
entry.lifecycle.transition("stopping");
|
|
3762
|
-
if (entry.unsubscribe) {
|
|
3763
|
-
entry.unsubscribe();
|
|
3764
|
-
entry.unsubscribe = void 0;
|
|
3765
|
-
}
|
|
3766
|
-
this.deviceRegistry.unregisterProviderDevices(id);
|
|
3767
|
-
await entry.provider.stop();
|
|
3768
|
-
entry.started = false;
|
|
3769
|
-
entry.lifecycle.transition("stopped");
|
|
3770
|
-
this.eventBus.emit({
|
|
3771
|
-
id: randomUUID7(),
|
|
3772
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
3773
|
-
source: { type: "core", id: "provider-manager" },
|
|
3774
|
-
category: "provider.stopped",
|
|
3775
|
-
data: { providerId: id }
|
|
3776
|
-
});
|
|
3777
|
-
this.logger.info(`Provider stopped: ${id}`);
|
|
3778
|
-
}
|
|
3779
|
-
async disableProvider(id) {
|
|
3780
|
-
const entry = this.providers.get(id);
|
|
3781
|
-
if (!entry) {
|
|
3782
|
-
throw new Error(`Provider "${id}" is not registered`);
|
|
3783
|
-
}
|
|
3784
|
-
if (entry.lifecycle.state === "running" || entry.started) {
|
|
3785
|
-
await this.stopProvider(id);
|
|
3786
|
-
}
|
|
3787
|
-
entry.lifecycle.transition("disabled");
|
|
3788
|
-
this.logger.info(`Provider disabled: ${id}`);
|
|
3789
|
-
}
|
|
3790
|
-
async enableProvider(id) {
|
|
3791
|
-
const entry = this.providers.get(id);
|
|
3792
|
-
if (!entry) {
|
|
3793
|
-
throw new Error(`Provider "${id}" is not registered`);
|
|
3794
|
-
}
|
|
3795
|
-
if (entry.lifecycle.state !== "disabled") {
|
|
3796
|
-
throw new Error(`Provider "${id}" is not disabled`);
|
|
3797
|
-
}
|
|
3798
|
-
entry.lifecycle.transition("stopped");
|
|
3799
|
-
this.logger.info(`Provider enabled: ${id}`);
|
|
3800
|
-
}
|
|
3801
|
-
async restartProvider(id) {
|
|
3802
|
-
await this.stopProvider(id);
|
|
3803
|
-
await this.startProvider(id);
|
|
3804
|
-
}
|
|
3805
|
-
getProvider(id) {
|
|
3806
|
-
const entry = this.providers.get(id);
|
|
3807
|
-
return entry?.provider ?? null;
|
|
3808
|
-
}
|
|
3809
|
-
getProviderStatus(id) {
|
|
3810
|
-
const entry = this.providers.get(id);
|
|
3811
|
-
return entry?.lifecycle.getStatus() ?? null;
|
|
3812
|
-
}
|
|
3813
|
-
listProviders() {
|
|
3814
|
-
return Array.from(this.providers.values()).map((entry) => ({
|
|
3815
|
-
id: entry.provider.id,
|
|
3816
|
-
type: entry.provider.type,
|
|
3817
|
-
name: entry.provider.name,
|
|
3818
|
-
status: entry.provider.getStatus(),
|
|
3819
|
-
started: entry.started,
|
|
3820
|
-
lifecycle: entry.lifecycle.getStatus()
|
|
3821
|
-
}));
|
|
3822
|
-
}
|
|
3823
|
-
async shutdownAll() {
|
|
3824
|
-
const startedIds = Array.from(this.providers.entries()).filter(([, entry]) => entry.started).map(([id]) => id);
|
|
3825
|
-
for (const id of startedIds) {
|
|
3826
|
-
await this.stopProvider(id);
|
|
3827
|
-
}
|
|
3828
|
-
this.logger.info(`All providers shut down (${startedIds.length} stopped)`);
|
|
3829
|
-
}
|
|
3830
|
-
};
|
|
3831
2165
|
export {
|
|
3832
2166
|
AddonApiFactory,
|
|
3833
2167
|
AddonRouteRegistry,
|
|
3834
|
-
|
|
3835
|
-
AgentRegistry,
|
|
3836
|
-
AgentTaskRunner,
|
|
2168
|
+
AlertCenterAddon,
|
|
3837
2169
|
ApiKeyManager,
|
|
3838
2170
|
AuthManager,
|
|
3839
2171
|
CORE_TABLE_DDL,
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
2172
|
+
ConfigStore,
|
|
2173
|
+
ConsoleDestination,
|
|
2174
|
+
ConsoleLoggingAddon,
|
|
2175
|
+
DeviceManagerAddon,
|
|
2176
|
+
DeviceStore,
|
|
2177
|
+
EngineManagerResolver,
|
|
3844
2178
|
EventBus,
|
|
3845
2179
|
FeatureManager,
|
|
3846
|
-
|
|
2180
|
+
FilesystemStorageAddon,
|
|
2181
|
+
FilesystemStorageProvider,
|
|
3847
2182
|
FsStorageBackend,
|
|
3848
|
-
|
|
2183
|
+
HubForwarderAddon,
|
|
2184
|
+
HubForwarderDestination,
|
|
3849
2185
|
IntegrationRegistry,
|
|
3850
2186
|
LifecycleStateMachine,
|
|
2187
|
+
LocalAuthAddon,
|
|
3851
2188
|
LocalBackupAddon,
|
|
3852
2189
|
LocalBackupService,
|
|
3853
2190
|
LogManager,
|
|
3854
2191
|
LogRingBuffer,
|
|
3855
|
-
ManagedProcess,
|
|
3856
2192
|
ModelDownloadService,
|
|
2193
|
+
NativeMetricsAddon,
|
|
2194
|
+
NativeMetricsProvider,
|
|
3857
2195
|
NetworkQualityTracker,
|
|
3858
2196
|
NotificationService,
|
|
3859
2197
|
PYTHON_VERSION,
|
|
3860
2198
|
PipelineRunner,
|
|
3861
2199
|
PipelineValidator,
|
|
3862
|
-
PlatformScorer,
|
|
3863
|
-
ProcessManager,
|
|
3864
|
-
ProviderManager,
|
|
3865
2200
|
PythonEnvManager,
|
|
3866
|
-
RecordTaskHandler,
|
|
3867
2201
|
ReplEngine,
|
|
3868
2202
|
ScopedLogger,
|
|
3869
2203
|
ScopedTokenManager,
|
|
3870
2204
|
SettingsStore,
|
|
3871
|
-
|
|
3872
|
-
|
|
2205
|
+
SqliteSettingsAddon,
|
|
2206
|
+
SqliteSettingsBackend,
|
|
3873
2207
|
StorageLocationManager,
|
|
3874
2208
|
StorageManager,
|
|
2209
|
+
SystemConfigAddon,
|
|
3875
2210
|
SystemEventBus,
|
|
3876
|
-
TaskDispatcher,
|
|
3877
2211
|
ToastService,
|
|
3878
2212
|
UserManager,
|
|
3879
2213
|
WinstonDestination,
|
|
3880
2214
|
WinstonLoggingAddon,
|
|
3881
2215
|
addonTableToDdl,
|
|
3882
2216
|
buildBinaryPath,
|
|
2217
|
+
deleteModelFromDisk,
|
|
3883
2218
|
downloadBinary,
|
|
3884
2219
|
downloadFile,
|
|
3885
2220
|
downloadModel,
|
|
3886
2221
|
ensureBinary,
|
|
3887
2222
|
ensureFfmpeg,
|
|
2223
|
+
ensureModel,
|
|
3888
2224
|
ensurePython,
|
|
3889
2225
|
ensureTlsCert,
|
|
3890
2226
|
fetchJson,
|
|
3891
2227
|
findInPath,
|
|
2228
|
+
formatLogLine,
|
|
3892
2229
|
getFfmpegDownloadUrl,
|
|
2230
|
+
getModelFilePath,
|
|
2231
|
+
getPidStats,
|
|
3893
2232
|
getPlatformInfo,
|
|
3894
2233
|
getPythonDownloadUrl,
|
|
2234
|
+
getSinglePidStats,
|
|
3895
2235
|
installPythonPackages,
|
|
2236
|
+
installPythonRequirements,
|
|
2237
|
+
isModelDownloaded,
|
|
3896
2238
|
loadTlsCert
|
|
3897
2239
|
};
|
|
3898
2240
|
//# sourceMappingURL=index.mjs.map
|