echoclaw-relay-agent 0.7.1 → 0.8.0
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/RelayClient.js +5 -0
- package/dist/chat/ChatHandler.d.ts +28 -0
- package/dist/chat/ChatHandler.js +104 -0
- package/dist/cli.js +17 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/package.json +1 -1
package/dist/RelayClient.js
CHANGED
|
@@ -250,6 +250,11 @@ export class RelayClient extends EventEmitter {
|
|
|
250
250
|
this.sessionId = result.sessionId;
|
|
251
251
|
this.pairingCode = result.pairingCode;
|
|
252
252
|
this.frameCrypto = new FrameCrypto(result.sessionKey);
|
|
253
|
+
// Update transport URL so auto-reconnect uses ?resume=sessionId
|
|
254
|
+
// instead of the original /client/connect (which would create a new session)
|
|
255
|
+
if (this.transport) {
|
|
256
|
+
this.transport.setUrl(`${this.config.relayServer}/client/connect?resume=${result.sessionId}`);
|
|
257
|
+
}
|
|
253
258
|
// Persist session for future auto-resume
|
|
254
259
|
await this.sessionStore.save({
|
|
255
260
|
pairingCode: result.pairingCode,
|
|
@@ -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';
|
|
@@ -304,18 +305,33 @@ async function runDaemon(relay, code, gateway, bridgePort) {
|
|
|
304
305
|
...(bridgePort ? { bridgePort } : {}),
|
|
305
306
|
} : undefined,
|
|
306
307
|
});
|
|
308
|
+
// Chat handler — routes messages to local OpenClaw (port 18789)
|
|
309
|
+
const chatHandler = new ChatHandler(18789);
|
|
307
310
|
agent.on('paired', (info) => {
|
|
308
311
|
console.log(` [paired] code=${info.pairingCode} session=${info.sessionId}`);
|
|
309
312
|
});
|
|
310
313
|
agent.on('connected', () => console.log(' [connected] relay connection active'));
|
|
311
314
|
agent.on('disconnected', (info) => {
|
|
312
315
|
console.log(` [disconnected] ${info.reason}`);
|
|
316
|
+
chatHandler.clearHistory();
|
|
313
317
|
});
|
|
314
318
|
agent.on('reconnecting', (info) => {
|
|
315
319
|
console.log(` [reconnecting] attempt=${info.attempt} delay=${info.delayMs}ms`);
|
|
316
320
|
});
|
|
317
|
-
agent.on('message', (payload) => {
|
|
321
|
+
agent.on('message', async (payload) => {
|
|
318
322
|
console.log(' [message]', JSON.stringify(payload));
|
|
323
|
+
const responses = await chatHandler.handle(payload);
|
|
324
|
+
for (const msg of responses) {
|
|
325
|
+
try {
|
|
326
|
+
await agent.send(msg);
|
|
327
|
+
if (msg.type === 'chat') {
|
|
328
|
+
console.log(` [chat→desktop] ${msg.text.slice(0, 80)}${msg.text.length > 80 ? '...' : ''}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
console.error(` [send-error] ${err.message}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
319
335
|
});
|
|
320
336
|
agent.on('error', (err) => {
|
|
321
337
|
console.error(` [error] ${err.message}`);
|
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