claw-control-center 0.1.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/bin/install-qclaw.mjs +15 -0
- package/dist/index.cjs +2676 -0
- package/dist/index.d.cts +181 -0
- package/dist/install-qclaw.cjs +288 -0
- package/dist/install-qclaw.d.cts +34 -0
- package/openclaw.plugin.json +130 -0
- package/package.json +39 -0
- package/web-dist/assets/index-CPMfZC6Y.css +1 -0
- package/web-dist/assets/index-_bUpQoIN.js +9 -0
- package/web-dist/index.html +13 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2676 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
createConsoleServer: () => createConsoleServer,
|
|
34
|
+
createGatewayClient: () => createGatewayClient,
|
|
35
|
+
createHub53AIBridge: () => createHub53AIBridge,
|
|
36
|
+
default: () => index_default,
|
|
37
|
+
installIntoOpenClaw: () => installIntoOpenClaw,
|
|
38
|
+
installIntoQClaw: () => installIntoQClaw,
|
|
39
|
+
parseHub53AIIncomingMessage: () => parseIncomingMessage,
|
|
40
|
+
runInstallCommand: () => runInstallCommand
|
|
41
|
+
});
|
|
42
|
+
module.exports = __toCommonJS(index_exports);
|
|
43
|
+
var import_node_fs5 = require("fs");
|
|
44
|
+
var import_node_path7 = require("path");
|
|
45
|
+
var import_node_crypto4 = require("crypto");
|
|
46
|
+
|
|
47
|
+
// src/console-server.ts
|
|
48
|
+
var import_node_crypto2 = require("crypto");
|
|
49
|
+
var import_node_fs2 = require("fs");
|
|
50
|
+
var import_promises3 = require("fs/promises");
|
|
51
|
+
var import_node_http = require("http");
|
|
52
|
+
var import_node_path4 = require("path");
|
|
53
|
+
var import_ws2 = require("ws");
|
|
54
|
+
|
|
55
|
+
// src/53aihub-client.ts
|
|
56
|
+
var import_node_crypto = require("crypto");
|
|
57
|
+
var import_promises = require("fs/promises");
|
|
58
|
+
var import_node_path = require("path");
|
|
59
|
+
var import_ws = __toESM(require("ws"), 1);
|
|
60
|
+
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
61
|
+
var DEFAULT_THINKING_MESSAGE = "\u6B63\u5728\u5904\u7406\u60A8\u7684\u8BF7\u6C42...";
|
|
62
|
+
var MAX_OUTBOX_FRAMES = 200;
|
|
63
|
+
var RUN_WAIT_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
64
|
+
function createHub53AIBridge(input) {
|
|
65
|
+
const statePath = (0, import_node_path.join)(input.stateDir, "claw-control-center-53aihub.json");
|
|
66
|
+
let state = { mappings: {}, outbox: [] };
|
|
67
|
+
let socket = null;
|
|
68
|
+
let heartbeatTimer = null;
|
|
69
|
+
let reconnectTimer = null;
|
|
70
|
+
let stopped = false;
|
|
71
|
+
let reconnectAttempts = 0;
|
|
72
|
+
let connectionStatus = input.config.enabled ? "disconnected" : "disabled";
|
|
73
|
+
let lastHeartbeatAt;
|
|
74
|
+
let lastConnectedAt;
|
|
75
|
+
let lastError;
|
|
76
|
+
let receivedMessageCount = 0;
|
|
77
|
+
let sentMessageCount = 0;
|
|
78
|
+
const chatQueues = /* @__PURE__ */ new Map();
|
|
79
|
+
const lastReplyByReq = /* @__PURE__ */ new Map();
|
|
80
|
+
async function start() {
|
|
81
|
+
await loadState();
|
|
82
|
+
if (!input.config.enabled) {
|
|
83
|
+
connectionStatus = "disabled";
|
|
84
|
+
notifyStatus();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
validateConfig(input.config);
|
|
88
|
+
stopped = false;
|
|
89
|
+
connect();
|
|
90
|
+
}
|
|
91
|
+
async function stop() {
|
|
92
|
+
stopped = true;
|
|
93
|
+
if (heartbeatTimer) {
|
|
94
|
+
clearInterval(heartbeatTimer);
|
|
95
|
+
heartbeatTimer = null;
|
|
96
|
+
}
|
|
97
|
+
if (reconnectTimer) {
|
|
98
|
+
clearTimeout(reconnectTimer);
|
|
99
|
+
reconnectTimer = null;
|
|
100
|
+
}
|
|
101
|
+
if (socket) {
|
|
102
|
+
socket.close();
|
|
103
|
+
socket = null;
|
|
104
|
+
}
|
|
105
|
+
await persistState();
|
|
106
|
+
}
|
|
107
|
+
function getStatus() {
|
|
108
|
+
return {
|
|
109
|
+
enabled: input.config.enabled,
|
|
110
|
+
configured: Boolean(input.config.botId && input.config.secret && input.config.wsUrl),
|
|
111
|
+
connectionStatus,
|
|
112
|
+
botId: maskBotId(input.config.botId),
|
|
113
|
+
wsUrl: sanitizeWsUrl(input.config.wsUrl),
|
|
114
|
+
lastHeartbeatAt,
|
|
115
|
+
lastConnectedAt,
|
|
116
|
+
lastError,
|
|
117
|
+
receivedMessageCount,
|
|
118
|
+
sentMessageCount,
|
|
119
|
+
pendingOutboundCount: state.outbox.length
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function connect() {
|
|
123
|
+
if (stopped) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
connectionStatus = "connecting";
|
|
127
|
+
lastError = void 0;
|
|
128
|
+
notifyStatus();
|
|
129
|
+
const authBase64 = Buffer.from(`${input.config.botId}:${input.config.secret}`).toString("base64");
|
|
130
|
+
socket = new import_ws.default(input.config.wsUrl, {
|
|
131
|
+
headers: {
|
|
132
|
+
Authorization: `Bearer ${input.config.secret}`,
|
|
133
|
+
"Proxy-Authorization": `Basic ${authBase64}`,
|
|
134
|
+
"X-Bot-Id": input.config.botId,
|
|
135
|
+
"X-Api-Key": input.config.secret
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
socket.on("open", () => {
|
|
139
|
+
reconnectAttempts = 0;
|
|
140
|
+
connectionStatus = "connected";
|
|
141
|
+
lastConnectedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
142
|
+
input.logger?.info?.(`[53aihub] connected to ${sanitizeWsUrl(input.config.wsUrl)}`);
|
|
143
|
+
sendAppPing();
|
|
144
|
+
void replayOutbox();
|
|
145
|
+
heartbeatTimer = setInterval(() => {
|
|
146
|
+
if (socket?.readyState === import_ws.default.OPEN) {
|
|
147
|
+
socket.ping?.();
|
|
148
|
+
sendAppPing();
|
|
149
|
+
}
|
|
150
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
151
|
+
notifyStatus();
|
|
152
|
+
});
|
|
153
|
+
socket.on("message", (data) => {
|
|
154
|
+
void handleRawMessage(data.toString()).catch((error) => {
|
|
155
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
156
|
+
input.logger?.error?.(`[53aihub] failed to process message: ${lastError}`);
|
|
157
|
+
notifyStatus();
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
socket.on("error", (error) => {
|
|
161
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
162
|
+
connectionStatus = "error";
|
|
163
|
+
input.logger?.error?.(`[53aihub] websocket error: ${lastError}`);
|
|
164
|
+
notifyStatus();
|
|
165
|
+
});
|
|
166
|
+
socket.on("close", (code, reason) => {
|
|
167
|
+
if (heartbeatTimer) {
|
|
168
|
+
clearInterval(heartbeatTimer);
|
|
169
|
+
heartbeatTimer = null;
|
|
170
|
+
}
|
|
171
|
+
if (socket) {
|
|
172
|
+
socket = null;
|
|
173
|
+
}
|
|
174
|
+
if (stopped) {
|
|
175
|
+
connectionStatus = "disabled";
|
|
176
|
+
notifyStatus();
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
connectionStatus = "disconnected";
|
|
180
|
+
lastError = `WebSocket closed: ${code}${reason.length ? ` ${reason.toString()}` : ""}`;
|
|
181
|
+
notifyStatus();
|
|
182
|
+
scheduleReconnect();
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
function scheduleReconnect() {
|
|
186
|
+
if (stopped) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (reconnectAttempts >= input.config.maxReconnectAttempts) {
|
|
190
|
+
connectionStatus = "error";
|
|
191
|
+
lastError = `Max reconnect attempts (${input.config.maxReconnectAttempts}) reached`;
|
|
192
|
+
notifyStatus();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
reconnectAttempts += 1;
|
|
196
|
+
const backoff = Math.min(input.config.reconnectBaseMs * 2 ** (reconnectAttempts - 1), 3e4);
|
|
197
|
+
reconnectTimer = setTimeout(connect, backoff);
|
|
198
|
+
}
|
|
199
|
+
async function handleRawMessage(rawPayload) {
|
|
200
|
+
const heartbeat = parseHeartbeat(rawPayload);
|
|
201
|
+
if (heartbeat === "pong") {
|
|
202
|
+
lastHeartbeatAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
203
|
+
notifyStatus();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (heartbeat === "ping") {
|
|
207
|
+
sendRaw(JSON.stringify({ action: "pong", data: { botId: input.config.botId } }));
|
|
208
|
+
lastHeartbeatAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
209
|
+
notifyStatus();
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const message = parseIncomingMessage(rawPayload);
|
|
213
|
+
if (!message) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
receivedMessageCount += 1;
|
|
217
|
+
notifyStatus();
|
|
218
|
+
const previous = chatQueues.get(message.chatId) ?? Promise.resolve();
|
|
219
|
+
const next = previous.catch(() => void 0).then(() => processIncomingMessage(message));
|
|
220
|
+
chatQueues.set(message.chatId, next);
|
|
221
|
+
await next.finally(() => {
|
|
222
|
+
if (chatQueues.get(message.chatId) === next) {
|
|
223
|
+
chatQueues.delete(message.chatId);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
async function processIncomingMessage(message) {
|
|
228
|
+
const accessResult = checkAccessPolicy(message);
|
|
229
|
+
if (!accessResult.allowed) {
|
|
230
|
+
await sendReply({
|
|
231
|
+
reqId: message.reqId,
|
|
232
|
+
text: `\u26A0\uFE0F \u8BBF\u95EE\u88AB\u62D2\u7EDD: ${accessResult.reason}`,
|
|
233
|
+
status: "error",
|
|
234
|
+
error: {
|
|
235
|
+
code: "ACCESS_DENIED",
|
|
236
|
+
message: accessResult.reason
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const session = await resolveSession(message);
|
|
242
|
+
await input.callbacks.onEnsureSessionStream(session.id);
|
|
243
|
+
await input.callbacks.onUserMessage({
|
|
244
|
+
id: `hub53ai-user-${message.msgId}`,
|
|
245
|
+
sessionId: session.id,
|
|
246
|
+
role: "user",
|
|
247
|
+
content: buildPrompt(message),
|
|
248
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
249
|
+
});
|
|
250
|
+
await input.callbacks.onSessionStatus(session.id, "running");
|
|
251
|
+
if (input.config.sendThinkingMessage) {
|
|
252
|
+
await sendReply({
|
|
253
|
+
reqId: message.reqId,
|
|
254
|
+
text: DEFAULT_THINKING_MESSAGE,
|
|
255
|
+
status: "thinking"
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
const close = input.gateway.subscribe(session.id, input.callbacks.getLastEventSeq(session.id), {
|
|
259
|
+
onEvent: (event) => {
|
|
260
|
+
void handleGatewayEvent(message, event);
|
|
261
|
+
},
|
|
262
|
+
onDisconnect: (error) => {
|
|
263
|
+
const messageText = error instanceof Error ? error.message : "gateway stream disconnected";
|
|
264
|
+
void sendReply({
|
|
265
|
+
reqId: message.reqId,
|
|
266
|
+
text: `\u26A0\uFE0F ${messageText}`,
|
|
267
|
+
status: "error",
|
|
268
|
+
error: {
|
|
269
|
+
code: "WEBSOCKET_ERROR",
|
|
270
|
+
message: messageText
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
const terminalPromise = waitForTerminalEvent(message.reqId);
|
|
276
|
+
try {
|
|
277
|
+
await input.gateway.sendMessage(session.id, buildPrompt(message));
|
|
278
|
+
await terminalPromise;
|
|
279
|
+
} catch (error) {
|
|
280
|
+
const messageText = error instanceof Error ? error.message : String(error);
|
|
281
|
+
await sendReply({
|
|
282
|
+
reqId: message.reqId,
|
|
283
|
+
text: `\u26A0\uFE0F ${messageText}`,
|
|
284
|
+
status: "error",
|
|
285
|
+
error: {
|
|
286
|
+
code: inferErrorCode(messageText),
|
|
287
|
+
message: messageText
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
} finally {
|
|
291
|
+
close();
|
|
292
|
+
clearTerminalResolver(message.reqId);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const terminalResolvers = /* @__PURE__ */ new Map();
|
|
296
|
+
function waitForTerminalEvent(reqId) {
|
|
297
|
+
return new Promise((resolve4, reject) => {
|
|
298
|
+
const timer = setTimeout(() => {
|
|
299
|
+
terminalResolvers.delete(reqId);
|
|
300
|
+
reject(new Error(`Timed out waiting for run completion (${reqId})`));
|
|
301
|
+
}, RUN_WAIT_TIMEOUT_MS);
|
|
302
|
+
terminalResolvers.set(reqId, { resolve: resolve4, reject, timer });
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
function resolveTerminalEvent(reqId) {
|
|
306
|
+
const resolver = terminalResolvers.get(reqId);
|
|
307
|
+
if (!resolver) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
clearTimeout(resolver.timer);
|
|
311
|
+
resolver.resolve();
|
|
312
|
+
terminalResolvers.delete(reqId);
|
|
313
|
+
}
|
|
314
|
+
function clearTerminalResolver(reqId) {
|
|
315
|
+
const resolver = terminalResolvers.get(reqId);
|
|
316
|
+
if (!resolver) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
clearTimeout(resolver.timer);
|
|
320
|
+
terminalResolvers.delete(reqId);
|
|
321
|
+
}
|
|
322
|
+
async function handleGatewayEvent(message, event) {
|
|
323
|
+
if (event.kind === "assistant.delta" || event.kind === "assistant.message") {
|
|
324
|
+
const content = String(event.payload?.content ?? "");
|
|
325
|
+
if (content) {
|
|
326
|
+
lastReplyByReq.set(message.reqId, content);
|
|
327
|
+
await sendReply({
|
|
328
|
+
reqId: message.reqId,
|
|
329
|
+
text: content,
|
|
330
|
+
status: "streaming"
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (event.kind === "tool.call" || event.kind === "tool.result" || event.kind === "status.update") {
|
|
336
|
+
const summary = summarizeVisibleActivity(event);
|
|
337
|
+
if (summary && input.config.sendThinkingMessage) {
|
|
338
|
+
await sendReply({
|
|
339
|
+
reqId: message.reqId,
|
|
340
|
+
text: summary,
|
|
341
|
+
status: "thinking"
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (event.kind === "run.completed") {
|
|
347
|
+
const finalText = String(event.payload?.content ?? lastReplyByReq.get(message.reqId) ?? "");
|
|
348
|
+
await sendReply({
|
|
349
|
+
reqId: message.reqId,
|
|
350
|
+
text: finalText,
|
|
351
|
+
status: "done"
|
|
352
|
+
});
|
|
353
|
+
lastReplyByReq.delete(message.reqId);
|
|
354
|
+
resolveTerminalEvent(message.reqId);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (event.kind === "run.failed" || event.kind === "run.interrupted") {
|
|
358
|
+
const errorText = String(event.payload?.error ?? event.payload?.message ?? event.kind);
|
|
359
|
+
await sendReply({
|
|
360
|
+
reqId: message.reqId,
|
|
361
|
+
text: `\u26A0\uFE0F ${errorText}`,
|
|
362
|
+
status: "error",
|
|
363
|
+
error: {
|
|
364
|
+
code: inferErrorCode(errorText),
|
|
365
|
+
message: errorText
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
lastReplyByReq.delete(message.reqId);
|
|
369
|
+
resolveTerminalEvent(message.reqId);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
async function resolveSession(message) {
|
|
373
|
+
const mappedId = state.mappings[message.chatId];
|
|
374
|
+
if (mappedId) {
|
|
375
|
+
const session2 = await input.gateway.getSession(mappedId);
|
|
376
|
+
await input.callbacks.onSessionUpsert(session2);
|
|
377
|
+
return session2;
|
|
378
|
+
}
|
|
379
|
+
const session = await input.gateway.createSession(`53AIHub ${message.chatId}`);
|
|
380
|
+
state.mappings[message.chatId] = session.id;
|
|
381
|
+
await persistState();
|
|
382
|
+
await input.callbacks.onSessionUpsert(session);
|
|
383
|
+
return session;
|
|
384
|
+
}
|
|
385
|
+
async function sendReply(inputReply) {
|
|
386
|
+
const frame = buildOutgoingChunk(inputReply.reqId, inputReply.text, inputReply.status, inputReply.error);
|
|
387
|
+
if (!sendRaw(JSON.stringify(frame), true)) {
|
|
388
|
+
state.outbox.push(frame);
|
|
389
|
+
state.outbox = state.outbox.slice(-MAX_OUTBOX_FRAMES);
|
|
390
|
+
await persistState();
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
function sendRaw(payload, countMessage = false) {
|
|
394
|
+
if (!socket || socket.readyState !== import_ws.default.OPEN) {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
try {
|
|
398
|
+
socket.send(payload);
|
|
399
|
+
if (countMessage) {
|
|
400
|
+
sentMessageCount += 1;
|
|
401
|
+
}
|
|
402
|
+
notifyStatus();
|
|
403
|
+
return true;
|
|
404
|
+
} catch (error) {
|
|
405
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
406
|
+
notifyStatus();
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
async function replayOutbox() {
|
|
411
|
+
if (state.outbox.length === 0) {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
const pending = [...state.outbox];
|
|
415
|
+
state.outbox = [];
|
|
416
|
+
for (const frame of pending) {
|
|
417
|
+
if (!sendRaw(JSON.stringify(frame), true)) {
|
|
418
|
+
state.outbox.push(frame);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
state.outbox = state.outbox.slice(-MAX_OUTBOX_FRAMES);
|
|
422
|
+
await persistState();
|
|
423
|
+
}
|
|
424
|
+
function sendAppPing() {
|
|
425
|
+
if (sendRaw(JSON.stringify({ action: "ping", data: { botId: input.config.botId } }))) {
|
|
426
|
+
lastHeartbeatAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
function checkAccessPolicy(message) {
|
|
430
|
+
if (input.config.accessPolicy === "open") {
|
|
431
|
+
return { allowed: true, reason: "" };
|
|
432
|
+
}
|
|
433
|
+
const allowed = new Set(input.config.allowFrom);
|
|
434
|
+
const candidates = [
|
|
435
|
+
message.userId,
|
|
436
|
+
message.chatId,
|
|
437
|
+
`user:${message.userId}`,
|
|
438
|
+
`53aihub:${message.userId}`,
|
|
439
|
+
`53aihub:${message.chatId}`
|
|
440
|
+
];
|
|
441
|
+
if (candidates.some((candidate) => allowed.has(candidate))) {
|
|
442
|
+
return { allowed: true, reason: "" };
|
|
443
|
+
}
|
|
444
|
+
return { allowed: false, reason: "user is not in allowlist" };
|
|
445
|
+
}
|
|
446
|
+
async function loadState() {
|
|
447
|
+
await (0, import_promises.mkdir)((0, import_node_path.dirname)(statePath), { recursive: true });
|
|
448
|
+
try {
|
|
449
|
+
const parsed = JSON.parse(await (0, import_promises.readFile)(statePath, "utf8"));
|
|
450
|
+
state = {
|
|
451
|
+
mappings: parsed?.mappings && typeof parsed.mappings === "object" ? parsed.mappings : {},
|
|
452
|
+
outbox: Array.isArray(parsed?.outbox) ? parsed.outbox : []
|
|
453
|
+
};
|
|
454
|
+
} catch {
|
|
455
|
+
await persistState();
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
async function persistState() {
|
|
459
|
+
await (0, import_promises.writeFile)(statePath, `${JSON.stringify(state, null, 2)}
|
|
460
|
+
`);
|
|
461
|
+
}
|
|
462
|
+
function notifyStatus() {
|
|
463
|
+
input.callbacks.onStatusChange();
|
|
464
|
+
}
|
|
465
|
+
return {
|
|
466
|
+
start,
|
|
467
|
+
stop,
|
|
468
|
+
getStatus
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
function parseIncomingMessage(rawJson) {
|
|
472
|
+
try {
|
|
473
|
+
const wsMsg = JSON.parse(rawJson);
|
|
474
|
+
if (wsMsg.action === "ping" || wsMsg.action === "pong") {
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
if (wsMsg.action === "chat") {
|
|
478
|
+
const openAIReq = toRecord(wsMsg.data);
|
|
479
|
+
const messages = Array.isArray(openAIReq.messages) ? openAIReq.messages : [];
|
|
480
|
+
const lastUserMsg = [...messages].reverse().find((message) => toRecord(message).role === "user");
|
|
481
|
+
if (!lastUserMsg) {
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
const userMessage = toRecord(lastUserMsg);
|
|
485
|
+
const content = userMessage.content;
|
|
486
|
+
const userId2 = stringOr(openAIReq.user, userMessage.name, `user-${String(wsMsg.req_id ?? (0, import_node_crypto.randomUUID)())}`);
|
|
487
|
+
const chatId2 = stringOr(openAIReq.conversation_id, userId2);
|
|
488
|
+
return {
|
|
489
|
+
type: "message",
|
|
490
|
+
msgId: String(wsMsg.req_id ?? (0, import_node_crypto.randomUUID)()),
|
|
491
|
+
reqId: String(wsMsg.req_id ?? (0, import_node_crypto.randomUUID)()),
|
|
492
|
+
chatId: chatId2,
|
|
493
|
+
userId: userId2,
|
|
494
|
+
text: extractTextFromContent(content),
|
|
495
|
+
imageUrls: extractImagesFromContent(content),
|
|
496
|
+
fileUrls: extractFilesFromContent(content)
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
const data = toRecord(wsMsg.data);
|
|
500
|
+
const chatId = stringOr(data.chatId, data.userId, "default-chat");
|
|
501
|
+
const userId = stringOr(data.userId, data.chatId, "default-user");
|
|
502
|
+
return {
|
|
503
|
+
type: stringOr(data.type, "message"),
|
|
504
|
+
msgId: stringOr(data.msgId, data.id, `msg-${Date.now()}`),
|
|
505
|
+
reqId: String(wsMsg.req_id ?? data.msgId ?? data.id ?? `msg-${Date.now()}`),
|
|
506
|
+
chatId,
|
|
507
|
+
userId,
|
|
508
|
+
text: stringOr(data.text, data.content, ""),
|
|
509
|
+
imageUrls: normalizeUrlList(data.imageUrls, data.images),
|
|
510
|
+
fileUrls: normalizeUrlList(data.fileUrls, data.files),
|
|
511
|
+
quoteContent: typeof data.quoteContent === "string" ? data.quoteContent : void 0
|
|
512
|
+
};
|
|
513
|
+
} catch {
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
function buildPrompt(message) {
|
|
518
|
+
const parts = [message.text.trim()].filter(Boolean);
|
|
519
|
+
if (message.imageUrls?.length) {
|
|
520
|
+
parts.push(`Images:
|
|
521
|
+
${message.imageUrls.join("\n")}`);
|
|
522
|
+
}
|
|
523
|
+
if (message.fileUrls?.length) {
|
|
524
|
+
parts.push(`Files:
|
|
525
|
+
${message.fileUrls.join("\n")}`);
|
|
526
|
+
}
|
|
527
|
+
return parts.join("\n\n");
|
|
528
|
+
}
|
|
529
|
+
function buildOutgoingChunk(reqId, text, status, error) {
|
|
530
|
+
return {
|
|
531
|
+
req_id: reqId,
|
|
532
|
+
action: "chat",
|
|
533
|
+
status,
|
|
534
|
+
data: {
|
|
535
|
+
id: reqId,
|
|
536
|
+
object: "chat.completion.chunk",
|
|
537
|
+
created: Math.floor(Date.now() / 1e3),
|
|
538
|
+
model: "openclaw-agent",
|
|
539
|
+
choices: [
|
|
540
|
+
{
|
|
541
|
+
index: 0,
|
|
542
|
+
delta: {
|
|
543
|
+
content: text,
|
|
544
|
+
role: "assistant"
|
|
545
|
+
},
|
|
546
|
+
finish_reason: status === "done" ? "stop" : status === "error" ? "error" : null
|
|
547
|
+
}
|
|
548
|
+
],
|
|
549
|
+
...error ? { error } : {}
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
function parseHeartbeat(rawPayload) {
|
|
554
|
+
try {
|
|
555
|
+
const parsed = JSON.parse(rawPayload);
|
|
556
|
+
if (parsed.action === "ping" || parsed.action === "pong") {
|
|
557
|
+
return parsed.action;
|
|
558
|
+
}
|
|
559
|
+
} catch {
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
function extractTextFromContent(content) {
|
|
565
|
+
if (typeof content === "string") {
|
|
566
|
+
return content;
|
|
567
|
+
}
|
|
568
|
+
if (!Array.isArray(content)) {
|
|
569
|
+
return "";
|
|
570
|
+
}
|
|
571
|
+
return content.map((item) => {
|
|
572
|
+
const record = toRecord(item);
|
|
573
|
+
if (record.type === "text" && typeof record.text === "string") {
|
|
574
|
+
return record.text;
|
|
575
|
+
}
|
|
576
|
+
return "";
|
|
577
|
+
}).filter(Boolean).join("\n");
|
|
578
|
+
}
|
|
579
|
+
function extractImagesFromContent(content) {
|
|
580
|
+
if (!Array.isArray(content)) {
|
|
581
|
+
return [];
|
|
582
|
+
}
|
|
583
|
+
return normalizeUrlList(
|
|
584
|
+
void 0,
|
|
585
|
+
content.map((item) => {
|
|
586
|
+
const record = toRecord(item);
|
|
587
|
+
if (record.type === "image_url") {
|
|
588
|
+
return toRecord(record.image_url).url;
|
|
589
|
+
}
|
|
590
|
+
if (record.type === "image") {
|
|
591
|
+
return record.url ?? toRecord(record.image).url;
|
|
592
|
+
}
|
|
593
|
+
return void 0;
|
|
594
|
+
}).filter(Boolean)
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
function extractFilesFromContent(content) {
|
|
598
|
+
if (!Array.isArray(content)) {
|
|
599
|
+
return [];
|
|
600
|
+
}
|
|
601
|
+
return normalizeUrlList(
|
|
602
|
+
void 0,
|
|
603
|
+
content.map((item) => {
|
|
604
|
+
const record = toRecord(item);
|
|
605
|
+
if (record.type === "file") {
|
|
606
|
+
return record.url ?? toRecord(record.file).url;
|
|
607
|
+
}
|
|
608
|
+
return void 0;
|
|
609
|
+
}).filter(Boolean)
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
function normalizeUrlList(primary, fallback) {
|
|
613
|
+
const source = Array.isArray(primary) ? primary : Array.isArray(fallback) ? fallback : [];
|
|
614
|
+
return source.map((entry) => {
|
|
615
|
+
if (typeof entry === "string") {
|
|
616
|
+
return entry;
|
|
617
|
+
}
|
|
618
|
+
const record = toRecord(entry);
|
|
619
|
+
return typeof record.url === "string" ? record.url : "";
|
|
620
|
+
}).filter(Boolean);
|
|
621
|
+
}
|
|
622
|
+
function summarizeVisibleActivity(event) {
|
|
623
|
+
const name = String(event.payload?.name ?? event.payload?.toolName ?? event.payload?.skillName ?? "").trim();
|
|
624
|
+
if (event.kind === "tool.call") {
|
|
625
|
+
return name ? `Used tool ${name}` : "Used a tool";
|
|
626
|
+
}
|
|
627
|
+
if (event.kind === "tool.result") {
|
|
628
|
+
return name ? `Tool ${name} returned a result` : "Tool returned a result";
|
|
629
|
+
}
|
|
630
|
+
const message = String(event.payload?.message ?? event.payload?.status ?? "").trim();
|
|
631
|
+
return message || null;
|
|
632
|
+
}
|
|
633
|
+
function validateConfig(config) {
|
|
634
|
+
if (!config.botId) {
|
|
635
|
+
throw new Error("hub53ai.botId is required when hub53ai.enabled is true");
|
|
636
|
+
}
|
|
637
|
+
if (!config.secret) {
|
|
638
|
+
throw new Error("hub53ai.secret is required when hub53ai.enabled is true");
|
|
639
|
+
}
|
|
640
|
+
if (!config.wsUrl) {
|
|
641
|
+
throw new Error("hub53ai.wsUrl is required when hub53ai.enabled is true");
|
|
642
|
+
}
|
|
643
|
+
if (!config.wsUrl.startsWith("ws://") && !config.wsUrl.startsWith("wss://")) {
|
|
644
|
+
throw new Error("hub53ai.wsUrl must start with ws:// or wss://");
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
function inferErrorCode(errorText) {
|
|
648
|
+
const lower = errorText.toLowerCase();
|
|
649
|
+
if (lower.includes("timeout")) {
|
|
650
|
+
return "TIMEOUT";
|
|
651
|
+
}
|
|
652
|
+
if (lower.includes("rate limit")) {
|
|
653
|
+
return "RATE_LIMITED";
|
|
654
|
+
}
|
|
655
|
+
if (lower.includes("quota")) {
|
|
656
|
+
return "INSUFFICIENT_QUOTA";
|
|
657
|
+
}
|
|
658
|
+
if (lower.includes("unauthorized") || lower.includes("access denied")) {
|
|
659
|
+
return "ACCESS_DENIED";
|
|
660
|
+
}
|
|
661
|
+
if (lower.includes("websocket")) {
|
|
662
|
+
return "WEBSOCKET_ERROR";
|
|
663
|
+
}
|
|
664
|
+
return "INTERNAL_ERROR";
|
|
665
|
+
}
|
|
666
|
+
function sanitizeWsUrl(wsUrl) {
|
|
667
|
+
if (!wsUrl) {
|
|
668
|
+
return void 0;
|
|
669
|
+
}
|
|
670
|
+
try {
|
|
671
|
+
const url = new URL(wsUrl);
|
|
672
|
+
url.username = "";
|
|
673
|
+
url.password = "";
|
|
674
|
+
url.search = "";
|
|
675
|
+
return url.toString();
|
|
676
|
+
} catch {
|
|
677
|
+
return wsUrl;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
function maskBotId(botId) {
|
|
681
|
+
if (!botId) {
|
|
682
|
+
return void 0;
|
|
683
|
+
}
|
|
684
|
+
if (botId.length <= 4) {
|
|
685
|
+
return `${botId.slice(0, 1)}***`;
|
|
686
|
+
}
|
|
687
|
+
return `${botId.slice(0, 2)}***${botId.slice(-2)}`;
|
|
688
|
+
}
|
|
689
|
+
function stringOr(...values) {
|
|
690
|
+
for (const value of values) {
|
|
691
|
+
if (typeof value === "string" && value.trim()) {
|
|
692
|
+
return value.trim();
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return "";
|
|
696
|
+
}
|
|
697
|
+
function toRecord(value) {
|
|
698
|
+
return value && typeof value === "object" ? value : {};
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// src/file-store.ts
|
|
702
|
+
var import_promises2 = require("fs/promises");
|
|
703
|
+
var import_node_path2 = require("path");
|
|
704
|
+
var FileSessionStore = class {
|
|
705
|
+
constructor(filePath, maxSessions) {
|
|
706
|
+
this.filePath = filePath;
|
|
707
|
+
this.maxSessions = maxSessions;
|
|
708
|
+
}
|
|
709
|
+
filePath;
|
|
710
|
+
maxSessions;
|
|
711
|
+
state = { sessions: {} };
|
|
712
|
+
persistChain = Promise.resolve();
|
|
713
|
+
async init() {
|
|
714
|
+
await (0, import_promises2.mkdir)((0, import_node_path2.dirname)(this.filePath), { recursive: true });
|
|
715
|
+
try {
|
|
716
|
+
const raw = await (0, import_promises2.readFile)(this.filePath, "utf8");
|
|
717
|
+
const parsed = JSON.parse(raw);
|
|
718
|
+
this.state = parsed?.sessions ? parsed : { sessions: {} };
|
|
719
|
+
} catch {
|
|
720
|
+
await this.persist();
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
listSessions() {
|
|
724
|
+
return Object.values(this.state.sessions).map((entry) => entry.session).sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
725
|
+
}
|
|
726
|
+
getSession(sessionId) {
|
|
727
|
+
return this.state.sessions[sessionId];
|
|
728
|
+
}
|
|
729
|
+
getLastEventSeq(sessionId) {
|
|
730
|
+
return this.state.sessions[sessionId]?.session.lastEventSeq ?? 0;
|
|
731
|
+
}
|
|
732
|
+
isHydrated(sessionId) {
|
|
733
|
+
return this.state.sessions[sessionId]?.hydrated ?? false;
|
|
734
|
+
}
|
|
735
|
+
async upsertSession(session) {
|
|
736
|
+
const existing = this.state.sessions[session.id];
|
|
737
|
+
this.state.sessions[session.id] = {
|
|
738
|
+
session,
|
|
739
|
+
messages: existing?.messages ?? [],
|
|
740
|
+
events: existing?.events ?? [],
|
|
741
|
+
hydrated: existing?.hydrated ?? false
|
|
742
|
+
};
|
|
743
|
+
this.trimSessions();
|
|
744
|
+
await this.persist();
|
|
745
|
+
}
|
|
746
|
+
async renameSession(sessionId, title) {
|
|
747
|
+
const record = this.requireSession(sessionId);
|
|
748
|
+
record.session.title = title;
|
|
749
|
+
record.session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
750
|
+
await this.persist();
|
|
751
|
+
}
|
|
752
|
+
async archiveSession(sessionId) {
|
|
753
|
+
await this.setSessionStatus(sessionId, "archived");
|
|
754
|
+
}
|
|
755
|
+
async setSessionStatus(sessionId, status) {
|
|
756
|
+
const record = this.requireSession(sessionId);
|
|
757
|
+
record.session.status = status;
|
|
758
|
+
record.session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
759
|
+
await this.persist();
|
|
760
|
+
}
|
|
761
|
+
async appendMessage(message) {
|
|
762
|
+
const record = this.requireSession(message.sessionId);
|
|
763
|
+
if (!record.messages.some((entry) => entry.id === message.id)) {
|
|
764
|
+
record.messages.push(message);
|
|
765
|
+
record.hydrated = true;
|
|
766
|
+
record.session.updatedAt = message.createdAt;
|
|
767
|
+
await this.persist();
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
async appendEvent(event) {
|
|
771
|
+
const record = this.requireSession(event.sessionId);
|
|
772
|
+
if (record.events.some((entry) => entry.seq === event.seq)) {
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
record.events.push(event);
|
|
776
|
+
record.events.sort((left, right) => left.seq - right.seq);
|
|
777
|
+
record.hydrated = true;
|
|
778
|
+
record.session.lastEventSeq = Math.max(record.session.lastEventSeq, event.seq);
|
|
779
|
+
record.session.updatedAt = event.createdAt;
|
|
780
|
+
await this.persist();
|
|
781
|
+
}
|
|
782
|
+
async replaceSessionDetail(sessionId, detail) {
|
|
783
|
+
const record = this.requireSession(sessionId);
|
|
784
|
+
record.messages = dedupeMessages(detail.messages);
|
|
785
|
+
record.events = dedupeEvents(detail.events);
|
|
786
|
+
record.hydrated = true;
|
|
787
|
+
record.session.lastEventSeq = record.events.at(-1)?.seq ?? record.session.lastEventSeq;
|
|
788
|
+
record.session.updatedAt = record.messages.at(-1)?.createdAt ?? record.events.at(-1)?.createdAt ?? record.session.updatedAt;
|
|
789
|
+
await this.persist();
|
|
790
|
+
}
|
|
791
|
+
requireSession(sessionId) {
|
|
792
|
+
const record = this.state.sessions[sessionId];
|
|
793
|
+
if (!record) {
|
|
794
|
+
throw new Error(`unknown session: ${sessionId}`);
|
|
795
|
+
}
|
|
796
|
+
return record;
|
|
797
|
+
}
|
|
798
|
+
trimSessions() {
|
|
799
|
+
const sessions = this.listSessions();
|
|
800
|
+
if (sessions.length <= this.maxSessions) {
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
for (const session of sessions.slice(this.maxSessions)) {
|
|
804
|
+
delete this.state.sessions[session.id];
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
async persist() {
|
|
808
|
+
const snapshot = JSON.stringify(this.state, null, 2);
|
|
809
|
+
this.persistChain = this.persistChain.then(() => (0, import_promises2.writeFile)(this.filePath, snapshot));
|
|
810
|
+
await this.persistChain;
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
function dedupeMessages(messages) {
|
|
814
|
+
return Array.from(new Map(messages.map((message) => [message.id, message])).values()).sort(
|
|
815
|
+
(left, right) => left.createdAt.localeCompare(right.createdAt)
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
function dedupeEvents(events) {
|
|
819
|
+
return Array.from(new Map(events.map((event) => [event.seq, event])).values()).sort((left, right) => left.seq - right.seq);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// src/host.ts
|
|
823
|
+
var import_node_fs = require("fs");
|
|
824
|
+
var import_node_os = require("os");
|
|
825
|
+
var import_node_path3 = require("path");
|
|
826
|
+
var SENSITIVE_KEY_PATTERN = /(token|secret|password|key|credential)/i;
|
|
827
|
+
function detectHostKind(pathHint) {
|
|
828
|
+
const normalized = (pathHint ?? "").replaceAll("\\", "/").toLowerCase();
|
|
829
|
+
if (normalized.includes("/.qclaw") || normalized.includes("/.qclow")) {
|
|
830
|
+
return "qclaw";
|
|
831
|
+
}
|
|
832
|
+
return "openclaw";
|
|
833
|
+
}
|
|
834
|
+
function resolvePluginConfig(config) {
|
|
835
|
+
return {
|
|
836
|
+
gateway: {
|
|
837
|
+
baseUrl: config?.gateway?.baseUrl?.trim() ?? "",
|
|
838
|
+
botId: config?.gateway?.botId?.trim() ?? "",
|
|
839
|
+
secret: config?.gateway?.secret?.trim() ?? "",
|
|
840
|
+
requestTimeoutMs: config?.gateway?.requestTimeoutMs ?? 15e3,
|
|
841
|
+
streamReconnectMs: config?.gateway?.streamReconnectMs ?? 2e3
|
|
842
|
+
},
|
|
843
|
+
hub53ai: {
|
|
844
|
+
enabled: config?.hub53ai?.enabled ?? false,
|
|
845
|
+
botId: config?.hub53ai?.botId?.trim() ?? "",
|
|
846
|
+
secret: config?.hub53ai?.secret?.trim() ?? "",
|
|
847
|
+
wsUrl: config?.hub53ai?.wsUrl?.trim() ?? "",
|
|
848
|
+
accessPolicy: config?.hub53ai?.accessPolicy === "allowlist" ? "allowlist" : "open",
|
|
849
|
+
allowFrom: Array.isArray(config?.hub53ai?.allowFrom) ? config.hub53ai.allowFrom.map((entry) => String(entry).trim()).filter(Boolean) : [],
|
|
850
|
+
sendThinkingMessage: config?.hub53ai?.sendThinkingMessage ?? true,
|
|
851
|
+
reconnectBaseMs: config?.hub53ai?.reconnectBaseMs ?? 2e3,
|
|
852
|
+
maxReconnectAttempts: config?.hub53ai?.maxReconnectAttempts ?? 10
|
|
853
|
+
},
|
|
854
|
+
console: {
|
|
855
|
+
enabled: config?.console?.enabled ?? true,
|
|
856
|
+
host: config?.console?.host ?? "127.0.0.1",
|
|
857
|
+
port: config?.console?.port ?? 4318
|
|
858
|
+
},
|
|
859
|
+
persistence: {
|
|
860
|
+
maxSessions: config?.persistence?.maxSessions ?? 100
|
|
861
|
+
},
|
|
862
|
+
logging: {
|
|
863
|
+
level: config?.logging?.level ?? "info"
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
function resolvePluginConfigWithHostDefaults(configPath, config) {
|
|
868
|
+
const resolved = resolvePluginConfig(config);
|
|
869
|
+
const hostConfig = readHostGatewayConfig(resolveHostConfigPath(configPath));
|
|
870
|
+
if (!resolved.gateway.baseUrl && hostConfig.baseUrl) {
|
|
871
|
+
resolved.gateway.baseUrl = hostConfig.baseUrl;
|
|
872
|
+
}
|
|
873
|
+
if (!resolved.gateway.secret && hostConfig.secret) {
|
|
874
|
+
resolved.gateway.secret = hostConfig.secret;
|
|
875
|
+
}
|
|
876
|
+
if (!resolved.hub53ai.botId && hostConfig.hub53ai?.botId) {
|
|
877
|
+
resolved.hub53ai.botId = hostConfig.hub53ai.botId;
|
|
878
|
+
}
|
|
879
|
+
if (!resolved.hub53ai.secret && hostConfig.hub53ai?.secret) {
|
|
880
|
+
resolved.hub53ai.secret = hostConfig.hub53ai.secret;
|
|
881
|
+
}
|
|
882
|
+
if (!resolved.hub53ai.wsUrl && hostConfig.hub53ai?.wsUrl) {
|
|
883
|
+
resolved.hub53ai.wsUrl = hostConfig.hub53ai.wsUrl;
|
|
884
|
+
}
|
|
885
|
+
return resolved;
|
|
886
|
+
}
|
|
887
|
+
function sanitizePluginConfig(config) {
|
|
888
|
+
if (!config) {
|
|
889
|
+
return {};
|
|
890
|
+
}
|
|
891
|
+
return JSON.parse(JSON.stringify(config), (key, value) => {
|
|
892
|
+
if (typeof value === "string" && key && SENSITIVE_KEY_PATTERN.test(key)) {
|
|
893
|
+
return "[redacted]";
|
|
894
|
+
}
|
|
895
|
+
return value;
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
function readHostRuntimeInfo(configPath) {
|
|
899
|
+
const resolvedPath = resolveHostConfigPath(configPath);
|
|
900
|
+
try {
|
|
901
|
+
const raw = (0, import_node_fs.readFileSync)(resolvedPath, "utf8");
|
|
902
|
+
const parsed = JSON.parse(raw);
|
|
903
|
+
const modelPrimary = typeof parsed.agents?.defaults?.model?.primary === "string" && parsed.agents.defaults.model.primary.trim().length > 0 ? parsed.agents.defaults.model.primary.trim() : void 0;
|
|
904
|
+
const enabledSkills = Object.entries(parsed.skills?.entries ?? {}).filter(([, config]) => config?.enabled === true).map(([skillName]) => skillName).sort((left, right) => left.localeCompare(right));
|
|
905
|
+
return {
|
|
906
|
+
modelPrimary,
|
|
907
|
+
enabledSkills
|
|
908
|
+
};
|
|
909
|
+
} catch {
|
|
910
|
+
return {
|
|
911
|
+
enabledSkills: []
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
function readHostGatewayConfig(configPath) {
|
|
916
|
+
try {
|
|
917
|
+
const raw = (0, import_node_fs.readFileSync)(configPath, "utf8");
|
|
918
|
+
const parsed = JSON.parse(raw);
|
|
919
|
+
const gateway = parsed.gateway ?? {};
|
|
920
|
+
const port = typeof gateway.port === "number" ? gateway.port : Number(gateway.port ?? parsed.port ?? 28789);
|
|
921
|
+
const host = typeof gateway.host === "string" && gateway.host.trim() ? gateway.host.trim() : "127.0.0.1";
|
|
922
|
+
const baseUrl = Number.isFinite(port) && port > 0 ? `ws://${host}:${port}` : void 0;
|
|
923
|
+
const auth = gateway.auth ?? parsed.auth ?? {};
|
|
924
|
+
const authMode = typeof auth.mode === "string" ? auth.mode : "token";
|
|
925
|
+
const secret = authMode === "password" ? typeof auth.password === "string" ? auth.password : void 0 : typeof auth.token === "string" ? auth.token : void 0;
|
|
926
|
+
const legacyHub = parsed.channels?.["53aihub"];
|
|
927
|
+
const hub53ai = legacyHub ? {
|
|
928
|
+
botId: typeof legacyHub.botId === "string" ? legacyHub.botId : void 0,
|
|
929
|
+
secret: typeof legacyHub.secret === "string" ? legacyHub.secret : typeof legacyHub.token === "string" ? legacyHub.token : void 0,
|
|
930
|
+
wsUrl: typeof legacyHub.WSUrl === "string" ? legacyHub.WSUrl : typeof legacyHub.websocketUrl === "string" ? legacyHub.websocketUrl : void 0
|
|
931
|
+
} : void 0;
|
|
932
|
+
return { baseUrl, secret, hub53ai };
|
|
933
|
+
} catch {
|
|
934
|
+
return {};
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
function resolveHostConfigPath(configPath) {
|
|
938
|
+
const candidates = [
|
|
939
|
+
configPath,
|
|
940
|
+
(0, import_node_path3.join)((0, import_node_os.homedir)(), ".qclaw", "openclaw.json"),
|
|
941
|
+
(0, import_node_path3.join)((0, import_node_os.homedir)(), ".openclaw", "openclaw.json")
|
|
942
|
+
];
|
|
943
|
+
return candidates.find((candidate) => (0, import_node_fs.existsSync)(candidate)) ?? configPath;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// src/console-server.ts
|
|
947
|
+
function createConsoleServer(input) {
|
|
948
|
+
const hostRuntime = input.hostRuntime ?? readHostRuntimeInfo(input.configPath);
|
|
949
|
+
const store = new FileSessionStore(
|
|
950
|
+
(0, import_node_path4.join)(input.stateDir, "claw-control-center-state.json"),
|
|
951
|
+
input.persistence.maxSessions
|
|
952
|
+
);
|
|
953
|
+
const sessionStreams = /* @__PURE__ */ new Map();
|
|
954
|
+
const sessionSockets = /* @__PURE__ */ new Map();
|
|
955
|
+
const statusSockets = /* @__PURE__ */ new Set();
|
|
956
|
+
let lastGatewayError = null;
|
|
957
|
+
let currentPort = input.consoleConfig.port;
|
|
958
|
+
const hub53ai = input.hub53aiConfig ? createHub53AIBridge({
|
|
959
|
+
stateDir: input.stateDir,
|
|
960
|
+
config: input.hub53aiConfig,
|
|
961
|
+
gateway: input.gateway,
|
|
962
|
+
callbacks: {
|
|
963
|
+
onSessionUpsert: async (session) => {
|
|
964
|
+
await store.upsertSession(session);
|
|
965
|
+
broadcastStatus();
|
|
966
|
+
},
|
|
967
|
+
onUserMessage: async (message) => {
|
|
968
|
+
await store.appendMessage(message);
|
|
969
|
+
},
|
|
970
|
+
onSessionStatus: async (sessionId, status) => {
|
|
971
|
+
await store.setSessionStatus(sessionId, status);
|
|
972
|
+
broadcastStatus();
|
|
973
|
+
},
|
|
974
|
+
onEnsureSessionStream: ensureSessionStream,
|
|
975
|
+
getLastEventSeq: (sessionId) => store.getLastEventSeq(sessionId),
|
|
976
|
+
onStatusChange: broadcastStatus
|
|
977
|
+
}
|
|
978
|
+
}) : void 0;
|
|
979
|
+
const httpServer = (0, import_node_http.createServer)(async (request, response) => {
|
|
980
|
+
try {
|
|
981
|
+
await routeRequest(request, response);
|
|
982
|
+
} catch (error) {
|
|
983
|
+
writeJson(response, 500, {
|
|
984
|
+
error: error instanceof Error ? error.message : String(error)
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
});
|
|
988
|
+
const wsServer = new import_ws2.WebSocketServer({ noServer: true });
|
|
989
|
+
httpServer.on("upgrade", (request, socket, head) => {
|
|
990
|
+
const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "localhost"}`);
|
|
991
|
+
if (!url.pathname.startsWith("/ws/")) {
|
|
992
|
+
socket.destroy();
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
wsServer.handleUpgrade(request, socket, head, (client) => {
|
|
996
|
+
handleSocket(client, url.pathname);
|
|
997
|
+
});
|
|
998
|
+
});
|
|
999
|
+
async function start() {
|
|
1000
|
+
await store.init();
|
|
1001
|
+
await syncRemoteSessions();
|
|
1002
|
+
await hub53ai?.start();
|
|
1003
|
+
await new Promise((resolvePromise) => {
|
|
1004
|
+
httpServer.listen(input.consoleConfig.port, input.consoleConfig.host, () => resolvePromise());
|
|
1005
|
+
});
|
|
1006
|
+
const address = httpServer.address();
|
|
1007
|
+
if (!address || typeof address === "string") {
|
|
1008
|
+
throw new Error("failed to resolve local server address");
|
|
1009
|
+
}
|
|
1010
|
+
currentPort = address.port;
|
|
1011
|
+
for (const session of store.listSessions()) {
|
|
1012
|
+
if (session.status === "running") {
|
|
1013
|
+
ensureSessionStream(session.id);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
async function stop() {
|
|
1018
|
+
await hub53ai?.stop();
|
|
1019
|
+
for (const close of sessionStreams.values()) {
|
|
1020
|
+
close();
|
|
1021
|
+
}
|
|
1022
|
+
sessionStreams.clear();
|
|
1023
|
+
for (const socket of statusSockets) {
|
|
1024
|
+
socket.close();
|
|
1025
|
+
}
|
|
1026
|
+
for (const sockets of sessionSockets.values()) {
|
|
1027
|
+
for (const socket of sockets) {
|
|
1028
|
+
socket.close();
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
await new Promise((resolvePromise) => httpServer.close(() => resolvePromise()));
|
|
1032
|
+
await input.gateway.stop();
|
|
1033
|
+
}
|
|
1034
|
+
function handleSocket(socket, pathname) {
|
|
1035
|
+
if (pathname === "/ws/status") {
|
|
1036
|
+
statusSockets.add(socket);
|
|
1037
|
+
socket.send(JSON.stringify(buildStatusSnapshot()));
|
|
1038
|
+
socket.on("close", () => statusSockets.delete(socket));
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
const sessionMatch = pathname.match(/^\/ws\/sessions\/([^/]+)$/);
|
|
1042
|
+
if (sessionMatch) {
|
|
1043
|
+
const sessionId = decodeURIComponent(sessionMatch[1]);
|
|
1044
|
+
const sockets = sessionSockets.get(sessionId) ?? /* @__PURE__ */ new Set();
|
|
1045
|
+
sockets.add(socket);
|
|
1046
|
+
sessionSockets.set(sessionId, sockets);
|
|
1047
|
+
socket.on("close", () => {
|
|
1048
|
+
sockets.delete(socket);
|
|
1049
|
+
if (sockets.size === 0) {
|
|
1050
|
+
sessionSockets.delete(sessionId);
|
|
1051
|
+
}
|
|
1052
|
+
});
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
socket.close();
|
|
1056
|
+
}
|
|
1057
|
+
async function routeRequest(request, response) {
|
|
1058
|
+
const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "localhost"}`);
|
|
1059
|
+
const method = request.method ?? "GET";
|
|
1060
|
+
if (method === "GET" && url.pathname === "/api/bootstrap") {
|
|
1061
|
+
writeJson(response, 200, {
|
|
1062
|
+
token: input.token,
|
|
1063
|
+
status: buildStatusSnapshot(),
|
|
1064
|
+
config: {
|
|
1065
|
+
gateway: {
|
|
1066
|
+
...input.gatewayConfig,
|
|
1067
|
+
secret: "[redacted]"
|
|
1068
|
+
},
|
|
1069
|
+
hub53ai: input.hub53aiConfig ? {
|
|
1070
|
+
...input.hub53aiConfig,
|
|
1071
|
+
secret: "[redacted]"
|
|
1072
|
+
} : void 0,
|
|
1073
|
+
config: {
|
|
1074
|
+
gateway: {
|
|
1075
|
+
...input.gatewayConfig,
|
|
1076
|
+
secret: "[redacted]"
|
|
1077
|
+
},
|
|
1078
|
+
hub53ai: input.hub53aiConfig ? {
|
|
1079
|
+
...input.hub53aiConfig,
|
|
1080
|
+
secret: "[redacted]"
|
|
1081
|
+
} : void 0,
|
|
1082
|
+
console: input.consoleConfig,
|
|
1083
|
+
persistence: input.persistence
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
if (method === "GET" && url.pathname === "/api/status") {
|
|
1090
|
+
writeJson(response, 200, buildStatusSnapshot());
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
if (method === "GET" && url.pathname === "/api/config") {
|
|
1094
|
+
writeJson(response, 200, {
|
|
1095
|
+
gateway: {
|
|
1096
|
+
...input.gatewayConfig,
|
|
1097
|
+
secret: "[redacted]"
|
|
1098
|
+
},
|
|
1099
|
+
hub53ai: input.hub53aiConfig ? {
|
|
1100
|
+
...input.hub53aiConfig,
|
|
1101
|
+
secret: "[redacted]"
|
|
1102
|
+
} : void 0,
|
|
1103
|
+
config: {
|
|
1104
|
+
hub53ai: input.hub53aiConfig ? {
|
|
1105
|
+
...input.hub53aiConfig,
|
|
1106
|
+
secret: "[redacted]"
|
|
1107
|
+
} : void 0,
|
|
1108
|
+
console: input.consoleConfig,
|
|
1109
|
+
persistence: input.persistence
|
|
1110
|
+
}
|
|
1111
|
+
});
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
if (method === "GET" && url.pathname === "/api/sessions") {
|
|
1115
|
+
writeJson(response, 200, {
|
|
1116
|
+
sessions: store.listSessions()
|
|
1117
|
+
});
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
if (method === "POST" && url.pathname === "/api/sessions") {
|
|
1121
|
+
requireToken(request, input.token);
|
|
1122
|
+
const body = await readJsonBody(request);
|
|
1123
|
+
const session = await input.gateway.createSession(
|
|
1124
|
+
body.title?.trim() || `Session ${store.listSessions().length + 1}`,
|
|
1125
|
+
body.initialPrompt ?? ""
|
|
1126
|
+
);
|
|
1127
|
+
await store.upsertSession(session);
|
|
1128
|
+
if (body.initialPrompt?.trim()) {
|
|
1129
|
+
await ensureSessionStream(session.id);
|
|
1130
|
+
await store.appendMessage(buildUserMessage(session.id, body.initialPrompt));
|
|
1131
|
+
await store.setSessionStatus(session.id, "running");
|
|
1132
|
+
await input.gateway.sendMessage(session.id, body.initialPrompt);
|
|
1133
|
+
}
|
|
1134
|
+
broadcastStatus();
|
|
1135
|
+
writeJson(response, 201, session);
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
const sessionMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)(?:\/(messages|control|events))?$/);
|
|
1139
|
+
if (sessionMatch) {
|
|
1140
|
+
const sessionId = decodeURIComponent(sessionMatch[1]);
|
|
1141
|
+
const suffix = sessionMatch[2];
|
|
1142
|
+
if (method === "GET" && !suffix) {
|
|
1143
|
+
await reconcileDerivedAssistantMessage(sessionId);
|
|
1144
|
+
let session = store.getSession(sessionId);
|
|
1145
|
+
if (!session || !store.isHydrated(sessionId)) {
|
|
1146
|
+
await hydrateSession(sessionId);
|
|
1147
|
+
session = store.getSession(sessionId);
|
|
1148
|
+
}
|
|
1149
|
+
if (!session) {
|
|
1150
|
+
writeJson(response, 404, { error: "session not found" });
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
writeJson(response, 200, {
|
|
1154
|
+
session: session.session,
|
|
1155
|
+
messages: session.messages
|
|
1156
|
+
});
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
if (method === "GET" && suffix === "events") {
|
|
1160
|
+
await reconcileDerivedAssistantMessage(sessionId);
|
|
1161
|
+
const afterSeq = Number(url.searchParams.get("afterSeq") ?? "0");
|
|
1162
|
+
let session = store.getSession(sessionId);
|
|
1163
|
+
if (!session || !store.isHydrated(sessionId)) {
|
|
1164
|
+
await hydrateSession(sessionId);
|
|
1165
|
+
session = store.getSession(sessionId);
|
|
1166
|
+
}
|
|
1167
|
+
if (!session) {
|
|
1168
|
+
writeJson(response, 404, { error: "session not found" });
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
writeJson(response, 200, {
|
|
1172
|
+
events: session.events.filter((event) => event.seq > afterSeq)
|
|
1173
|
+
});
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
if (method === "POST" && suffix === "messages") {
|
|
1177
|
+
requireToken(request, input.token);
|
|
1178
|
+
const body = await readJsonBody(request);
|
|
1179
|
+
if (!body.content?.trim()) {
|
|
1180
|
+
writeJson(response, 400, { error: "message content is required" });
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
const existing = store.getSession(sessionId);
|
|
1184
|
+
if (!existing) {
|
|
1185
|
+
writeJson(response, 404, { error: "session not found" });
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
await ensureSessionStream(sessionId);
|
|
1189
|
+
await store.appendMessage(buildUserMessage(sessionId, body.content));
|
|
1190
|
+
await store.setSessionStatus(sessionId, "running");
|
|
1191
|
+
try {
|
|
1192
|
+
await input.gateway.sendMessage(sessionId, body.content);
|
|
1193
|
+
} catch (error) {
|
|
1194
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1195
|
+
await store.setSessionStatus(sessionId, "failed");
|
|
1196
|
+
await appendEvent({
|
|
1197
|
+
id: (0, import_node_crypto2.randomUUID)(),
|
|
1198
|
+
sessionId,
|
|
1199
|
+
seq: existing.session.lastEventSeq + 1,
|
|
1200
|
+
kind: "run.failed",
|
|
1201
|
+
payload: { error: message },
|
|
1202
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1203
|
+
});
|
|
1204
|
+
throw error;
|
|
1205
|
+
}
|
|
1206
|
+
broadcastStatus();
|
|
1207
|
+
writeJson(response, 202, { ok: true });
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
if (method === "POST" && suffix === "control") {
|
|
1211
|
+
requireToken(request, input.token);
|
|
1212
|
+
const body = await readJsonBody(request);
|
|
1213
|
+
const action = body.action;
|
|
1214
|
+
if (!action) {
|
|
1215
|
+
writeJson(response, 400, { error: "action is required" });
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
if (action === "rename") {
|
|
1219
|
+
await store.renameSession(sessionId, body.title?.trim() || "Renamed session");
|
|
1220
|
+
} else if (action === "archive") {
|
|
1221
|
+
await store.archiveSession(sessionId);
|
|
1222
|
+
}
|
|
1223
|
+
await input.gateway.controlSession(sessionId, action, body.title);
|
|
1224
|
+
broadcastStatus();
|
|
1225
|
+
writeJson(response, 200, { ok: true });
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
await serveStatic(url.pathname, response);
|
|
1230
|
+
}
|
|
1231
|
+
async function serveStatic(pathname, response) {
|
|
1232
|
+
if (!input.webDir) {
|
|
1233
|
+
writeJson(response, 404, { error: "not found" });
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
const requestedPath = pathname === "/" ? "/index.html" : pathname;
|
|
1237
|
+
const filePath = (0, import_node_path4.resolve)(input.webDir, `.${(0, import_node_path4.normalize)(requestedPath)}`);
|
|
1238
|
+
if (!filePath.startsWith((0, import_node_path4.resolve)(input.webDir))) {
|
|
1239
|
+
writeJson(response, 403, { error: "forbidden" });
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
try {
|
|
1243
|
+
await (0, import_promises3.access)(filePath);
|
|
1244
|
+
response.writeHead(200, {
|
|
1245
|
+
"Content-Type": contentTypeFor(filePath)
|
|
1246
|
+
});
|
|
1247
|
+
(0, import_node_fs2.createReadStream)(filePath).pipe(response);
|
|
1248
|
+
} catch {
|
|
1249
|
+
const indexPath = (0, import_node_path4.resolve)(input.webDir, "index.html");
|
|
1250
|
+
await (0, import_promises3.access)(indexPath);
|
|
1251
|
+
response.writeHead(200, {
|
|
1252
|
+
"Content-Type": "text/html; charset=utf-8"
|
|
1253
|
+
});
|
|
1254
|
+
(0, import_node_fs2.createReadStream)(indexPath).pipe(response);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
async function ensureSessionStream(sessionId) {
|
|
1258
|
+
if (sessionStreams.has(sessionId)) {
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
const lastSeq = store.getLastEventSeq(sessionId);
|
|
1262
|
+
const close = input.gateway.subscribe(sessionId, lastSeq, {
|
|
1263
|
+
onEvent: (event) => {
|
|
1264
|
+
lastGatewayError = null;
|
|
1265
|
+
void handleGatewayEvent(event);
|
|
1266
|
+
},
|
|
1267
|
+
onDisconnect: (error) => {
|
|
1268
|
+
lastGatewayError = error ?? new Error("gateway stream disconnected");
|
|
1269
|
+
sessionStreams.delete(sessionId);
|
|
1270
|
+
broadcastStatus();
|
|
1271
|
+
setTimeout(() => {
|
|
1272
|
+
void recoverSessionStream(sessionId);
|
|
1273
|
+
}, input.gatewayConfig.streamReconnectMs);
|
|
1274
|
+
}
|
|
1275
|
+
});
|
|
1276
|
+
sessionStreams.set(sessionId, close);
|
|
1277
|
+
}
|
|
1278
|
+
async function recoverSessionStream(sessionId) {
|
|
1279
|
+
try {
|
|
1280
|
+
const events = await input.gateway.listEvents(sessionId, store.getLastEventSeq(sessionId));
|
|
1281
|
+
for (const event of events) {
|
|
1282
|
+
await handleGatewayEvent(event);
|
|
1283
|
+
}
|
|
1284
|
+
await ensureSessionStream(sessionId);
|
|
1285
|
+
} catch (error) {
|
|
1286
|
+
lastGatewayError = error instanceof Error ? error : new Error(String(error));
|
|
1287
|
+
broadcastStatus();
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
async function handleGatewayEvent(event) {
|
|
1291
|
+
await appendEvent(event);
|
|
1292
|
+
const nextStatus = nextStatusForEvent(event.kind);
|
|
1293
|
+
if (nextStatus) {
|
|
1294
|
+
await store.setSessionStatus(event.sessionId, nextStatus);
|
|
1295
|
+
}
|
|
1296
|
+
if (event.kind === "assistant.message") {
|
|
1297
|
+
const content = String(event.payload.content ?? "");
|
|
1298
|
+
if (content) {
|
|
1299
|
+
await store.appendMessage({
|
|
1300
|
+
id: `assistant-${event.seq}`,
|
|
1301
|
+
sessionId: event.sessionId,
|
|
1302
|
+
role: "assistant",
|
|
1303
|
+
content,
|
|
1304
|
+
createdAt: event.createdAt
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
if (isTerminalEvent(event.kind)) {
|
|
1309
|
+
await reconcileDerivedAssistantMessage(event.sessionId);
|
|
1310
|
+
}
|
|
1311
|
+
broadcastSessionEvent(event.sessionId, event);
|
|
1312
|
+
broadcastStatus();
|
|
1313
|
+
}
|
|
1314
|
+
async function appendEvent(event) {
|
|
1315
|
+
await store.appendEvent(event);
|
|
1316
|
+
}
|
|
1317
|
+
async function syncRemoteSessions() {
|
|
1318
|
+
try {
|
|
1319
|
+
const remoteSessions = await input.gateway.listSessions(input.persistence.maxSessions);
|
|
1320
|
+
for (const session of remoteSessions) {
|
|
1321
|
+
await store.upsertSession(session);
|
|
1322
|
+
}
|
|
1323
|
+
} catch (error) {
|
|
1324
|
+
lastGatewayError = error instanceof Error ? error : new Error(String(error));
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
async function hydrateSession(sessionId) {
|
|
1328
|
+
const session = await input.gateway.getSession(sessionId);
|
|
1329
|
+
await store.upsertSession(session);
|
|
1330
|
+
const messages = await input.gateway.getSessionMessages(sessionId, 200);
|
|
1331
|
+
const events = await input.gateway.listEvents(sessionId, 0);
|
|
1332
|
+
await store.replaceSessionDetail(sessionId, { messages, events });
|
|
1333
|
+
await reconcileDerivedAssistantMessage(sessionId);
|
|
1334
|
+
}
|
|
1335
|
+
async function reconcileDerivedAssistantMessage(sessionId) {
|
|
1336
|
+
const record = store.getSession(sessionId);
|
|
1337
|
+
if (!record) {
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
const derived = deriveDerivedAssistantMessage(record.messages, record.events, record.session.status, sessionId);
|
|
1341
|
+
if (!derived) {
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
await store.appendMessage(derived);
|
|
1345
|
+
}
|
|
1346
|
+
function buildStatusSnapshot() {
|
|
1347
|
+
const sessions = store.listSessions();
|
|
1348
|
+
return {
|
|
1349
|
+
hostKind: input.hostKind,
|
|
1350
|
+
stateDir: input.stateDir,
|
|
1351
|
+
configPath: input.configPath,
|
|
1352
|
+
serviceVersion: input.pluginVersion,
|
|
1353
|
+
pluginVersion: input.pluginVersion,
|
|
1354
|
+
port: currentPort,
|
|
1355
|
+
pid: process.pid,
|
|
1356
|
+
runnerCommand: "gateway",
|
|
1357
|
+
activeSessionCount: sessions.length,
|
|
1358
|
+
runningSessionCount: sessions.filter((session) => session.status === "running").length,
|
|
1359
|
+
healthy: lastGatewayError === null,
|
|
1360
|
+
modelPrimary: hostRuntime.modelPrimary,
|
|
1361
|
+
enabledSkills: hostRuntime.enabledSkills,
|
|
1362
|
+
hub53ai: hub53ai?.getStatus()
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
function broadcastStatus() {
|
|
1366
|
+
const snapshot = JSON.stringify(buildStatusSnapshot());
|
|
1367
|
+
for (const socket of statusSockets) {
|
|
1368
|
+
socket.send(snapshot);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
function broadcastSessionEvent(sessionId, event) {
|
|
1372
|
+
const sockets = sessionSockets.get(sessionId);
|
|
1373
|
+
if (!sockets) {
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
const payload = JSON.stringify(event);
|
|
1377
|
+
for (const socket of sockets) {
|
|
1378
|
+
socket.send(payload);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
return {
|
|
1382
|
+
start,
|
|
1383
|
+
stop,
|
|
1384
|
+
get baseUrl() {
|
|
1385
|
+
return `http://${input.consoleConfig.host}:${currentPort}`;
|
|
1386
|
+
}
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
function buildUserMessage(sessionId, content) {
|
|
1390
|
+
return {
|
|
1391
|
+
id: `user-${(0, import_node_crypto2.randomUUID)()}`,
|
|
1392
|
+
sessionId,
|
|
1393
|
+
role: "user",
|
|
1394
|
+
content,
|
|
1395
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
function requireToken(request, token) {
|
|
1399
|
+
if (!token) {
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
if (request.headers["x-plugin-token"] !== token) {
|
|
1403
|
+
throw new Error("request is missing a valid plugin token");
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
async function readJsonBody(request) {
|
|
1407
|
+
const chunks = [];
|
|
1408
|
+
for await (const chunk of request) {
|
|
1409
|
+
chunks.push(Buffer.from(chunk));
|
|
1410
|
+
}
|
|
1411
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
1412
|
+
return raw ? JSON.parse(raw) : {};
|
|
1413
|
+
}
|
|
1414
|
+
function writeJson(response, statusCode, payload) {
|
|
1415
|
+
response.writeHead(statusCode, {
|
|
1416
|
+
"Content-Type": "application/json"
|
|
1417
|
+
});
|
|
1418
|
+
response.end(JSON.stringify(payload));
|
|
1419
|
+
}
|
|
1420
|
+
function nextStatusForEvent(kind) {
|
|
1421
|
+
switch (kind) {
|
|
1422
|
+
case "run.completed":
|
|
1423
|
+
return "completed";
|
|
1424
|
+
case "run.failed":
|
|
1425
|
+
return "failed";
|
|
1426
|
+
case "run.interrupted":
|
|
1427
|
+
return "interrupted";
|
|
1428
|
+
case "run.started":
|
|
1429
|
+
case "assistant.delta":
|
|
1430
|
+
case "assistant.message":
|
|
1431
|
+
case "tool.call":
|
|
1432
|
+
case "tool.result":
|
|
1433
|
+
case "status.update":
|
|
1434
|
+
return "running";
|
|
1435
|
+
default:
|
|
1436
|
+
return void 0;
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
function isTerminalEvent(kind) {
|
|
1440
|
+
return kind === "run.completed" || kind === "run.failed" || kind === "run.interrupted";
|
|
1441
|
+
}
|
|
1442
|
+
function deriveDerivedAssistantMessage(messages, events, status, sessionId) {
|
|
1443
|
+
if (!["completed", "failed", "interrupted"].includes(status)) {
|
|
1444
|
+
return null;
|
|
1445
|
+
}
|
|
1446
|
+
const lastTerminalEvent = [...events].reverse().find((event) => isTerminalEvent(event.kind));
|
|
1447
|
+
if (!lastTerminalEvent) {
|
|
1448
|
+
return null;
|
|
1449
|
+
}
|
|
1450
|
+
const lastDeltaEvent = [...events].reverse().find(
|
|
1451
|
+
(event) => event.kind === "assistant.delta" && event.seq <= lastTerminalEvent.seq && typeof event.payload?.content === "string" && String(event.payload.content).trim().length > 0
|
|
1452
|
+
);
|
|
1453
|
+
if (!lastDeltaEvent) {
|
|
1454
|
+
return null;
|
|
1455
|
+
}
|
|
1456
|
+
const lastUserIndex = findLastIndex(messages, (message) => message.role === "user");
|
|
1457
|
+
const assistantMessagesSinceLastUser = messages.slice(lastUserIndex + 1).filter((message) => message.role === "assistant");
|
|
1458
|
+
const cumulative = String(lastDeltaEvent.payload.content ?? "").trim();
|
|
1459
|
+
if (!cumulative) {
|
|
1460
|
+
return null;
|
|
1461
|
+
}
|
|
1462
|
+
if (assistantMessagesSinceLastUser.some((message) => message.content.trim() === cumulative)) {
|
|
1463
|
+
return null;
|
|
1464
|
+
}
|
|
1465
|
+
const combinedPrefix = assistantMessagesSinceLastUser.map((message) => message.content).join("");
|
|
1466
|
+
const remainder = stripKnownAssistantPrefix(cumulative, combinedPrefix, assistantMessagesSinceLastUser);
|
|
1467
|
+
if (!remainder) {
|
|
1468
|
+
return null;
|
|
1469
|
+
}
|
|
1470
|
+
if (messages.some((message) => message.role === "assistant" && message.content.trim() === remainder)) {
|
|
1471
|
+
return null;
|
|
1472
|
+
}
|
|
1473
|
+
return {
|
|
1474
|
+
id: `assistant-derived-${lastDeltaEvent.seq}`,
|
|
1475
|
+
sessionId,
|
|
1476
|
+
role: "assistant",
|
|
1477
|
+
content: remainder,
|
|
1478
|
+
createdAt: lastDeltaEvent.createdAt
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
function stripKnownAssistantPrefix(cumulative, combinedPrefix, assistantMessages) {
|
|
1482
|
+
let remainder = cumulative;
|
|
1483
|
+
if (combinedPrefix.trim() && remainder.startsWith(combinedPrefix)) {
|
|
1484
|
+
remainder = remainder.slice(combinedPrefix.length).trim();
|
|
1485
|
+
} else {
|
|
1486
|
+
for (const message of assistantMessages) {
|
|
1487
|
+
const prefix = message.content.trim();
|
|
1488
|
+
if (!prefix) {
|
|
1489
|
+
continue;
|
|
1490
|
+
}
|
|
1491
|
+
if (remainder.startsWith(prefix)) {
|
|
1492
|
+
remainder = remainder.slice(prefix.length).trimStart();
|
|
1493
|
+
} else {
|
|
1494
|
+
break;
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
remainder = remainder.trim();
|
|
1498
|
+
}
|
|
1499
|
+
return remainder.length > 0 ? remainder : null;
|
|
1500
|
+
}
|
|
1501
|
+
function findLastIndex(values, predicate) {
|
|
1502
|
+
for (let index = values.length - 1; index >= 0; index -= 1) {
|
|
1503
|
+
if (predicate(values[index])) {
|
|
1504
|
+
return index;
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
return -1;
|
|
1508
|
+
}
|
|
1509
|
+
function contentTypeFor(filePath) {
|
|
1510
|
+
if (filePath.endsWith(".js")) {
|
|
1511
|
+
return "text/javascript; charset=utf-8";
|
|
1512
|
+
}
|
|
1513
|
+
if (filePath.endsWith(".css")) {
|
|
1514
|
+
return "text/css; charset=utf-8";
|
|
1515
|
+
}
|
|
1516
|
+
if (filePath.endsWith(".json")) {
|
|
1517
|
+
return "application/json; charset=utf-8";
|
|
1518
|
+
}
|
|
1519
|
+
if (filePath.endsWith(".svg")) {
|
|
1520
|
+
return "image/svg+xml";
|
|
1521
|
+
}
|
|
1522
|
+
return "text/html; charset=utf-8";
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// src/gateway-client.ts
|
|
1526
|
+
var import_node_crypto3 = require("crypto");
|
|
1527
|
+
var import_node_fs3 = require("fs");
|
|
1528
|
+
var import_node_path5 = require("path");
|
|
1529
|
+
var import_node_url = require("url");
|
|
1530
|
+
var import_ws3 = __toESM(require("ws"), 1);
|
|
1531
|
+
var DEFAULT_SCOPES = ["operator.read", "operator.write"];
|
|
1532
|
+
function createGatewayClient(config) {
|
|
1533
|
+
const resolved = resolveGatewayConfig(config);
|
|
1534
|
+
const transport = createTransport(resolved);
|
|
1535
|
+
const subscriptions = /* @__PURE__ */ new Map();
|
|
1536
|
+
const ensureSubscribed = async (sessionId) => {
|
|
1537
|
+
await transport.request("sessions.subscribe", {});
|
|
1538
|
+
await transport.request("sessions.messages.subscribe", { key: sessionId });
|
|
1539
|
+
};
|
|
1540
|
+
transport.onEvent((frame) => {
|
|
1541
|
+
const sessionId = extractSessionKey(frame.payload);
|
|
1542
|
+
if (!sessionId) {
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
const subscription = subscriptions.get(sessionId);
|
|
1546
|
+
if (!subscription) {
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
const events = mapGatewayFrameToEvents(frame, subscription.lastSeq);
|
|
1550
|
+
for (const event of events) {
|
|
1551
|
+
subscription.lastSeq = Math.max(subscription.lastSeq, event.seq);
|
|
1552
|
+
for (const handler of subscription.handlers) {
|
|
1553
|
+
handler.onEvent(event);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
});
|
|
1557
|
+
transport.onDisconnect((error) => {
|
|
1558
|
+
for (const subscription of subscriptions.values()) {
|
|
1559
|
+
for (const handler of subscription.handlers) {
|
|
1560
|
+
handler.onDisconnect(error);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
});
|
|
1564
|
+
return {
|
|
1565
|
+
async listSessions(limit = 50) {
|
|
1566
|
+
const payload = await transport.request("sessions.list", {
|
|
1567
|
+
limit,
|
|
1568
|
+
includeGlobal: true,
|
|
1569
|
+
includeUnknown: true,
|
|
1570
|
+
includeDerivedTitles: true,
|
|
1571
|
+
includeLastMessage: true
|
|
1572
|
+
});
|
|
1573
|
+
return extractSessions(payload);
|
|
1574
|
+
},
|
|
1575
|
+
async createSession(title) {
|
|
1576
|
+
const payload = await transport.request("sessions.create", {
|
|
1577
|
+
label: title,
|
|
1578
|
+
agentId: "main"
|
|
1579
|
+
});
|
|
1580
|
+
return {
|
|
1581
|
+
...normalizeSession(extractCreatedSession(payload), title),
|
|
1582
|
+
title
|
|
1583
|
+
};
|
|
1584
|
+
},
|
|
1585
|
+
async getSession(sessionId) {
|
|
1586
|
+
const sessions = await this.listSessions(200);
|
|
1587
|
+
const match = sessions.find((session) => session.id === sessionId);
|
|
1588
|
+
return match ?? fallbackSession(sessionId);
|
|
1589
|
+
},
|
|
1590
|
+
async getSessionMessages(sessionId, limit = 200) {
|
|
1591
|
+
const payload = await transport.request("chat.history", {
|
|
1592
|
+
sessionKey: sessionId,
|
|
1593
|
+
limit
|
|
1594
|
+
});
|
|
1595
|
+
return extractMessages(sessionId, payload);
|
|
1596
|
+
},
|
|
1597
|
+
async sendMessage(sessionId, content) {
|
|
1598
|
+
await transport.request("chat.send", {
|
|
1599
|
+
sessionKey: sessionId,
|
|
1600
|
+
message: content,
|
|
1601
|
+
deliver: false,
|
|
1602
|
+
idempotencyKey: (0, import_node_crypto3.randomUUID)()
|
|
1603
|
+
});
|
|
1604
|
+
},
|
|
1605
|
+
async controlSession(sessionId, action, title) {
|
|
1606
|
+
if (action === "stop") {
|
|
1607
|
+
await transport.request("chat.abort", {
|
|
1608
|
+
sessionKey: sessionId
|
|
1609
|
+
});
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
if (action === "rename") {
|
|
1613
|
+
await transport.request("sessions.patch", {
|
|
1614
|
+
key: sessionId,
|
|
1615
|
+
label: title ?? "Renamed session"
|
|
1616
|
+
});
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
if (action === "archive") {
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
if (action === "retry") {
|
|
1623
|
+
const messages = await this.getSessionMessages(sessionId, 50);
|
|
1624
|
+
const lastUserMessage = [...messages].reverse().find((message) => message.role === "user");
|
|
1625
|
+
if (!lastUserMessage?.content.trim()) {
|
|
1626
|
+
throw new Error("cannot retry a session without a previous user message");
|
|
1627
|
+
}
|
|
1628
|
+
await this.sendMessage(sessionId, lastUserMessage.content);
|
|
1629
|
+
}
|
|
1630
|
+
},
|
|
1631
|
+
async listEvents(sessionId, afterSeq = 0) {
|
|
1632
|
+
const history = await transport.request("chat.history", {
|
|
1633
|
+
sessionKey: sessionId,
|
|
1634
|
+
limit: 200
|
|
1635
|
+
});
|
|
1636
|
+
return synthesizeEventsFromHistory(sessionId, history, afterSeq);
|
|
1637
|
+
},
|
|
1638
|
+
subscribe(sessionId, afterSeq, handlers) {
|
|
1639
|
+
const subscription = subscriptions.get(sessionId) ?? {
|
|
1640
|
+
handlers: /* @__PURE__ */ new Set(),
|
|
1641
|
+
lastSeq: afterSeq
|
|
1642
|
+
};
|
|
1643
|
+
subscription.lastSeq = Math.max(subscription.lastSeq, afterSeq);
|
|
1644
|
+
subscription.handlers.add(handlers);
|
|
1645
|
+
subscriptions.set(sessionId, subscription);
|
|
1646
|
+
void ensureSubscribed(sessionId).catch((error) => {
|
|
1647
|
+
handlers.onDisconnect(error instanceof Error ? error : new Error(String(error)));
|
|
1648
|
+
});
|
|
1649
|
+
return () => {
|
|
1650
|
+
const existing = subscriptions.get(sessionId);
|
|
1651
|
+
if (!existing) {
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
existing.handlers.delete(handlers);
|
|
1655
|
+
if (existing.handlers.size === 0) {
|
|
1656
|
+
subscriptions.delete(sessionId);
|
|
1657
|
+
void transport.request("sessions.messages.unsubscribe", { key: sessionId }, { timeoutMs: 2e3 }).catch(() => {
|
|
1658
|
+
});
|
|
1659
|
+
}
|
|
1660
|
+
};
|
|
1661
|
+
},
|
|
1662
|
+
async stop() {
|
|
1663
|
+
subscriptions.clear();
|
|
1664
|
+
await transport.stop();
|
|
1665
|
+
}
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
function resolveGatewayConfig(config) {
|
|
1669
|
+
return {
|
|
1670
|
+
baseUrl: normalizeGatewayUrl(String(config.baseUrl ?? "")),
|
|
1671
|
+
botId: String(config.botId ?? ""),
|
|
1672
|
+
secret: String(config.secret ?? ""),
|
|
1673
|
+
requestTimeoutMs: Number(config.requestTimeoutMs ?? 15e3),
|
|
1674
|
+
streamReconnectMs: Number(config.streamReconnectMs ?? 2e3),
|
|
1675
|
+
runtimeRoot: typeof config.runtimeRoot === "string" ? config.runtimeRoot : void 0
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
function createTransport(config) {
|
|
1679
|
+
const officialModulePath = resolveOfficialGatewayClientModule(config.runtimeRoot);
|
|
1680
|
+
if (officialModulePath) {
|
|
1681
|
+
return createOfficialTransport(config, officialModulePath);
|
|
1682
|
+
}
|
|
1683
|
+
return new RpcSocketClient(config);
|
|
1684
|
+
}
|
|
1685
|
+
function resolveOfficialGatewayClientModule(runtimeRoot) {
|
|
1686
|
+
const candidates = [
|
|
1687
|
+
runtimeRoot ? (0, import_node_path5.join)(runtimeRoot, "node_modules", "openclaw", "dist") : null,
|
|
1688
|
+
runtimeRoot ? (0, import_node_path5.resolve)(runtimeRoot, "..", "..", "..", "node_modules", "openclaw", "dist") : null,
|
|
1689
|
+
process.env.HOME ? (0, import_node_path5.join)(process.env.HOME, "Library", "Application Support", "QClaw", "openclaw", "node_modules", "openclaw", "dist") : null
|
|
1690
|
+
].filter((value) => Boolean(value));
|
|
1691
|
+
for (const candidate of candidates) {
|
|
1692
|
+
if (!(0, import_node_fs3.existsSync)(candidate)) {
|
|
1693
|
+
continue;
|
|
1694
|
+
}
|
|
1695
|
+
const file = (0, import_node_fs3.readdirSync)(candidate).find((entry) => /^client-.*\.js$/.test(entry));
|
|
1696
|
+
if (file) {
|
|
1697
|
+
return (0, import_node_path5.join)(candidate, file);
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
return null;
|
|
1701
|
+
}
|
|
1702
|
+
function createOfficialTransport(config, modulePath) {
|
|
1703
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1704
|
+
const disconnectListeners = /* @__PURE__ */ new Set();
|
|
1705
|
+
let clientPromise = null;
|
|
1706
|
+
let stopped = false;
|
|
1707
|
+
const ensureClient = async () => {
|
|
1708
|
+
if (clientPromise) {
|
|
1709
|
+
return await clientPromise;
|
|
1710
|
+
}
|
|
1711
|
+
clientPromise = (async () => {
|
|
1712
|
+
const module2 = await import((0, import_node_url.pathToFileURL)(modulePath).href);
|
|
1713
|
+
const GatewayClientCtor = resolveGatewayClientCtor(module2);
|
|
1714
|
+
if (!GatewayClientCtor) {
|
|
1715
|
+
throw new Error(`could not locate GatewayClient export in ${modulePath}`);
|
|
1716
|
+
}
|
|
1717
|
+
return await new Promise((resolve4, reject) => {
|
|
1718
|
+
const client = new GatewayClientCtor({
|
|
1719
|
+
url: config.baseUrl,
|
|
1720
|
+
token: config.secret,
|
|
1721
|
+
clientName: "cli",
|
|
1722
|
+
clientDisplayName: "Claw Control Center",
|
|
1723
|
+
clientVersion: "claw-control-center",
|
|
1724
|
+
mode: "cli",
|
|
1725
|
+
scopes: DEFAULT_SCOPES,
|
|
1726
|
+
requestTimeoutMs: config.requestTimeoutMs,
|
|
1727
|
+
onHelloOk: () => resolve4(client),
|
|
1728
|
+
onEvent: (event) => {
|
|
1729
|
+
listeners.forEach(
|
|
1730
|
+
(listener) => listener({
|
|
1731
|
+
type: "event",
|
|
1732
|
+
event: event.event,
|
|
1733
|
+
seq: event.seq,
|
|
1734
|
+
payload: event.payload
|
|
1735
|
+
})
|
|
1736
|
+
);
|
|
1737
|
+
},
|
|
1738
|
+
onConnectError: (error) => {
|
|
1739
|
+
clientPromise = null;
|
|
1740
|
+
reject(error);
|
|
1741
|
+
},
|
|
1742
|
+
onClose: (_code, reason, error) => {
|
|
1743
|
+
clientPromise = null;
|
|
1744
|
+
if (stopped) {
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
const disconnectError = error instanceof Error ? error : reason ? new Error(reason) : new Error("gateway client closed");
|
|
1748
|
+
disconnectListeners.forEach((listener) => listener(disconnectError));
|
|
1749
|
+
}
|
|
1750
|
+
});
|
|
1751
|
+
client.start();
|
|
1752
|
+
});
|
|
1753
|
+
})();
|
|
1754
|
+
return await clientPromise;
|
|
1755
|
+
};
|
|
1756
|
+
return {
|
|
1757
|
+
async request(method, params) {
|
|
1758
|
+
const client = await ensureClient();
|
|
1759
|
+
return await client.request(method, params);
|
|
1760
|
+
},
|
|
1761
|
+
onEvent(listener) {
|
|
1762
|
+
listeners.add(listener);
|
|
1763
|
+
},
|
|
1764
|
+
onDisconnect(listener) {
|
|
1765
|
+
disconnectListeners.add(listener);
|
|
1766
|
+
},
|
|
1767
|
+
async stop() {
|
|
1768
|
+
stopped = true;
|
|
1769
|
+
if (!clientPromise) {
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
const client = await clientPromise.catch(() => null);
|
|
1773
|
+
clientPromise = null;
|
|
1774
|
+
if (client && typeof client.stopAndWait === "function") {
|
|
1775
|
+
await client.stopAndWait({ timeoutMs: 1e3 }).catch(() => client.stop());
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
if (client && typeof client.stop === "function") {
|
|
1779
|
+
client.stop();
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
};
|
|
1783
|
+
}
|
|
1784
|
+
function resolveGatewayClientCtor(module2) {
|
|
1785
|
+
const directCandidates = [module2.GatewayClient, module2.default, module2.t];
|
|
1786
|
+
for (const candidate of directCandidates) {
|
|
1787
|
+
if (typeof candidate === "function") {
|
|
1788
|
+
return candidate;
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
for (const candidate of Object.values(module2)) {
|
|
1792
|
+
if (typeof candidate === "function") {
|
|
1793
|
+
return candidate;
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
return null;
|
|
1797
|
+
}
|
|
1798
|
+
var RpcSocketClient = class {
|
|
1799
|
+
constructor(config) {
|
|
1800
|
+
this.config = config;
|
|
1801
|
+
}
|
|
1802
|
+
config;
|
|
1803
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1804
|
+
disconnectListeners = /* @__PURE__ */ new Set();
|
|
1805
|
+
pending = /* @__PURE__ */ new Map();
|
|
1806
|
+
socket = null;
|
|
1807
|
+
connectPromise = null;
|
|
1808
|
+
ready = false;
|
|
1809
|
+
challengeNonce = null;
|
|
1810
|
+
stopped = false;
|
|
1811
|
+
onEvent(listener) {
|
|
1812
|
+
this.listeners.add(listener);
|
|
1813
|
+
}
|
|
1814
|
+
onDisconnect(listener) {
|
|
1815
|
+
this.disconnectListeners.add(listener);
|
|
1816
|
+
}
|
|
1817
|
+
async request(method, params, options) {
|
|
1818
|
+
await this.ensureConnected();
|
|
1819
|
+
if (!this.socket || this.socket.readyState !== import_ws3.default.OPEN) {
|
|
1820
|
+
throw new Error("gateway websocket is not connected");
|
|
1821
|
+
}
|
|
1822
|
+
const id = (0, import_node_crypto3.randomUUID)();
|
|
1823
|
+
const frame = {
|
|
1824
|
+
type: "req",
|
|
1825
|
+
id,
|
|
1826
|
+
method,
|
|
1827
|
+
params
|
|
1828
|
+
};
|
|
1829
|
+
const timeoutMs = options?.timeoutMs === null ? null : Math.max(1, Math.floor(options?.timeoutMs ?? this.config.requestTimeoutMs));
|
|
1830
|
+
const response = new Promise((resolve4, reject) => {
|
|
1831
|
+
const timeout = timeoutMs === null ? null : setTimeout(() => {
|
|
1832
|
+
this.pending.delete(id);
|
|
1833
|
+
reject(new Error(`gateway request timeout for ${method}`));
|
|
1834
|
+
}, timeoutMs);
|
|
1835
|
+
this.pending.set(id, { resolve: resolve4, reject, timeout });
|
|
1836
|
+
});
|
|
1837
|
+
this.socket.send(JSON.stringify(frame));
|
|
1838
|
+
return await response;
|
|
1839
|
+
}
|
|
1840
|
+
async stop() {
|
|
1841
|
+
this.stopped = true;
|
|
1842
|
+
const socket = this.socket;
|
|
1843
|
+
this.socket = null;
|
|
1844
|
+
this.ready = false;
|
|
1845
|
+
this.challengeNonce = null;
|
|
1846
|
+
this.connectPromise = null;
|
|
1847
|
+
if (!socket) {
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1850
|
+
await new Promise((resolve4) => {
|
|
1851
|
+
socket.once("close", () => resolve4());
|
|
1852
|
+
socket.close();
|
|
1853
|
+
});
|
|
1854
|
+
}
|
|
1855
|
+
async ensureConnected() {
|
|
1856
|
+
if (this.ready && this.socket?.readyState === import_ws3.default.OPEN) {
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
if (this.connectPromise) {
|
|
1860
|
+
await this.connectPromise;
|
|
1861
|
+
return;
|
|
1862
|
+
}
|
|
1863
|
+
this.connectPromise = this.connect();
|
|
1864
|
+
try {
|
|
1865
|
+
await this.connectPromise;
|
|
1866
|
+
} finally {
|
|
1867
|
+
this.connectPromise = null;
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
async connect() {
|
|
1871
|
+
if (this.stopped) {
|
|
1872
|
+
throw new Error("gateway client stopped");
|
|
1873
|
+
}
|
|
1874
|
+
await new Promise((resolve4, reject) => {
|
|
1875
|
+
const socket = new import_ws3.default(this.config.baseUrl);
|
|
1876
|
+
this.socket = socket;
|
|
1877
|
+
this.ready = false;
|
|
1878
|
+
this.challengeNonce = null;
|
|
1879
|
+
let settled = false;
|
|
1880
|
+
const finishError = (error) => {
|
|
1881
|
+
if (settled) {
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
settled = true;
|
|
1885
|
+
if (this.socket === socket) {
|
|
1886
|
+
this.socket = null;
|
|
1887
|
+
}
|
|
1888
|
+
this.ready = false;
|
|
1889
|
+
this.challengeNonce = null;
|
|
1890
|
+
reject(error);
|
|
1891
|
+
};
|
|
1892
|
+
const finishReady = () => {
|
|
1893
|
+
if (settled) {
|
|
1894
|
+
return;
|
|
1895
|
+
}
|
|
1896
|
+
settled = true;
|
|
1897
|
+
resolve4();
|
|
1898
|
+
};
|
|
1899
|
+
socket.on("open", () => {
|
|
1900
|
+
});
|
|
1901
|
+
socket.on("message", (raw) => {
|
|
1902
|
+
try {
|
|
1903
|
+
const frame = JSON.parse(raw.toString());
|
|
1904
|
+
this.handleFrame(frame, finishReady, finishError);
|
|
1905
|
+
} catch (error) {
|
|
1906
|
+
finishError(error instanceof Error ? error : new Error(String(error)));
|
|
1907
|
+
}
|
|
1908
|
+
});
|
|
1909
|
+
socket.on("error", (error) => {
|
|
1910
|
+
finishError(error instanceof Error ? error : new Error(String(error)));
|
|
1911
|
+
});
|
|
1912
|
+
socket.on("close", (_code, reason) => {
|
|
1913
|
+
const error = reason.length > 0 ? new Error(reason.toString()) : void 0;
|
|
1914
|
+
this.flushPending(error ?? new Error("gateway websocket closed"));
|
|
1915
|
+
this.ready = false;
|
|
1916
|
+
this.challengeNonce = null;
|
|
1917
|
+
if (this.socket === socket) {
|
|
1918
|
+
this.socket = null;
|
|
1919
|
+
}
|
|
1920
|
+
if (!settled) {
|
|
1921
|
+
finishError(error ?? new Error("gateway websocket closed before handshake completed"));
|
|
1922
|
+
}
|
|
1923
|
+
for (const listener of this.disconnectListeners) {
|
|
1924
|
+
listener(error);
|
|
1925
|
+
}
|
|
1926
|
+
});
|
|
1927
|
+
});
|
|
1928
|
+
}
|
|
1929
|
+
handleFrame(frame, markReady, rejectConnect) {
|
|
1930
|
+
if (frame.type === "event") {
|
|
1931
|
+
if (frame.event === "connect.challenge") {
|
|
1932
|
+
const nonce = typeof frame.payload?.nonce === "string" ? frame.payload.nonce : null;
|
|
1933
|
+
if (!nonce) {
|
|
1934
|
+
rejectConnect(new Error("gateway connect challenge missing nonce"));
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
this.challengeNonce = nonce;
|
|
1938
|
+
this.sendConnect();
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1941
|
+
for (const listener of this.listeners) {
|
|
1942
|
+
listener(frame);
|
|
1943
|
+
}
|
|
1944
|
+
return;
|
|
1945
|
+
}
|
|
1946
|
+
const pending = this.pending.get(frame.id);
|
|
1947
|
+
if (!pending) {
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
this.pending.delete(frame.id);
|
|
1951
|
+
if (pending.timeout) {
|
|
1952
|
+
clearTimeout(pending.timeout);
|
|
1953
|
+
}
|
|
1954
|
+
if (!frame.ok) {
|
|
1955
|
+
const message = frame.error?.message ?? "gateway request failed";
|
|
1956
|
+
const details = frame.error?.details ? `: ${JSON.stringify(frame.error.details)}` : "";
|
|
1957
|
+
const error = new Error(`${message}${details}`);
|
|
1958
|
+
if (frame.id === "connect-handshake") {
|
|
1959
|
+
rejectConnect(error);
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
pending.reject(error);
|
|
1963
|
+
return;
|
|
1964
|
+
}
|
|
1965
|
+
if (frame.id === "connect-handshake") {
|
|
1966
|
+
this.ready = true;
|
|
1967
|
+
pending.resolve(frame.payload);
|
|
1968
|
+
markReady();
|
|
1969
|
+
return;
|
|
1970
|
+
}
|
|
1971
|
+
pending.resolve(frame.payload);
|
|
1972
|
+
}
|
|
1973
|
+
sendConnect() {
|
|
1974
|
+
if (!this.socket || this.socket.readyState !== import_ws3.default.OPEN || !this.challengeNonce) {
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
const frame = {
|
|
1978
|
+
type: "req",
|
|
1979
|
+
id: "connect-handshake",
|
|
1980
|
+
method: "connect",
|
|
1981
|
+
params: {
|
|
1982
|
+
minProtocol: 3,
|
|
1983
|
+
maxProtocol: 3,
|
|
1984
|
+
client: {
|
|
1985
|
+
id: "cli",
|
|
1986
|
+
displayName: "Claw Control Center",
|
|
1987
|
+
version: "claw-control-center",
|
|
1988
|
+
platform: process.platform,
|
|
1989
|
+
mode: "cli"
|
|
1990
|
+
},
|
|
1991
|
+
role: "operator",
|
|
1992
|
+
scopes: DEFAULT_SCOPES,
|
|
1993
|
+
auth: {
|
|
1994
|
+
token: this.config.secret
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
};
|
|
1998
|
+
const timeout = setTimeout(() => {
|
|
1999
|
+
this.pending.delete("connect-handshake");
|
|
2000
|
+
}, this.config.requestTimeoutMs);
|
|
2001
|
+
this.pending.set("connect-handshake", {
|
|
2002
|
+
resolve: () => {
|
|
2003
|
+
},
|
|
2004
|
+
reject: () => {
|
|
2005
|
+
},
|
|
2006
|
+
timeout
|
|
2007
|
+
});
|
|
2008
|
+
this.socket.send(JSON.stringify(frame));
|
|
2009
|
+
}
|
|
2010
|
+
flushPending(error) {
|
|
2011
|
+
for (const [id, pending] of this.pending.entries()) {
|
|
2012
|
+
if (pending.timeout) {
|
|
2013
|
+
clearTimeout(pending.timeout);
|
|
2014
|
+
}
|
|
2015
|
+
this.pending.delete(id);
|
|
2016
|
+
pending.reject(error);
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
};
|
|
2020
|
+
function normalizeGatewayUrl(raw) {
|
|
2021
|
+
const value = raw.trim();
|
|
2022
|
+
if (!value) {
|
|
2023
|
+
return "";
|
|
2024
|
+
}
|
|
2025
|
+
const parsed = new URL(value.includes("://") ? value : `ws://${value}`);
|
|
2026
|
+
if (parsed.protocol === "http:") {
|
|
2027
|
+
parsed.protocol = "ws:";
|
|
2028
|
+
}
|
|
2029
|
+
if (parsed.protocol === "https:") {
|
|
2030
|
+
parsed.protocol = "wss:";
|
|
2031
|
+
}
|
|
2032
|
+
if (!parsed.pathname || parsed.pathname === "/") {
|
|
2033
|
+
parsed.pathname = "/";
|
|
2034
|
+
}
|
|
2035
|
+
return parsed.toString().replace(/\/$/, "");
|
|
2036
|
+
}
|
|
2037
|
+
function extractSessions(payload) {
|
|
2038
|
+
const sessions = Array.isArray(payload?.sessions) ? payload.sessions ?? [] : [];
|
|
2039
|
+
return sessions.map((session) => normalizeSession(session));
|
|
2040
|
+
}
|
|
2041
|
+
function extractCreatedSession(payload) {
|
|
2042
|
+
return typeof payload === "object" && payload ? payload : {};
|
|
2043
|
+
}
|
|
2044
|
+
function extractMessages(sessionId, payload) {
|
|
2045
|
+
const rawMessages = Array.isArray(payload?.messages) ? payload.messages ?? [] : [];
|
|
2046
|
+
return rawMessages.map((message) => normalizeMessage(sessionId, message)).filter((message) => message !== null);
|
|
2047
|
+
}
|
|
2048
|
+
function normalizeSession(payload, fallbackTitle = "Untitled session") {
|
|
2049
|
+
const entry = payload && typeof payload === "object" ? payload : {};
|
|
2050
|
+
const key = String(entry.key ?? entry.sessionKey ?? entry.sessionId ?? (0, import_node_crypto3.randomUUID)());
|
|
2051
|
+
const updatedAtMs = Number(entry.updatedAt ?? entry.startedAt ?? Date.now());
|
|
2052
|
+
const createdAt = toIsoString(entry.startedAt ?? entry.updatedAt ?? Date.now());
|
|
2053
|
+
const updatedAt = toIsoString(updatedAtMs);
|
|
2054
|
+
const title = readableSessionTitle(entry, fallbackTitle);
|
|
2055
|
+
return {
|
|
2056
|
+
id: key,
|
|
2057
|
+
title,
|
|
2058
|
+
status: normalizeStatus(entry.status),
|
|
2059
|
+
hostKind: "qclaw",
|
|
2060
|
+
runnerCommand: "openclaw-gateway",
|
|
2061
|
+
createdAt,
|
|
2062
|
+
updatedAt,
|
|
2063
|
+
lastEventSeq: Number(entry.messageSeq ?? 0)
|
|
2064
|
+
};
|
|
2065
|
+
}
|
|
2066
|
+
function readableSessionTitle(entry, fallbackTitle) {
|
|
2067
|
+
const candidates = [entry.label, entry.displayName, entry.key];
|
|
2068
|
+
for (const candidate of candidates) {
|
|
2069
|
+
if (typeof candidate === "string" && candidate.trim()) {
|
|
2070
|
+
return candidate;
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
return fallbackTitle;
|
|
2074
|
+
}
|
|
2075
|
+
function normalizeMessage(sessionId, payload) {
|
|
2076
|
+
if (!payload || typeof payload !== "object") {
|
|
2077
|
+
return null;
|
|
2078
|
+
}
|
|
2079
|
+
const message = payload;
|
|
2080
|
+
const role = typeof message.role === "string" ? message.role : "assistant";
|
|
2081
|
+
const content = extractTextContent(message.content) ?? (typeof message.content === "string" ? message.content : "");
|
|
2082
|
+
if (!content.trim()) {
|
|
2083
|
+
return null;
|
|
2084
|
+
}
|
|
2085
|
+
const rawSeq = message.__openclaw && typeof message.__openclaw === "object" ? message.__openclaw.seq : void 0;
|
|
2086
|
+
const seq = typeof rawSeq === "number" ? rawSeq : 0;
|
|
2087
|
+
return {
|
|
2088
|
+
id: `${sessionId}:${role}:${seq}`,
|
|
2089
|
+
sessionId,
|
|
2090
|
+
role,
|
|
2091
|
+
content,
|
|
2092
|
+
createdAt: toIsoString(message.timestamp ?? Date.now())
|
|
2093
|
+
};
|
|
2094
|
+
}
|
|
2095
|
+
function synthesizeEventsFromHistory(sessionId, payload, afterSeq) {
|
|
2096
|
+
const rawMessages = Array.isArray(payload?.messages) ? payload.messages ?? [] : [];
|
|
2097
|
+
return rawMessages.flatMap((message) => {
|
|
2098
|
+
if (!message || typeof message !== "object") {
|
|
2099
|
+
return [];
|
|
2100
|
+
}
|
|
2101
|
+
const entry = message;
|
|
2102
|
+
const role = typeof entry.role === "string" ? entry.role : "";
|
|
2103
|
+
if (role !== "assistant") {
|
|
2104
|
+
return [];
|
|
2105
|
+
}
|
|
2106
|
+
const rawMeta = entry.__openclaw && typeof entry.__openclaw === "object" ? entry.__openclaw : {};
|
|
2107
|
+
const seq = typeof rawMeta.seq === "number" ? rawMeta.seq : 0;
|
|
2108
|
+
if (seq <= afterSeq) {
|
|
2109
|
+
return [];
|
|
2110
|
+
}
|
|
2111
|
+
return [
|
|
2112
|
+
{
|
|
2113
|
+
id: `${sessionId}:history:${seq}`,
|
|
2114
|
+
sessionId,
|
|
2115
|
+
seq,
|
|
2116
|
+
kind: "assistant.message",
|
|
2117
|
+
payload: {
|
|
2118
|
+
content: extractTextContent(entry.content) ?? ""
|
|
2119
|
+
},
|
|
2120
|
+
createdAt: toIsoString(entry.timestamp ?? Date.now())
|
|
2121
|
+
}
|
|
2122
|
+
];
|
|
2123
|
+
}).sort((left, right) => left.seq - right.seq);
|
|
2124
|
+
}
|
|
2125
|
+
function mapGatewayFrameToEvents(frame, lastSeq) {
|
|
2126
|
+
const sessionId = extractSessionKey(frame.payload);
|
|
2127
|
+
const payload = toRecord2(frame.payload);
|
|
2128
|
+
const message = toRecord2(payload.message);
|
|
2129
|
+
const messageMeta = toRecord2(message.__openclaw);
|
|
2130
|
+
const session = toRecord2(payload.session);
|
|
2131
|
+
if (!sessionId) {
|
|
2132
|
+
return [];
|
|
2133
|
+
}
|
|
2134
|
+
if (frame.event === "chat") {
|
|
2135
|
+
const state = String(payload.state ?? "delta");
|
|
2136
|
+
const content = extractTextContent(message.content) ?? "";
|
|
2137
|
+
if (!content.trim()) {
|
|
2138
|
+
return [];
|
|
2139
|
+
}
|
|
2140
|
+
return [
|
|
2141
|
+
{
|
|
2142
|
+
id: `${sessionId}:chat:${Number(payload.seq ?? lastSeq + 1)}`,
|
|
2143
|
+
sessionId,
|
|
2144
|
+
seq: Number(payload.seq ?? lastSeq + 1),
|
|
2145
|
+
kind: "assistant.delta",
|
|
2146
|
+
payload: {
|
|
2147
|
+
content,
|
|
2148
|
+
state,
|
|
2149
|
+
runId: payload.runId
|
|
2150
|
+
},
|
|
2151
|
+
createdAt: toIsoString(message.timestamp ?? Date.now())
|
|
2152
|
+
}
|
|
2153
|
+
];
|
|
2154
|
+
}
|
|
2155
|
+
if (frame.event === "session.message") {
|
|
2156
|
+
const role = typeof message.role === "string" ? message.role : "";
|
|
2157
|
+
if (role !== "assistant") {
|
|
2158
|
+
return [];
|
|
2159
|
+
}
|
|
2160
|
+
const content = extractTextContent(message.content) ?? "";
|
|
2161
|
+
if (!content.trim()) {
|
|
2162
|
+
return [];
|
|
2163
|
+
}
|
|
2164
|
+
const seq = Number(payload.messageSeq ?? messageMeta.seq ?? lastSeq + 1);
|
|
2165
|
+
return [
|
|
2166
|
+
{
|
|
2167
|
+
id: `${sessionId}:message:${seq}`,
|
|
2168
|
+
sessionId,
|
|
2169
|
+
seq,
|
|
2170
|
+
kind: "assistant.message",
|
|
2171
|
+
payload: {
|
|
2172
|
+
content,
|
|
2173
|
+
thinking: extractThinkingContent(message.content)
|
|
2174
|
+
},
|
|
2175
|
+
createdAt: toIsoString(message.timestamp ?? Date.now())
|
|
2176
|
+
}
|
|
2177
|
+
];
|
|
2178
|
+
}
|
|
2179
|
+
if (frame.event === "session.tool") {
|
|
2180
|
+
const seq = Number(payload.seq ?? lastSeq + 1);
|
|
2181
|
+
const kind = String(payload.phase ?? "tool");
|
|
2182
|
+
return [
|
|
2183
|
+
{
|
|
2184
|
+
id: `${sessionId}:tool:${seq}`,
|
|
2185
|
+
sessionId,
|
|
2186
|
+
seq,
|
|
2187
|
+
kind: kind === "result" ? "tool.result" : "tool.call",
|
|
2188
|
+
payload: frame.payload ?? {},
|
|
2189
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2190
|
+
}
|
|
2191
|
+
];
|
|
2192
|
+
}
|
|
2193
|
+
if (frame.event === "sessions.changed") {
|
|
2194
|
+
const phase = String(payload.phase ?? "");
|
|
2195
|
+
const seq = lastSeq + 1;
|
|
2196
|
+
if (phase === "start") {
|
|
2197
|
+
return [
|
|
2198
|
+
{
|
|
2199
|
+
id: `${sessionId}:status:${seq}`,
|
|
2200
|
+
sessionId,
|
|
2201
|
+
seq,
|
|
2202
|
+
kind: "run.started",
|
|
2203
|
+
payload,
|
|
2204
|
+
createdAt: toIsoString(payload.startedAt ?? payload.ts ?? Date.now())
|
|
2205
|
+
}
|
|
2206
|
+
];
|
|
2207
|
+
}
|
|
2208
|
+
if (phase === "end") {
|
|
2209
|
+
const status = String(payload.status ?? session.status ?? "");
|
|
2210
|
+
const eventKind = status === "done" ? "run.completed" : status === "aborted" ? "run.interrupted" : "status.update";
|
|
2211
|
+
return [
|
|
2212
|
+
{
|
|
2213
|
+
id: `${sessionId}:status:${seq}`,
|
|
2214
|
+
sessionId,
|
|
2215
|
+
seq,
|
|
2216
|
+
kind: eventKind,
|
|
2217
|
+
payload,
|
|
2218
|
+
createdAt: toIsoString(payload.endedAt ?? payload.ts ?? Date.now())
|
|
2219
|
+
}
|
|
2220
|
+
];
|
|
2221
|
+
}
|
|
2222
|
+
return [
|
|
2223
|
+
{
|
|
2224
|
+
id: `${sessionId}:status:${seq}`,
|
|
2225
|
+
sessionId,
|
|
2226
|
+
seq,
|
|
2227
|
+
kind: "status.update",
|
|
2228
|
+
payload,
|
|
2229
|
+
createdAt: toIsoString(payload.updatedAt ?? payload.ts ?? Date.now())
|
|
2230
|
+
}
|
|
2231
|
+
];
|
|
2232
|
+
}
|
|
2233
|
+
return [];
|
|
2234
|
+
}
|
|
2235
|
+
function extractSessionKey(payload) {
|
|
2236
|
+
const record = toRecord2(payload);
|
|
2237
|
+
const session = toRecord2(record.session);
|
|
2238
|
+
const sessionKey = record.sessionKey ?? session.key;
|
|
2239
|
+
return typeof sessionKey === "string" && sessionKey.trim() ? sessionKey : null;
|
|
2240
|
+
}
|
|
2241
|
+
function extractTextContent(content) {
|
|
2242
|
+
if (typeof content === "string") {
|
|
2243
|
+
return content;
|
|
2244
|
+
}
|
|
2245
|
+
if (!Array.isArray(content)) {
|
|
2246
|
+
return null;
|
|
2247
|
+
}
|
|
2248
|
+
const lines = content.flatMap((item) => {
|
|
2249
|
+
if (!item || typeof item !== "object") {
|
|
2250
|
+
return [];
|
|
2251
|
+
}
|
|
2252
|
+
const record = item;
|
|
2253
|
+
if (record.type === "text" && typeof record.text === "string") {
|
|
2254
|
+
return [record.text];
|
|
2255
|
+
}
|
|
2256
|
+
return [];
|
|
2257
|
+
}).filter((value) => value.trim().length > 0);
|
|
2258
|
+
return lines.length > 0 ? lines.join("\n") : null;
|
|
2259
|
+
}
|
|
2260
|
+
function extractThinkingContent(content) {
|
|
2261
|
+
if (!Array.isArray(content)) {
|
|
2262
|
+
return null;
|
|
2263
|
+
}
|
|
2264
|
+
const lines = content.flatMap((item) => {
|
|
2265
|
+
if (!item || typeof item !== "object") {
|
|
2266
|
+
return [];
|
|
2267
|
+
}
|
|
2268
|
+
const record = item;
|
|
2269
|
+
if (record.type === "thinking" && typeof record.thinking === "string") {
|
|
2270
|
+
return [record.thinking];
|
|
2271
|
+
}
|
|
2272
|
+
return [];
|
|
2273
|
+
}).filter((value) => value.trim().length > 0);
|
|
2274
|
+
return lines.length > 0 ? lines.join("\n") : null;
|
|
2275
|
+
}
|
|
2276
|
+
function normalizeStatus(status) {
|
|
2277
|
+
switch (status) {
|
|
2278
|
+
case "running":
|
|
2279
|
+
return "running";
|
|
2280
|
+
case "done":
|
|
2281
|
+
case "completed":
|
|
2282
|
+
return "completed";
|
|
2283
|
+
case "failed":
|
|
2284
|
+
return "failed";
|
|
2285
|
+
case "aborted":
|
|
2286
|
+
case "interrupted":
|
|
2287
|
+
return "interrupted";
|
|
2288
|
+
case "archived":
|
|
2289
|
+
return "archived";
|
|
2290
|
+
default:
|
|
2291
|
+
return "idle";
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
function toIsoString(value) {
|
|
2295
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
2296
|
+
return new Date(value).toISOString();
|
|
2297
|
+
}
|
|
2298
|
+
if (typeof value === "string" && value.trim()) {
|
|
2299
|
+
const asNumber = Number(value);
|
|
2300
|
+
if (Number.isFinite(asNumber)) {
|
|
2301
|
+
return new Date(asNumber).toISOString();
|
|
2302
|
+
}
|
|
2303
|
+
const date = new Date(value);
|
|
2304
|
+
if (!Number.isNaN(date.getTime())) {
|
|
2305
|
+
return date.toISOString();
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
2309
|
+
}
|
|
2310
|
+
function fallbackSession(sessionId) {
|
|
2311
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2312
|
+
return {
|
|
2313
|
+
id: sessionId,
|
|
2314
|
+
title: sessionId,
|
|
2315
|
+
status: "idle",
|
|
2316
|
+
hostKind: "qclaw",
|
|
2317
|
+
runnerCommand: "openclaw-gateway",
|
|
2318
|
+
createdAt: now,
|
|
2319
|
+
updatedAt: now,
|
|
2320
|
+
lastEventSeq: 0
|
|
2321
|
+
};
|
|
2322
|
+
}
|
|
2323
|
+
function toRecord2(value) {
|
|
2324
|
+
return value && typeof value === "object" ? value : {};
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
// src/install-qclaw.ts
|
|
2328
|
+
var import_promises4 = require("fs/promises");
|
|
2329
|
+
var import_node_fs4 = require("fs");
|
|
2330
|
+
var import_node_os2 = require("os");
|
|
2331
|
+
var import_node_path6 = require("path");
|
|
2332
|
+
var PLUGIN_ID = "claw-control-center";
|
|
2333
|
+
var DEFAULT_QCLAW_HOME = (0, import_node_path6.resolve)((0, import_node_os2.homedir)(), ".qclaw");
|
|
2334
|
+
var DEFAULT_OPENCLAW_HOME = (0, import_node_path6.resolve)((0, import_node_os2.homedir)(), ".openclaw");
|
|
2335
|
+
var DEFAULT_EXTENSIONS_DIR = (0, import_node_path6.resolve)(
|
|
2336
|
+
(0, import_node_os2.homedir)(),
|
|
2337
|
+
"Library/Application Support/QClaw/openclaw/config/extensions"
|
|
2338
|
+
);
|
|
2339
|
+
var DEFAULT_OPENCLAW_EXTENSIONS_DIR = (0, import_node_path6.resolve)(DEFAULT_OPENCLAW_HOME, "extensions");
|
|
2340
|
+
var COPY_ITEMS = ["dist", "openclaw.plugin.json", "package.json", "bin", "web-dist"];
|
|
2341
|
+
var INSTALL_TARGETS = {
|
|
2342
|
+
qclaw: {
|
|
2343
|
+
label: "QClaw",
|
|
2344
|
+
defaultConfigPath: (0, import_node_path6.join)(DEFAULT_QCLAW_HOME, "openclaw.json"),
|
|
2345
|
+
defaultExtensionsDir: DEFAULT_EXTENSIONS_DIR
|
|
2346
|
+
},
|
|
2347
|
+
openclaw: {
|
|
2348
|
+
label: "OpenClaw",
|
|
2349
|
+
defaultConfigPath: (0, import_node_path6.join)(DEFAULT_OPENCLAW_HOME, "openclaw.json"),
|
|
2350
|
+
defaultExtensionsDir: DEFAULT_OPENCLAW_EXTENSIONS_DIR
|
|
2351
|
+
}
|
|
2352
|
+
};
|
|
2353
|
+
async function installIntoQClaw(input) {
|
|
2354
|
+
return installIntoHost(input, "QClaw");
|
|
2355
|
+
}
|
|
2356
|
+
async function installIntoOpenClaw(input) {
|
|
2357
|
+
return installIntoHost(input, "OpenClaw");
|
|
2358
|
+
}
|
|
2359
|
+
async function installIntoHost(input, hostLabel) {
|
|
2360
|
+
await (0, import_promises4.mkdir)(input.extensionsDir, { recursive: true });
|
|
2361
|
+
const destination = (0, import_node_path6.join)(input.extensionsDir, PLUGIN_ID);
|
|
2362
|
+
await (0, import_promises4.mkdir)(destination, { recursive: true });
|
|
2363
|
+
await copyPublishablePackage(input.packageRoot, destination);
|
|
2364
|
+
await (0, import_promises4.mkdir)((0, import_node_path6.dirname)(input.configPath), { recursive: true });
|
|
2365
|
+
const config = await readOpenClawConfig(input.configPath);
|
|
2366
|
+
const inferredGateway = inferGatewaySettings(config);
|
|
2367
|
+
const inferredHub53AI = inferHub53AISettings(config);
|
|
2368
|
+
const gatewayBaseUrl = input.gateway?.trim() || inferredGateway.baseUrl;
|
|
2369
|
+
if (!gatewayBaseUrl) {
|
|
2370
|
+
throw new Error(`missing gateway URL and no local ${hostLabel} gateway could be inferred`);
|
|
2371
|
+
}
|
|
2372
|
+
const secret = input.secret?.trim() || inferredGateway.secret;
|
|
2373
|
+
if (!secret) {
|
|
2374
|
+
throw new Error(`missing gateway secret and no local ${hostLabel} gateway token could be inferred`);
|
|
2375
|
+
}
|
|
2376
|
+
const botId = input.botId?.trim();
|
|
2377
|
+
const hubWsUrl = input.hubWsUrl?.trim() || inferredHub53AI.wsUrl;
|
|
2378
|
+
const hubBotId = input.hubBotId?.trim() || inferredHub53AI.botId;
|
|
2379
|
+
const hubSecret = input.hubSecret?.trim() || inferredHub53AI.secret;
|
|
2380
|
+
const hubConfigured = Boolean(hubWsUrl && hubBotId && hubSecret);
|
|
2381
|
+
const hubInputProvided = Boolean(input.hubWsUrl?.trim() || input.hubBotId?.trim() || input.hubSecret?.trim());
|
|
2382
|
+
const hubEnabled = input.hubEnabled ?? (hubConfigured ? hubInputProvided ? true : inferredHub53AI.enabled : false);
|
|
2383
|
+
if (hubEnabled && !hubConfigured) {
|
|
2384
|
+
throw new Error("hub53ai requires --hub-ws-url, --hub-bot-id, and --hub-secret when enabled");
|
|
2385
|
+
}
|
|
2386
|
+
const consoleHost = input.consoleHost?.trim();
|
|
2387
|
+
const consolePort = normalizePort(input.consolePort);
|
|
2388
|
+
const plugins = ensureObject(config, "plugins");
|
|
2389
|
+
plugins.enabled = true;
|
|
2390
|
+
plugins.allow = dedupeStrings([...Array.isArray(plugins.allow) ? plugins.allow : [], PLUGIN_ID]);
|
|
2391
|
+
const load = ensureObject(plugins, "load");
|
|
2392
|
+
load.paths = dedupeStrings([...Array.isArray(load.paths) ? load.paths : [], input.extensionsDir]);
|
|
2393
|
+
const entries = ensureObject(plugins, "entries");
|
|
2394
|
+
const previousEntry = ensureObject(entries, PLUGIN_ID);
|
|
2395
|
+
const previousConfig = ensureObject(previousEntry, "config");
|
|
2396
|
+
const previousGateway = ensureObject(previousConfig, "gateway");
|
|
2397
|
+
previousGateway.baseUrl = gatewayBaseUrl;
|
|
2398
|
+
previousGateway.secret = secret;
|
|
2399
|
+
if (botId) {
|
|
2400
|
+
previousGateway.botId = botId;
|
|
2401
|
+
}
|
|
2402
|
+
if (hubConfigured || input.hubEnabled !== void 0) {
|
|
2403
|
+
const previousHub = ensureObject(previousConfig, "hub53ai");
|
|
2404
|
+
previousHub.enabled = hubEnabled;
|
|
2405
|
+
if (hubBotId) {
|
|
2406
|
+
previousHub.botId = hubBotId;
|
|
2407
|
+
}
|
|
2408
|
+
if (hubSecret) {
|
|
2409
|
+
previousHub.secret = hubSecret;
|
|
2410
|
+
}
|
|
2411
|
+
if (hubWsUrl) {
|
|
2412
|
+
previousHub.wsUrl = hubWsUrl;
|
|
2413
|
+
}
|
|
2414
|
+
previousHub.accessPolicy = inferredHub53AI.accessPolicy || previousHub.accessPolicy || "open";
|
|
2415
|
+
previousHub.allowFrom = inferredHub53AI.allowFrom ?? previousHub.allowFrom ?? [];
|
|
2416
|
+
previousHub.sendThinkingMessage = inferredHub53AI.sendThinkingMessage ?? previousHub.sendThinkingMessage ?? true;
|
|
2417
|
+
}
|
|
2418
|
+
if (consoleHost || consolePort !== void 0) {
|
|
2419
|
+
const previousConsole = ensureObject(previousConfig, "console");
|
|
2420
|
+
if (consoleHost) {
|
|
2421
|
+
previousConsole.host = consoleHost;
|
|
2422
|
+
}
|
|
2423
|
+
if (consolePort !== void 0) {
|
|
2424
|
+
previousConsole.port = consolePort;
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
previousEntry.enabled = true;
|
|
2428
|
+
await (0, import_promises4.writeFile)(input.configPath, `${JSON.stringify(config, null, 2)}
|
|
2429
|
+
`);
|
|
2430
|
+
return {
|
|
2431
|
+
configPath: input.configPath,
|
|
2432
|
+
extensionsDir: input.extensionsDir,
|
|
2433
|
+
destination,
|
|
2434
|
+
gatewayBaseUrl,
|
|
2435
|
+
hub53aiConfigured: hubConfigured
|
|
2436
|
+
};
|
|
2437
|
+
}
|
|
2438
|
+
async function runInstallCommand(input) {
|
|
2439
|
+
const argv = input.argv ?? process.argv.slice(2);
|
|
2440
|
+
if (argv[0] !== "install") {
|
|
2441
|
+
throw new Error("expected subcommand: install");
|
|
2442
|
+
}
|
|
2443
|
+
const args = parseArgs(argv.slice(1));
|
|
2444
|
+
const target = parseInstallTarget(args.target);
|
|
2445
|
+
if (!target) {
|
|
2446
|
+
throw new Error("expected --target qclaw or --target openclaw");
|
|
2447
|
+
}
|
|
2448
|
+
const targetInfo = INSTALL_TARGETS[target];
|
|
2449
|
+
const configPath = (0, import_node_path6.resolve)(args["config-path"] ?? targetInfo.defaultConfigPath);
|
|
2450
|
+
const extensionsDir = (0, import_node_path6.resolve)(args["extensions-dir"] ?? targetInfo.defaultExtensionsDir);
|
|
2451
|
+
const install = target === "openclaw" ? installIntoOpenClaw : installIntoQClaw;
|
|
2452
|
+
const result = await install({
|
|
2453
|
+
packageRoot: input.packageRoot,
|
|
2454
|
+
extensionsDir,
|
|
2455
|
+
configPath,
|
|
2456
|
+
gateway: args.gateway,
|
|
2457
|
+
botId: args["bot-id"],
|
|
2458
|
+
secret: args.secret,
|
|
2459
|
+
hubWsUrl: args["hub-ws-url"],
|
|
2460
|
+
hubBotId: args["hub-bot-id"],
|
|
2461
|
+
hubSecret: args["hub-secret"],
|
|
2462
|
+
hubEnabled: parseOptionalBoolean(args["hub-enabled"]),
|
|
2463
|
+
consoleHost: args["console-host"],
|
|
2464
|
+
consolePort: args["console-port"] ? Number(args["console-port"]) : void 0
|
|
2465
|
+
});
|
|
2466
|
+
process.stdout.write(
|
|
2467
|
+
[
|
|
2468
|
+
`Installed ${PLUGIN_ID} into ${targetInfo.label}.`,
|
|
2469
|
+
`Extensions: ${result.extensionsDir}`,
|
|
2470
|
+
`Config: ${result.configPath}`,
|
|
2471
|
+
`Gateway: ${result.gatewayBaseUrl}`,
|
|
2472
|
+
`53AIHub: ${result.hub53aiConfigured ? "configured" : "not configured"}`,
|
|
2473
|
+
`Restart ${targetInfo.label} to load the plugin.`
|
|
2474
|
+
].join("\n") + "\n"
|
|
2475
|
+
);
|
|
2476
|
+
}
|
|
2477
|
+
function parseArgs(argv) {
|
|
2478
|
+
const parsed = {};
|
|
2479
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
2480
|
+
const entry = argv[index];
|
|
2481
|
+
if (!entry.startsWith("--")) {
|
|
2482
|
+
continue;
|
|
2483
|
+
}
|
|
2484
|
+
const next = argv[index + 1];
|
|
2485
|
+
if (next && !next.startsWith("--")) {
|
|
2486
|
+
parsed[entry.slice(2)] = next;
|
|
2487
|
+
index += 1;
|
|
2488
|
+
} else {
|
|
2489
|
+
parsed[entry.slice(2)] = "true";
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
return parsed;
|
|
2493
|
+
}
|
|
2494
|
+
function parseInstallTarget(target) {
|
|
2495
|
+
if (target === "qclaw" || target === "openclaw") {
|
|
2496
|
+
return target;
|
|
2497
|
+
}
|
|
2498
|
+
return void 0;
|
|
2499
|
+
}
|
|
2500
|
+
async function copyPublishablePackage(packageRoot, destination) {
|
|
2501
|
+
if (!(0, import_node_fs4.existsSync)(packageRoot)) {
|
|
2502
|
+
throw new Error(`package root does not exist: ${packageRoot}`);
|
|
2503
|
+
}
|
|
2504
|
+
for (const relativePath of COPY_ITEMS) {
|
|
2505
|
+
const source = (0, import_node_path6.join)(packageRoot, relativePath);
|
|
2506
|
+
if (!(0, import_node_fs4.existsSync)(source)) {
|
|
2507
|
+
continue;
|
|
2508
|
+
}
|
|
2509
|
+
const target = (0, import_node_path6.join)(destination, relativePath);
|
|
2510
|
+
await (0, import_promises4.rm)(target, { recursive: true, force: true });
|
|
2511
|
+
await (0, import_promises4.cp)(source, target, { recursive: true, force: true });
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
async function readOpenClawConfig(configPath) {
|
|
2515
|
+
if (!(0, import_node_fs4.existsSync)(configPath)) {
|
|
2516
|
+
return {};
|
|
2517
|
+
}
|
|
2518
|
+
return JSON.parse(await (0, import_promises4.readFile)(configPath, "utf8"));
|
|
2519
|
+
}
|
|
2520
|
+
function inferGatewaySettings(config) {
|
|
2521
|
+
const gateway = config.gateway ?? {};
|
|
2522
|
+
const host = typeof gateway.host === "string" && gateway.host.trim() ? gateway.host.trim() : "127.0.0.1";
|
|
2523
|
+
const port = Number(gateway.port ?? 28789);
|
|
2524
|
+
const baseUrl = Number.isFinite(port) && port > 0 ? `ws://${host}:${port}` : "";
|
|
2525
|
+
const auth = gateway.auth ?? {};
|
|
2526
|
+
const mode = typeof auth.mode === "string" ? auth.mode : "token";
|
|
2527
|
+
const secret = mode === "password" ? typeof auth.password === "string" ? auth.password : "" : typeof auth.token === "string" ? auth.token : "";
|
|
2528
|
+
return { baseUrl, secret };
|
|
2529
|
+
}
|
|
2530
|
+
function inferHub53AISettings(config) {
|
|
2531
|
+
const legacy = config.channels?.["53aihub"];
|
|
2532
|
+
if (!legacy) {
|
|
2533
|
+
return {
|
|
2534
|
+
enabled: false,
|
|
2535
|
+
botId: "",
|
|
2536
|
+
secret: "",
|
|
2537
|
+
wsUrl: ""
|
|
2538
|
+
};
|
|
2539
|
+
}
|
|
2540
|
+
return {
|
|
2541
|
+
enabled: legacy.enabled !== false,
|
|
2542
|
+
botId: typeof legacy.botId === "string" ? legacy.botId : "",
|
|
2543
|
+
secret: typeof legacy.secret === "string" ? legacy.secret : typeof legacy.token === "string" ? legacy.token : "",
|
|
2544
|
+
wsUrl: typeof legacy.WSUrl === "string" ? legacy.WSUrl : typeof legacy.websocketUrl === "string" ? legacy.websocketUrl : "",
|
|
2545
|
+
accessPolicy: typeof legacy.accessPolicy === "string" ? legacy.accessPolicy : void 0,
|
|
2546
|
+
allowFrom: Array.isArray(legacy.allowFrom) ? legacy.allowFrom.map((entry) => String(entry)).filter(Boolean) : void 0,
|
|
2547
|
+
sendThinkingMessage: typeof legacy.sendThinkingMessage === "boolean" ? legacy.sendThinkingMessage : void 0
|
|
2548
|
+
};
|
|
2549
|
+
}
|
|
2550
|
+
function parseOptionalBoolean(value) {
|
|
2551
|
+
if (value === void 0) {
|
|
2552
|
+
return void 0;
|
|
2553
|
+
}
|
|
2554
|
+
if (value === "" || value === "true" || value === "1" || value === "yes") {
|
|
2555
|
+
return true;
|
|
2556
|
+
}
|
|
2557
|
+
if (value === "false" || value === "0" || value === "no") {
|
|
2558
|
+
return false;
|
|
2559
|
+
}
|
|
2560
|
+
throw new Error(`invalid boolean value: ${value}`);
|
|
2561
|
+
}
|
|
2562
|
+
function normalizePort(port) {
|
|
2563
|
+
if (port === void 0) {
|
|
2564
|
+
return void 0;
|
|
2565
|
+
}
|
|
2566
|
+
if (!Number.isFinite(port) || port <= 0) {
|
|
2567
|
+
throw new Error("console port must be a positive number");
|
|
2568
|
+
}
|
|
2569
|
+
return Math.floor(port);
|
|
2570
|
+
}
|
|
2571
|
+
function ensureObject(record, key) {
|
|
2572
|
+
const existing = record[key];
|
|
2573
|
+
if (existing && typeof existing === "object" && !Array.isArray(existing)) {
|
|
2574
|
+
return existing;
|
|
2575
|
+
}
|
|
2576
|
+
const created = {};
|
|
2577
|
+
record[key] = created;
|
|
2578
|
+
return created;
|
|
2579
|
+
}
|
|
2580
|
+
function dedupeStrings(values) {
|
|
2581
|
+
return Array.from(new Set(values.filter((value) => typeof value === "string")));
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
// src/index.ts
|
|
2585
|
+
var plugin = {
|
|
2586
|
+
id: "claw-control-center",
|
|
2587
|
+
name: "Claw Control Center",
|
|
2588
|
+
register(api) {
|
|
2589
|
+
let runtime;
|
|
2590
|
+
const pluginConfig = api.pluginConfig ?? {};
|
|
2591
|
+
api.registerService({
|
|
2592
|
+
id: "claw-control-center-service",
|
|
2593
|
+
async start(ctx) {
|
|
2594
|
+
if (runtime) {
|
|
2595
|
+
return;
|
|
2596
|
+
}
|
|
2597
|
+
runtime = await createRuntime({
|
|
2598
|
+
rootDir: api.rootDir ?? process.cwd(),
|
|
2599
|
+
stateDir: ctx.stateDir,
|
|
2600
|
+
configPath: resolveConfigPath(ctx),
|
|
2601
|
+
pluginConfig,
|
|
2602
|
+
version: api.version ?? "dev"
|
|
2603
|
+
});
|
|
2604
|
+
await runtime.start();
|
|
2605
|
+
api.logger.info(`Claw Control Center gateway console started at ${runtime.baseUrl}`);
|
|
2606
|
+
},
|
|
2607
|
+
async stop() {
|
|
2608
|
+
if (!runtime) {
|
|
2609
|
+
return;
|
|
2610
|
+
}
|
|
2611
|
+
await runtime.stop();
|
|
2612
|
+
runtime = void 0;
|
|
2613
|
+
}
|
|
2614
|
+
});
|
|
2615
|
+
}
|
|
2616
|
+
};
|
|
2617
|
+
var index_default = plugin;
|
|
2618
|
+
function resolveConfigPath(ctx) {
|
|
2619
|
+
const candidates = [
|
|
2620
|
+
ctx.stateDir ? (0, import_node_path7.join)(ctx.stateDir, "openclaw.json") : void 0,
|
|
2621
|
+
process.env.HOME ? (0, import_node_path7.join)(process.env.HOME, ".qclaw", "openclaw.json") : void 0,
|
|
2622
|
+
process.env.HOME ? (0, import_node_path7.join)(process.env.HOME, ".openclaw", "openclaw.json") : void 0
|
|
2623
|
+
].filter((candidate) => Boolean(candidate));
|
|
2624
|
+
return candidates.find((candidate) => (0, import_node_fs5.existsSync)(candidate)) ?? candidates[0] ?? "openclaw.json";
|
|
2625
|
+
}
|
|
2626
|
+
function resolveWebDir(rootDir) {
|
|
2627
|
+
const candidates = [
|
|
2628
|
+
(0, import_node_path7.join)(rootDir, "web-dist"),
|
|
2629
|
+
(0, import_node_path7.join)(rootDir, "..", "web", "dist"),
|
|
2630
|
+
(0, import_node_path7.join)(rootDir, "web", "dist")
|
|
2631
|
+
];
|
|
2632
|
+
return candidates.find((candidate) => (0, import_node_fs5.existsSync)(candidate));
|
|
2633
|
+
}
|
|
2634
|
+
async function createRuntime(input) {
|
|
2635
|
+
const config = resolvePluginConfigWithHostDefaults(input.configPath, input.pluginConfig);
|
|
2636
|
+
if (!config.gateway.baseUrl) {
|
|
2637
|
+
throw new Error("Missing gateway.baseUrl and no local OpenClaw/QClaw gateway could be inferred");
|
|
2638
|
+
}
|
|
2639
|
+
if (!config.gateway.secret) {
|
|
2640
|
+
throw new Error("Missing gateway.secret and no local gateway auth token could be inferred");
|
|
2641
|
+
}
|
|
2642
|
+
const gatewayConfig = {
|
|
2643
|
+
...config.gateway,
|
|
2644
|
+
runtimeRoot: input.rootDir
|
|
2645
|
+
};
|
|
2646
|
+
const hostRuntime = readHostRuntimeInfo(input.configPath);
|
|
2647
|
+
const gateway = createGatewayClient(gatewayConfig);
|
|
2648
|
+
const server = createConsoleServer({
|
|
2649
|
+
stateDir: input.stateDir,
|
|
2650
|
+
configPath: input.configPath,
|
|
2651
|
+
hostKind: detectHostKind(input.stateDir),
|
|
2652
|
+
pluginVersion: input.version,
|
|
2653
|
+
token: (0, import_node_crypto4.randomUUID)(),
|
|
2654
|
+
gatewayConfig,
|
|
2655
|
+
hub53aiConfig: config.hub53ai,
|
|
2656
|
+
consoleConfig: config.console,
|
|
2657
|
+
persistence: config.persistence,
|
|
2658
|
+
hostRuntime,
|
|
2659
|
+
gateway,
|
|
2660
|
+
webDir: resolveWebDir(input.rootDir)
|
|
2661
|
+
});
|
|
2662
|
+
return {
|
|
2663
|
+
...server,
|
|
2664
|
+
sanitizedConfig: sanitizePluginConfig(config)
|
|
2665
|
+
};
|
|
2666
|
+
}
|
|
2667
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2668
|
+
0 && (module.exports = {
|
|
2669
|
+
createConsoleServer,
|
|
2670
|
+
createGatewayClient,
|
|
2671
|
+
createHub53AIBridge,
|
|
2672
|
+
installIntoOpenClaw,
|
|
2673
|
+
installIntoQClaw,
|
|
2674
|
+
parseHub53AIIncomingMessage,
|
|
2675
|
+
runInstallCommand
|
|
2676
|
+
});
|