@camstack/server 1.1.0 → 1.1.1
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/api/addon-upload.js +7 -1
- package/dist/api/model-distributor.js +128 -0
- package/dist/api/trpc/generated-cap-mounts.js +20 -1
- package/dist/api/trpc/generated-cap-routers.js +1068 -1025
- package/dist/api/trpc/trpc.router.js +1 -0
- package/dist/core/agent/agent-registry.service.js +9 -1
- package/dist/main.js +8 -0
- package/package.json +1 -1
package/dist/api/addon-upload.js
CHANGED
|
@@ -54,7 +54,13 @@ function getDeployStageRegistry() {
|
|
|
54
54
|
* the agent in the `$agent.deploy` `hub-http` source descriptor.
|
|
55
55
|
*/
|
|
56
56
|
function registerDeployBundleRoute(fastify) {
|
|
57
|
-
|
|
57
|
+
// `compress: false` — never gzip/brotli this route. The body is a large
|
|
58
|
+
// binary tarball (addon bundle / model artifact) the agent pulls with undici
|
|
59
|
+
// + sha256 verification. With global @fastify/compress on, the 1–2MB response
|
|
60
|
+
// was Brotli-encoded and undici's BrotliDecompress aborted the stream
|
|
61
|
+
// ("TypeError: terminated"), silently breaking EVERY agent-pull (addon-deploy
|
|
62
|
+
// AND model distribution). Identity bytes also keep the byte-length check valid.
|
|
63
|
+
fastify.get('/api/addons/deploy-bundle/:id', { compress: false }, (request, reply) => {
|
|
58
64
|
const auth = request.headers.authorization ?? '';
|
|
59
65
|
const token = auth.startsWith('Bearer ') ? auth.slice('Bearer '.length) : '';
|
|
60
66
|
if (!token) {
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.tarModelFiles = tarModelFiles;
|
|
37
|
+
exports.distributeModelWith = distributeModelWith;
|
|
38
|
+
exports.buildModelDistributorProvider = buildModelDistributorProvider;
|
|
39
|
+
const fs = __importStar(require("node:fs"));
|
|
40
|
+
const path = __importStar(require("node:path"));
|
|
41
|
+
const node_child_process_1 = require("node:child_process");
|
|
42
|
+
const system_1 = require("@camstack/system");
|
|
43
|
+
const addon_upload_js_1 = require("./addon-upload.js");
|
|
44
|
+
const AGENT_DISTRIBUTE_TIMEOUT_MS = 60_000;
|
|
45
|
+
function hubModelsDir() {
|
|
46
|
+
return path.join(process.env['CAMSTACK_DATA'] ?? 'camstack-data', 'models');
|
|
47
|
+
}
|
|
48
|
+
function hubBundleBaseUrl() {
|
|
49
|
+
return process.env['CAMSTACK_HUB_PUBLIC_URL'] ?? 'https://127.0.0.1:4443';
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* `tar -czf -` the given files relative to `cwd`, buffering the gzip stream.
|
|
53
|
+
* Async (spawn, not execFileSync) so a large model doesn't block the event
|
|
54
|
+
* loop. The agent untars this exact buffer into its own modelsDir.
|
|
55
|
+
*/
|
|
56
|
+
function tarModelFiles(cwd, files) {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const child = (0, node_child_process_1.spawn)('tar', ['-czf', '-', '-C', cwd, ...files]);
|
|
59
|
+
const chunks = [];
|
|
60
|
+
const errChunks = [];
|
|
61
|
+
child.stdout.on('data', (c) => chunks.push(c));
|
|
62
|
+
child.stderr.on('data', (c) => errChunks.push(c));
|
|
63
|
+
child.on('error', reject);
|
|
64
|
+
child.on('close', (code) => {
|
|
65
|
+
if (code === 0)
|
|
66
|
+
resolve(Buffer.concat(chunks));
|
|
67
|
+
else
|
|
68
|
+
reject(new Error(`tar exited ${code}: ${Buffer.concat(errChunks).toString().slice(0, 300)}`));
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Core distribution logic (pure orchestration over the seam):
|
|
74
|
+
* precondition-check → collect → tar → stage → trigger the agent pull.
|
|
75
|
+
* `nodeId === 'hub'` is a no-op (the artifact is already resident).
|
|
76
|
+
*/
|
|
77
|
+
async function distributeModelWith(seam, input) {
|
|
78
|
+
const { nodeId, modelId, format, entry } = input;
|
|
79
|
+
if (nodeId === 'hub') {
|
|
80
|
+
return { ok: true, sha256: '', bytes: 0, path: seam.modelsDir };
|
|
81
|
+
}
|
|
82
|
+
if (!seam.isPresent(entry, format)) {
|
|
83
|
+
throw new Error(`model ${modelId}/${format} not present on hub — download or convert it first`);
|
|
84
|
+
}
|
|
85
|
+
const files = (0, system_1.collectModelFiles)(entry, format).filter((f) => seam.fileExists(f));
|
|
86
|
+
if (files.length === 0) {
|
|
87
|
+
throw new Error(`model ${modelId}/${format} resolved no files to distribute`);
|
|
88
|
+
}
|
|
89
|
+
const tgz = await seam.tar(files);
|
|
90
|
+
const source = seam.stage(tgz);
|
|
91
|
+
const res = await seam.callAgent(nodeId, { modelId, format, source });
|
|
92
|
+
return { ok: true, sha256: source.sha256, bytes: source.bytes, path: res.path ?? '' };
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Build the hub-resident `model-distributor` singleton provider. Mounted as a
|
|
96
|
+
* service-backed cap override in `trpc.router.ts` because it needs the
|
|
97
|
+
* Moleculer broker + the deploy-stage registry, which a forked addon can't
|
|
98
|
+
* reach.
|
|
99
|
+
*/
|
|
100
|
+
function buildModelDistributorProvider(moleculer, loggingService) {
|
|
101
|
+
const logger = loggingService.createLogger('model-distributor');
|
|
102
|
+
const modelsDir = hubModelsDir();
|
|
103
|
+
const seam = {
|
|
104
|
+
modelsDir,
|
|
105
|
+
isPresent: (entry, format) => (0, system_1.isModelDownloaded)(modelsDir, entry, format),
|
|
106
|
+
fileExists: (rel) => fs.existsSync(path.join(modelsDir, rel)),
|
|
107
|
+
tar: (files) => tarModelFiles(modelsDir, files),
|
|
108
|
+
stage: (buffer) => (0, addon_upload_js_1.buildHubHttpSource)(buffer, hubBundleBaseUrl()),
|
|
109
|
+
callAgent: async (nodeId, payload) => {
|
|
110
|
+
const broker = moleculer?.broker;
|
|
111
|
+
if (!broker)
|
|
112
|
+
throw new Error('model-distributor: Moleculer broker unavailable');
|
|
113
|
+
const raw = await broker.call('$agent.distributeModel', payload, {
|
|
114
|
+
nodeID: nodeId,
|
|
115
|
+
timeout: AGENT_DISTRIBUTE_TIMEOUT_MS,
|
|
116
|
+
});
|
|
117
|
+
return (raw ?? {});
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
return {
|
|
121
|
+
distributeModel: async (input) => {
|
|
122
|
+
logger.info('Distributing model to node', {
|
|
123
|
+
meta: { modelId: input.modelId, format: input.format, nodeId: input.nodeId },
|
|
124
|
+
});
|
|
125
|
+
return distributeModelWith(seam, input);
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// AUTO-GENERATED by scripts/generate-cap-mounts.ts — DO NOT EDIT
|
|
3
3
|
// Re-run: npx tsx scripts/generate-cap-mounts.ts
|
|
4
4
|
//
|
|
5
|
-
// Mounted:
|
|
5
|
+
// Mounted: 130 Skipped (legacy): 6
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
exports.LEGACY_SHAPE_SKIP = void 0;
|
|
8
8
|
exports.mountAllCaps = mountAllCaps;
|
|
@@ -121,6 +121,22 @@ function mountAllCaps(services) {
|
|
|
121
121
|
contact: (0, generated_cap_routers_js_1.createCapRouter_contact)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'contact'), remoteCapProxy),
|
|
122
122
|
control: (0, generated_cap_routers_js_1.createCapRouter_control)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'control'), remoteCapProxy),
|
|
123
123
|
cover: (0, generated_cap_routers_js_1.createCapRouter_cover)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'cover'), remoteCapProxy),
|
|
124
|
+
customModelRegistry: (0, generated_cap_routers_js_1.createCapRouter_customModelRegistry)((_ctx, addonId) => {
|
|
125
|
+
if (!reg)
|
|
126
|
+
return null;
|
|
127
|
+
if (addonId !== undefined) {
|
|
128
|
+
return reg.getProviderByAddonId('custom-model-registry', addonId);
|
|
129
|
+
}
|
|
130
|
+
const entries = reg.getCollectionEntries('custom-model-registry');
|
|
131
|
+
if (entries.length === 0)
|
|
132
|
+
return null;
|
|
133
|
+
const providers = entries.map(([, p]) => p);
|
|
134
|
+
const first = providers[0];
|
|
135
|
+
return {
|
|
136
|
+
...first,
|
|
137
|
+
listModels: (0, cap_mount_helpers_js_1.concatCollection)(providers, 'listModels'),
|
|
138
|
+
};
|
|
139
|
+
}, remoteCapProxy),
|
|
124
140
|
decoder: (0, generated_cap_routers_js_1.createCapRouter_decoder)((_ctx) => reg?.getSingleton('decoder') ?? null, remoteCapProxy),
|
|
125
141
|
detectionPipeline: (0, generated_cap_routers_js_1.createCapRouter_detectionPipeline)((_ctx) => reg?.getSingleton('detection-pipeline') ?? null, remoteCapProxy),
|
|
126
142
|
deviceAdoption: (0, generated_cap_routers_js_1.createCapRouter_deviceAdoption)((_ctx) => reg?.getSingleton('device-adoption') ??
|
|
@@ -204,6 +220,9 @@ function mountAllCaps(services) {
|
|
|
204
220
|
}, remoteCapProxy),
|
|
205
221
|
metricsProvider: (0, generated_cap_routers_js_1.createCapRouter_metricsProvider)((_ctx) => reg?.getSingleton('metrics-provider') ??
|
|
206
222
|
null, remoteCapProxy),
|
|
223
|
+
modelConvert: (0, generated_cap_routers_js_1.createCapRouter_modelConvert)((_ctx) => reg?.getSingleton('model-convert') ?? null, remoteCapProxy),
|
|
224
|
+
modelDistributor: (0, generated_cap_routers_js_1.createCapRouter_modelDistributor)((_ctx) => reg?.getSingleton('model-distributor') ??
|
|
225
|
+
null, remoteCapProxy),
|
|
207
226
|
motion: (0, generated_cap_routers_js_1.createCapRouter_motion)((_ctx) => reg?.getSingleton('motion') ?? null, remoteCapProxy),
|
|
208
227
|
motionDetection: (0, generated_cap_routers_js_1.createCapRouter_motionDetection)((_ctx) => reg?.getSingleton('motion-detection') ??
|
|
209
228
|
null, remoteCapProxy),
|