echoclaw-relay-agent 0.7.2 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/RelayAgent.js +11 -2
- package/dist/chat/ChatHandler.d.ts +28 -0
- package/dist/chat/ChatHandler.js +104 -0
- package/dist/cli.js +50 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/package.json +1 -1
package/dist/RelayAgent.js
CHANGED
|
@@ -245,12 +245,21 @@ export class RelayAgent extends EventEmitter {
|
|
|
245
245
|
await this.onPaired(result);
|
|
246
246
|
}
|
|
247
247
|
catch (err) {
|
|
248
|
-
// Resume failed — clear
|
|
248
|
+
// Resume failed — only clear session for permanent errors
|
|
249
249
|
this.transport?.disconnect();
|
|
250
250
|
this.transport = null;
|
|
251
251
|
this.frameCrypto = null;
|
|
252
252
|
this.setStatus('disconnected');
|
|
253
|
-
|
|
253
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
254
|
+
const isPermanent = errMsg.includes('DEVICE_TOKEN_MISMATCH') ||
|
|
255
|
+
errMsg.includes('SESSION_NOT_FOUND') ||
|
|
256
|
+
errMsg.includes('SESSION_EXPIRED') ||
|
|
257
|
+
errMsg.includes('INVALID_SESSION');
|
|
258
|
+
if (isPermanent) {
|
|
259
|
+
// Permanent rejection — session is truly dead, clear it
|
|
260
|
+
await this.sessionStore.clear();
|
|
261
|
+
}
|
|
262
|
+
// Transient errors (network, timeout) — keep session.json for retry
|
|
254
263
|
throw err;
|
|
255
264
|
}
|
|
256
265
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatHandler — Routes chat messages to local OpenClaw via OpenAI-compatible API.
|
|
3
|
+
*
|
|
4
|
+
* Receives decrypted messages from desktop, forwards to localhost:18789,
|
|
5
|
+
* returns AI responses back through the relay.
|
|
6
|
+
*
|
|
7
|
+
* Message types handled:
|
|
8
|
+
* - system_prompt: stored as conversation context
|
|
9
|
+
* - chat: forwarded to OpenClaw /v1/chat/completions
|
|
10
|
+
*/
|
|
11
|
+
export interface ChatMessage {
|
|
12
|
+
type: string;
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
export declare class ChatHandler {
|
|
16
|
+
private history;
|
|
17
|
+
private readonly openClawUrl;
|
|
18
|
+
constructor(openClawPort?: number);
|
|
19
|
+
/**
|
|
20
|
+
* Handle an incoming message from desktop.
|
|
21
|
+
* Returns an array of response messages to send back.
|
|
22
|
+
*/
|
|
23
|
+
handle(payload: any): Promise<ChatMessage[]>;
|
|
24
|
+
/** Clear conversation history (call on disconnect/reconnect). */
|
|
25
|
+
clearHistory(): void;
|
|
26
|
+
private forwardToOpenClaw;
|
|
27
|
+
private trimHistory;
|
|
28
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatHandler — Routes chat messages to local OpenClaw via OpenAI-compatible API.
|
|
3
|
+
*
|
|
4
|
+
* Receives decrypted messages from desktop, forwards to localhost:18789,
|
|
5
|
+
* returns AI responses back through the relay.
|
|
6
|
+
*
|
|
7
|
+
* Message types handled:
|
|
8
|
+
* - system_prompt: stored as conversation context
|
|
9
|
+
* - chat: forwarded to OpenClaw /v1/chat/completions
|
|
10
|
+
*/
|
|
11
|
+
const MAX_HISTORY = 50;
|
|
12
|
+
const FETCH_TIMEOUT_MS = 120000; // 2 min — AI inference can be slow
|
|
13
|
+
export class ChatHandler {
|
|
14
|
+
constructor(openClawPort = 18789) {
|
|
15
|
+
Object.defineProperty(this, "history", {
|
|
16
|
+
enumerable: true,
|
|
17
|
+
configurable: true,
|
|
18
|
+
writable: true,
|
|
19
|
+
value: []
|
|
20
|
+
});
|
|
21
|
+
Object.defineProperty(this, "openClawUrl", {
|
|
22
|
+
enumerable: true,
|
|
23
|
+
configurable: true,
|
|
24
|
+
writable: true,
|
|
25
|
+
value: void 0
|
|
26
|
+
});
|
|
27
|
+
this.openClawUrl = `http://localhost:${openClawPort}`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Handle an incoming message from desktop.
|
|
31
|
+
* Returns an array of response messages to send back.
|
|
32
|
+
*/
|
|
33
|
+
async handle(payload) {
|
|
34
|
+
if (payload?.type === 'system_prompt') {
|
|
35
|
+
this.history.push({ role: 'system', content: payload.content });
|
|
36
|
+
this.trimHistory();
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
if (payload?.type === 'chat' && payload.text) {
|
|
40
|
+
const agentId = payload.agentId || 'openclaw-1';
|
|
41
|
+
const responses = [];
|
|
42
|
+
responses.push({ type: 'typing' });
|
|
43
|
+
try {
|
|
44
|
+
const reply = await this.forwardToOpenClaw(payload.text);
|
|
45
|
+
responses.push({ type: 'typing_stop' });
|
|
46
|
+
responses.push({ type: 'chat', text: reply, agentId });
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
responses.push({ type: 'typing_stop' });
|
|
50
|
+
responses.push({
|
|
51
|
+
type: 'chat',
|
|
52
|
+
text: `Error: ${err.message}`,
|
|
53
|
+
agentId,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return responses;
|
|
57
|
+
}
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
/** Clear conversation history (call on disconnect/reconnect). */
|
|
61
|
+
clearHistory() {
|
|
62
|
+
this.history = [];
|
|
63
|
+
}
|
|
64
|
+
async forwardToOpenClaw(text) {
|
|
65
|
+
const messages = [...this.history, { role: 'user', content: text }];
|
|
66
|
+
const controller = new AbortController();
|
|
67
|
+
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
68
|
+
try {
|
|
69
|
+
const res = await fetch(`${this.openClawUrl}/v1/chat/completions`, {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
headers: { 'Content-Type': 'application/json' },
|
|
72
|
+
body: JSON.stringify({ model: 'default', messages, stream: false }),
|
|
73
|
+
signal: controller.signal,
|
|
74
|
+
});
|
|
75
|
+
if (!res.ok) {
|
|
76
|
+
throw new Error(`OpenClaw returned ${res.status}: ${res.statusText}`);
|
|
77
|
+
}
|
|
78
|
+
const data = (await res.json());
|
|
79
|
+
const reply = data.choices?.[0]?.message?.content || 'No response from OpenClaw';
|
|
80
|
+
// Append to history for multi-turn conversation
|
|
81
|
+
this.history.push({ role: 'user', content: text });
|
|
82
|
+
this.history.push({ role: 'assistant', content: reply });
|
|
83
|
+
this.trimHistory();
|
|
84
|
+
return reply;
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
if (err.name === 'AbortError') {
|
|
88
|
+
throw new Error('OpenClaw request timed out (120s)');
|
|
89
|
+
}
|
|
90
|
+
throw err;
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
clearTimeout(timeout);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
trimHistory() {
|
|
97
|
+
// Keep system messages + last MAX_HISTORY user/assistant pairs
|
|
98
|
+
const systemMsgs = this.history.filter((m) => m.role === 'system');
|
|
99
|
+
const nonSystem = this.history.filter((m) => m.role !== 'system');
|
|
100
|
+
if (nonSystem.length > MAX_HISTORY) {
|
|
101
|
+
this.history = [...systemMsgs, ...nonSystem.slice(-MAX_HISTORY)];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { RelayAgent } from './RelayAgent.js';
|
|
13
13
|
import { getServiceManager } from './service/platform.js';
|
|
14
|
+
import { ChatHandler } from './chat/ChatHandler.js';
|
|
14
15
|
const DEFAULT_RELAY = 'wss://relay.echoclaw.me';
|
|
15
16
|
// ── ASCII Banners ───────────────────────────────────────────
|
|
16
17
|
const RESET = '\x1b[0m';
|
|
@@ -233,6 +234,13 @@ async function runSetup(code, relay, bridgePort) {
|
|
|
233
234
|
console.log();
|
|
234
235
|
// Step 2: Stop the foreground agent (service will take over)
|
|
235
236
|
await agent.stop();
|
|
237
|
+
// Verify session.json exists before installing service
|
|
238
|
+
const { SessionStore } = await import('./relay/SessionStore.js');
|
|
239
|
+
const store = new SessionStore();
|
|
240
|
+
const savedSession = await store.load();
|
|
241
|
+
if (!savedSession) {
|
|
242
|
+
throw new Error('Session file not found after pairing — cannot install service. Try setup again.');
|
|
243
|
+
}
|
|
236
244
|
// Step 3: Install as system service (WITHOUT pairing code — it resumes from session)
|
|
237
245
|
printStep('2/3', 'Installing system service...');
|
|
238
246
|
const svc = await getServiceManager();
|
|
@@ -304,18 +312,33 @@ async function runDaemon(relay, code, gateway, bridgePort) {
|
|
|
304
312
|
...(bridgePort ? { bridgePort } : {}),
|
|
305
313
|
} : undefined,
|
|
306
314
|
});
|
|
315
|
+
// Chat handler — routes messages to local OpenClaw (port 18789)
|
|
316
|
+
const chatHandler = new ChatHandler(18789);
|
|
307
317
|
agent.on('paired', (info) => {
|
|
308
318
|
console.log(` [paired] code=${info.pairingCode} session=${info.sessionId}`);
|
|
309
319
|
});
|
|
310
320
|
agent.on('connected', () => console.log(' [connected] relay connection active'));
|
|
311
321
|
agent.on('disconnected', (info) => {
|
|
312
322
|
console.log(` [disconnected] ${info.reason}`);
|
|
323
|
+
chatHandler.clearHistory();
|
|
313
324
|
});
|
|
314
325
|
agent.on('reconnecting', (info) => {
|
|
315
326
|
console.log(` [reconnecting] attempt=${info.attempt} delay=${info.delayMs}ms`);
|
|
316
327
|
});
|
|
317
|
-
agent.on('message', (payload) => {
|
|
328
|
+
agent.on('message', async (payload) => {
|
|
318
329
|
console.log(' [message]', JSON.stringify(payload));
|
|
330
|
+
const responses = await chatHandler.handle(payload);
|
|
331
|
+
for (const msg of responses) {
|
|
332
|
+
try {
|
|
333
|
+
await agent.send(msg);
|
|
334
|
+
if (msg.type === 'chat') {
|
|
335
|
+
console.log(` [chat→desktop] ${msg.text.slice(0, 80)}${msg.text.length > 80 ? '...' : ''}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
catch (err) {
|
|
339
|
+
console.error(` [send-error] ${err.message}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
319
342
|
});
|
|
320
343
|
agent.on('error', (err) => {
|
|
321
344
|
console.error(` [error] ${err.message}`);
|
|
@@ -338,7 +361,32 @@ async function runDaemon(relay, code, gateway, bridgePort) {
|
|
|
338
361
|
process.once('SIGINT', shutdown);
|
|
339
362
|
process.once('SIGTERM', shutdown);
|
|
340
363
|
console.log(` [start] relay=${relay} gateway=${!!gateway}`);
|
|
341
|
-
|
|
364
|
+
// Retry logic — transient failures (network, server restart) shouldn't kill the service
|
|
365
|
+
const MAX_RETRIES = 5;
|
|
366
|
+
const RETRY_DELAY = 3000;
|
|
367
|
+
let lastErr;
|
|
368
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
369
|
+
try {
|
|
370
|
+
await agent.start(code);
|
|
371
|
+
return; // success — stay alive
|
|
372
|
+
}
|
|
373
|
+
catch (err) {
|
|
374
|
+
lastErr = err;
|
|
375
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
376
|
+
console.error(` [start-failed] attempt=${attempt}/${MAX_RETRIES} error=${msg}`);
|
|
377
|
+
// Permanent errors — don't retry
|
|
378
|
+
if (msg.includes('No saved session found') ||
|
|
379
|
+
msg.includes('DEVICE_TOKEN_MISMATCH') ||
|
|
380
|
+
msg.includes('SESSION_NOT_FOUND')) {
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
if (attempt < MAX_RETRIES) {
|
|
384
|
+
console.log(` [retry] waiting ${RETRY_DELAY / 1000}s...`);
|
|
385
|
+
await new Promise(r => setTimeout(r, RETRY_DELAY));
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
throw lastErr;
|
|
342
390
|
}
|
|
343
391
|
// ── Main ─────────────────────────────────────────────────────
|
|
344
392
|
async function main() {
|
package/dist/index.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export { RelayTransport } from './relay/RelayTransport.js';
|
|
|
16
16
|
export { PairingProtocol } from './relay/PairingProtocol.js';
|
|
17
17
|
export { RelayClient } from './RelayClient.js';
|
|
18
18
|
export { RelayAgent } from './RelayAgent.js';
|
|
19
|
+
export { ChatHandler } from './chat/ChatHandler.js';
|
|
20
|
+
export type { ChatMessage } from './chat/ChatHandler.js';
|
|
19
21
|
export { GatewayManager } from './gateway/index.js';
|
|
20
22
|
export { DeviceIdentity } from './gateway/index.js';
|
|
21
23
|
export { TokenDiscovery } from './gateway/index.js';
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,8 @@ export { PairingProtocol } from './relay/PairingProtocol.js';
|
|
|
18
18
|
// ── Entry Points ─────────────────────────────────────────────────
|
|
19
19
|
export { RelayClient } from './RelayClient.js';
|
|
20
20
|
export { RelayAgent } from './RelayAgent.js';
|
|
21
|
+
// ── Chat ────────────────────────────────────────────────────────
|
|
22
|
+
export { ChatHandler } from './chat/ChatHandler.js';
|
|
21
23
|
// ── Gateway V2 ──────────────────────────────────────────────────
|
|
22
24
|
export { GatewayManager } from './gateway/index.js';
|
|
23
25
|
export { DeviceIdentity } from './gateway/index.js';
|
package/package.json
CHANGED