oomi-ai 0.2.28 → 0.2.38
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/README.md +257 -145
- package/bin/oomi-ai.js +2130 -1354
- package/lib/openclawDevGateway.js +384 -0
- package/lib/openclawPaths.js +78 -0
- package/lib/openclawProfile.js +265 -0
- package/lib/personaApiClient.js +304 -253
- package/lib/personaJobExecutor.js +35 -11
- package/lib/personaPortAllocator.js +36 -0
- package/lib/personaRuntimeManager.js +364 -0
- package/lib/personaRuntimeProcess.js +378 -121
- package/lib/personaRuntimeRegistry.js +67 -0
- package/lib/personaRuntimeSupervisor.js +193 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import WebSocket, { WebSocketServer } from 'ws';
|
|
4
|
+
|
|
5
|
+
import { resolveOpenclawConfigCandidates } from './openclawPaths.js';
|
|
6
|
+
import { inferSpokenMetadataFromContent } from './spokenMetadata.js';
|
|
7
|
+
|
|
8
|
+
const DEFAULT_GATEWAY_HOST = '127.0.0.1';
|
|
9
|
+
const DEFAULT_GATEWAY_PORT = 18789;
|
|
10
|
+
const PRIMER_MARKER = '[oomi:primer:v1]';
|
|
11
|
+
|
|
12
|
+
function trimString(value, fallback = '') {
|
|
13
|
+
return typeof value === 'string' && value.trim() ? value.trim() : fallback;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function readJsonSafe(filePath) {
|
|
17
|
+
if (!filePath || !fs.existsSync(filePath)) return null;
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function resolveGatewayConfig() {
|
|
26
|
+
const config = resolveOpenclawConfigCandidates()
|
|
27
|
+
.map((candidate) => readJsonSafe(candidate))
|
|
28
|
+
.find((entry) => entry && typeof entry === 'object');
|
|
29
|
+
|
|
30
|
+
const gateway = config?.gateway && typeof config.gateway === 'object' ? config.gateway : {};
|
|
31
|
+
const auth = gateway.auth && typeof gateway.auth === 'object' ? gateway.auth : {};
|
|
32
|
+
const port = Number(gateway.port);
|
|
33
|
+
return {
|
|
34
|
+
host: trimString(process.env.OPENCLAW_GATEWAY_HOST, DEFAULT_GATEWAY_HOST),
|
|
35
|
+
port: Number.isFinite(port) && port > 0 ? Math.floor(port) : DEFAULT_GATEWAY_PORT,
|
|
36
|
+
token: trimString(process.env.OPENCLAW_GATEWAY_TOKEN, trimString(auth.token)),
|
|
37
|
+
password: trimString(process.env.OPENCLAW_GATEWAY_PASSWORD, trimString(auth.password)),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function chunkText(text, maxChunkLength = 32) {
|
|
42
|
+
const words = trimString(text).split(/\s+/).filter(Boolean);
|
|
43
|
+
if (words.length === 0) return [];
|
|
44
|
+
|
|
45
|
+
const chunks = [];
|
|
46
|
+
let current = '';
|
|
47
|
+
for (const word of words) {
|
|
48
|
+
const next = current ? `${current} ${word}` : word;
|
|
49
|
+
if (current && next.length > maxChunkLength) {
|
|
50
|
+
chunks.push(current);
|
|
51
|
+
current = word;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
current = next;
|
|
55
|
+
}
|
|
56
|
+
if (current) {
|
|
57
|
+
chunks.push(current);
|
|
58
|
+
}
|
|
59
|
+
return chunks;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function buildHistoryMessage({ role, content, metadata, timestamp = Date.now() }) {
|
|
63
|
+
const message = {
|
|
64
|
+
role,
|
|
65
|
+
content: [{ type: 'text', text: content }],
|
|
66
|
+
ts: timestamp,
|
|
67
|
+
};
|
|
68
|
+
if (metadata && typeof metadata === 'object') {
|
|
69
|
+
message.metadata = metadata;
|
|
70
|
+
}
|
|
71
|
+
return message;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function buildLocalGatewayAssistantText(userText) {
|
|
75
|
+
const normalized = trimString(userText);
|
|
76
|
+
if (!normalized) {
|
|
77
|
+
return 'Local OpenClaw dev agent is connected and ready.';
|
|
78
|
+
}
|
|
79
|
+
return `Local OpenClaw dev agent received: ${normalized}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function createLocalGatewayAssistantFrames({ sessionKey, replyText, runId, seqStart = 1, timestampStart = Date.now() }) {
|
|
83
|
+
const spoken = inferSpokenMetadataFromContent(replyText);
|
|
84
|
+
const chunks = chunkText(replyText);
|
|
85
|
+
const frames = [];
|
|
86
|
+
let seq = seqStart;
|
|
87
|
+
let ts = timestampStart;
|
|
88
|
+
|
|
89
|
+
frames.push({
|
|
90
|
+
type: 'event',
|
|
91
|
+
event: 'agent',
|
|
92
|
+
payload: {
|
|
93
|
+
runId,
|
|
94
|
+
stream: 'lifecycle',
|
|
95
|
+
data: { phase: 'start' },
|
|
96
|
+
sessionKey,
|
|
97
|
+
seq: seq++,
|
|
98
|
+
ts: ts++,
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
for (const chunk of chunks) {
|
|
103
|
+
frames.push({
|
|
104
|
+
type: 'event',
|
|
105
|
+
event: 'agent',
|
|
106
|
+
payload: {
|
|
107
|
+
runId,
|
|
108
|
+
stream: 'assistant',
|
|
109
|
+
delta: chunk,
|
|
110
|
+
sessionKey,
|
|
111
|
+
seq: seq++,
|
|
112
|
+
ts: ts++,
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
frames.push({
|
|
118
|
+
type: 'event',
|
|
119
|
+
event: 'chat',
|
|
120
|
+
payload: {
|
|
121
|
+
runId,
|
|
122
|
+
sessionKey,
|
|
123
|
+
seq: seq++,
|
|
124
|
+
ts: ts++,
|
|
125
|
+
state: 'final',
|
|
126
|
+
message: {
|
|
127
|
+
role: 'assistant',
|
|
128
|
+
content: [{ type: 'text', text: replyText }],
|
|
129
|
+
timestamp: Date.now(),
|
|
130
|
+
metadata: spoken ? { spoken } : {},
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
frames.push({
|
|
136
|
+
type: 'event',
|
|
137
|
+
event: 'agent',
|
|
138
|
+
payload: {
|
|
139
|
+
runId,
|
|
140
|
+
stream: 'lifecycle',
|
|
141
|
+
data: { phase: 'end' },
|
|
142
|
+
sessionKey,
|
|
143
|
+
seq: seq++,
|
|
144
|
+
ts: ts++,
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return frames;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function createUnauthorizedResponse(id) {
|
|
152
|
+
return {
|
|
153
|
+
type: 'res',
|
|
154
|
+
id,
|
|
155
|
+
ok: false,
|
|
156
|
+
error: {
|
|
157
|
+
code: 'unauthorized',
|
|
158
|
+
message: 'Local gateway auth rejected the connection request.',
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function createOkResponse(id, payload = {}) {
|
|
164
|
+
return {
|
|
165
|
+
type: 'res',
|
|
166
|
+
id,
|
|
167
|
+
ok: true,
|
|
168
|
+
payload,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function createAbortedEvent(sessionKey) {
|
|
173
|
+
return {
|
|
174
|
+
type: 'event',
|
|
175
|
+
event: 'chat',
|
|
176
|
+
payload: {
|
|
177
|
+
sessionKey,
|
|
178
|
+
state: 'aborted',
|
|
179
|
+
timestamp: Date.now(),
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function scheduleAssistantFrames({ socket, sessionKey, frames, pendingReplies, logger }) {
|
|
185
|
+
const existing = pendingReplies.get(sessionKey);
|
|
186
|
+
if (existing?.timers) {
|
|
187
|
+
for (const timer of existing.timers) {
|
|
188
|
+
clearTimeout(timer);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const timers = [];
|
|
193
|
+
frames.forEach((frame, index) => {
|
|
194
|
+
const timer = setTimeout(() => {
|
|
195
|
+
if (socket.readyState !== WebSocket.OPEN) return;
|
|
196
|
+
socket.send(JSON.stringify(frame));
|
|
197
|
+
if (index === frames.length - 1) {
|
|
198
|
+
pendingReplies.delete(sessionKey);
|
|
199
|
+
}
|
|
200
|
+
}, index * 30);
|
|
201
|
+
timers.push(timer);
|
|
202
|
+
});
|
|
203
|
+
pendingReplies.set(sessionKey, { timers });
|
|
204
|
+
logger?.(`[dev-gateway] queued assistant reply for ${sessionKey} (${frames.length} frames)`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function startLocalGatewayAgentServer({ host, port, token, password, logger = () => {} } = {}) {
|
|
208
|
+
const gatewayConfig = resolveGatewayConfig();
|
|
209
|
+
const bindHost = trimString(host, gatewayConfig.host);
|
|
210
|
+
const bindPort = Number.isFinite(Number(port)) && Number(port) > 0 ? Math.floor(Number(port)) : gatewayConfig.port;
|
|
211
|
+
const authToken = trimString(token, gatewayConfig.token);
|
|
212
|
+
const authPassword = trimString(password, gatewayConfig.password);
|
|
213
|
+
const histories = new Map();
|
|
214
|
+
const pendingReplies = new Map();
|
|
215
|
+
|
|
216
|
+
const server = new WebSocketServer({
|
|
217
|
+
host: bindHost,
|
|
218
|
+
port: bindPort,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const closePendingForSession = (sessionKey) => {
|
|
222
|
+
const pending = pendingReplies.get(sessionKey);
|
|
223
|
+
if (!pending?.timers) return false;
|
|
224
|
+
for (const timer of pending.timers) {
|
|
225
|
+
clearTimeout(timer);
|
|
226
|
+
}
|
|
227
|
+
pendingReplies.delete(sessionKey);
|
|
228
|
+
return true;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const appendHistory = (sessionKey, message) => {
|
|
232
|
+
const history = histories.get(sessionKey) || [];
|
|
233
|
+
history.push(message);
|
|
234
|
+
histories.set(sessionKey, history);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
server.on('connection', (socket) => {
|
|
238
|
+
const challengeNonce = `dev-nonce-${randomUUID()}`;
|
|
239
|
+
socket.send(JSON.stringify({
|
|
240
|
+
type: 'event',
|
|
241
|
+
event: 'connect.challenge',
|
|
242
|
+
payload: { nonce: challengeNonce },
|
|
243
|
+
}));
|
|
244
|
+
logger(`[dev-gateway] connection opened, challenge sent (${challengeNonce})`);
|
|
245
|
+
|
|
246
|
+
socket.on('message', (rawMessage) => {
|
|
247
|
+
let frame;
|
|
248
|
+
try {
|
|
249
|
+
frame = JSON.parse(String(rawMessage));
|
|
250
|
+
} catch {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (!frame || frame.type !== 'req') {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const requestId = trimString(frame.id);
|
|
259
|
+
const method = trimString(frame.method);
|
|
260
|
+
const params = frame.params && typeof frame.params === 'object' ? frame.params : {};
|
|
261
|
+
const sessionKey = trimString(params.sessionKey, 'agent:main:webchat:channel:oomi');
|
|
262
|
+
|
|
263
|
+
if (method === 'connect') {
|
|
264
|
+
const requestAuth = params.auth && typeof params.auth === 'object' ? params.auth : {};
|
|
265
|
+
const providedToken = trimString(requestAuth.token);
|
|
266
|
+
const providedPassword = trimString(requestAuth.password);
|
|
267
|
+
const authorized =
|
|
268
|
+
(!authToken || providedToken === authToken) &&
|
|
269
|
+
(!authPassword || providedPassword === authPassword);
|
|
270
|
+
socket.send(JSON.stringify(authorized ? createOkResponse(requestId, { sessionKey }) : createUnauthorizedResponse(requestId)));
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (method === 'chat.history') {
|
|
275
|
+
const messages = histories.get(sessionKey) || [];
|
|
276
|
+
socket.send(JSON.stringify(createOkResponse(requestId, { messages })));
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (method === 'chat.abort') {
|
|
281
|
+
closePendingForSession(sessionKey);
|
|
282
|
+
socket.send(JSON.stringify(createOkResponse(requestId)));
|
|
283
|
+
socket.send(JSON.stringify(createAbortedEvent(sessionKey)));
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (method !== 'chat.send') {
|
|
288
|
+
socket.send(JSON.stringify({
|
|
289
|
+
type: 'res',
|
|
290
|
+
id: requestId,
|
|
291
|
+
ok: false,
|
|
292
|
+
error: {
|
|
293
|
+
code: 'unsupported_method',
|
|
294
|
+
message: `Local dev gateway does not handle ${method || 'unknown'}.`,
|
|
295
|
+
},
|
|
296
|
+
}));
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const messageText = trimString(params.message);
|
|
301
|
+
appendHistory(sessionKey, buildHistoryMessage({
|
|
302
|
+
role: 'user',
|
|
303
|
+
content: messageText,
|
|
304
|
+
}));
|
|
305
|
+
socket.send(JSON.stringify(createOkResponse(requestId)));
|
|
306
|
+
|
|
307
|
+
if (!messageText || messageText.includes(PRIMER_MARKER)) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const replyText = buildLocalGatewayAssistantText(messageText);
|
|
312
|
+
const runId = `dev-run-${randomUUID()}`;
|
|
313
|
+
const assistantMessage = buildHistoryMessage({
|
|
314
|
+
role: 'assistant',
|
|
315
|
+
content: replyText,
|
|
316
|
+
metadata: {
|
|
317
|
+
spoken: inferSpokenMetadataFromContent(replyText),
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
appendHistory(sessionKey, assistantMessage);
|
|
321
|
+
const frames = createLocalGatewayAssistantFrames({
|
|
322
|
+
sessionKey,
|
|
323
|
+
replyText,
|
|
324
|
+
runId,
|
|
325
|
+
});
|
|
326
|
+
scheduleAssistantFrames({
|
|
327
|
+
socket,
|
|
328
|
+
sessionKey,
|
|
329
|
+
frames,
|
|
330
|
+
pendingReplies,
|
|
331
|
+
logger,
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
socket.on('close', () => {
|
|
336
|
+
for (const [sessionKey, pending] of pendingReplies.entries()) {
|
|
337
|
+
if (!pending?.timers) continue;
|
|
338
|
+
for (const timer of pending.timers) {
|
|
339
|
+
clearTimeout(timer);
|
|
340
|
+
}
|
|
341
|
+
pendingReplies.delete(sessionKey);
|
|
342
|
+
}
|
|
343
|
+
logger('[dev-gateway] connection closed');
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
await new Promise((resolve, reject) => {
|
|
348
|
+
server.once('listening', resolve);
|
|
349
|
+
server.once('error', reject);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
logger(`[dev-gateway] listening on ws://${bindHost}:${bindPort}`);
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
host: bindHost,
|
|
356
|
+
port: bindPort,
|
|
357
|
+
token: authToken,
|
|
358
|
+
password: authPassword,
|
|
359
|
+
close: async () => {
|
|
360
|
+
for (const pending of pendingReplies.values()) {
|
|
361
|
+
if (!pending?.timers) continue;
|
|
362
|
+
for (const timer of pending.timers) {
|
|
363
|
+
clearTimeout(timer);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
pendingReplies.clear();
|
|
367
|
+
await new Promise((resolve, reject) => {
|
|
368
|
+
server.close((error) => {
|
|
369
|
+
if (error) {
|
|
370
|
+
reject(error);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
resolve();
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
},
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export {
|
|
381
|
+
buildLocalGatewayAssistantText,
|
|
382
|
+
createLocalGatewayAssistantFrames,
|
|
383
|
+
startLocalGatewayAgentServer,
|
|
384
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
function trimString(value) {
|
|
6
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function resolveOpenclawHome() {
|
|
10
|
+
const explicitHome = trimString(process.env.OPENCLAW_HOME);
|
|
11
|
+
if (explicitHome) {
|
|
12
|
+
return path.resolve(explicitHome);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return path.join(os.homedir(), '.openclaw');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function resolveOpenclawWorkspaceRoot() {
|
|
19
|
+
const explicitWorkspace = trimString(process.env.OPENCLAW_WORKSPACE);
|
|
20
|
+
if (explicitWorkspace) {
|
|
21
|
+
return path.resolve(explicitWorkspace);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const openclawHome = resolveOpenclawHome();
|
|
25
|
+
const managedWorkspace = path.join(openclawHome, 'workspace');
|
|
26
|
+
if (fs.existsSync(managedWorkspace)) {
|
|
27
|
+
return managedWorkspace;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return openclawHome;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function resolveOpenclawPath(...parts) {
|
|
34
|
+
return path.join(resolveOpenclawHome(), ...parts);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function resolveOpenclawConfigCandidates() {
|
|
38
|
+
return [
|
|
39
|
+
resolveOpenclawPath('clawdbot.json'),
|
|
40
|
+
resolveOpenclawPath('openclaw.json'),
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function resolveOpenclawSkillsDir() {
|
|
45
|
+
return resolveOpenclawPath('skills');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function resolveOpenclawPersonasDir() {
|
|
49
|
+
return resolveOpenclawPath('personas');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function resolveOpenclawIdentityPath() {
|
|
53
|
+
return resolveOpenclawPath('identity', 'device.json');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function resolveOpenclawUpdateStatePath() {
|
|
57
|
+
return resolveOpenclawPath('oomi-ai-update-check.json');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function resolveOpenclawBridgeStatePath() {
|
|
61
|
+
return resolveOpenclawPath('oomi-bridge.json');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function resolveOpenclawBridgeStatusPath() {
|
|
65
|
+
return resolveOpenclawPath('oomi-bridge-status.json');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function resolveOpenclawBridgeLockPath() {
|
|
69
|
+
return resolveOpenclawPath('oomi-bridge.lock');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function resolveOpenclawBridgeLiveLogPath() {
|
|
73
|
+
return resolveOpenclawPath('logs', 'oomi-bridge-live.log');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function resolveOpenclawProfilePath(fileName = 'oomi-openclaw-profile.json') {
|
|
77
|
+
return resolveOpenclawPath(fileName);
|
|
78
|
+
}
|