@rethinkingstudio/clawpilot 2.0.0-beta.0 → 2.0.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/commands/pair.js +3 -5
- package/dist/commands/pair.js.map +1 -1
- package/dist/platform/service-manager.js +9 -13
- package/dist/platform/service-manager.js.map +1 -1
- package/package.json +7 -1
- package/rethinkingstudio-clawpilot-1.1.17.tgz +0 -0
- package/src/commands/assistant-send.ts +0 -91
- package/src/commands/install.ts +0 -131
- package/src/commands/local-handlers.ts +0 -533
- package/src/commands/openclaw-cli.ts +0 -354
- package/src/commands/pair.ts +0 -128
- package/src/commands/provider-config.ts +0 -275
- package/src/commands/provider-handlers.ts +0 -184
- package/src/commands/provider-registry.ts +0 -138
- package/src/commands/run.ts +0 -34
- package/src/commands/send.ts +0 -42
- package/src/commands/session-key.ts +0 -77
- package/src/commands/set-token.ts +0 -45
- package/src/commands/status.ts +0 -157
- package/src/commands/upload.ts +0 -141
- package/src/config/config.ts +0 -137
- package/src/generated/build-config.ts +0 -1
- package/src/i18n/index.ts +0 -185
- package/src/index.ts +0 -166
- package/src/media/assistant-attachments.ts +0 -205
- package/src/media/oss-uploader.ts +0 -306
- package/src/platform/service-manager.ts +0 -919
- package/src/relay/gateway-client.ts +0 -359
- package/src/relay/reconnect.ts +0 -37
- package/src/relay/relay-manager.ts +0 -328
- package/test-chat.mjs +0 -64
- package/test-direct.mjs +0 -171
- package/tsconfig.json +0 -16
|
@@ -1,328 +0,0 @@
|
|
|
1
|
-
import { WebSocket } from "ws";
|
|
2
|
-
import { OpenClawGatewayClient } from "./gateway-client.js";
|
|
3
|
-
import { handleLocalCommand } from "../commands/local-handlers.js";
|
|
4
|
-
import { handleProviderCommand } from "../commands/provider-handlers.js";
|
|
5
|
-
import { getServicePlatform } from "../platform/service-manager.js";
|
|
6
|
-
import { uploadAssistantAttachments } from "../media/assistant-attachments.js";
|
|
7
|
-
import { homedir } from "os";
|
|
8
|
-
import { extname, join } from "path";
|
|
9
|
-
import { mkdir, writeFile } from "fs/promises";
|
|
10
|
-
import { randomUUID } from "crypto";
|
|
11
|
-
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
// Constants
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
|
|
16
|
-
const OUTBOUND_DIR = join(homedir(), ".openclaw", "media", "outbound");
|
|
17
|
-
|
|
18
|
-
function isSuspiciousShortReply(text: string | null | undefined): boolean {
|
|
19
|
-
const normalized = (text ?? "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
20
|
-
if (!normalized) return true;
|
|
21
|
-
return normalized.length <= 4 || ["no", "ok", "nope", "n/a", "no_reply"].includes(normalized);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function extensionFromMimeType(mimeType: string): string {
|
|
25
|
-
const map: Record<string, string> = {
|
|
26
|
-
"image/jpeg": ".jpg",
|
|
27
|
-
"image/png": ".png",
|
|
28
|
-
"image/gif": ".gif",
|
|
29
|
-
"image/webp": ".webp",
|
|
30
|
-
"video/mp4": ".mp4",
|
|
31
|
-
"video/quicktime": ".mov",
|
|
32
|
-
"audio/mpeg": ".mp3",
|
|
33
|
-
"audio/mp4": ".m4a",
|
|
34
|
-
"audio/wav": ".wav",
|
|
35
|
-
"audio/x-wav": ".wav",
|
|
36
|
-
"audio/aac": ".aac",
|
|
37
|
-
"application/pdf": ".pdf",
|
|
38
|
-
"text/plain": ".txt",
|
|
39
|
-
"text/markdown": ".md",
|
|
40
|
-
"application/json": ".json",
|
|
41
|
-
};
|
|
42
|
-
return map[mimeType] ?? ".bin";
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
|
-
// Messages: relay client ↔ relay server
|
|
47
|
-
// ---------------------------------------------------------------------------
|
|
48
|
-
|
|
49
|
-
/** Messages the relay client sends to the relay server. */
|
|
50
|
-
type ToServer =
|
|
51
|
-
| { type: "relay_hello"; platform: "macos" | "linux" | "windows" | "unsupported" }
|
|
52
|
-
| { type: "gateway_connected" }
|
|
53
|
-
| { type: "gateway_disconnected"; reason: string }
|
|
54
|
-
| { type: "event"; event: string; payload: unknown }
|
|
55
|
-
| { type: "res"; id: string; ok: boolean; payload?: unknown; error?: { message?: string } };
|
|
56
|
-
|
|
57
|
-
/** Messages the relay server sends to the relay client. */
|
|
58
|
-
interface FromServer {
|
|
59
|
-
type: "cmd";
|
|
60
|
-
id?: string;
|
|
61
|
-
method: string;
|
|
62
|
-
params: unknown;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// ---------------------------------------------------------------------------
|
|
66
|
-
// Options
|
|
67
|
-
// ---------------------------------------------------------------------------
|
|
68
|
-
|
|
69
|
-
export interface RelayManagerOptions {
|
|
70
|
-
relayServerUrl: string;
|
|
71
|
-
gatewayId: string;
|
|
72
|
-
relaySecret: string;
|
|
73
|
-
gatewayUrl: string;
|
|
74
|
-
gatewayToken?: string;
|
|
75
|
-
gatewayPassword?: string;
|
|
76
|
-
onConnected?: () => void;
|
|
77
|
-
onDisconnected?: () => void;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// ---------------------------------------------------------------------------
|
|
81
|
-
// Main entry point
|
|
82
|
-
// ---------------------------------------------------------------------------
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Connects to the cloud relay server and the local OpenClaw Gateway,
|
|
86
|
-
* then bridges messages between them indefinitely.
|
|
87
|
-
*
|
|
88
|
-
* The gateway client runs for as long as this relay connection is alive.
|
|
89
|
-
* Returns a Promise that resolves `true` (retry) when the relay server
|
|
90
|
-
* connection closes.
|
|
91
|
-
*/
|
|
92
|
-
export async function runRelayManager(opts: RelayManagerOptions): Promise<boolean> {
|
|
93
|
-
const wsUrl = buildRelayUrl(opts.relayServerUrl, opts.gatewayId, opts.relaySecret);
|
|
94
|
-
|
|
95
|
-
return new Promise<boolean>((resolve) => {
|
|
96
|
-
let relayWs: WebSocket;
|
|
97
|
-
try {
|
|
98
|
-
relayWs = new WebSocket(wsUrl);
|
|
99
|
-
} catch (err) {
|
|
100
|
-
console.error("Failed to create relay WebSocket:", err);
|
|
101
|
-
resolve(true);
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
let gatewayClient: OpenClawGatewayClient | null = null;
|
|
106
|
-
|
|
107
|
-
function send(msg: ToServer): void {
|
|
108
|
-
if (relayWs.readyState === WebSocket.OPEN) {
|
|
109
|
-
relayWs.send(JSON.stringify(msg));
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
relayWs.on("open", () => {
|
|
114
|
-
console.log(`Connected to relay server (gatewayId=${opts.gatewayId})`);
|
|
115
|
-
send({ type: "relay_hello", platform: getServicePlatform() });
|
|
116
|
-
opts.onConnected?.();
|
|
117
|
-
|
|
118
|
-
// Start the persistent gateway connection as soon as we're connected
|
|
119
|
-
// to the relay server. Its lifetime is tied to this relay session.
|
|
120
|
-
gatewayClient = new OpenClawGatewayClient({
|
|
121
|
-
url: opts.gatewayUrl,
|
|
122
|
-
token: opts.gatewayToken,
|
|
123
|
-
password: opts.gatewayPassword,
|
|
124
|
-
|
|
125
|
-
onConnected: () => {
|
|
126
|
-
console.log("Gateway connected.");
|
|
127
|
-
send({ type: "gateway_connected" });
|
|
128
|
-
},
|
|
129
|
-
|
|
130
|
-
onDisconnected: (reason) => {
|
|
131
|
-
console.log(`Gateway disconnected: ${reason}`);
|
|
132
|
-
send({ type: "gateway_disconnected", reason });
|
|
133
|
-
},
|
|
134
|
-
|
|
135
|
-
onEvent: (event, payload) => {
|
|
136
|
-
// On chat final, fetch history to get actual content + extract media attachments.
|
|
137
|
-
// OpenClaw 2026.3.2+ no longer includes message content in chat final payload.
|
|
138
|
-
if (event === "chat") {
|
|
139
|
-
const p = payload as { state?: string; sessionKey?: string; runId?: string; message?: unknown };
|
|
140
|
-
if (p?.state === "final" && p?.sessionKey) {
|
|
141
|
-
const sessionKey = p.sessionKey;
|
|
142
|
-
const runId = p.runId;
|
|
143
|
-
type ContentBlock = { type: string; text?: string; source?: { type?: string; media_type?: string; data?: string; path?: string } };
|
|
144
|
-
type HistoryResponse = { messages?: Array<{ role: string; content?: ContentBlock[] }> };
|
|
145
|
-
const fetchHistory = () =>
|
|
146
|
-
gatewayClient!.request<HistoryResponse>("chat.history", { sessionKey, limit: 10 });
|
|
147
|
-
const extractText = (h: HistoryResponse | undefined) => {
|
|
148
|
-
const msgs = h?.messages ?? [];
|
|
149
|
-
const last = [...msgs].reverse().find((m) => m.role === "assistant");
|
|
150
|
-
return last?.content?.find((b) => b.type === "text")?.text;
|
|
151
|
-
};
|
|
152
|
-
fetchHistory()
|
|
153
|
-
.then(async (history) => {
|
|
154
|
-
let text = extractText(history);
|
|
155
|
-
// Retry once after 600ms if OpenClaw hasn't committed the message yet
|
|
156
|
-
if (!text) {
|
|
157
|
-
await new Promise((resolve) => setTimeout(resolve, 600));
|
|
158
|
-
const retryHistory = await fetchHistory();
|
|
159
|
-
text = extractText(retryHistory);
|
|
160
|
-
history = retryHistory;
|
|
161
|
-
}
|
|
162
|
-
if (text) {
|
|
163
|
-
(p as Record<string, unknown>).message = { content: [{ type: "text", text }] };
|
|
164
|
-
}
|
|
165
|
-
if (isSuspiciousShortReply(text)) {
|
|
166
|
-
console.warn(
|
|
167
|
-
`[relay] suspicious chat final history text runId=${runId} sessionKey=${sessionKey} text=${JSON.stringify(text ?? "")}`
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Upload any media blocks found in the history
|
|
172
|
-
try {
|
|
173
|
-
const attachments = await uploadAssistantAttachments(
|
|
174
|
-
history ?? {},
|
|
175
|
-
opts.relayServerUrl,
|
|
176
|
-
opts.gatewayId,
|
|
177
|
-
opts.relaySecret
|
|
178
|
-
);
|
|
179
|
-
if (attachments.length > 0) {
|
|
180
|
-
(p as Record<string, unknown>).attachments = attachments;
|
|
181
|
-
console.log(`[relay] chat final: injected ${attachments.length} attachment(s) runId=${runId}`);
|
|
182
|
-
}
|
|
183
|
-
} catch (mediaErr) {
|
|
184
|
-
console.error(`[relay] media upload error (non-fatal): ${mediaErr}`);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
console.log(`[relay] chat final (history fetched): runId=${runId} textLength=${text?.length ?? 0}`);
|
|
188
|
-
send({ type: "event", event, payload });
|
|
189
|
-
})
|
|
190
|
-
.catch((err) => {
|
|
191
|
-
console.error(`[relay] chat.history fetch failed: ${err}`);
|
|
192
|
-
console.warn(
|
|
193
|
-
`[relay] suspicious chat final history fetch failure runId=${runId} sessionKey=${sessionKey}`
|
|
194
|
-
);
|
|
195
|
-
send({ type: "event", event, payload });
|
|
196
|
-
});
|
|
197
|
-
return; // will send after history fetch + media upload
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
send({ type: "event", event, payload });
|
|
201
|
-
},
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
gatewayClient.start();
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
relayWs.on("message", async (raw) => {
|
|
208
|
-
let msg: FromServer;
|
|
209
|
-
try {
|
|
210
|
-
msg = JSON.parse(raw.toString()) as FromServer;
|
|
211
|
-
} catch {
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
if (msg.type !== "cmd" || !msg.method) return;
|
|
216
|
-
|
|
217
|
-
const requestId = msg.id;
|
|
218
|
-
console.log(`[relay] cmd received method=${msg.method} id=${requestId ?? "(no-id)"}`);
|
|
219
|
-
|
|
220
|
-
// Handle clawpilot.provider.* commands locally (async)
|
|
221
|
-
const providerPromise = handleProviderCommand(msg.method, msg.params);
|
|
222
|
-
if (providerPromise !== null) {
|
|
223
|
-
const result = await providerPromise;
|
|
224
|
-
if (requestId) {
|
|
225
|
-
send({
|
|
226
|
-
type: "res",
|
|
227
|
-
id: requestId,
|
|
228
|
-
ok: result.ok,
|
|
229
|
-
...(result.ok
|
|
230
|
-
? { payload: result.payload }
|
|
231
|
-
: { error: { message: result.error } }),
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// Handle clawpilot.* commands locally without forwarding to the gateway
|
|
238
|
-
const localResult = handleLocalCommand(msg.method, msg.params);
|
|
239
|
-
if (localResult !== null) {
|
|
240
|
-
if (requestId) {
|
|
241
|
-
if (localResult.ok) {
|
|
242
|
-
send({ type: "res", id: requestId, ok: true, payload: localResult.payload });
|
|
243
|
-
} else {
|
|
244
|
-
send({ type: "res", id: requestId, ok: false, error: { message: localResult.error } });
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Handle chat.send with attachments - save to disk and add path reference
|
|
251
|
-
if (msg.method === "chat.send") {
|
|
252
|
-
const params = msg.params as any;
|
|
253
|
-
// Always set deliver:false so OpenClaw responds via WebSocket (not external channel)
|
|
254
|
-
params.deliver = false;
|
|
255
|
-
if (params.attachments && params.attachments.length > 0) {
|
|
256
|
-
const fileReferences: string[] = [];
|
|
257
|
-
|
|
258
|
-
// Ensure outbound directory exists
|
|
259
|
-
await mkdir(OUTBOUND_DIR, { recursive: true });
|
|
260
|
-
|
|
261
|
-
// Save each attachment to disk and create path reference
|
|
262
|
-
for (const att of params.attachments) {
|
|
263
|
-
try {
|
|
264
|
-
// Decode base64 to buffer
|
|
265
|
-
const buffer = Buffer.from(att.content, "base64");
|
|
266
|
-
const ext = extname(att.fileName ?? "") || extensionFromMimeType(att.mimeType);
|
|
267
|
-
const stagedFileName = `${randomUUID()}${ext}`;
|
|
268
|
-
const stagedPath = join(OUTBOUND_DIR, stagedFileName);
|
|
269
|
-
|
|
270
|
-
// Write to disk
|
|
271
|
-
await writeFile(stagedPath, buffer);
|
|
272
|
-
console.log(`[relay] Saved attachment to: ${stagedPath}`);
|
|
273
|
-
|
|
274
|
-
// Create path reference (same format as ClawX)
|
|
275
|
-
fileReferences.push(
|
|
276
|
-
`[media attached: ${stagedPath} (${att.mimeType}) | ${stagedPath}]`
|
|
277
|
-
);
|
|
278
|
-
} catch (err) {
|
|
279
|
-
console.error(`[relay] Failed to save attachment: ${err}`);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Append file references to message
|
|
284
|
-
if (fileReferences.length > 0) {
|
|
285
|
-
const refs = fileReferences.join("\n");
|
|
286
|
-
params.message = params.message ? `${params.message}\n\n${refs}` : refs;
|
|
287
|
-
console.log(`[relay] Added file references to message`);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
gatewayClient
|
|
293
|
-
?.request(msg.method, msg.params)
|
|
294
|
-
.then((result) => {
|
|
295
|
-
console.log(`[relay] cmd ok method=${msg.method} id=${requestId ?? "(no-id)"}`);
|
|
296
|
-
if (requestId) {
|
|
297
|
-
send({ type: "res", id: requestId, ok: true, payload: result });
|
|
298
|
-
}
|
|
299
|
-
})
|
|
300
|
-
.catch((err: unknown) => {
|
|
301
|
-
console.error(`[relay] cmd failed method=${msg.method} id=${requestId ?? "(no-id)"}: ${String(err)}`);
|
|
302
|
-
if (requestId) {
|
|
303
|
-
send({ type: "res", id: requestId, ok: false, error: { message: String(err) } });
|
|
304
|
-
}
|
|
305
|
-
});
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
relayWs.on("close", (code, reason) => {
|
|
309
|
-
console.log(`Relay connection closed: ${code} ${reason.toString()}`);
|
|
310
|
-
opts.onDisconnected?.();
|
|
311
|
-
gatewayClient?.stop();
|
|
312
|
-
gatewayClient = null;
|
|
313
|
-
// Code 4000 = server kicked us because another relay client took over.
|
|
314
|
-
// Stop retrying so the two instances don't bounce each other forever.
|
|
315
|
-
resolve(code !== 4000);
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
relayWs.on("error", (err) => {
|
|
319
|
-
console.error("Relay WebSocket error:", err.message);
|
|
320
|
-
// close event will follow
|
|
321
|
-
});
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
function buildRelayUrl(serverUrl: string, gatewayId: string, relaySecret: string): string {
|
|
326
|
-
const base = serverUrl.replace(/\/+$/, "").replace(/^http/, "ws");
|
|
327
|
-
return `${base}/relay/${gatewayId}?secret=${encodeURIComponent(relaySecret)}`;
|
|
328
|
-
}
|
package/test-chat.mjs
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { WebSocket } from "ws";
|
|
2
|
-
import { randomUUID } from "crypto";
|
|
3
|
-
import { readFileSync } from "fs";
|
|
4
|
-
|
|
5
|
-
const token = readFileSync("/tmp/test_token.txt", "utf8").trim();
|
|
6
|
-
const url = `ws://localhost:3000/gw/295c6252c69746124af48ebbe7001f11?token=${token}`;
|
|
7
|
-
|
|
8
|
-
const ws = new WebSocket(url);
|
|
9
|
-
|
|
10
|
-
ws.on("open", () => {
|
|
11
|
-
console.log("[ios] connected to relay server");
|
|
12
|
-
|
|
13
|
-
ws.send(JSON.stringify({ method: "sessions.reset", params: { key: "main" } }));
|
|
14
|
-
console.log("[ios] → sessions.reset { key: 'main' }");
|
|
15
|
-
|
|
16
|
-
setTimeout(() => {
|
|
17
|
-
ws.send(JSON.stringify({
|
|
18
|
-
method: "chat.send",
|
|
19
|
-
params: {
|
|
20
|
-
sessionKey: "main",
|
|
21
|
-
message: "hello, please reply with just one short sentence",
|
|
22
|
-
idempotencyKey: randomUUID(),
|
|
23
|
-
},
|
|
24
|
-
}));
|
|
25
|
-
console.log("[ios] → chat.send");
|
|
26
|
-
}, 800);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
let lastText = "";
|
|
30
|
-
ws.on("message", (raw) => {
|
|
31
|
-
const msg = JSON.parse(raw.toString());
|
|
32
|
-
if (msg.type === "connected") { console.log("[ios] ← gateway online"); return; }
|
|
33
|
-
if (msg.type === "disconnected") { console.log("[gateway] ← disconnected:", msg.reason); return; }
|
|
34
|
-
if (msg.type === "event" && msg.event === "chat") {
|
|
35
|
-
const p = msg.payload;
|
|
36
|
-
if (p.state === "delta") {
|
|
37
|
-
const text = p.message?.content?.[0]?.text ?? "";
|
|
38
|
-
if (text !== lastText) {
|
|
39
|
-
process.stdout.write("\r[delta] " + text.slice(-80).padEnd(80));
|
|
40
|
-
lastText = text;
|
|
41
|
-
}
|
|
42
|
-
} else if (p.state === "final") {
|
|
43
|
-
const text = p.message?.content?.[0]?.text ?? "(no text)";
|
|
44
|
-
console.log("\n\n[FINAL RESPONSE]\n" + text);
|
|
45
|
-
ws.close();
|
|
46
|
-
process.exit(0);
|
|
47
|
-
} else if (p.state === "error") {
|
|
48
|
-
console.log("\n[ERROR]", p.errorMessage);
|
|
49
|
-
ws.close();
|
|
50
|
-
process.exit(1);
|
|
51
|
-
}
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
if (msg.event !== "health" && msg.event !== "presence") {
|
|
55
|
-
console.log("[event]", JSON.stringify(msg).slice(0, 120));
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
ws.on("error", (e) => console.error("[ws error]", e.message));
|
|
60
|
-
setTimeout(() => {
|
|
61
|
-
console.log("\n[timeout after 60s]");
|
|
62
|
-
ws.close();
|
|
63
|
-
process.exit(1);
|
|
64
|
-
}, 60000);
|
package/test-direct.mjs
DELETED
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Direct test: connect to OpenClaw Gateway and send chat.send
|
|
3
|
-
* Uses same logic as gateway-client.ts
|
|
4
|
-
*/
|
|
5
|
-
import { WebSocket } from "ws";
|
|
6
|
-
import { randomUUID, generateKeyPairSync, createPrivateKey, sign, createPublicKey, createHash } from "node:crypto";
|
|
7
|
-
import { readFileSync, existsSync } from "node:fs";
|
|
8
|
-
import { join } from "node:path";
|
|
9
|
-
import { homedir } from "node:os";
|
|
10
|
-
|
|
11
|
-
const IDENTITY_PATH = join(homedir(), ".clawai", "device-identity.json");
|
|
12
|
-
const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
|
|
13
|
-
|
|
14
|
-
function base64UrlEncode(buf) {
|
|
15
|
-
return buf.toString("base64").replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/g, "");
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function rawPublicKeyBytes(publicKeyPem) {
|
|
19
|
-
const key = createPublicKey(publicKeyPem);
|
|
20
|
-
const spki = key.export({ type: "spki", format: "der" });
|
|
21
|
-
if (spki.length === ED25519_SPKI_PREFIX.length + 32 && spki.subarray(0, ED25519_SPKI_PREFIX.length).equals(ED25519_SPKI_PREFIX)) {
|
|
22
|
-
return spki.subarray(ED25519_SPKI_PREFIX.length);
|
|
23
|
-
}
|
|
24
|
-
return spki;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const stored = JSON.parse(readFileSync(IDENTITY_PATH, "utf8"));
|
|
28
|
-
const identity = { deviceId: stored.deviceId, publicKeyPem: stored.publicKeyPem, privateKeyPem: stored.privateKeyPem };
|
|
29
|
-
|
|
30
|
-
const openclawCfg = JSON.parse(readFileSync(join(homedir(), ".openclaw", "openclaw.json"), "utf8"));
|
|
31
|
-
const authToken = openclawCfg?.gateway?.auth?.token;
|
|
32
|
-
const port = openclawCfg?.gateway?.port ?? 18789;
|
|
33
|
-
|
|
34
|
-
console.log("Device ID:", identity.deviceId);
|
|
35
|
-
console.log("Auth token:", authToken ? authToken.slice(0, 8) + "..." : "(none)");
|
|
36
|
-
console.log("Connecting to ws://localhost:" + port);
|
|
37
|
-
|
|
38
|
-
const ws = new WebSocket("ws://localhost:" + port, { maxPayload: 25 * 1024 * 1024 });
|
|
39
|
-
const pending = new Map();
|
|
40
|
-
let connectNonce = null;
|
|
41
|
-
let connectSent = false;
|
|
42
|
-
let connected = false;
|
|
43
|
-
|
|
44
|
-
function sendFrame(obj) {
|
|
45
|
-
ws.send(JSON.stringify(obj));
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function request(method, params) {
|
|
49
|
-
const id = randomUUID();
|
|
50
|
-
return new Promise((resolve, reject) => {
|
|
51
|
-
pending.set(id, { resolve, reject });
|
|
52
|
-
sendFrame({ type: "req", id, method, params });
|
|
53
|
-
console.log(`→ req [${id.slice(0, 8)}] ${method}`);
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function sendConnect(nonce) {
|
|
58
|
-
if (connectSent) return;
|
|
59
|
-
connectSent = true;
|
|
60
|
-
const role = "operator";
|
|
61
|
-
const scopes = ["operator.admin"];
|
|
62
|
-
const clientId = "gateway-client";
|
|
63
|
-
const clientMode = "backend";
|
|
64
|
-
const signedAtMs = Date.now();
|
|
65
|
-
|
|
66
|
-
const version = nonce ? "v2" : "v1";
|
|
67
|
-
const payload = [version, identity.deviceId, clientId, clientMode, role, scopes.join(","), String(signedAtMs), authToken ?? "", ...(nonce ? [nonce] : [])].join("|");
|
|
68
|
-
const key = createPrivateKey(identity.privateKeyPem);
|
|
69
|
-
const signature = base64UrlEncode(sign(null, Buffer.from(payload, "utf8"), key));
|
|
70
|
-
const device = {
|
|
71
|
-
id: identity.deviceId,
|
|
72
|
-
publicKey: base64UrlEncode(rawPublicKeyBytes(identity.publicKeyPem)),
|
|
73
|
-
signature,
|
|
74
|
-
signedAt: signedAtMs,
|
|
75
|
-
nonce,
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const params = {
|
|
79
|
-
minProtocol: 3, maxProtocol: 3,
|
|
80
|
-
role, scopes, caps: [], commands: [],
|
|
81
|
-
client: { id: clientId, displayName: "ClawAI Direct Test", version: "1.0.0", platform: process.platform, mode: clientMode },
|
|
82
|
-
device,
|
|
83
|
-
auth: authToken ? { token: authToken } : undefined,
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
request("connect", params)
|
|
87
|
-
.then((res) => {
|
|
88
|
-
console.log("✓ connect OK:", JSON.stringify(res).slice(0, 100));
|
|
89
|
-
connected = true;
|
|
90
|
-
runTest();
|
|
91
|
-
})
|
|
92
|
-
.catch((err) => {
|
|
93
|
-
console.error("✗ connect FAILED:", err.message);
|
|
94
|
-
process.exit(1);
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async function runTest() {
|
|
99
|
-
console.log("\n--- Running test ---");
|
|
100
|
-
try {
|
|
101
|
-
const r1 = await request("sessions.reset", { key: "main" });
|
|
102
|
-
console.log("✓ sessions.reset:", JSON.stringify(r1));
|
|
103
|
-
|
|
104
|
-
const r2 = await request("chat.send", {
|
|
105
|
-
sessionKey: "main",
|
|
106
|
-
message: "hello, please reply with just one short sentence",
|
|
107
|
-
idempotencyKey: randomUUID(),
|
|
108
|
-
});
|
|
109
|
-
console.log("✓ chat.send:", JSON.stringify(r2));
|
|
110
|
-
console.log("\nWaiting for chat events...");
|
|
111
|
-
} catch (err) {
|
|
112
|
-
console.error("✗ command failed:", err.message);
|
|
113
|
-
process.exit(1);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
ws.on("open", () => {
|
|
118
|
-
console.log("WebSocket open, waiting for challenge...");
|
|
119
|
-
setTimeout(() => { if (!connectSent) { console.log("(no challenge, sending connect now)"); sendConnect(null); } }, 1000);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
ws.on("message", (raw) => {
|
|
123
|
-
const msg = JSON.parse(raw.toString());
|
|
124
|
-
if (msg.type === "event") {
|
|
125
|
-
if (msg.event === "connect.challenge") {
|
|
126
|
-
const nonce = msg.payload?.nonce;
|
|
127
|
-
console.log("← challenge nonce:", nonce);
|
|
128
|
-
sendConnect(nonce ?? null);
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
if (msg.event === "chat") {
|
|
132
|
-
const p = msg.payload;
|
|
133
|
-
if (p.state === "delta") {
|
|
134
|
-
process.stdout.write("\r[delta] " + (p.message?.content?.[0]?.text ?? "").slice(-80).padEnd(80));
|
|
135
|
-
} else if (p.state === "final") {
|
|
136
|
-
console.log("\n\n[FINAL] " + (p.message?.content?.[0]?.text ?? "(no text)"));
|
|
137
|
-
ws.close();
|
|
138
|
-
process.exit(0);
|
|
139
|
-
} else if (p.state === "error") {
|
|
140
|
-
console.log("\n[ERROR]", p.errorMessage);
|
|
141
|
-
ws.close();
|
|
142
|
-
process.exit(1);
|
|
143
|
-
}
|
|
144
|
-
} else if (msg.event !== "health" && msg.event !== "presence" && msg.event !== "tick") {
|
|
145
|
-
console.log("\n← event:", msg.event, JSON.stringify(msg.payload ?? {}).slice(0, 80));
|
|
146
|
-
}
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
if (msg.type === "res") {
|
|
150
|
-
const p = pending.get(msg.id);
|
|
151
|
-
if (p) {
|
|
152
|
-
pending.delete(msg.id);
|
|
153
|
-
console.log(`← res [${msg.id.slice(0, 8)}] ok=${msg.ok}` + (msg.error ? " error=" + msg.error.message : ""));
|
|
154
|
-
if (msg.ok) p.resolve(msg.payload);
|
|
155
|
-
else p.reject(new Error(msg.error?.message ?? "gateway error"));
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
ws.on("close", (code, reason) => {
|
|
161
|
-
console.log("\n← close", code, reason.toString() || "(no reason)");
|
|
162
|
-
process.exit(code === 1000 ? 0 : 1);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
ws.on("error", (err) => console.error("WS error:", err.message));
|
|
166
|
-
|
|
167
|
-
setTimeout(() => {
|
|
168
|
-
console.log("\n[timeout 60s]");
|
|
169
|
-
ws.close();
|
|
170
|
-
process.exit(1);
|
|
171
|
-
}, 60000);
|
package/tsconfig.json
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "NodeNext",
|
|
5
|
-
"moduleResolution": "NodeNext",
|
|
6
|
-
"outDir": "dist",
|
|
7
|
-
"rootDir": "src",
|
|
8
|
-
"strict": true,
|
|
9
|
-
"esModuleInterop": true,
|
|
10
|
-
"skipLibCheck": true,
|
|
11
|
-
"declaration": true,
|
|
12
|
-
"sourceMap": true
|
|
13
|
-
},
|
|
14
|
-
"include": ["src/**/*"],
|
|
15
|
-
"exclude": ["node_modules", "dist"]
|
|
16
|
-
}
|