indieclaw-agent 2.4.0 → 2.4.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/index.js +298 -149
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const { WebSocketServer } = require('ws');
|
|
4
|
+
const WebSocket = require('ws');
|
|
4
5
|
const { execSync, exec } = require('child_process');
|
|
5
6
|
const fs = require('fs');
|
|
6
7
|
const path = require('path');
|
|
@@ -8,6 +9,7 @@ const os = require('os');
|
|
|
8
9
|
const crypto = require('crypto');
|
|
9
10
|
const http = require('http');
|
|
10
11
|
const https = require('https');
|
|
12
|
+
const net = require('net');
|
|
11
13
|
|
|
12
14
|
// --- Version from package.json ---
|
|
13
15
|
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8'));
|
|
@@ -89,7 +91,7 @@ if (TLS_ENABLED) {
|
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
const terminals = new Map(); // id -> pty process
|
|
92
|
-
const activeChats = new Map(); //
|
|
94
|
+
const activeChats = new Map(); // chatId -> { runId, _ws }
|
|
93
95
|
const activeSearches = new Map(); // ws -> child_process (one search per connection)
|
|
94
96
|
|
|
95
97
|
// --- Deep Link & QR Code ---
|
|
@@ -143,77 +145,275 @@ try {
|
|
|
143
145
|
// qrcode-terminal not installed, skip QR display
|
|
144
146
|
}
|
|
145
147
|
|
|
146
|
-
// --- OpenClaw Detection &
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
];
|
|
148
|
+
// --- OpenClaw Detection, Config & Gateway Client ---
|
|
149
|
+
|
|
150
|
+
// Search multiple possible config locations (systemd may resolve homedir differently)
|
|
151
|
+
function getConfigPaths() {
|
|
152
|
+
const paths = [];
|
|
153
|
+
const home = os.homedir();
|
|
154
|
+
paths.push(path.join(home, '.openclaw', 'openclaw.json'));
|
|
155
|
+
paths.push(path.join(home, '.openclaw', 'openclaw.json5'));
|
|
156
|
+
// Also check /root explicitly (systemd services may not resolve ~ to /root)
|
|
157
|
+
if (home !== '/root') {
|
|
158
|
+
paths.push('/root/.openclaw/openclaw.json');
|
|
159
|
+
paths.push('/root/.openclaw/openclaw.json5');
|
|
160
|
+
}
|
|
161
|
+
return paths;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const OPENCLAW_CONFIG_PATHS = getConfigPaths();
|
|
165
|
+
|
|
166
|
+
function stripJsonComments(raw) {
|
|
167
|
+
// Remove comments WITHOUT corrupting URLs inside strings
|
|
168
|
+
// Walk char-by-char, track if we're inside a string
|
|
169
|
+
let result = '';
|
|
170
|
+
let inString = false;
|
|
171
|
+
let escaped = false;
|
|
172
|
+
for (let i = 0; i < raw.length; i++) {
|
|
173
|
+
const ch = raw[i];
|
|
174
|
+
if (inString) {
|
|
175
|
+
result += ch;
|
|
176
|
+
if (escaped) { escaped = false; continue; }
|
|
177
|
+
if (ch === '\\') { escaped = true; continue; }
|
|
178
|
+
if (ch === '"') { inString = false; }
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
// Not in string
|
|
182
|
+
if (ch === '"') { inString = true; result += ch; continue; }
|
|
183
|
+
// Line comment
|
|
184
|
+
if (ch === '/' && raw[i + 1] === '/') {
|
|
185
|
+
// Skip to end of line
|
|
186
|
+
while (i < raw.length && raw[i] !== '\n') i++;
|
|
187
|
+
result += '\n';
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
// Block comment
|
|
191
|
+
if (ch === '/' && raw[i + 1] === '*') {
|
|
192
|
+
i += 2;
|
|
193
|
+
while (i < raw.length && !(raw[i] === '*' && raw[i + 1] === '/')) i++;
|
|
194
|
+
i++; // skip closing /
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
result += ch;
|
|
198
|
+
}
|
|
199
|
+
return result;
|
|
200
|
+
}
|
|
151
201
|
|
|
152
|
-
// Read OpenClaw gateway config (port + auth token) from local config file
|
|
153
202
|
function readOpenClawConfig() {
|
|
154
|
-
for (const
|
|
203
|
+
for (const cfgPath of OPENCLAW_CONFIG_PATHS) {
|
|
155
204
|
try {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
raw =
|
|
160
|
-
// Strip
|
|
205
|
+
const exists = fs.existsSync(cfgPath);
|
|
206
|
+
console.log(` [OpenClaw] Config ${cfgPath}: ${exists ? 'EXISTS' : 'not found'}`);
|
|
207
|
+
if (!exists) continue;
|
|
208
|
+
let raw = fs.readFileSync(cfgPath, 'utf-8');
|
|
209
|
+
// Strip comments safely (preserves URLs in strings)
|
|
210
|
+
raw = stripJsonComments(raw);
|
|
211
|
+
// Remove trailing commas
|
|
161
212
|
raw = raw.replace(/,\s*([\]}])/g, '$1');
|
|
162
213
|
const config = JSON.parse(raw);
|
|
163
214
|
const gw = config.gateway || {};
|
|
164
|
-
|
|
215
|
+
const result = {
|
|
165
216
|
port: gw.port || 18789,
|
|
166
217
|
token: gw.auth?.token || gw.auth?.password || null,
|
|
167
218
|
host: gw.bind || '127.0.0.1',
|
|
168
219
|
};
|
|
169
|
-
|
|
220
|
+
console.log(` [OpenClaw] Config parsed: port=${result.port}, host=${result.host}, token=${result.token ? 'SET' : 'NONE'}`);
|
|
221
|
+
return result;
|
|
222
|
+
} catch (err) {
|
|
223
|
+
console.log(` [OpenClaw] Config parse error for ${cfgPath}: ${err.message}`);
|
|
224
|
+
}
|
|
170
225
|
}
|
|
171
|
-
|
|
226
|
+
// Fallback: check env var
|
|
227
|
+
console.log(' [OpenClaw] No config file found, using defaults (port 18789)');
|
|
228
|
+
return {
|
|
229
|
+
port: parseInt(process.env.OPENCLAW_GATEWAY_PORT || '18789', 10),
|
|
230
|
+
token: process.env.OPENCLAW_GATEWAY_TOKEN || null,
|
|
231
|
+
host: '127.0.0.1',
|
|
232
|
+
};
|
|
172
233
|
}
|
|
173
234
|
|
|
174
|
-
// Cache the config on startup
|
|
175
235
|
let openClawConfig = readOpenClawConfig();
|
|
176
236
|
|
|
177
|
-
|
|
237
|
+
// TCP port check — fast and reliable, no auth needed
|
|
238
|
+
function isPortListening(port, host = '127.0.0.1') {
|
|
178
239
|
return new Promise((resolve) => {
|
|
179
|
-
|
|
240
|
+
console.log(` [OpenClaw] TCP probe ${host}:${port}...`);
|
|
241
|
+
const sock = net.createConnection({ port, host }, () => {
|
|
242
|
+
console.log(` [OpenClaw] TCP probe ${host}:${port} → OPEN`);
|
|
243
|
+
sock.destroy();
|
|
244
|
+
resolve(true);
|
|
245
|
+
});
|
|
246
|
+
sock.on('error', (err) => {
|
|
247
|
+
console.log(` [OpenClaw] TCP probe ${host}:${port} → ERROR: ${err.message}`);
|
|
248
|
+
resolve(false);
|
|
249
|
+
});
|
|
250
|
+
sock.setTimeout(2000, () => {
|
|
251
|
+
console.log(` [OpenClaw] TCP probe ${host}:${port} → TIMEOUT`);
|
|
252
|
+
sock.destroy();
|
|
253
|
+
resolve(false);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function detectOpenClaw() {
|
|
259
|
+
console.log(' [OpenClaw] Running detection...');
|
|
260
|
+
openClawConfig = readOpenClawConfig();
|
|
261
|
+
|
|
262
|
+
// Always probe 127.0.0.1 — the gateway listens on loopback
|
|
263
|
+
const port = openClawConfig.port || 18789;
|
|
264
|
+
const listening = await isPortListening(port, '127.0.0.1');
|
|
265
|
+
if (listening) {
|
|
266
|
+
console.log(` [OpenClaw] Detection result: AVAILABLE (port ${port})`);
|
|
267
|
+
return { available: true, models: ['openclaw'], port };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Fallback: check if config file exists (installed but not running)
|
|
271
|
+
const installed = OPENCLAW_CONFIG_PATHS.some((p) => fs.existsSync(p));
|
|
272
|
+
if (installed) {
|
|
273
|
+
console.log(` [OpenClaw] Detection result: INSTALLED but gateway not running`);
|
|
274
|
+
return { available: false, models: [], port, installed: true };
|
|
275
|
+
}
|
|
276
|
+
console.log(' [OpenClaw] Detection result: NOT FOUND');
|
|
277
|
+
return { available: false, models: [], port: null };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// --- OpenClaw Gateway WebSocket Client ---
|
|
281
|
+
let ocGateway = null;
|
|
282
|
+
let ocReady = false;
|
|
283
|
+
const ocPending = new Map();
|
|
284
|
+
const ocChatCallbacks = new Map(); // runId -> { ws, chatId }
|
|
285
|
+
|
|
286
|
+
function connectOcGateway() {
|
|
287
|
+
return new Promise((resolve, reject) => {
|
|
180
288
|
openClawConfig = readOpenClawConfig();
|
|
289
|
+
const url = `ws://127.0.0.1:${openClawConfig.port}`;
|
|
290
|
+
console.log(` [OpenClaw] Connecting to gateway: ${url}`);
|
|
291
|
+
|
|
292
|
+
if (ocGateway) { try { ocGateway.close(); } catch {} }
|
|
293
|
+
ocGateway = null;
|
|
294
|
+
ocReady = false;
|
|
295
|
+
|
|
296
|
+
const ws = new WebSocket(url);
|
|
297
|
+
let connectReqId = null;
|
|
298
|
+
const timeout = setTimeout(() => { console.log(' [OpenClaw] Gateway connection TIMEOUT'); ws.close(); reject(new Error('Gateway timeout')); }, 10000);
|
|
299
|
+
|
|
300
|
+
ws.on('message', (data) => {
|
|
301
|
+
let msg;
|
|
302
|
+
try { msg = JSON.parse(data.toString()); } catch { return; }
|
|
303
|
+
|
|
304
|
+
console.log(` [OpenClaw] Gateway msg: type=${msg.type}, event=${msg.event || ''}, id=${msg.id || ''}, ok=${msg.ok}`);
|
|
305
|
+
|
|
306
|
+
// Step 1: Gateway sends connect.challenge
|
|
307
|
+
if (msg.type === 'event' && msg.event === 'connect.challenge') {
|
|
308
|
+
connectReqId = crypto.randomUUID();
|
|
309
|
+
const params = {
|
|
310
|
+
minProtocol: 3, maxProtocol: 3,
|
|
311
|
+
client: { id: 'indieclaw-agent', version: VERSION, platform: os.platform(), mode: 'operator' },
|
|
312
|
+
role: 'operator',
|
|
313
|
+
scopes: ['operator.read', 'operator.write'],
|
|
314
|
+
};
|
|
315
|
+
if (openClawConfig.token) params.auth = { token: openClawConfig.token };
|
|
316
|
+
ws.send(JSON.stringify({ type: 'req', id: connectReqId, method: 'connect', params }));
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
181
319
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
320
|
+
// Step 2: Handle hello-ok response
|
|
321
|
+
if (msg.type === 'res' && msg.id === connectReqId) {
|
|
322
|
+
clearTimeout(timeout);
|
|
323
|
+
if (msg.ok) {
|
|
324
|
+
ocGateway = ws;
|
|
325
|
+
ocReady = true;
|
|
326
|
+
resolve(ws);
|
|
327
|
+
} else {
|
|
328
|
+
ws.close();
|
|
329
|
+
reject(new Error(msg.error?.message || 'Gateway auth failed'));
|
|
330
|
+
}
|
|
331
|
+
return;
|
|
187
332
|
}
|
|
188
333
|
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
334
|
+
// Handle other req/res
|
|
335
|
+
if (msg.type === 'res' && msg.id) {
|
|
336
|
+
const pending = ocPending.get(msg.id);
|
|
337
|
+
if (pending) {
|
|
338
|
+
ocPending.delete(msg.id);
|
|
339
|
+
clearTimeout(pending.timeout);
|
|
340
|
+
if (msg.ok) pending.resolve(msg.payload);
|
|
341
|
+
else pending.reject(new Error(msg.error?.message || 'Request failed'));
|
|
193
342
|
}
|
|
343
|
+
}
|
|
194
344
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
345
|
+
// Handle chat streaming events
|
|
346
|
+
if (msg.type === 'event' && msg.event === 'chat') {
|
|
347
|
+
const p = msg.payload;
|
|
348
|
+
const cb = ocChatCallbacks.get(p.runId);
|
|
349
|
+
if (!cb) return;
|
|
350
|
+
|
|
351
|
+
if (p.state === 'delta') {
|
|
352
|
+
// Extract text from delta — try common field paths
|
|
353
|
+
const text = typeof p.message === 'string' ? p.message
|
|
354
|
+
: p.message?.content || p.message?.text || '';
|
|
355
|
+
if (text) send(cb.ws, { type: 'chat.stream', id: cb.chatId, content: text });
|
|
356
|
+
} else if (p.state === 'final') {
|
|
357
|
+
const text = typeof p.message === 'string' ? p.message
|
|
358
|
+
: p.message?.content || p.message?.text || '';
|
|
359
|
+
if (text) send(cb.ws, { type: 'chat.stream', id: cb.chatId, content: text });
|
|
360
|
+
send(cb.ws, { type: 'chat.done', id: cb.chatId });
|
|
361
|
+
ocChatCallbacks.delete(p.runId);
|
|
362
|
+
} else if (p.state === 'error') {
|
|
363
|
+
send(cb.ws, { type: 'chat.done', id: cb.chatId, error: p.errorMessage || 'OpenClaw error' });
|
|
364
|
+
ocChatCallbacks.delete(p.runId);
|
|
365
|
+
} else if (p.state === 'aborted') {
|
|
366
|
+
send(cb.ws, { type: 'chat.done', id: cb.chatId });
|
|
367
|
+
ocChatCallbacks.delete(p.runId);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
ws.on('error', (err) => { console.log(` [OpenClaw] Gateway error: ${err.message}`); clearTimeout(timeout); ocReady = false; reject(err); });
|
|
373
|
+
ws.on('close', () => {
|
|
374
|
+
console.log(' [OpenClaw] Gateway connection closed');
|
|
375
|
+
ocReady = false;
|
|
376
|
+
ocGateway = null;
|
|
377
|
+
// Notify all pending chat streams that gateway disconnected
|
|
378
|
+
for (const [runId, cb] of ocChatCallbacks) {
|
|
379
|
+
send(cb.ws, { type: 'chat.done', id: cb.chatId, error: 'OpenClaw gateway disconnected' });
|
|
380
|
+
ocChatCallbacks.delete(runId);
|
|
381
|
+
}
|
|
382
|
+
// Reject all pending requests
|
|
383
|
+
for (const [id, p] of ocPending) {
|
|
384
|
+
clearTimeout(p.timeout);
|
|
385
|
+
p.reject(new Error('Gateway disconnected'));
|
|
386
|
+
ocPending.delete(id);
|
|
387
|
+
}
|
|
209
388
|
});
|
|
210
389
|
});
|
|
211
390
|
}
|
|
212
391
|
|
|
392
|
+
async function getOcGateway() {
|
|
393
|
+
if (ocGateway && ocGateway.readyState === WebSocket.OPEN && ocReady) return ocGateway;
|
|
394
|
+
return connectOcGateway();
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function ocRequest(method, params) {
|
|
398
|
+
return new Promise(async (resolve, reject) => {
|
|
399
|
+
try {
|
|
400
|
+
const ws = await getOcGateway();
|
|
401
|
+
const id = crypto.randomUUID();
|
|
402
|
+
const t = setTimeout(() => { ocPending.delete(id); reject(new Error('Timeout')); }, 30000);
|
|
403
|
+
ocPending.set(id, { resolve, reject, timeout: t });
|
|
404
|
+
ws.send(JSON.stringify({ type: 'req', id, method, params }));
|
|
405
|
+
} catch (err) { reject(err); }
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
213
409
|
// Run detection on startup
|
|
214
410
|
detectOpenClaw().then((oc) => {
|
|
215
411
|
if (oc.available) {
|
|
216
412
|
console.log(` [OpenClaw] Detected (gateway on port ${oc.port})`);
|
|
413
|
+
// Pre-connect to gateway
|
|
414
|
+
connectOcGateway().catch(() => {});
|
|
415
|
+
} else if (oc.installed) {
|
|
416
|
+
console.log(` [OpenClaw] Installed but gateway not running (port ${oc.port})`);
|
|
217
417
|
} else {
|
|
218
418
|
console.log(' [OpenClaw] Not detected');
|
|
219
419
|
}
|
|
@@ -261,6 +461,7 @@ wss.on('connection', (ws) => {
|
|
|
261
461
|
if (msg.type === 'auth' && msg.token === AUTH_TOKEN) {
|
|
262
462
|
authenticated = true;
|
|
263
463
|
const openclaw = await detectOpenClaw();
|
|
464
|
+
console.log(` [Auth] Sending auth success, openclaw:`, JSON.stringify(openclaw));
|
|
264
465
|
return ws.send(JSON.stringify({ type: 'auth', success: true, openclaw }));
|
|
265
466
|
}
|
|
266
467
|
if (msg.type === 'ping') {
|
|
@@ -289,10 +490,13 @@ wss.on('connection', (ws) => {
|
|
|
289
490
|
}
|
|
290
491
|
}
|
|
291
492
|
// Clean up any active chat streams owned by this connection
|
|
292
|
-
for (const [
|
|
293
|
-
if (
|
|
294
|
-
|
|
295
|
-
|
|
493
|
+
for (const [chatId, chat] of activeChats) {
|
|
494
|
+
if (chat._ws === ws) {
|
|
495
|
+
if (chat.runId) {
|
|
496
|
+
ocRequest('chat.abort', { runId: chat.runId }).catch(() => {});
|
|
497
|
+
ocChatCallbacks.delete(chat.runId);
|
|
498
|
+
}
|
|
499
|
+
activeChats.delete(chatId);
|
|
296
500
|
}
|
|
297
501
|
}
|
|
298
502
|
// Kill any active search process for this connection
|
|
@@ -364,7 +568,7 @@ async function handleMessage(ws, msg) {
|
|
|
364
568
|
case 'terminal.stop':
|
|
365
569
|
return handleTerminalStop(ws, msg);
|
|
366
570
|
case 'chat.send':
|
|
367
|
-
return handleChatSend(ws, msg);
|
|
571
|
+
return await handleChatSend(ws, msg);
|
|
368
572
|
case 'chat.stop':
|
|
369
573
|
return handleChatStop(ws, msg);
|
|
370
574
|
case 'push.register':
|
|
@@ -977,110 +1181,53 @@ function handleTerminalStop(ws, { id }) {
|
|
|
977
1181
|
reply(ws, id, { stopped: true });
|
|
978
1182
|
}
|
|
979
1183
|
|
|
980
|
-
// --- Chat (OpenClaw Proxy) ---
|
|
981
|
-
function handleChatSend(ws, { id, messages }) {
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
const host = openClawConfig.host || '127.0.0.1';
|
|
985
|
-
const token = openClawConfig.token;
|
|
986
|
-
|
|
987
|
-
const body = JSON.stringify({
|
|
988
|
-
model: 'openclaw:main',
|
|
989
|
-
messages,
|
|
990
|
-
stream: true,
|
|
991
|
-
});
|
|
1184
|
+
// --- Chat (OpenClaw Gateway Proxy) ---
|
|
1185
|
+
async function handleChatSend(ws, { id, messages }) {
|
|
1186
|
+
try {
|
|
1187
|
+
console.log(` [Chat] handleChatSend id=${id}, messages=${messages.length}`);
|
|
992
1188
|
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
'
|
|
996
|
-
};
|
|
997
|
-
if (token) {
|
|
998
|
-
headers['Authorization'] = `Bearer ${token}`;
|
|
999
|
-
}
|
|
1189
|
+
// Connect to OpenClaw gateway (auto-reconnects if needed)
|
|
1190
|
+
await getOcGateway();
|
|
1191
|
+
console.log(' [Chat] Gateway connected');
|
|
1000
1192
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
headers,
|
|
1008
|
-
},
|
|
1009
|
-
(res) => {
|
|
1010
|
-
if (res.statusCode !== 200) {
|
|
1011
|
-
let errorBody = '';
|
|
1012
|
-
res.on('data', (chunk) => (errorBody += chunk));
|
|
1013
|
-
res.on('end', () => {
|
|
1014
|
-
send(ws, { type: 'chat.done', id, error: `OpenClaw error ${res.statusCode}: ${errorBody}` });
|
|
1015
|
-
activeChats.delete(id);
|
|
1016
|
-
});
|
|
1017
|
-
return;
|
|
1018
|
-
}
|
|
1193
|
+
// Extract the last user message from the conversation
|
|
1194
|
+
const lastUserMsg = [...messages].reverse().find(m => m.role === 'user');
|
|
1195
|
+
if (!lastUserMsg) {
|
|
1196
|
+
return send(ws, { type: 'chat.done', id, error: 'No user message found' });
|
|
1197
|
+
}
|
|
1198
|
+
console.log(` [Chat] Sending to OpenClaw: "${lastUserMsg.content.substring(0, 80)}..."`);
|
|
1019
1199
|
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
res.on('data', (chunk) => {
|
|
1024
|
-
buffer += chunk.toString();
|
|
1025
|
-
const lines = buffer.split('\n');
|
|
1026
|
-
buffer = lines.pop() || '';
|
|
1027
|
-
|
|
1028
|
-
for (const line of lines) {
|
|
1029
|
-
const trimmed = line.trim();
|
|
1030
|
-
if (!trimmed || trimmed.startsWith(':')) continue;
|
|
1031
|
-
if (trimmed === 'data: [DONE]') {
|
|
1032
|
-
if (!sentDone) {
|
|
1033
|
-
sentDone = true;
|
|
1034
|
-
send(ws, { type: 'chat.done', id });
|
|
1035
|
-
activeChats.delete(id);
|
|
1036
|
-
}
|
|
1037
|
-
return;
|
|
1038
|
-
}
|
|
1039
|
-
if (trimmed.startsWith('data: ')) {
|
|
1040
|
-
try {
|
|
1041
|
-
const json = JSON.parse(trimmed.slice(6));
|
|
1042
|
-
const content = json.choices?.[0]?.delta?.content;
|
|
1043
|
-
if (content) {
|
|
1044
|
-
send(ws, { type: 'chat.stream', id, content });
|
|
1045
|
-
}
|
|
1046
|
-
} catch {}
|
|
1047
|
-
}
|
|
1048
|
-
}
|
|
1049
|
-
});
|
|
1200
|
+
// Build a session key — unique per chat conversation
|
|
1201
|
+
const sessionKey = `agent:indieclaw:mobile:${id}`;
|
|
1202
|
+
const idempotencyKey = crypto.randomUUID();
|
|
1050
1203
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
});
|
|
1204
|
+
// Send chat request to gateway via WebSocket protocol
|
|
1205
|
+
const result = await ocRequest('chat.send', {
|
|
1206
|
+
sessionKey,
|
|
1207
|
+
message: { role: 'user', content: lastUserMsg.content },
|
|
1208
|
+
idempotencyKey,
|
|
1209
|
+
});
|
|
1058
1210
|
|
|
1059
|
-
|
|
1060
|
-
if (!sentDone) {
|
|
1061
|
-
sentDone = true;
|
|
1062
|
-
send(ws, { type: 'chat.done', id, error: err.message });
|
|
1063
|
-
activeChats.delete(id);
|
|
1064
|
-
}
|
|
1065
|
-
});
|
|
1066
|
-
}
|
|
1067
|
-
);
|
|
1211
|
+
console.log(` [Chat] ocRequest result:`, JSON.stringify(result).substring(0, 200));
|
|
1068
1212
|
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1213
|
+
// Register for streaming events using the runId from the response
|
|
1214
|
+
const runId = result?.runId || result?.id || id;
|
|
1215
|
+
console.log(` [Chat] Registered callback for runId=${runId}`);
|
|
1216
|
+
ocChatCallbacks.set(runId, { ws, chatId: id });
|
|
1217
|
+
activeChats.set(id, { runId, _ws: ws });
|
|
1073
1218
|
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1219
|
+
} catch (err) {
|
|
1220
|
+
console.log(` [Chat] ERROR: ${err.message}`);
|
|
1221
|
+
send(ws, { type: 'chat.done', id, error: `OpenClaw: ${err.message}` });
|
|
1222
|
+
}
|
|
1078
1223
|
}
|
|
1079
1224
|
|
|
1080
1225
|
function handleChatStop(ws, { id, chatId }) {
|
|
1081
|
-
const
|
|
1082
|
-
if (
|
|
1083
|
-
|
|
1226
|
+
const chat = activeChats.get(chatId);
|
|
1227
|
+
if (chat && chat.runId) {
|
|
1228
|
+
// Abort the chat via OpenClaw gateway
|
|
1229
|
+
ocRequest('chat.abort', { runId: chat.runId }).catch(() => {});
|
|
1230
|
+
ocChatCallbacks.delete(chat.runId);
|
|
1084
1231
|
activeChats.delete(chatId);
|
|
1085
1232
|
}
|
|
1086
1233
|
reply(ws, id, { stopped: true });
|
|
@@ -1304,19 +1451,21 @@ function handleAgentLogs(ws, { id, lines = 200 }) {
|
|
|
1304
1451
|
}
|
|
1305
1452
|
|
|
1306
1453
|
// --- Graceful Shutdown ---
|
|
1307
|
-
|
|
1308
|
-
console.log('\n[agent] Shutting down...');
|
|
1454
|
+
function gracefulShutdown() {
|
|
1309
1455
|
for (const [, term] of terminals) term.kill();
|
|
1310
|
-
|
|
1456
|
+
activeChats.clear();
|
|
1457
|
+
ocChatCallbacks.clear();
|
|
1458
|
+
if (ocGateway) { try { ocGateway.close(); } catch {} }
|
|
1311
1459
|
wss.close();
|
|
1312
1460
|
if (server) server.close();
|
|
1313
1461
|
process.exit(0);
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
process.on('SIGINT', () => {
|
|
1465
|
+
console.log('\n[agent] Shutting down...');
|
|
1466
|
+
gracefulShutdown();
|
|
1314
1467
|
});
|
|
1315
1468
|
|
|
1316
1469
|
process.on('SIGTERM', () => {
|
|
1317
|
-
|
|
1318
|
-
for (const [, req] of activeChats) req.destroy();
|
|
1319
|
-
wss.close();
|
|
1320
|
-
if (server) server.close();
|
|
1321
|
-
process.exit(0);
|
|
1470
|
+
gracefulShutdown();
|
|
1322
1471
|
});
|