oomi-ai 0.2.41 → 0.2.45
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/bin/oomi-ai.js +419 -99
- package/lib/personaRuntimeManager.js +70 -58
- package/lib/personaRuntimeProcess.js +153 -19
- package/lib/personaRuntimeSupervisor.js +128 -4
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/templates/persona-app/oomi.runtime.json +1 -1
- package/templates/persona-app/package.json +3 -3
- package/templates/persona-app/template.json +1 -1
package/bin/oomi-ai.js
CHANGED
|
@@ -14,35 +14,37 @@ import { startPersonaJobPoller } from '../lib/personaJobPoller.js';
|
|
|
14
14
|
import { startPersonaRuntimeSupervisor } from '../lib/personaRuntimeSupervisor.js';
|
|
15
15
|
import { executePersonaJob, extractPersonaJobPayload } from '../lib/personaJobExecutor.js';
|
|
16
16
|
import { inferSpokenMetadataFromContent, normalizeSpokenMetadata } from '../lib/spokenMetadata.js';
|
|
17
|
-
import {
|
|
18
|
-
resolveOpenclawBridgeLiveLogPath,
|
|
19
|
-
resolveOpenclawBridgeLockPath,
|
|
20
|
-
resolveOpenclawBridgeStatePath,
|
|
21
|
-
resolveOpenclawBridgeStatusPath,
|
|
22
|
-
resolveOpenclawConfigCandidates,
|
|
23
|
-
resolveOpenclawHome,
|
|
24
|
-
resolveOpenclawIdentityPath,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
17
|
+
import {
|
|
18
|
+
resolveOpenclawBridgeLiveLogPath,
|
|
19
|
+
resolveOpenclawBridgeLockPath,
|
|
20
|
+
resolveOpenclawBridgeStatePath,
|
|
21
|
+
resolveOpenclawBridgeStatusPath,
|
|
22
|
+
resolveOpenclawConfigCandidates,
|
|
23
|
+
resolveOpenclawHome,
|
|
24
|
+
resolveOpenclawIdentityPath,
|
|
25
|
+
resolveOpenclawLegacyPersonasDir,
|
|
26
|
+
resolveOpenclawProfilePath,
|
|
27
|
+
resolveOpenclawSkillsDir,
|
|
28
|
+
resolveOpenclawUpdateStatePath,
|
|
29
|
+
resolveOpenclawWorkspaceRoot,
|
|
30
|
+
} from '../lib/openclawPaths.js';
|
|
30
31
|
import {
|
|
31
32
|
applyOpenclawProfile,
|
|
32
33
|
buildOomiDevLocalProfile,
|
|
33
34
|
readOpenclawProfile,
|
|
34
35
|
writeOpenclawProfile,
|
|
35
36
|
} from '../lib/openclawProfile.js';
|
|
36
|
-
import {
|
|
37
|
-
buildLocalPersonaRuntime,
|
|
38
|
-
defaultPersonaWorkspaceRoot,
|
|
39
|
-
installPersonaWorkspace,
|
|
40
|
-
isPersonaWorkspaceProcessRunning,
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
37
|
+
import {
|
|
38
|
+
buildLocalPersonaRuntime,
|
|
39
|
+
defaultPersonaWorkspaceRoot,
|
|
40
|
+
installPersonaWorkspace,
|
|
41
|
+
isPersonaWorkspaceProcessRunning,
|
|
42
|
+
resolvePersonaHealthPath,
|
|
43
|
+
resolvePersonaDevCommand,
|
|
44
|
+
startPersonaWorkspace,
|
|
45
|
+
stopPersonaWorkspace,
|
|
46
|
+
waitForPersonaRuntime,
|
|
47
|
+
} from '../lib/personaRuntimeProcess.js';
|
|
46
48
|
import {
|
|
47
49
|
destroyManagedPersonaRuntime,
|
|
48
50
|
getManagedPersonaRuntimeStatus,
|
|
@@ -50,13 +52,17 @@ import {
|
|
|
50
52
|
slugifyPersonaName,
|
|
51
53
|
stopManagedPersonaRuntime,
|
|
52
54
|
} from '../lib/personaRuntimeManager.js';
|
|
55
|
+
import {
|
|
56
|
+
readPersonaRuntimeState,
|
|
57
|
+
resolvePersonaWorkspacePath,
|
|
58
|
+
} from '../lib/personaRuntimeRegistry.js';
|
|
53
59
|
import { startLocalGatewayAgentServer } from '../lib/openclawDevGateway.js';
|
|
54
60
|
import { ensureSessionBridge, flushSessionQueue, flushWaitingForConnect, forwardFrameToSession } from './sessionBridgeState.js';
|
|
55
61
|
|
|
56
62
|
const MARKER_START = '<oomi-agent-instructions>';
|
|
57
63
|
const MARKER_END = '</oomi-agent-instructions>';
|
|
58
64
|
|
|
59
|
-
const PACKAGE_ROOT = path.resolve(path.dirname(
|
|
65
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
60
66
|
const UPDATE_STATE_FILE = resolveOpenclawUpdateStatePath();
|
|
61
67
|
const DEFAULT_UPDATE_CHECK_INTERVAL_MS = 12 * 60 * 60 * 1000;
|
|
62
68
|
const DEFAULT_UPDATE_CHECK_TIMEOUT_MS = 1200;
|
|
@@ -231,10 +237,12 @@ Commands:
|
|
|
231
237
|
Replay an assistant chat.final frame through spoken-metadata normalization.
|
|
232
238
|
openclaw debug tts-pipeline
|
|
233
239
|
Replay an assistant chat.final through local backend voice handling.
|
|
234
|
-
openclaw debug local-gateway-agent
|
|
235
|
-
Run a tiny local OpenClaw gateway/agent for Docker dev testing.
|
|
236
|
-
openclaw debug persona-runtime
|
|
237
|
-
Scaffold, launch, and stop a managed persona runtime locally.
|
|
240
|
+
openclaw debug local-gateway-agent
|
|
241
|
+
Run a tiny local OpenClaw gateway/agent for Docker dev testing.
|
|
242
|
+
openclaw debug persona-runtime
|
|
243
|
+
Scaffold, launch, and stop a managed persona runtime locally.
|
|
244
|
+
openclaw refresh
|
|
245
|
+
Restart the bridge and running managed persona runtimes after an oomi-ai update.
|
|
238
246
|
|
|
239
247
|
openclaw pair
|
|
240
248
|
Pair this OpenClaw host with Oomi and start bridge (single command).
|
|
@@ -320,18 +328,20 @@ Common flags:
|
|
|
320
328
|
--out PATH Output directory for scaffolded persona app
|
|
321
329
|
--template-version V Scaffold template version (default: v1)
|
|
322
330
|
--force Overwrite files in an existing output directory
|
|
323
|
-
--force-install Reinstall persona workspace dependencies before launch
|
|
324
|
-
--
|
|
325
|
-
--no-
|
|
331
|
+
--force-install Reinstall persona workspace dependencies before launch
|
|
332
|
+
--include-stopped Relaunch managed persona runtimes even if not currently marked running
|
|
333
|
+
--no-sync Skip backend sync (for create)
|
|
334
|
+
--no-create Do not create a managed persona record if one does not already exist
|
|
326
335
|
--local-port N Local runtime port for persona runtime callbacks
|
|
327
336
|
--endpoint URL Runtime endpoint for persona runtime callbacks
|
|
328
337
|
--entry-url URL Viewer URL to register for a launched persona runtime
|
|
329
|
-
--health-path PATH Runtime health path (default:
|
|
338
|
+
--health-path PATH Runtime health path override (default: workspace-specific)
|
|
330
339
|
--healthcheck-url URL Runtime healthcheck URL override
|
|
331
|
-
--transport TEXT Runtime transport label (default: local, relay when --entry-url is used)
|
|
340
|
+
--transport TEXT Runtime transport label (default: local, relay when --entry-url is used)
|
|
332
341
|
--workspace-root PATH Persona workspace root (default: OPENCLAW_WORKSPACE/personas)
|
|
333
|
-
--restart Restart an existing managed persona runtime before launch
|
|
334
|
-
--
|
|
342
|
+
--restart Restart an existing managed persona runtime before launch
|
|
343
|
+
--skip-version-check Skip checking npm for the latest oomi-ai version during refresh
|
|
344
|
+
--started-at ISO Start timestamp override
|
|
335
345
|
--observed-at ISO Heartbeat timestamp override
|
|
336
346
|
--completed-at ISO Completion timestamp override
|
|
337
347
|
--code TEXT Error code for fail callbacks
|
|
@@ -829,14 +839,91 @@ function resolvePersonaEntryUrl(flags = {}) {
|
|
|
829
839
|
return String(flags['entry-url'] || '').trim();
|
|
830
840
|
}
|
|
831
841
|
|
|
832
|
-
function resolvePersonaLaunchTransport(flags = {}) {
|
|
833
|
-
const explicitTransport = String(flags.transport || '').trim();
|
|
834
|
-
if (explicitTransport) {
|
|
835
|
-
return explicitTransport;
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
return resolvePersonaEntryUrl(flags) ? 'relay' : 'local';
|
|
839
|
-
}
|
|
842
|
+
function resolvePersonaLaunchTransport(flags = {}) {
|
|
843
|
+
const explicitTransport = String(flags.transport || '').trim();
|
|
844
|
+
if (explicitTransport) {
|
|
845
|
+
return explicitTransport;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
return resolvePersonaEntryUrl(flags) ? 'relay' : 'local';
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function listPersonaWorkspaceRoots(workspaceRoot = defaultPersonaWorkspaceRoot()) {
|
|
852
|
+
const roots = [
|
|
853
|
+
String(workspaceRoot || '').trim(),
|
|
854
|
+
String(resolveOpenclawLegacyPersonasDir() || '').trim(),
|
|
855
|
+
].filter(Boolean);
|
|
856
|
+
|
|
857
|
+
const deduped = [];
|
|
858
|
+
const seen = new Set();
|
|
859
|
+
for (const root of roots) {
|
|
860
|
+
const resolved = path.resolve(root);
|
|
861
|
+
if (seen.has(resolved) || !fs.existsSync(resolved)) {
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
seen.add(resolved);
|
|
865
|
+
deduped.push(resolved);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
return deduped;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function collectManagedPersonaRefreshTargets({
|
|
872
|
+
workspaceRoot = defaultPersonaWorkspaceRoot(),
|
|
873
|
+
includeStopped = false,
|
|
874
|
+
} = {}) {
|
|
875
|
+
const targets = [];
|
|
876
|
+
const seenWorkspacePaths = new Set();
|
|
877
|
+
const seenSlugs = new Set();
|
|
878
|
+
|
|
879
|
+
for (const root of listPersonaWorkspaceRoots(workspaceRoot)) {
|
|
880
|
+
const entries = fs.readdirSync(root, { withFileTypes: true });
|
|
881
|
+
for (const entry of entries) {
|
|
882
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) {
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const workspacePath = path.join(root, entry.name);
|
|
887
|
+
let dedupeKey = workspacePath;
|
|
888
|
+
try {
|
|
889
|
+
dedupeKey = fs.realpathSync(workspacePath);
|
|
890
|
+
} catch {
|
|
891
|
+
dedupeKey = path.resolve(workspacePath);
|
|
892
|
+
}
|
|
893
|
+
if (seenWorkspacePaths.has(dedupeKey)) {
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
seenWorkspacePaths.add(dedupeKey);
|
|
897
|
+
|
|
898
|
+
const state = readPersonaRuntimeState(workspacePath);
|
|
899
|
+
if (!state || Object.keys(state).length === 0) {
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const slug = String(state.slug || entry.name || '').trim();
|
|
904
|
+
if (!slug || seenSlugs.has(slug)) {
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
const pid = normalizePid(state.pid);
|
|
909
|
+
const processRunning = pid ? isPersonaWorkspaceProcessRunning(pid) : false;
|
|
910
|
+
const runtimeStatus = String(state.status || '').trim().toLowerCase();
|
|
911
|
+
if (!includeStopped && !processRunning && runtimeStatus !== 'running') {
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
seenSlugs.add(slug);
|
|
916
|
+
targets.push({
|
|
917
|
+
slug,
|
|
918
|
+
workspacePath,
|
|
919
|
+
state,
|
|
920
|
+
processRunning,
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
return targets.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
926
|
+
}
|
|
840
927
|
|
|
841
928
|
async function findExistingManagedPersona(client, slug) {
|
|
842
929
|
try {
|
|
@@ -1114,31 +1201,81 @@ async function runManagedPersonaJobExecution({
|
|
|
1114
1201
|
}
|
|
1115
1202
|
}
|
|
1116
1203
|
|
|
1117
|
-
function
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
const
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1204
|
+
function resolvePersonaRuntimeWorkspacePath({
|
|
1205
|
+
slug = '',
|
|
1206
|
+
workspacePath = '',
|
|
1207
|
+
workspaceRoot = '',
|
|
1208
|
+
} = {}) {
|
|
1209
|
+
const explicitWorkspacePath = String(workspacePath || '').trim();
|
|
1210
|
+
if (explicitWorkspacePath) {
|
|
1211
|
+
return path.resolve(explicitWorkspacePath);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
const safeSlug = String(slug || '').trim();
|
|
1215
|
+
if (!safeSlug) {
|
|
1216
|
+
return '';
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
const safeWorkspaceRoot = String(workspaceRoot || defaultPersonaWorkspaceRoot()).trim();
|
|
1220
|
+
if (!safeWorkspaceRoot) {
|
|
1221
|
+
return '';
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
return resolvePersonaWorkspacePath({
|
|
1225
|
+
workspaceRoot: safeWorkspaceRoot,
|
|
1226
|
+
slug: safeSlug,
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
function resolvePersonaRuntimeInput(flags = {}, defaults = {}, options = {}) {
|
|
1231
|
+
const localPort = parseOptionalPositiveInteger(flags['local-port'] || flags.localPort || defaults.localPort);
|
|
1232
|
+
const endpoint = String(flags.endpoint || defaults.endpoint || '').trim();
|
|
1233
|
+
const explicitHealthPath = String(flags['health-path'] || defaults.healthPath || '').trim();
|
|
1234
|
+
const healthcheckUrl = String(flags['healthcheck-url'] || defaults.healthcheckUrl || '').trim();
|
|
1235
|
+
const transport = String(flags.transport || defaults.transport || 'local').trim() || 'local';
|
|
1236
|
+
const workspacePath = resolvePersonaRuntimeWorkspacePath({
|
|
1237
|
+
slug: options.slug || defaults.slug || flags.slug,
|
|
1238
|
+
workspacePath:
|
|
1239
|
+
options.workspacePath ||
|
|
1240
|
+
defaults.workspacePath ||
|
|
1241
|
+
flags['workspace-path'] ||
|
|
1242
|
+
flags.workspacePath,
|
|
1243
|
+
workspaceRoot:
|
|
1244
|
+
options.workspaceRoot ||
|
|
1245
|
+
defaults.workspaceRoot ||
|
|
1246
|
+
flags['workspace-root'] ||
|
|
1247
|
+
flags.workspaceRoot,
|
|
1248
|
+
});
|
|
1249
|
+
const healthPath = explicitHealthPath ||
|
|
1250
|
+
resolvePersonaHealthPath({
|
|
1251
|
+
workspacePath,
|
|
1252
|
+
fallback: '/oomi.health.json',
|
|
1253
|
+
});
|
|
1254
|
+
|
|
1255
|
+
if (endpoint) {
|
|
1256
|
+
return {
|
|
1257
|
+
endpoint,
|
|
1258
|
+
healthcheckUrl: healthcheckUrl || `${endpoint.replace(/\/$/, '')}${healthPath}`,
|
|
1128
1259
|
localPort,
|
|
1129
1260
|
transport,
|
|
1130
1261
|
};
|
|
1131
1262
|
}
|
|
1132
1263
|
|
|
1133
|
-
if (!localPort) {
|
|
1134
|
-
throw new Error('Runtime endpoint or local port is required.');
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
localPort,
|
|
1139
|
-
healthPath,
|
|
1140
|
-
});
|
|
1141
|
-
|
|
1264
|
+
if (!localPort) {
|
|
1265
|
+
throw new Error('Runtime endpoint or local port is required.');
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
const runtime = buildLocalPersonaRuntime({
|
|
1269
|
+
localPort,
|
|
1270
|
+
healthPath,
|
|
1271
|
+
});
|
|
1272
|
+
|
|
1273
|
+
return {
|
|
1274
|
+
...runtime,
|
|
1275
|
+
endpoint: runtime.reachableEndpoint || runtime.endpoint,
|
|
1276
|
+
localEndpoint: runtime.endpoint,
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1142
1279
|
|
|
1143
1280
|
function parseIsoTimestamp(rawValue, label) {
|
|
1144
1281
|
const value = String(rawValue || '').trim();
|
|
@@ -1175,12 +1312,19 @@ function printStructuredResult(result, asJson = false) {
|
|
|
1175
1312
|
}
|
|
1176
1313
|
}
|
|
1177
1314
|
|
|
1178
|
-
async function handlePersonaRuntimeRegisterCommand(slug, flags = {}) {
|
|
1179
|
-
const client = createCliPersonaApiClient(flags);
|
|
1180
|
-
const runtime = resolvePersonaRuntimeInput(
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1315
|
+
async function handlePersonaRuntimeRegisterCommand(slug, flags = {}) {
|
|
1316
|
+
const client = createCliPersonaApiClient(flags);
|
|
1317
|
+
const runtime = resolvePersonaRuntimeInput(
|
|
1318
|
+
flags,
|
|
1319
|
+
{},
|
|
1320
|
+
{
|
|
1321
|
+
slug,
|
|
1322
|
+
workspaceRoot: resolvePersonaWorkspaceRoot(flags),
|
|
1323
|
+
},
|
|
1324
|
+
);
|
|
1325
|
+
const payload = await client.registerRuntime({
|
|
1326
|
+
slug,
|
|
1327
|
+
endpoint: runtime.endpoint,
|
|
1184
1328
|
healthcheckUrl: runtime.healthcheckUrl,
|
|
1185
1329
|
localPort: runtime.localPort,
|
|
1186
1330
|
transport: runtime.transport,
|
|
@@ -1189,12 +1333,19 @@ async function handlePersonaRuntimeRegisterCommand(slug, flags = {}) {
|
|
|
1189
1333
|
printStructuredResult(payload, isTruthyFlag(flags.json));
|
|
1190
1334
|
}
|
|
1191
1335
|
|
|
1192
|
-
async function handlePersonaHeartbeatCommand(slug, flags = {}) {
|
|
1193
|
-
const client = createCliPersonaApiClient(flags);
|
|
1194
|
-
const runtime = resolvePersonaRuntimeInput(
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1336
|
+
async function handlePersonaHeartbeatCommand(slug, flags = {}) {
|
|
1337
|
+
const client = createCliPersonaApiClient(flags);
|
|
1338
|
+
const runtime = resolvePersonaRuntimeInput(
|
|
1339
|
+
flags,
|
|
1340
|
+
{},
|
|
1341
|
+
{
|
|
1342
|
+
slug,
|
|
1343
|
+
workspaceRoot: resolvePersonaWorkspaceRoot(flags),
|
|
1344
|
+
},
|
|
1345
|
+
);
|
|
1346
|
+
const payload = await client.heartbeatRuntime({
|
|
1347
|
+
slug,
|
|
1348
|
+
endpoint: runtime.endpoint,
|
|
1198
1349
|
healthcheckUrl: runtime.healthcheckUrl,
|
|
1199
1350
|
localPort: runtime.localPort,
|
|
1200
1351
|
transport: runtime.transport,
|
|
@@ -1231,16 +1382,22 @@ async function handlePersonaJobStartCommand(jobId, flags = {}) {
|
|
|
1231
1382
|
printStructuredResult(payload, isTruthyFlag(flags.json));
|
|
1232
1383
|
}
|
|
1233
1384
|
|
|
1234
|
-
async function handlePersonaJobSucceedCommand(jobId, flags = {}) {
|
|
1235
|
-
const client = createCliPersonaApiClient(flags);
|
|
1236
|
-
const
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1385
|
+
async function handlePersonaJobSucceedCommand(jobId, flags = {}) {
|
|
1386
|
+
const client = createCliPersonaApiClient(flags);
|
|
1387
|
+
const workspacePath = String(flags['workspace-path'] || flags.workspacePath || '').trim();
|
|
1388
|
+
if (!workspacePath) {
|
|
1389
|
+
throw new Error('Workspace path is required. Use --workspace-path.');
|
|
1390
|
+
}
|
|
1391
|
+
const runtime = resolvePersonaRuntimeInput(
|
|
1392
|
+
flags,
|
|
1393
|
+
{},
|
|
1394
|
+
{
|
|
1395
|
+
workspacePath,
|
|
1396
|
+
},
|
|
1397
|
+
);
|
|
1398
|
+
|
|
1399
|
+
const payload = await client.succeedJob({
|
|
1400
|
+
jobId,
|
|
1244
1401
|
workspacePath,
|
|
1245
1402
|
localPort: runtime.localPort,
|
|
1246
1403
|
transport: runtime.transport,
|
|
@@ -1408,6 +1565,162 @@ async function handlePersonaDeleteCommand(slug, flags = {}) {
|
|
|
1408
1565
|
});
|
|
1409
1566
|
printStructuredResult(result, isTruthyFlag(flags.json));
|
|
1410
1567
|
}
|
|
1568
|
+
|
|
1569
|
+
async function restartManagedPersonaRefreshTargets(flags = {}) {
|
|
1570
|
+
const workspaceRoot = resolvePersonaWorkspaceRoot(flags);
|
|
1571
|
+
const targets = collectManagedPersonaRefreshTargets({
|
|
1572
|
+
workspaceRoot,
|
|
1573
|
+
includeStopped: isTruthyFlag(flags['include-stopped']),
|
|
1574
|
+
});
|
|
1575
|
+
const results = [];
|
|
1576
|
+
|
|
1577
|
+
let client = null;
|
|
1578
|
+
let registrationError = '';
|
|
1579
|
+
try {
|
|
1580
|
+
client = createCliPersonaApiClient(flags);
|
|
1581
|
+
} catch (error) {
|
|
1582
|
+
registrationError = error instanceof Error ? error.message : String(error);
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
for (const target of targets) {
|
|
1586
|
+
const state = target.state && typeof target.state === 'object' ? target.state : {};
|
|
1587
|
+
const launchResult = await launchManagedPersonaRuntime({
|
|
1588
|
+
slug: target.slug,
|
|
1589
|
+
name: String(state.name || target.slug).trim() || target.slug,
|
|
1590
|
+
description: String(state.description || state.name || target.slug).trim() || target.slug,
|
|
1591
|
+
workspaceRoot,
|
|
1592
|
+
templateVersion: String(state.templateVersion || 'v1').trim() || 'v1',
|
|
1593
|
+
forceInstall: isTruthyFlag(flags['force-install']),
|
|
1594
|
+
restart: true,
|
|
1595
|
+
logFilePath: String(state.logFilePath || '').trim(),
|
|
1596
|
+
entryUrl: '',
|
|
1597
|
+
transport: 'local',
|
|
1598
|
+
});
|
|
1599
|
+
|
|
1600
|
+
let registration = null;
|
|
1601
|
+
if (client) {
|
|
1602
|
+
try {
|
|
1603
|
+
registration = await client.registerRuntime({
|
|
1604
|
+
slug: launchResult.slug,
|
|
1605
|
+
endpoint: launchResult.runtime.endpoint,
|
|
1606
|
+
healthcheckUrl: launchResult.runtime.healthcheckUrl,
|
|
1607
|
+
localPort: launchResult.runtime.localPort,
|
|
1608
|
+
transport: launchResult.runtime.transport,
|
|
1609
|
+
startedAt: new Date().toISOString(),
|
|
1610
|
+
});
|
|
1611
|
+
} catch (error) {
|
|
1612
|
+
registration = {
|
|
1613
|
+
ok: false,
|
|
1614
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
results.push({
|
|
1620
|
+
slug: launchResult.slug,
|
|
1621
|
+
workspacePath: launchResult.workspacePath,
|
|
1622
|
+
localPort: launchResult.runtime.localPort,
|
|
1623
|
+
endpoint: launchResult.runtime.endpoint,
|
|
1624
|
+
healthcheckUrl: launchResult.runtime.healthcheckUrl,
|
|
1625
|
+
registration,
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
return {
|
|
1630
|
+
workspaceRoot,
|
|
1631
|
+
targets,
|
|
1632
|
+
results,
|
|
1633
|
+
registrationError,
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
async function refreshBridgeForUpdate(flags = {}) {
|
|
1638
|
+
if (process.platform === 'darwin') {
|
|
1639
|
+
const launchdStatus = readBridgeLaunchdStatus();
|
|
1640
|
+
if (launchdStatus.installed) {
|
|
1641
|
+
await stopBridgeLaunchdService();
|
|
1642
|
+
startBridgeLaunchdService();
|
|
1643
|
+
incrementBridgeMetric('bridge_restart_count');
|
|
1644
|
+
return {
|
|
1645
|
+
restarted: true,
|
|
1646
|
+
mode: 'service',
|
|
1647
|
+
target: launchdStatus.target,
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
const running = findRunningBridgeProcess();
|
|
1653
|
+
if (!running) {
|
|
1654
|
+
return {
|
|
1655
|
+
restarted: false,
|
|
1656
|
+
mode: 'process',
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
const stopResult = await stopBridgeProcesses();
|
|
1661
|
+
if (Array.isArray(stopResult.stillAlive) && stopResult.stillAlive.length > 0) {
|
|
1662
|
+
throw new Error(`Failed to stop bridge processes: ${stopResult.stillAlive.join(', ')}`);
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
const detachedResult = startBridgeDetachedProcess(flags);
|
|
1666
|
+
incrementBridgeMetric('bridge_restart_count');
|
|
1667
|
+
return {
|
|
1668
|
+
restarted: true,
|
|
1669
|
+
mode: 'process',
|
|
1670
|
+
pid: detachedResult.pid,
|
|
1671
|
+
alreadyRunning: Boolean(detachedResult.alreadyRunning),
|
|
1672
|
+
};
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
async function handleOpenclawRefreshCommand(flags = {}) {
|
|
1676
|
+
const currentVersion = currentPackageVersion();
|
|
1677
|
+
let latestVersion = '';
|
|
1678
|
+
if (!isTruthyFlag(flags['skip-version-check'])) {
|
|
1679
|
+
latestVersion = await fetchLatestPublishedVersion('oomi-ai');
|
|
1680
|
+
if (latestVersion && compareVersions(currentVersion, latestVersion) < 0) {
|
|
1681
|
+
throw new Error(
|
|
1682
|
+
`Installed oomi-ai ${currentVersion} is behind npm ${latestVersion}. Update first, then rerun: oomi openclaw refresh`
|
|
1683
|
+
);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
const personaRefresh = await restartManagedPersonaRefreshTargets(flags);
|
|
1688
|
+
const bridgeRefresh = await refreshBridgeForUpdate(flags);
|
|
1689
|
+
const payload = {
|
|
1690
|
+
ok: true,
|
|
1691
|
+
currentVersion,
|
|
1692
|
+
latestVersion: latestVersion || currentVersion,
|
|
1693
|
+
personas: {
|
|
1694
|
+
discovered: personaRefresh.targets.length,
|
|
1695
|
+
restarted: personaRefresh.results.length,
|
|
1696
|
+
registrationError: personaRefresh.registrationError || null,
|
|
1697
|
+
results: personaRefresh.results,
|
|
1698
|
+
},
|
|
1699
|
+
bridge: bridgeRefresh,
|
|
1700
|
+
};
|
|
1701
|
+
|
|
1702
|
+
if (isTruthyFlag(flags.json)) {
|
|
1703
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
1704
|
+
return;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
console.log(`Oomi refresh complete (${payload.currentVersion})`);
|
|
1708
|
+
console.log(`Personas restarted: ${payload.personas.restarted}/${payload.personas.discovered}`);
|
|
1709
|
+
if (payload.personas.registrationError) {
|
|
1710
|
+
console.log(`Backend registration skipped: ${payload.personas.registrationError}`);
|
|
1711
|
+
}
|
|
1712
|
+
payload.personas.results.forEach((result) => {
|
|
1713
|
+
console.log(`- ${result.slug}: ${result.endpoint}`);
|
|
1714
|
+
if (result.registration && result.registration.ok === false) {
|
|
1715
|
+
console.log(` registration: ${result.registration.error}`);
|
|
1716
|
+
}
|
|
1717
|
+
});
|
|
1718
|
+
if (payload.bridge.restarted) {
|
|
1719
|
+
console.log(`Bridge restarted (${payload.bridge.mode}).`);
|
|
1720
|
+
} else {
|
|
1721
|
+
console.log('Bridge not running; no restart performed.');
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1411
1724
|
|
|
1412
1725
|
function resolveOpenclawConfigPath() {
|
|
1413
1726
|
const candidates = resolveOpenclawConfigCandidates();
|
|
@@ -5339,15 +5652,20 @@ async function main() {
|
|
|
5339
5652
|
return;
|
|
5340
5653
|
}
|
|
5341
5654
|
|
|
5342
|
-
if (command === 'openclaw' && subcommand === 'debug') {
|
|
5343
|
-
await handleOpenclawDebugCommand(args.positionals[0], args.flags);
|
|
5344
|
-
return;
|
|
5345
|
-
}
|
|
5346
|
-
|
|
5347
|
-
if (command === '
|
|
5348
|
-
await
|
|
5349
|
-
return;
|
|
5350
|
-
}
|
|
5655
|
+
if (command === 'openclaw' && subcommand === 'debug') {
|
|
5656
|
+
await handleOpenclawDebugCommand(args.positionals[0], args.flags);
|
|
5657
|
+
return;
|
|
5658
|
+
}
|
|
5659
|
+
|
|
5660
|
+
if (command === 'openclaw' && subcommand === 'refresh') {
|
|
5661
|
+
await handleOpenclawRefreshCommand(args.flags);
|
|
5662
|
+
return;
|
|
5663
|
+
}
|
|
5664
|
+
|
|
5665
|
+
if (command === 'personas' && subcommand === 'sync') {
|
|
5666
|
+
await syncPersonas({ backendUrl: args.flags['backend-url'], root: args.flags.root });
|
|
5667
|
+
return;
|
|
5668
|
+
}
|
|
5351
5669
|
|
|
5352
5670
|
if (command === 'personas' && subcommand === 'create') {
|
|
5353
5671
|
const id = args.positionals[0];
|
|
@@ -5504,8 +5822,10 @@ export {
|
|
|
5504
5822
|
runBridgeCallbackSafely,
|
|
5505
5823
|
extractGatewayRequestMeta,
|
|
5506
5824
|
extractGatewayResponseMeta,
|
|
5507
|
-
isServiceManagedBridgeStart,
|
|
5508
|
-
isGatewayRunStartedFrame,
|
|
5509
|
-
isBridgeWorkerCommand,
|
|
5510
|
-
parsePositiveInteger,
|
|
5511
|
-
|
|
5825
|
+
isServiceManagedBridgeStart,
|
|
5826
|
+
isGatewayRunStartedFrame,
|
|
5827
|
+
isBridgeWorkerCommand,
|
|
5828
|
+
parsePositiveInteger,
|
|
5829
|
+
collectManagedPersonaRefreshTargets,
|
|
5830
|
+
resolvePersonaRuntimeInput,
|
|
5831
|
+
};
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
defaultPersonaWorkspaceRoot,
|
|
9
9
|
installPersonaWorkspace,
|
|
10
10
|
isPersonaWorkspaceProcessRunning,
|
|
11
|
+
resolvePersonaHealthPath,
|
|
11
12
|
resolvePersonaDevCommand,
|
|
12
13
|
syncLegacyWebSpatialScaffoldFiles,
|
|
13
14
|
syncVendoredWebSpatialPackages,
|
|
@@ -143,20 +144,12 @@ export function slugifyPersonaName(name) {
|
|
|
143
144
|
return normalized;
|
|
144
145
|
}
|
|
145
146
|
|
|
146
|
-
function resolveHealthPath(workspacePath) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
try {
|
|
153
|
-
const parsed = JSON.parse(fs.readFileSync(runtimeConfigPath, 'utf8'));
|
|
154
|
-
const healthPath = trimString(parsed?.healthPath);
|
|
155
|
-
return healthPath || '/oomi.health.json';
|
|
156
|
-
} catch {
|
|
157
|
-
return '/oomi.health.json';
|
|
158
|
-
}
|
|
159
|
-
}
|
|
147
|
+
function resolveHealthPath(workspacePath) {
|
|
148
|
+
return resolvePersonaHealthPath({
|
|
149
|
+
workspacePath,
|
|
150
|
+
fallback: '/oomi.health.json',
|
|
151
|
+
});
|
|
152
|
+
}
|
|
160
153
|
|
|
161
154
|
function workspaceNeedsScaffold(workspacePath) {
|
|
162
155
|
return !fs.existsSync(path.join(workspacePath, 'package.json'));
|
|
@@ -210,28 +203,32 @@ async function ensureWorkspaceInstall({
|
|
|
210
203
|
return true;
|
|
211
204
|
}
|
|
212
205
|
|
|
213
|
-
function buildRuntimeRegistration({
|
|
214
|
-
localRuntime,
|
|
215
|
-
entryUrl,
|
|
216
|
-
transport,
|
|
217
|
-
}) {
|
|
218
|
-
const safeEntryUrl = trimString(entryUrl);
|
|
219
|
-
if (safeEntryUrl) {
|
|
220
|
-
return {
|
|
221
|
-
endpoint: safeEntryUrl,
|
|
222
|
-
transport: trimString(transport) || 'relay',
|
|
223
|
-
healthcheckUrl: localRuntime.healthcheckUrl,
|
|
224
|
-
localPort: localRuntime.localPort,
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
206
|
+
function buildRuntimeRegistration({
|
|
207
|
+
localRuntime,
|
|
208
|
+
entryUrl,
|
|
209
|
+
transport,
|
|
210
|
+
}) {
|
|
211
|
+
const safeEntryUrl = trimString(entryUrl);
|
|
212
|
+
if (safeEntryUrl) {
|
|
213
|
+
return {
|
|
214
|
+
endpoint: safeEntryUrl,
|
|
215
|
+
transport: trimString(transport) || 'relay',
|
|
216
|
+
healthcheckUrl: localRuntime.healthcheckUrl,
|
|
217
|
+
localPort: localRuntime.localPort,
|
|
218
|
+
localEndpoint: localRuntime.endpoint,
|
|
219
|
+
reachableEndpoint: localRuntime.reachableEndpoint,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
endpoint: localRuntime.reachableEndpoint || localRuntime.endpoint,
|
|
225
|
+
transport: trimString(transport) || localRuntime.transport,
|
|
226
|
+
healthcheckUrl: localRuntime.healthcheckUrl,
|
|
227
|
+
localPort: localRuntime.localPort,
|
|
228
|
+
localEndpoint: localRuntime.endpoint,
|
|
229
|
+
reachableEndpoint: localRuntime.reachableEndpoint,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
235
232
|
|
|
236
233
|
export async function launchManagedPersonaRuntime({
|
|
237
234
|
slug,
|
|
@@ -266,19 +263,31 @@ export async function launchManagedPersonaRuntime({
|
|
|
266
263
|
workspacePath,
|
|
267
264
|
templateVersion,
|
|
268
265
|
});
|
|
269
|
-
const installed = await ensureWorkspaceInstall({
|
|
270
|
-
workspacePath,
|
|
271
|
-
forceInstall,
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
const healthPath =
|
|
275
|
-
const preferredPort = previousState.localPort || scaffoldInfo.defaultPort;
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
266
|
+
const installed = await ensureWorkspaceInstall({
|
|
267
|
+
workspacePath,
|
|
268
|
+
forceInstall,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const healthPath = resolveHealthPath(workspacePath);
|
|
272
|
+
const preferredPort = previousState.localPort || scaffoldInfo.defaultPort;
|
|
273
|
+
const expectedDevCommand = resolvePersonaDevCommand({
|
|
274
|
+
workspacePath,
|
|
275
|
+
localPort: preferredPort,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
let reusingRunningProcess = false;
|
|
279
|
+
if (
|
|
280
|
+
!restart &&
|
|
281
|
+
Number.isInteger(previousState.pid) &&
|
|
282
|
+
isPersonaWorkspaceProcessRunning(previousState.pid, {
|
|
283
|
+
workspacePath,
|
|
284
|
+
expectedCommand: expectedDevCommand,
|
|
285
|
+
localPort: preferredPort,
|
|
286
|
+
})
|
|
287
|
+
) {
|
|
288
|
+
try {
|
|
289
|
+
await waitForPersonaRuntime({
|
|
290
|
+
healthcheckUrl: previousState.healthcheckUrl || buildLocalPersonaRuntime({
|
|
282
291
|
localPort: preferredPort,
|
|
283
292
|
healthPath,
|
|
284
293
|
}).healthcheckUrl,
|
|
@@ -326,24 +335,27 @@ export async function launchManagedPersonaRuntime({
|
|
|
326
335
|
entryUrl,
|
|
327
336
|
transport,
|
|
328
337
|
});
|
|
329
|
-
const runtimeState = updatePersonaRuntimeState(workspacePath, {
|
|
338
|
+
const runtimeState = updatePersonaRuntimeState(workspacePath, {
|
|
330
339
|
slug: safeSlug,
|
|
331
340
|
name: safeName,
|
|
332
341
|
description: safeDescription,
|
|
333
342
|
workspacePath,
|
|
334
343
|
templateVersion,
|
|
335
|
-
localPort: localRuntime.localPort,
|
|
336
|
-
localEndpoint: localRuntime.endpoint,
|
|
337
|
-
|
|
338
|
-
|
|
344
|
+
localPort: localRuntime.localPort,
|
|
345
|
+
localEndpoint: localRuntime.endpoint,
|
|
346
|
+
reachableEndpoint: localRuntime.reachableEndpoint,
|
|
347
|
+
bindHost: localRuntime.bindHost,
|
|
348
|
+
reachableHost: localRuntime.reachableHost,
|
|
349
|
+
endpoint: registration.endpoint,
|
|
350
|
+
entryUrl: registration.endpoint,
|
|
339
351
|
transport: registration.transport,
|
|
340
352
|
healthcheckUrl: localRuntime.healthcheckUrl,
|
|
341
353
|
pid: processInfo.pid,
|
|
342
354
|
logFilePath: processInfo.logFilePath,
|
|
343
|
-
status: 'running',
|
|
344
|
-
lastStartedAt: new Date().toISOString(),
|
|
345
|
-
devCommand: resolvePersonaDevCommand({ workspacePath, localPort }),
|
|
346
|
-
});
|
|
355
|
+
status: 'running',
|
|
356
|
+
lastStartedAt: new Date().toISOString(),
|
|
357
|
+
devCommand: resolvePersonaDevCommand({ workspacePath, localPort }),
|
|
358
|
+
});
|
|
347
359
|
|
|
348
360
|
return {
|
|
349
361
|
ok: true,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { spawn, spawnSync } from 'node:child_process';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
@@ -29,12 +30,25 @@ const WEBSPATIAL_VENDOR_ROOT = path.join(
|
|
|
29
30
|
);
|
|
30
31
|
const VENDORED_WEBSPATIAL_CORE_SPEC = 'file:./vendor/webspatial/core-sdk';
|
|
31
32
|
const VENDORED_WEBSPATIAL_REACT_SPEC = 'file:./vendor/webspatial/react-sdk';
|
|
33
|
+
const WEBSPATIAL_RUNTIME_BASE_PATH = '/webspatial/avp';
|
|
32
34
|
const WEBSPATIAL_TEMPLATE_DEV_DEPENDENCIES = [
|
|
33
35
|
'@webspatial/builder',
|
|
34
36
|
'@webspatial/platform-visionos',
|
|
35
37
|
'@webspatial/vite-plugin',
|
|
36
38
|
];
|
|
37
39
|
const LEGACY_WEBSPATIAL_TEMPLATE_FILE_RULES = [
|
|
40
|
+
{
|
|
41
|
+
relativePath: 'oomi.runtime.json',
|
|
42
|
+
shouldReplace: (content) =>
|
|
43
|
+
content.includes('"renderMode": "webspatial"') &&
|
|
44
|
+
content.includes('"healthPath": "/oomi.health.json"'),
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
relativePath: 'package.json',
|
|
48
|
+
shouldReplace: (content) =>
|
|
49
|
+
content.includes('vite --host 127.0.0.1 --port 4789') ||
|
|
50
|
+
content.includes('vite preview --host 127.0.0.1 --port 4789'),
|
|
51
|
+
},
|
|
38
52
|
{
|
|
39
53
|
relativePath: path.join('src', 'spatial.ts'),
|
|
40
54
|
shouldReplace: (content) =>
|
|
@@ -198,15 +212,117 @@ function normalizePositiveInteger(value) {
|
|
|
198
212
|
return Math.floor(parsed);
|
|
199
213
|
}
|
|
200
214
|
|
|
201
|
-
function
|
|
215
|
+
function isWildcardHost(host) {
|
|
216
|
+
const normalized = String(host || '').trim().toLowerCase();
|
|
217
|
+
return normalized === '0.0.0.0' || normalized === '::' || normalized === '[::]';
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function isLoopbackHost(host) {
|
|
221
|
+
const normalized = String(host || '').trim().toLowerCase();
|
|
222
|
+
return normalized === 'localhost' || normalized === '127.0.0.1' || normalized === '::1' || normalized === '[::1]';
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function isPrivateIpv4(address) {
|
|
226
|
+
if (!/^\d+\.\d+\.\d+\.\d+$/u.test(address)) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const [first, second] = address.split('.').map((segment) => Number(segment));
|
|
231
|
+
if (first === 10) return true;
|
|
232
|
+
if (first === 192 && second === 168) return true;
|
|
233
|
+
if (first === 172 && second >= 16 && second <= 31) return true;
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function scorePersonaNetworkCandidate(candidate) {
|
|
238
|
+
let score = 0;
|
|
239
|
+
const name = String(candidate.name || '').toLowerCase();
|
|
240
|
+
const address = String(candidate.address || '');
|
|
241
|
+
|
|
242
|
+
if (isPrivateIpv4(address)) score += 40;
|
|
243
|
+
if (address.startsWith('192.168.')) score += 8;
|
|
244
|
+
if (address.startsWith('10.')) score += 6;
|
|
245
|
+
if (/ethernet|wi-?fi|wlan|en\d|eth\d/iu.test(name)) score += 12;
|
|
246
|
+
if (/hyper-v|vethernet|wsl|docker|vmware|virtualbox|tailscale|loopback|bridge/iu.test(name)) score -= 30;
|
|
247
|
+
if (address.startsWith('169.254.')) score -= 100;
|
|
248
|
+
return score;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function formatPersonaRuntimeHostForUrl(host) {
|
|
252
|
+
const safeHost = String(host || '').trim();
|
|
253
|
+
if (!safeHost) {
|
|
254
|
+
return '127.0.0.1';
|
|
255
|
+
}
|
|
256
|
+
if (safeHost.includes(':') && !safeHost.startsWith('[')) {
|
|
257
|
+
return `[${safeHost}]`;
|
|
258
|
+
}
|
|
259
|
+
return safeHost;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function resolvePersonaBindHost() {
|
|
202
263
|
const value = String(process.env.OOMI_PERSONA_BIND_HOST || '').trim();
|
|
203
|
-
return value || '
|
|
264
|
+
return value || '0.0.0.0';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function resolvePersonaReachableHost({
|
|
268
|
+
bindHost = resolvePersonaBindHost(),
|
|
269
|
+
env = process.env,
|
|
270
|
+
networkInterfaces = os.networkInterfaces(),
|
|
271
|
+
} = {}) {
|
|
272
|
+
const explicit = String(env.OOMI_PERSONA_PUBLIC_HOST || '').trim();
|
|
273
|
+
if (explicit) {
|
|
274
|
+
return explicit;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const safeBindHost = String(bindHost || '').trim();
|
|
278
|
+
if (safeBindHost && !isWildcardHost(safeBindHost) && !isLoopbackHost(safeBindHost)) {
|
|
279
|
+
return safeBindHost;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const candidates = [];
|
|
283
|
+
for (const [name, entries] of Object.entries(networkInterfaces || {})) {
|
|
284
|
+
for (const entry of Array.isArray(entries) ? entries : []) {
|
|
285
|
+
if (!entry || entry.internal || entry.family !== 'IPv4') {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const address = String(entry.address || '').trim();
|
|
290
|
+
if (!address || isLoopbackHost(address) || address.startsWith('169.254.')) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
candidates.push({
|
|
295
|
+
name,
|
|
296
|
+
address,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const winner = candidates
|
|
302
|
+
.sort((left, right) => scorePersonaNetworkCandidate(right) - scorePersonaNetworkCandidate(left))[0];
|
|
303
|
+
|
|
304
|
+
return winner?.address || '127.0.0.1';
|
|
204
305
|
}
|
|
205
306
|
|
|
206
307
|
function trimString(value) {
|
|
207
308
|
return typeof value === 'string' ? value.trim() : '';
|
|
208
309
|
}
|
|
209
310
|
|
|
311
|
+
function normalizePersonaHealthPath(healthPath, runtimeConfig = {}) {
|
|
312
|
+
const safeHealthPath = trimString(healthPath) || '/oomi.health.json';
|
|
313
|
+
const renderMode = trimString(runtimeConfig?.renderMode).toLowerCase();
|
|
314
|
+
if (renderMode !== 'webspatial') {
|
|
315
|
+
return safeHealthPath;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (safeHealthPath.startsWith(`${WEBSPATIAL_RUNTIME_BASE_PATH}/`)) {
|
|
319
|
+
return safeHealthPath;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const normalizedSuffix = safeHealthPath.startsWith('/') ? safeHealthPath : `/${safeHealthPath}`;
|
|
323
|
+
return `${WEBSPATIAL_RUNTIME_BASE_PATH}${normalizedSuffix}`;
|
|
324
|
+
}
|
|
325
|
+
|
|
210
326
|
function readPersonaConfigLiteral(source, key) {
|
|
211
327
|
if (!source) {
|
|
212
328
|
return '';
|
|
@@ -576,6 +692,17 @@ export function resolvePersonaDevEnvironment({
|
|
|
576
692
|
return {};
|
|
577
693
|
}
|
|
578
694
|
|
|
695
|
+
export function resolvePersonaHealthPath({
|
|
696
|
+
workspacePath,
|
|
697
|
+
fallback = '/oomi.health.json',
|
|
698
|
+
} = {}) {
|
|
699
|
+
const runtimeConfig = readPersonaRuntimeConfig(workspacePath);
|
|
700
|
+
return normalizePersonaHealthPath(
|
|
701
|
+
trimString(runtimeConfig?.healthPath) || fallback,
|
|
702
|
+
runtimeConfig,
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
|
|
579
706
|
export function startPersonaWorkspace({
|
|
580
707
|
workspacePath,
|
|
581
708
|
logFilePath,
|
|
@@ -754,23 +881,30 @@ export async function waitForPersonaRuntime({
|
|
|
754
881
|
throw new Error(`Timed out waiting for persona runtime healthcheck: ${message}`);
|
|
755
882
|
}
|
|
756
883
|
|
|
757
|
-
export function buildLocalPersonaRuntime({
|
|
758
|
-
localPort,
|
|
759
|
-
healthPath,
|
|
760
|
-
}) {
|
|
761
|
-
const port = Number(localPort);
|
|
762
|
-
if (!Number.isFinite(port) || port <= 0) {
|
|
763
|
-
throw new Error('Local port is required.');
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
const
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
884
|
+
export function buildLocalPersonaRuntime({
|
|
885
|
+
localPort,
|
|
886
|
+
healthPath,
|
|
887
|
+
}) {
|
|
888
|
+
const port = Number(localPort);
|
|
889
|
+
if (!Number.isFinite(port) || port <= 0) {
|
|
890
|
+
throw new Error('Local port is required.');
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
const bindHost = resolvePersonaBindHost();
|
|
894
|
+
const reachableHost = resolvePersonaReachableHost({ bindHost });
|
|
895
|
+
const endpoint = `http://127.0.0.1:${port}`;
|
|
896
|
+
const reachableEndpoint = `http://${formatPersonaRuntimeHostForUrl(reachableHost)}:${port}`;
|
|
897
|
+
const normalizedHealthPath = healthPath || '/oomi.health.json';
|
|
898
|
+
return {
|
|
899
|
+
transport: 'local',
|
|
900
|
+
endpoint,
|
|
901
|
+
reachableEndpoint,
|
|
902
|
+
bindHost,
|
|
903
|
+
reachableHost,
|
|
904
|
+
localPort: port,
|
|
905
|
+
healthcheckUrl: `${endpoint}${normalizedHealthPath}`,
|
|
906
|
+
};
|
|
907
|
+
}
|
|
774
908
|
|
|
775
909
|
export function defaultPersonaWorkspaceRoot() {
|
|
776
910
|
return resolveOpenclawPersonasDir();
|
|
@@ -4,8 +4,13 @@ import path from 'node:path';
|
|
|
4
4
|
import { resolveOpenclawLegacyPersonasDir } from './openclawPaths.js';
|
|
5
5
|
import { createPersonaApiClient } from './personaApiClient.js';
|
|
6
6
|
import { launchManagedPersonaRuntime } from './personaRuntimeManager.js';
|
|
7
|
-
import { readPersonaRuntimeState } from './personaRuntimeRegistry.js';
|
|
8
|
-
import {
|
|
7
|
+
import { readPersonaRuntimeState, updatePersonaRuntimeState } from './personaRuntimeRegistry.js';
|
|
8
|
+
import {
|
|
9
|
+
buildLocalPersonaRuntime,
|
|
10
|
+
isPersonaWorkspaceProcessRunning,
|
|
11
|
+
resolvePersonaDevCommand,
|
|
12
|
+
resolvePersonaHealthPath,
|
|
13
|
+
} from './personaRuntimeProcess.js';
|
|
9
14
|
|
|
10
15
|
function trimString(value) {
|
|
11
16
|
return typeof value === 'string' ? value.trim() : '';
|
|
@@ -76,14 +81,32 @@ async function reconcileWorkspace({
|
|
|
76
81
|
const runtime = {
|
|
77
82
|
slug,
|
|
78
83
|
endpoint: trimString(state.endpoint || state.entryUrl || state.localEndpoint),
|
|
84
|
+
localEndpoint: trimString(state.localEndpoint),
|
|
85
|
+
reachableEndpoint: trimString(state.reachableEndpoint),
|
|
79
86
|
healthcheckUrl: trimString(state.healthcheckUrl),
|
|
80
87
|
transport: trimString(state.transport) || 'local',
|
|
81
88
|
localPort: Number.isFinite(Number(state.localPort)) ? Number(state.localPort) : null,
|
|
82
89
|
};
|
|
83
90
|
|
|
91
|
+
const localRuntime = runtime.localPort
|
|
92
|
+
? buildLocalPersonaRuntime({
|
|
93
|
+
localPort: runtime.localPort,
|
|
94
|
+
healthPath: resolvePersonaHealthPath({
|
|
95
|
+
workspacePath,
|
|
96
|
+
fallback: '/oomi.health.json',
|
|
97
|
+
}),
|
|
98
|
+
})
|
|
99
|
+
: null;
|
|
100
|
+
|
|
101
|
+
const expectedDevCommand = runtime.localPort
|
|
102
|
+
? resolvePersonaDevCommand({
|
|
103
|
+
workspacePath,
|
|
104
|
+
localPort: runtime.localPort,
|
|
105
|
+
})
|
|
106
|
+
: state.devCommand;
|
|
84
107
|
const processRunning = isPersonaWorkspaceProcessRunning(state.pid, {
|
|
85
108
|
workspacePath,
|
|
86
|
-
expectedCommand:
|
|
109
|
+
expectedCommand: expectedDevCommand,
|
|
87
110
|
localPort: runtime.localPort,
|
|
88
111
|
});
|
|
89
112
|
|
|
@@ -110,6 +133,8 @@ async function reconcileWorkspace({
|
|
|
110
133
|
effectiveRuntime = {
|
|
111
134
|
slug,
|
|
112
135
|
endpoint: launchResult.runtime.endpoint,
|
|
136
|
+
localEndpoint: launchResult.runtime.localEndpoint || launchResult.localRuntime.endpoint,
|
|
137
|
+
reachableEndpoint: launchResult.runtime.reachableEndpoint || launchResult.localRuntime.reachableEndpoint,
|
|
113
138
|
healthcheckUrl: launchResult.runtime.healthcheckUrl,
|
|
114
139
|
transport: launchResult.runtime.transport,
|
|
115
140
|
localPort: launchResult.runtime.localPort,
|
|
@@ -135,7 +160,106 @@ async function reconcileWorkspace({
|
|
|
135
160
|
|
|
136
161
|
const healthy = await healthcheckOk(effectiveRuntime.healthcheckUrl);
|
|
137
162
|
if (!healthy) {
|
|
138
|
-
|
|
163
|
+
if (!autoRestart) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const recovered = await launchManagedPersonaRuntime({
|
|
169
|
+
slug,
|
|
170
|
+
name: trimString(state.name) || slug,
|
|
171
|
+
description: trimString(state.description) || trimString(state.name) || slug,
|
|
172
|
+
workspaceRoot,
|
|
173
|
+
templateVersion: trimString(state.templateVersion) || 'v1',
|
|
174
|
+
forceInstall: false,
|
|
175
|
+
restart: true,
|
|
176
|
+
logFilePath: trimString(state.logFilePath),
|
|
177
|
+
entryUrl: '',
|
|
178
|
+
transport: 'local',
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
effectiveRuntime = {
|
|
182
|
+
slug,
|
|
183
|
+
endpoint: recovered.runtime.endpoint,
|
|
184
|
+
localEndpoint: recovered.runtime.localEndpoint || recovered.localRuntime.endpoint,
|
|
185
|
+
reachableEndpoint: recovered.runtime.reachableEndpoint || recovered.localRuntime.reachableEndpoint,
|
|
186
|
+
healthcheckUrl: recovered.runtime.healthcheckUrl,
|
|
187
|
+
transport: recovered.runtime.transport,
|
|
188
|
+
localPort: recovered.runtime.localPort,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
await client.registerRuntime({
|
|
192
|
+
slug,
|
|
193
|
+
endpoint: effectiveRuntime.endpoint,
|
|
194
|
+
healthcheckUrl: effectiveRuntime.healthcheckUrl,
|
|
195
|
+
localPort: effectiveRuntime.localPort,
|
|
196
|
+
transport: effectiveRuntime.transport,
|
|
197
|
+
startedAt: new Date().toISOString(),
|
|
198
|
+
});
|
|
199
|
+
} catch (error) {
|
|
200
|
+
logger.warn?.(
|
|
201
|
+
`[persona-runtime] unhealthy runtime restart failed for ${slug}: ${
|
|
202
|
+
error instanceof Error ? error.message : String(error)
|
|
203
|
+
}`
|
|
204
|
+
);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!healthy) {
|
|
210
|
+
const recoveredHealthy = await healthcheckOk(effectiveRuntime.healthcheckUrl);
|
|
211
|
+
if (!recoveredHealthy) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (localRuntime) {
|
|
217
|
+
const refreshedLocalRuntime = buildLocalPersonaRuntime({
|
|
218
|
+
localPort: effectiveRuntime.localPort,
|
|
219
|
+
healthPath: resolvePersonaHealthPath({
|
|
220
|
+
workspacePath,
|
|
221
|
+
fallback: '/oomi.health.json',
|
|
222
|
+
}),
|
|
223
|
+
});
|
|
224
|
+
const desiredEndpoint = refreshedLocalRuntime.reachableEndpoint || runtime.endpoint;
|
|
225
|
+
const endpointChanged = desiredEndpoint && desiredEndpoint !== effectiveRuntime.endpoint;
|
|
226
|
+
const localEndpointChanged = refreshedLocalRuntime.endpoint !== effectiveRuntime.localEndpoint;
|
|
227
|
+
const reachableEndpointChanged = refreshedLocalRuntime.reachableEndpoint !== effectiveRuntime.reachableEndpoint;
|
|
228
|
+
|
|
229
|
+
if (endpointChanged || localEndpointChanged || reachableEndpointChanged) {
|
|
230
|
+
effectiveRuntime = {
|
|
231
|
+
...effectiveRuntime,
|
|
232
|
+
endpoint: desiredEndpoint,
|
|
233
|
+
localEndpoint: refreshedLocalRuntime.endpoint,
|
|
234
|
+
reachableEndpoint: refreshedLocalRuntime.reachableEndpoint,
|
|
235
|
+
};
|
|
236
|
+
updatePersonaRuntimeState(workspacePath, {
|
|
237
|
+
endpoint: desiredEndpoint,
|
|
238
|
+
entryUrl: desiredEndpoint,
|
|
239
|
+
localEndpoint: refreshedLocalRuntime.endpoint,
|
|
240
|
+
reachableEndpoint: refreshedLocalRuntime.reachableEndpoint,
|
|
241
|
+
bindHost: refreshedLocalRuntime.bindHost,
|
|
242
|
+
reachableHost: refreshedLocalRuntime.reachableHost,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
await client.registerRuntime({
|
|
247
|
+
slug,
|
|
248
|
+
endpoint: effectiveRuntime.endpoint,
|
|
249
|
+
healthcheckUrl: effectiveRuntime.healthcheckUrl,
|
|
250
|
+
localPort: effectiveRuntime.localPort,
|
|
251
|
+
transport: effectiveRuntime.transport,
|
|
252
|
+
startedAt: new Date().toISOString(),
|
|
253
|
+
});
|
|
254
|
+
} catch (error) {
|
|
255
|
+
logger.warn?.(
|
|
256
|
+
`[persona-runtime] registration refresh failed for ${slug}: ${
|
|
257
|
+
error instanceof Error ? error.message : String(error)
|
|
258
|
+
}`
|
|
259
|
+
);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
139
263
|
}
|
|
140
264
|
|
|
141
265
|
try {
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"install:clean": "npm install",
|
|
8
|
-
"dev": "vite --host
|
|
9
|
-
"dev:avp": "cross-env XR_ENV=avp vite --host
|
|
8
|
+
"dev": "vite --host 0.0.0.0 --port 4789 --strictPort",
|
|
9
|
+
"dev:avp": "cross-env XR_ENV=avp vite --host 0.0.0.0 --port 4789 --strictPort",
|
|
10
10
|
"build": "vite build && cross-env XR_ENV=avp vite build",
|
|
11
|
-
"preview": "vite preview --host
|
|
11
|
+
"preview": "vite preview --host 0.0.0.0 --port 4789 --strictPort",
|
|
12
12
|
"lint": "eslint ."
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|