robopark 3.1.1 → 3.1.2
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/robopark/standalone.js +81 -5
- package/package.json +3 -2
|
@@ -150,6 +150,7 @@ export async function connectDevice(token, opts = {}) {
|
|
|
150
150
|
deviceToken: enrolled.device_token,
|
|
151
151
|
name,
|
|
152
152
|
connectedAt: new Date().toISOString(),
|
|
153
|
+
controlPort: pairing.controlPort,
|
|
153
154
|
};
|
|
154
155
|
atomicJson(CONNECTION_PATH, connection);
|
|
155
156
|
writeFileSync(join(STATE_DIR, 'device_token'), connection.deviceToken, { encoding: 'utf8', mode: 0o600 });
|
|
@@ -196,6 +197,53 @@ function positiveInteger(value, fallback, label) {
|
|
|
196
197
|
throw new Error(`${label} must be a positive integer`);
|
|
197
198
|
return parsed;
|
|
198
199
|
}
|
|
200
|
+
function openBrowser(url) {
|
|
201
|
+
// Use detached platform-native launchers so this CLI remains usable from
|
|
202
|
+
// terminals and does not inherit browser stdio or lifecycle.
|
|
203
|
+
const command = process.platform === 'win32'
|
|
204
|
+
? 'cmd.exe'
|
|
205
|
+
: process.platform === 'darwin'
|
|
206
|
+
? 'open'
|
|
207
|
+
: 'xdg-open';
|
|
208
|
+
const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
209
|
+
try {
|
|
210
|
+
spawn(command, args, { detached: true, stdio: 'ignore', windowsHide: true }).unref();
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
console.log(chalk.yellow(` ⚠ could not open browser: ${err instanceof Error ? err.message : String(err)}`));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Opens the character-specific ElevenLabs voice-join page (served by the Park
|
|
218
|
+
* Control Center, not the scheduler) for the device just configured. Best
|
|
219
|
+
* effort only: a browser-less robot host or an unreachable control server
|
|
220
|
+
* must never fail `robopark start`/`robopark connect`.
|
|
221
|
+
*/
|
|
222
|
+
async function openCharacterCallPage(connection, character) {
|
|
223
|
+
try {
|
|
224
|
+
const preset = await jsonRequest(`${connection.schedulerUrl}/api/character-presets/${encodeURIComponent(character)}`, {});
|
|
225
|
+
if (!preset.elevenlabs_agent_id) {
|
|
226
|
+
console.log(chalk.yellow(` ⚠ character "${character}" has no ElevenLabs agent configured; not opening a call page.`));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const controlPort = connection.controlPort ?? 47913;
|
|
230
|
+
const controlHost = new URL(connection.schedulerUrl).hostname;
|
|
231
|
+
const params = new URLSearchParams({
|
|
232
|
+
character,
|
|
233
|
+
agent_id: preset.elevenlabs_agent_id,
|
|
234
|
+
mode: 'production',
|
|
235
|
+
robot: connection.deviceId,
|
|
236
|
+
});
|
|
237
|
+
if (preset.elevenlabs_branch_id)
|
|
238
|
+
params.set('branch_id', preset.elevenlabs_branch_id);
|
|
239
|
+
const url = `http://${controlHost}:${controlPort}/voice-join.html?${params.toString()}`;
|
|
240
|
+
console.log(` call page: ${chalk.cyan(url)}`);
|
|
241
|
+
openBrowser(url);
|
|
242
|
+
}
|
|
243
|
+
catch (err) {
|
|
244
|
+
console.log(chalk.yellow(` ⚠ could not open the character call page: ${err instanceof Error ? err.message : String(err)}`));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
199
247
|
export async function startDevice(opts) {
|
|
200
248
|
const connection = readConnection();
|
|
201
249
|
const desired = {
|
|
@@ -216,6 +264,9 @@ export async function startDevice(opts) {
|
|
|
216
264
|
}
|
|
217
265
|
await updateDeviceConfiguration(desired);
|
|
218
266
|
atomicJson(DESIRED_PATH, desired);
|
|
267
|
+
if (desired.character) {
|
|
268
|
+
await openCharacterCallPage(connection, desired.character);
|
|
269
|
+
}
|
|
219
270
|
if (opts.foreground) {
|
|
220
271
|
await superviseDevice(DESIRED_PATH);
|
|
221
272
|
return;
|
|
@@ -424,20 +475,36 @@ export async function gatewayStart(opts) {
|
|
|
424
475
|
}
|
|
425
476
|
const schedulerUrl = localPublicUrl(host, port, opts.publicUrl);
|
|
426
477
|
const healthUrl = `http://127.0.0.1:${port}`;
|
|
478
|
+
const controlHealthUrl = `http://127.0.0.1:${controlPort}/fed/ui-build`;
|
|
427
479
|
const deadline = Date.now() + 30_000;
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
480
|
+
let schedulerReady = false;
|
|
481
|
+
let controlReady = false;
|
|
482
|
+
while (!schedulerReady || !controlReady) {
|
|
483
|
+
[schedulerReady, controlReady] = await Promise.all([
|
|
484
|
+
schedulerHealthy(healthUrl, 1_000),
|
|
485
|
+
fetch(controlHealthUrl, { signal: AbortSignal.timeout(1_000) })
|
|
486
|
+
.then(async (response) => {
|
|
487
|
+
if (!response.ok)
|
|
488
|
+
return false;
|
|
489
|
+
const body = await response.json();
|
|
490
|
+
return body.owner === 'robopark';
|
|
491
|
+
})
|
|
492
|
+
.catch(() => false),
|
|
493
|
+
]);
|
|
494
|
+
if (Date.now() >= deadline) {
|
|
495
|
+
throw new Error(`gateway did not become healthy (scheduler ${port}: ${schedulerReady ? 'ready' : 'down'}, `
|
|
496
|
+
+ `Control Center ${controlPort}: ${controlReady ? 'ready' : 'down'})`);
|
|
497
|
+
}
|
|
431
498
|
await wait(500);
|
|
432
499
|
}
|
|
433
|
-
const token = await createGatewayTokenAt(healthUrl, schedulerUrl);
|
|
500
|
+
const token = await createGatewayTokenAt(healthUrl, schedulerUrl, controlPort);
|
|
434
501
|
const controlHost = new URL(schedulerUrl).hostname;
|
|
435
502
|
console.log(` Park Control Center: ${chalk.cyan(`http://${controlHost}:${controlPort}/`)}`);
|
|
436
503
|
console.log(chalk.bold('\n Connect a device'));
|
|
437
504
|
console.log(` ${chalk.cyan(`robopark connect ${token}`)}`);
|
|
438
505
|
console.log(chalk.dim(' This pairing token expires in 24 hours. Connected devices keep their own durable credentials.\n'));
|
|
439
506
|
}
|
|
440
|
-
async function createGatewayTokenAt(localUrl, publicUrl) {
|
|
507
|
+
async function createGatewayTokenAt(localUrl, publicUrl, controlPort) {
|
|
441
508
|
await jsonRequest(`${localUrl}/api/settings`, {
|
|
442
509
|
method: 'PUT',
|
|
443
510
|
headers: { 'content-type': 'application/json' },
|
|
@@ -449,6 +516,7 @@ async function createGatewayTokenAt(localUrl, publicUrl) {
|
|
|
449
516
|
schedulerUrl: publicUrl,
|
|
450
517
|
enrollmentToken: result.enrollment_token,
|
|
451
518
|
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
|
519
|
+
controlPort,
|
|
452
520
|
});
|
|
453
521
|
atomicJson(GATEWAY_PATH, { schedulerUrl: publicUrl, pairingToken: pairing, createdAt: new Date().toISOString() });
|
|
454
522
|
return pairing;
|
|
@@ -471,6 +539,14 @@ export async function runGatewayService(opts) {
|
|
|
471
539
|
open: false,
|
|
472
540
|
foreground: true,
|
|
473
541
|
});
|
|
542
|
+
// roboparkServe wires the scheduler child's lifecycle handlers and then
|
|
543
|
+
// returns once it has launched. Keep this outer gateway process alive so
|
|
544
|
+
// the sibling Control Center server is not closed by the finally block.
|
|
545
|
+
await new Promise((resolve) => {
|
|
546
|
+
const stop = () => resolve();
|
|
547
|
+
process.once('SIGINT', stop);
|
|
548
|
+
process.once('SIGTERM', stop);
|
|
549
|
+
});
|
|
474
550
|
}
|
|
475
551
|
finally {
|
|
476
552
|
control.close();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "robopark",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.2",
|
|
4
4
|
"description": "Standalone packaged RoboPark control center and supervised robot runtime.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
"commander": "^12.1.0",
|
|
34
34
|
"conf": "^12.0.0",
|
|
35
35
|
"execa": "^9.3.0",
|
|
36
|
-
"livekit-client": "^2.20.1"
|
|
36
|
+
"livekit-client": "^2.20.1",
|
|
37
|
+
"robopark": "^3.1.1"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
39
40
|
"@types/node": "^20.14.0",
|