@agentclub/openclaw-adapter 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +171 -0
- package/dist/adapter.js +165 -0
- package/dist/agenthub-client.js +180 -0
- package/dist/cli.js +246 -0
- package/dist/config.js +182 -0
- package/dist/index.js +6 -0
- package/dist/openclaw-client.js +412 -0
- package/dist/prompt.js +80 -0
- package/dist/types.js +1 -0
- package/dist/utils.js +106 -0
- package/package.json +50 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
function normalizeAgentId(value) {
|
|
3
|
+
const normalized = value.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '-');
|
|
4
|
+
return normalized || 'main';
|
|
5
|
+
}
|
|
6
|
+
function buildThreadSlug(threadId) {
|
|
7
|
+
const normalized = threadId.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
8
|
+
return normalized.slice(0, 48) || 'thread';
|
|
9
|
+
}
|
|
10
|
+
function buildThreadHash(threadId) {
|
|
11
|
+
return createHash('sha256').update(threadId).digest('hex').slice(0, 16);
|
|
12
|
+
}
|
|
13
|
+
function buildSessionKey(config, threadId) {
|
|
14
|
+
return config.openClaw.sessionKeyTemplate
|
|
15
|
+
.replaceAll('{agentId}', normalizeAgentId(config.openClaw.agentId))
|
|
16
|
+
.replaceAll('{threadId}', encodeURIComponent(threadId))
|
|
17
|
+
.replaceAll('{threadSlug}', buildThreadSlug(threadId))
|
|
18
|
+
.replaceAll('{threadHash}', buildThreadHash(threadId));
|
|
19
|
+
}
|
|
20
|
+
function extractTextFromMessage(message) {
|
|
21
|
+
if (!message || typeof message !== 'object') {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const content = message.content;
|
|
25
|
+
if (!Array.isArray(content)) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
for (const item of content) {
|
|
29
|
+
if (!item || typeof item !== 'object') {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const block = item;
|
|
33
|
+
if (block.type === 'text' && typeof block.text === 'string' && block.text.trim()) {
|
|
34
|
+
return block.text.trim();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
function findLatestAssistantMessageText(payload) {
|
|
40
|
+
if (!payload || typeof payload !== 'object') {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const messages = payload.messages;
|
|
44
|
+
if (!Array.isArray(messages)) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
48
|
+
const item = messages[i];
|
|
49
|
+
if (!item || typeof item !== 'object') {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const role = item.role;
|
|
53
|
+
if (role !== 'assistant') {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const text = extractTextFromMessage(item);
|
|
57
|
+
if (text) {
|
|
58
|
+
return text;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
function readWebSocketMessage(data) {
|
|
64
|
+
if (typeof data === 'string') {
|
|
65
|
+
return data;
|
|
66
|
+
}
|
|
67
|
+
if (data instanceof ArrayBuffer) {
|
|
68
|
+
return Buffer.from(data).toString('utf8');
|
|
69
|
+
}
|
|
70
|
+
if (ArrayBuffer.isView(data)) {
|
|
71
|
+
return Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString('utf8');
|
|
72
|
+
}
|
|
73
|
+
return String(data);
|
|
74
|
+
}
|
|
75
|
+
function isGatewayEventFrame(value) {
|
|
76
|
+
return Boolean(value &&
|
|
77
|
+
typeof value === 'object' &&
|
|
78
|
+
value.type === 'event' &&
|
|
79
|
+
typeof value.event === 'string');
|
|
80
|
+
}
|
|
81
|
+
function isGatewayResponseFrame(value) {
|
|
82
|
+
return Boolean(value &&
|
|
83
|
+
typeof value === 'object' &&
|
|
84
|
+
value.type === 'res' &&
|
|
85
|
+
typeof value.id === 'string' &&
|
|
86
|
+
typeof value.ok === 'boolean');
|
|
87
|
+
}
|
|
88
|
+
class OpenClawGatewayClient {
|
|
89
|
+
config;
|
|
90
|
+
logger;
|
|
91
|
+
connectPromise = null;
|
|
92
|
+
connectReject = null;
|
|
93
|
+
connectResolve = null;
|
|
94
|
+
hello = null;
|
|
95
|
+
pendingRequests = new Map();
|
|
96
|
+
pendingRuns = new Map();
|
|
97
|
+
ws = null;
|
|
98
|
+
constructor(config, logger) {
|
|
99
|
+
this.config = config;
|
|
100
|
+
this.logger = logger;
|
|
101
|
+
}
|
|
102
|
+
getHello() {
|
|
103
|
+
return this.hello;
|
|
104
|
+
}
|
|
105
|
+
async ensureConnected(signal) {
|
|
106
|
+
if (this.isConnected()) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (!this.connectPromise) {
|
|
110
|
+
this.connectPromise = new Promise((resolve, reject) => {
|
|
111
|
+
this.connectResolve = resolve;
|
|
112
|
+
this.connectReject = reject;
|
|
113
|
+
});
|
|
114
|
+
this.openSocket();
|
|
115
|
+
}
|
|
116
|
+
await this.withAbort(this.connectPromise, signal, 'gateway connect aborted');
|
|
117
|
+
}
|
|
118
|
+
async sendChat(params) {
|
|
119
|
+
await this.ensureConnected(params.signal);
|
|
120
|
+
const sessionKey = buildSessionKey(this.config, params.threadId);
|
|
121
|
+
const runId = randomUUID();
|
|
122
|
+
const replyPromise = new Promise((resolve, reject) => {
|
|
123
|
+
this.pendingRuns.set(runId, {
|
|
124
|
+
runId,
|
|
125
|
+
sessionKey,
|
|
126
|
+
resolve,
|
|
127
|
+
reject,
|
|
128
|
+
text: '',
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
const abortHandler = () => {
|
|
132
|
+
const pending = this.pendingRuns.get(runId);
|
|
133
|
+
if (!pending) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
this.pendingRuns.delete(runId);
|
|
137
|
+
pending.reject(new Error('openclaw chat aborted'));
|
|
138
|
+
void this.request('chat.abort', { runId, sessionKey }).catch(() => undefined);
|
|
139
|
+
};
|
|
140
|
+
params.signal.addEventListener('abort', abortHandler, { once: true });
|
|
141
|
+
try {
|
|
142
|
+
await this.request('chat.send', {
|
|
143
|
+
deliver: false,
|
|
144
|
+
idempotencyKey: runId,
|
|
145
|
+
message: params.incomingMessage,
|
|
146
|
+
sessionKey,
|
|
147
|
+
thinking: this.config.openClaw.thinking || undefined,
|
|
148
|
+
timeoutMs: params.timeoutMs,
|
|
149
|
+
});
|
|
150
|
+
const reply = await this.withAbort(replyPromise, params.signal, 'openclaw chat aborted');
|
|
151
|
+
if (!reply.trim()) {
|
|
152
|
+
throw new Error('openclaw gateway produced empty reply');
|
|
153
|
+
}
|
|
154
|
+
return reply.trim();
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
const pending = this.pendingRuns.get(runId);
|
|
158
|
+
if (pending) {
|
|
159
|
+
this.pendingRuns.delete(runId);
|
|
160
|
+
pending.reject(error instanceof Error ? error : new Error(String(error)));
|
|
161
|
+
}
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
params.signal.removeEventListener('abort', abortHandler);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
isConnected() {
|
|
169
|
+
return Boolean(this.ws && this.ws.readyState === WebSocket.OPEN && this.hello);
|
|
170
|
+
}
|
|
171
|
+
openSocket() {
|
|
172
|
+
this.ws = new WebSocket(this.config.openClaw.gatewayUrl);
|
|
173
|
+
this.ws.addEventListener('open', () => {
|
|
174
|
+
this.logger.info('[adapter] openclaw gateway socket opened');
|
|
175
|
+
});
|
|
176
|
+
this.ws.addEventListener('message', (event) => {
|
|
177
|
+
this.handleFrame(readWebSocketMessage(event.data)).catch((error) => {
|
|
178
|
+
this.logger.warn('[adapter] openclaw gateway frame error', {
|
|
179
|
+
error: error instanceof Error ? error.message : String(error),
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
this.ws.addEventListener('close', (event) => {
|
|
184
|
+
const error = new Error(`openclaw gateway closed (${event.code}): ${event.reason || 'no reason'}`);
|
|
185
|
+
this.resetConnection(error);
|
|
186
|
+
});
|
|
187
|
+
this.ws.addEventListener('error', () => {
|
|
188
|
+
if (this.hello || !this.connectReject) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
this.resetConnection(new Error('openclaw gateway connection error'));
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
async handleFrame(raw) {
|
|
195
|
+
const parsed = JSON.parse(raw);
|
|
196
|
+
if (isGatewayEventFrame(parsed)) {
|
|
197
|
+
await this.handleEventFrame(parsed);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (isGatewayResponseFrame(parsed)) {
|
|
201
|
+
this.handleResponseFrame(parsed);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async handleEventFrame(frame) {
|
|
205
|
+
if (frame.event === 'connect.challenge') {
|
|
206
|
+
const nonce = typeof frame.payload?.nonce === 'string' ? frame.payload.nonce.trim() : '';
|
|
207
|
+
if (!nonce) {
|
|
208
|
+
throw new Error('openclaw gateway connect challenge missing nonce');
|
|
209
|
+
}
|
|
210
|
+
await this.sendConnect(nonce);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (frame.event !== 'chat') {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const payload = frame.payload;
|
|
217
|
+
if (!payload) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const runId = typeof payload?.runId === 'string' ? payload.runId : '';
|
|
221
|
+
if (!runId) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const pending = this.pendingRuns.get(runId);
|
|
225
|
+
if (!pending) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const state = typeof payload?.state === 'string' ? payload.state : '';
|
|
229
|
+
if (state === 'delta') {
|
|
230
|
+
const text = extractTextFromMessage(payload.message);
|
|
231
|
+
if (text) {
|
|
232
|
+
pending.text = text;
|
|
233
|
+
}
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (state === 'final') {
|
|
237
|
+
const finalText = extractTextFromMessage(payload.message) ||
|
|
238
|
+
pending.text ||
|
|
239
|
+
(await this.loadLatestAssistantMessage(pending.sessionKey));
|
|
240
|
+
this.pendingRuns.delete(runId);
|
|
241
|
+
pending.resolve(finalText || '');
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (state === 'error') {
|
|
245
|
+
this.pendingRuns.delete(runId);
|
|
246
|
+
pending.reject(new Error(typeof payload?.errorMessage === 'string' ? payload.errorMessage : 'openclaw chat failed'));
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (state === 'aborted') {
|
|
250
|
+
this.pendingRuns.delete(runId);
|
|
251
|
+
pending.reject(new Error('openclaw chat aborted'));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
handleResponseFrame(frame) {
|
|
255
|
+
const pending = this.pendingRequests.get(frame.id);
|
|
256
|
+
if (!pending) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const status = this.readPayloadStatus(frame.payload);
|
|
260
|
+
if (pending.expectAcceptedOnly && status === 'accepted') {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
this.pendingRequests.delete(frame.id);
|
|
264
|
+
if (frame.ok) {
|
|
265
|
+
pending.resolve(frame.payload);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
pending.reject(new Error(frame.error?.message || 'openclaw gateway request failed'));
|
|
269
|
+
}
|
|
270
|
+
async loadLatestAssistantMessage(sessionKey) {
|
|
271
|
+
try {
|
|
272
|
+
const payload = await this.request('chat.history', { limit: 12, sessionKey });
|
|
273
|
+
return findLatestAssistantMessageText(payload);
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
async sendConnect(_nonce) {
|
|
280
|
+
const hello = (await this.request('connect', {
|
|
281
|
+
auth: {
|
|
282
|
+
password: this.config.openClaw.gatewayPassword || undefined,
|
|
283
|
+
token: this.config.openClaw.gatewayToken || undefined,
|
|
284
|
+
},
|
|
285
|
+
caps: [],
|
|
286
|
+
client: {
|
|
287
|
+
displayName: 'Agent Hub Adapter',
|
|
288
|
+
id: 'gateway-client',
|
|
289
|
+
instanceId: randomUUID(),
|
|
290
|
+
mode: 'backend',
|
|
291
|
+
platform: process.platform,
|
|
292
|
+
version: '0.1.0',
|
|
293
|
+
},
|
|
294
|
+
locale: 'zh-CN',
|
|
295
|
+
maxProtocol: 3,
|
|
296
|
+
minProtocol: 3,
|
|
297
|
+
pathEnv: process.env.PATH,
|
|
298
|
+
role: 'operator',
|
|
299
|
+
scopes: ['operator.admin'],
|
|
300
|
+
userAgent: 'agenthub-openclaw-adapter',
|
|
301
|
+
}));
|
|
302
|
+
this.hello = hello;
|
|
303
|
+
this.logger.info('[adapter] openclaw gateway authorized', {
|
|
304
|
+
serverVersion: hello.server?.version ?? 'unknown',
|
|
305
|
+
});
|
|
306
|
+
this.connectResolve?.();
|
|
307
|
+
this.connectResolve = null;
|
|
308
|
+
this.connectReject = null;
|
|
309
|
+
}
|
|
310
|
+
request(method, params, options) {
|
|
311
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
312
|
+
throw new Error('openclaw gateway not connected');
|
|
313
|
+
}
|
|
314
|
+
const id = randomUUID();
|
|
315
|
+
const frame = {
|
|
316
|
+
id,
|
|
317
|
+
method,
|
|
318
|
+
params,
|
|
319
|
+
type: 'req',
|
|
320
|
+
};
|
|
321
|
+
const requestPromise = new Promise((resolve, reject) => {
|
|
322
|
+
this.pendingRequests.set(id, {
|
|
323
|
+
expectAcceptedOnly: options?.expectAcceptedOnly === true,
|
|
324
|
+
reject,
|
|
325
|
+
resolve,
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
this.ws.send(JSON.stringify(frame));
|
|
329
|
+
return requestPromise;
|
|
330
|
+
}
|
|
331
|
+
readPayloadStatus(payload) {
|
|
332
|
+
if (!payload || typeof payload !== 'object') {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
return typeof payload.status === 'string'
|
|
336
|
+
? (payload.status || null)
|
|
337
|
+
: null;
|
|
338
|
+
}
|
|
339
|
+
resetConnection(error) {
|
|
340
|
+
if (this.ws) {
|
|
341
|
+
this.ws = null;
|
|
342
|
+
}
|
|
343
|
+
this.hello = null;
|
|
344
|
+
this.connectReject?.(error);
|
|
345
|
+
this.connectReject = null;
|
|
346
|
+
this.connectResolve = null;
|
|
347
|
+
this.connectPromise = null;
|
|
348
|
+
for (const [id, pending] of this.pendingRequests.entries()) {
|
|
349
|
+
this.pendingRequests.delete(id);
|
|
350
|
+
pending.reject(error);
|
|
351
|
+
}
|
|
352
|
+
for (const [runId, pending] of this.pendingRuns.entries()) {
|
|
353
|
+
this.pendingRuns.delete(runId);
|
|
354
|
+
pending.reject(error);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
withAbort(promise, signal, message) {
|
|
358
|
+
if (!signal) {
|
|
359
|
+
return promise;
|
|
360
|
+
}
|
|
361
|
+
if (signal.aborted) {
|
|
362
|
+
return Promise.reject(new Error(message));
|
|
363
|
+
}
|
|
364
|
+
return new Promise((resolve, reject) => {
|
|
365
|
+
const onAbort = () => {
|
|
366
|
+
reject(new Error(message));
|
|
367
|
+
};
|
|
368
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
369
|
+
promise.then((value) => {
|
|
370
|
+
signal.removeEventListener('abort', onAbort);
|
|
371
|
+
resolve(value);
|
|
372
|
+
}, (error) => {
|
|
373
|
+
signal.removeEventListener('abort', onAbort);
|
|
374
|
+
reject(error);
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
export class OpenClawClient {
|
|
380
|
+
config;
|
|
381
|
+
logger;
|
|
382
|
+
gateway;
|
|
383
|
+
constructor(config, logger) {
|
|
384
|
+
this.config = config;
|
|
385
|
+
this.logger = logger;
|
|
386
|
+
this.gateway = new OpenClawGatewayClient(config, logger);
|
|
387
|
+
}
|
|
388
|
+
async connect(signal) {
|
|
389
|
+
await this.gateway.ensureConnected(signal);
|
|
390
|
+
}
|
|
391
|
+
getHello() {
|
|
392
|
+
return this.gateway.getHello();
|
|
393
|
+
}
|
|
394
|
+
async generateReply(params) {
|
|
395
|
+
const reply = await this.gateway.sendChat({
|
|
396
|
+
incomingMessage: params.incomingMessage,
|
|
397
|
+
signal: params.signal,
|
|
398
|
+
threadId: params.threadId,
|
|
399
|
+
timeoutMs: this.config.openClaw.timeoutMs,
|
|
400
|
+
});
|
|
401
|
+
return {
|
|
402
|
+
reply: `${this.config.adapter.replyPrefix}${reply}`,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
export const __internal = {
|
|
407
|
+
buildSessionKey,
|
|
408
|
+
buildThreadHash,
|
|
409
|
+
buildThreadSlug,
|
|
410
|
+
extractTextFromMessage,
|
|
411
|
+
findLatestAssistantMessageText,
|
|
412
|
+
};
|
package/dist/prompt.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
2
|
+
import { createInterface } from 'node:readline/promises';
|
|
3
|
+
import { DEFAULT_OPENCLAW_GATEWAY_URL, buildDefaultConfigFromEnv } from './config.js';
|
|
4
|
+
import { normalizeBaseUrl, normalizeGatewayUrl, parseNumber } from './utils.js';
|
|
5
|
+
async function ask(rl, label, fallback) {
|
|
6
|
+
const value = await rl.question(`${label}${fallback ? ` [${fallback}]` : ''}: `);
|
|
7
|
+
return value.trim() || fallback;
|
|
8
|
+
}
|
|
9
|
+
async function askYesNo(rl, label, fallback) {
|
|
10
|
+
const hint = fallback ? 'Y/n' : 'y/N';
|
|
11
|
+
const value = (await rl.question(`${label} [${hint}]: `)).trim().toLowerCase();
|
|
12
|
+
if (!value) {
|
|
13
|
+
return fallback;
|
|
14
|
+
}
|
|
15
|
+
return ['y', 'yes', '1', 'true'].includes(value);
|
|
16
|
+
}
|
|
17
|
+
export async function promptConfigInteractive(options) {
|
|
18
|
+
const defaults = options?.defaults ?? buildDefaultConfigFromEnv();
|
|
19
|
+
const rl = createInterface({ input, output });
|
|
20
|
+
try {
|
|
21
|
+
output.write('\nAgent Hub OpenClaw Adapter 初始化\n');
|
|
22
|
+
if (options?.advanced) {
|
|
23
|
+
output.write('模式:高级模式\n\n');
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
output.write('模式:Agent 模式(只询问最小必填项,其他使用推荐默认值)\n\n');
|
|
27
|
+
}
|
|
28
|
+
const agentHubBaseUrl = normalizeBaseUrl(await ask(rl, 'Agent Hub Base URL', defaults.agentHub.baseUrl));
|
|
29
|
+
const clientId = await ask(rl, 'Agent Hub clientId', defaults.agentHub.clientId);
|
|
30
|
+
const clientSecret = await ask(rl, 'Agent Hub clientSecret', defaults.agentHub.clientSecret);
|
|
31
|
+
const agentId = await ask(rl, 'OpenClaw agentId', defaults.openClaw.agentId);
|
|
32
|
+
const gatewayAuthEnabled = defaults.openClaw.gatewayToken !== '' || defaults.openClaw.gatewayPassword !== ''
|
|
33
|
+
? true
|
|
34
|
+
: await askYesNo(rl, 'OpenClaw Gateway 是否启用鉴权', false);
|
|
35
|
+
const gatewayToken = await ask(rl, 'OpenClaw Gateway token(可留空)', defaults.openClaw.gatewayToken);
|
|
36
|
+
const gatewayPassword = await ask(rl, 'OpenClaw Gateway password(可留空)', defaults.openClaw.gatewayPassword);
|
|
37
|
+
let openClawGatewayUrl = defaults.openClaw.gatewayUrl;
|
|
38
|
+
let sessionKeyTemplate = defaults.openClaw.sessionKeyTemplate;
|
|
39
|
+
let thinking = defaults.openClaw.thinking;
|
|
40
|
+
let replyPrefix = defaults.adapter.replyPrefix;
|
|
41
|
+
if (options?.advanced) {
|
|
42
|
+
openClawGatewayUrl = normalizeGatewayUrl(await ask(rl, 'OpenClaw Gateway WebSocket URL', defaults.openClaw.gatewayUrl));
|
|
43
|
+
sessionKeyTemplate = await ask(rl, 'OpenClaw sessionKey template(支持 {agentId} / {threadSlug} / {threadHash} / {threadId})', defaults.openClaw.sessionKeyTemplate);
|
|
44
|
+
thinking = await ask(rl, 'OpenClaw thinking level(可留空)', defaults.openClaw.thinking);
|
|
45
|
+
replyPrefix = await ask(rl, '自动回复前缀(可留空)', defaults.adapter.replyPrefix);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
output.write(`\n将使用默认 OpenClaw Gateway:${DEFAULT_OPENCLAW_GATEWAY_URL}\n` +
|
|
49
|
+
`如需自定义 Gateway 地址、session key 或 thinking,请使用 init --advanced\n\n`);
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
version: 1,
|
|
53
|
+
agentHub: {
|
|
54
|
+
baseUrl: agentHubBaseUrl,
|
|
55
|
+
clientId,
|
|
56
|
+
clientSecret,
|
|
57
|
+
},
|
|
58
|
+
openClaw: {
|
|
59
|
+
gatewayUrl: normalizeGatewayUrl(openClawGatewayUrl),
|
|
60
|
+
gatewayToken: gatewayAuthEnabled ? gatewayToken : '',
|
|
61
|
+
gatewayPassword: gatewayAuthEnabled ? gatewayPassword : '',
|
|
62
|
+
agentId,
|
|
63
|
+
sessionKeyTemplate,
|
|
64
|
+
timeoutMs: defaults.openClaw.timeoutMs,
|
|
65
|
+
thinking,
|
|
66
|
+
},
|
|
67
|
+
adapter: {
|
|
68
|
+
replyPrefix,
|
|
69
|
+
requestTimeoutMs: parseNumber(undefined, defaults.adapter.requestTimeoutMs),
|
|
70
|
+
reconnectInitialDelayMs: parseNumber(undefined, defaults.adapter.reconnectInitialDelayMs),
|
|
71
|
+
reconnectMaxDelayMs: parseNumber(undefined, defaults.adapter.reconnectMaxDelayMs),
|
|
72
|
+
handledTtlMs: parseNumber(undefined, defaults.adapter.handledTtlMs),
|
|
73
|
+
handledLimit: parseNumber(undefined, defaults.adapter.handledLimit),
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
rl.close();
|
|
79
|
+
}
|
|
80
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export function normalizeBaseUrl(value) {
|
|
2
|
+
return value.trim().replace(/\/+$/, '');
|
|
3
|
+
}
|
|
4
|
+
export function normalizeGatewayUrl(value) {
|
|
5
|
+
return value.trim().replace(/\/+$/, '');
|
|
6
|
+
}
|
|
7
|
+
export function parseNumber(value, fallback) {
|
|
8
|
+
if (!value) {
|
|
9
|
+
return fallback;
|
|
10
|
+
}
|
|
11
|
+
const parsed = Number(value);
|
|
12
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
13
|
+
return fallback;
|
|
14
|
+
}
|
|
15
|
+
return Math.floor(parsed);
|
|
16
|
+
}
|
|
17
|
+
export function safeErrorMessage(error) {
|
|
18
|
+
if (error instanceof Error) {
|
|
19
|
+
return error.message;
|
|
20
|
+
}
|
|
21
|
+
return String(error);
|
|
22
|
+
}
|
|
23
|
+
export function buildUrl(baseUrl, path) {
|
|
24
|
+
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
|
25
|
+
return `${baseUrl}${normalizedPath}`;
|
|
26
|
+
}
|
|
27
|
+
export function parseSseDataFrames(chunkBuffer) {
|
|
28
|
+
const normalized = chunkBuffer.replace(/\r\n/g, '\n');
|
|
29
|
+
const frames = [];
|
|
30
|
+
let start = 0;
|
|
31
|
+
while (start < normalized.length) {
|
|
32
|
+
const split = normalized.indexOf('\n\n', start);
|
|
33
|
+
if (split === -1) {
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
const block = normalized.slice(start, split);
|
|
37
|
+
start = split + 2;
|
|
38
|
+
if (!block.trim()) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const lines = block.split('\n');
|
|
42
|
+
const dataLines = [];
|
|
43
|
+
for (const line of lines) {
|
|
44
|
+
if (line.startsWith(':')) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (line.startsWith('data:')) {
|
|
48
|
+
dataLines.push(line.slice('data:'.length).trimStart());
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (dataLines.length) {
|
|
52
|
+
frames.push(dataLines.join('\n'));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
frames,
|
|
57
|
+
rest: normalized.slice(start),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export function sleep(ms, signal) {
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const timer = setTimeout(resolve, ms);
|
|
63
|
+
if (!signal) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (signal.aborted) {
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
reject(new Error('aborted'));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const onAbort = () => {
|
|
72
|
+
clearTimeout(timer);
|
|
73
|
+
reject(new Error('aborted'));
|
|
74
|
+
};
|
|
75
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
export const defaultLogger = {
|
|
79
|
+
info(message, extra) {
|
|
80
|
+
if (extra) {
|
|
81
|
+
// eslint-disable-next-line no-console
|
|
82
|
+
console.log(message, extra);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
// eslint-disable-next-line no-console
|
|
86
|
+
console.log(message);
|
|
87
|
+
},
|
|
88
|
+
warn(message, extra) {
|
|
89
|
+
if (extra) {
|
|
90
|
+
// eslint-disable-next-line no-console
|
|
91
|
+
console.warn(message, extra);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
// eslint-disable-next-line no-console
|
|
95
|
+
console.warn(message);
|
|
96
|
+
},
|
|
97
|
+
error(message, extra) {
|
|
98
|
+
if (extra) {
|
|
99
|
+
// eslint-disable-next-line no-console
|
|
100
|
+
console.error(message, extra);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// eslint-disable-next-line no-console
|
|
104
|
+
console.error(message);
|
|
105
|
+
},
|
|
106
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agentclub/openclaw-adapter",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Agent Hub adapter for local OpenClaw Gateway agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"agentclub-openclaw-adapter": "dist/cli.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc -p tsconfig.json",
|
|
20
|
+
"dev": "tsx src/cli.ts",
|
|
21
|
+
"start": "node dist/cli.js start",
|
|
22
|
+
"check": "node dist/cli.js check",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"prepublishOnly": "npm test && npm run build"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"agenthub",
|
|
28
|
+
"openclaw",
|
|
29
|
+
"agent",
|
|
30
|
+
"adapter",
|
|
31
|
+
"sse",
|
|
32
|
+
"gateway",
|
|
33
|
+
"chatbot"
|
|
34
|
+
],
|
|
35
|
+
"author": "",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=20"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^22.13.10",
|
|
46
|
+
"tsx": "^4.19.3",
|
|
47
|
+
"typescript": "^5.8.2",
|
|
48
|
+
"vitest": "^2.1.8"
|
|
49
|
+
}
|
|
50
|
+
}
|