adp-openclaw 0.0.13 → 0.0.15

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "adp-openclaw",
3
- "version": "0.0.13",
4
- "description": "OpenClaw demo channel plugin (Go WebSocket backend)",
3
+ "version": "0.0.15",
4
+ "description": "ADP-OpenClaw demo channel plugin (Go WebSocket backend)",
5
5
  "type": "module",
6
6
  "dependencies": {
7
7
  "ws": "^8.16.0",
@@ -16,10 +16,10 @@
16
16
  ],
17
17
  "channel": {
18
18
  "id": "adp-openclaw",
19
- "label": "ADP OpenClaw",
20
- "selectionLabel": "ADP OpenClaw",
19
+ "label": "ADP-OpenClaw",
20
+ "selectionLabel": "ADP-OpenClaw",
21
21
  "docsPath": "/channels/adp-openclaw",
22
- "blurb": "ADP channel backed by a Go WebSocket server.",
22
+ "blurb": "ADP-OpenClaw channel backed by a Go WebSocket server.",
23
23
  "order": 999
24
24
  }
25
25
  }
package/src/channel.ts CHANGED
@@ -7,14 +7,15 @@ import {
7
7
  DEFAULT_ACCOUNT_ID,
8
8
  } from "openclaw/plugin-sdk";
9
9
 
10
+ // Default WebSocket URL for ADP OpenClaw
11
+ const DEFAULT_WS_URL = "wss://wss.lke.cloud.tencent.com/bot/gateway/ws/";
12
+
10
13
  // Channel-level config type (from channels["adp-openclaw"])
11
14
  export type AdpOpenclawChannelConfig = {
12
15
  enabled?: boolean;
13
- serverUrl?: string;
14
- wsUrl?: string;
16
+ wsUrl?: string; // WebSocket URL (optional, has default)
15
17
  clientToken?: string;
16
- hmacKey?: string; // HMAC key for signature generation
17
- pollIntervalMs?: number;
18
+ signKey?: string; // HMAC key for signature generation
18
19
  };
19
20
 
20
21
  export type ResolvedAdpOpenclawAccount = {
@@ -22,29 +23,20 @@ export type ResolvedAdpOpenclawAccount = {
22
23
  name: string;
23
24
  enabled: boolean;
24
25
  configured: boolean;
25
- serverUrl: string;
26
+ wsUrl: string; // WebSocket URL
26
27
  clientToken: string;
27
- hmacKey: string; // HMAC key for signature generation
28
- pollIntervalMs: number;
28
+ signKey: string; // HMAC key for signature generation
29
29
  };
30
30
 
31
31
  function resolveAdpOpenclawCredentials(channelCfg?: AdpOpenclawChannelConfig): {
32
- serverUrl: string;
32
+ wsUrl: string;
33
33
  clientToken: string;
34
- hmacKey: string;
35
- pollIntervalMs: number;
34
+ signKey: string;
36
35
  } | null {
37
- // Get serverUrl: support both serverUrl and wsUrl
38
- let serverUrl = channelCfg?.serverUrl?.trim();
39
- if (!serverUrl && channelCfg?.wsUrl) {
40
- // Convert ws://host:port/ws to http://host:port
41
- serverUrl = channelCfg.wsUrl
42
- .replace(/^wss:\/\//, "https://")
43
- .replace(/^ws:\/\//, "http://")
44
- .replace(/\/ws\/?$/, "");
45
- }
46
- if (!serverUrl) {
47
- serverUrl = process.env.ADP_OPENCLAW_SERVER_URL || "";
36
+ // Get wsUrl from config or env (has default value)
37
+ let wsUrl = channelCfg?.wsUrl?.trim();
38
+ if (!wsUrl) {
39
+ wsUrl = process.env.ADP_OPENCLAW_WS_URL || DEFAULT_WS_URL;
48
40
  }
49
41
 
50
42
  // Get clientToken from config or env
@@ -53,20 +45,18 @@ function resolveAdpOpenclawCredentials(channelCfg?: AdpOpenclawChannelConfig): {
53
45
  clientToken = process.env.ADP_OPENCLAW_CLIENT_TOKEN || "";
54
46
  }
55
47
 
56
- // Get hmacKey from config or env (default: ADPOpenClaw)
57
- let hmacKey = channelCfg?.hmacKey?.trim();
58
- if (!hmacKey) {
59
- hmacKey = process.env.ADP_OPENCLAW_HMAC_KEY || "ADPOpenClaw";
48
+ // Get signKey from config or env (default: ADPOpenClaw)
49
+ let signKey = channelCfg?.signKey?.trim();
50
+ if (!signKey) {
51
+ signKey = process.env.ADP_OPENCLAW_SIGN_KEY || "ADPOpenClaw";
60
52
  }
61
53
 
62
- // Both are required for configured status
63
- if (!serverUrl || !clientToken) {
54
+ // clientToken is required for configured status (wsUrl has default)
55
+ if (!clientToken) {
64
56
  return null;
65
57
  }
66
58
 
67
- const pollIntervalMs = channelCfg?.pollIntervalMs ?? 1000;
68
-
69
- return { serverUrl, clientToken, hmacKey, pollIntervalMs };
59
+ return { wsUrl, clientToken, signKey };
70
60
  }
71
61
 
72
62
  function resolveAccount(cfg: ClawdbotConfig, accountId?: string): ResolvedAdpOpenclawAccount {
@@ -79,10 +69,9 @@ function resolveAccount(cfg: ClawdbotConfig, accountId?: string): ResolvedAdpOpe
79
69
  name: "ADP OpenClaw",
80
70
  enabled,
81
71
  configured: Boolean(creds),
82
- serverUrl: creds?.serverUrl || "http://localhost:9876",
72
+ wsUrl: creds?.wsUrl || DEFAULT_WS_URL,
83
73
  clientToken: creds?.clientToken || "",
84
- hmacKey: creds?.hmacKey || "ADPOpenClaw",
85
- pollIntervalMs: creds?.pollIntervalMs || 1000,
74
+ signKey: creds?.signKey || "ADPOpenClaw",
86
75
  };
87
76
  }
88
77
 
@@ -115,11 +104,9 @@ export const adpOpenclawPlugin: ChannelPlugin<ResolvedAdpOpenclawAccount> = {
115
104
  additionalProperties: false,
116
105
  properties: {
117
106
  enabled: { type: "boolean" },
118
- serverUrl: { type: "string" },
119
- wsUrl: { type: "string" },
107
+ wsUrl: { type: "string" }, // WebSocket URL (optional, default: wss://wss.lke.cloud.tencent.com/bot/gateway/ws/)
120
108
  clientToken: { type: "string" },
121
- hmacKey: { type: "string" },
122
- pollIntervalMs: { type: "integer", minimum: 100 },
109
+ signKey: { type: "string" },
123
110
  },
124
111
  },
125
112
  },
@@ -155,7 +142,7 @@ export const adpOpenclawPlugin: ChannelPlugin<ResolvedAdpOpenclawAccount> = {
155
142
  name: account.name,
156
143
  enabled: account.enabled,
157
144
  configured: account.configured,
158
- baseUrl: account.serverUrl,
145
+ wsUrl: account.wsUrl,
159
146
  }),
160
147
  resolveAllowFrom: () => [],
161
148
  formatAllowFrom: ({ allowFrom }) => allowFrom,
@@ -184,39 +171,27 @@ export const adpOpenclawPlugin: ChannelPlugin<ResolvedAdpOpenclawAccount> = {
184
171
  collectStatusIssues: () => [],
185
172
  buildChannelSummary: ({ snapshot }) => ({
186
173
  configured: snapshot.configured ?? false,
187
- baseUrl: snapshot.baseUrl ?? null,
174
+ wsUrl: snapshot.wsUrl ?? null,
188
175
  running: snapshot.running ?? false,
189
176
  lastStartAt: snapshot.lastStartAt ?? null,
190
177
  lastStopAt: snapshot.lastStopAt ?? null,
191
178
  lastError: snapshot.lastError ?? null,
192
179
  }),
193
- probeAccount: async ({ cfg, timeoutMs }) => {
180
+ probeAccount: async ({ cfg }) => {
181
+ // For WebSocket-only architecture, we just check if config is valid
194
182
  const account = resolveAccount(cfg);
195
183
  const start = Date.now();
196
- try {
197
- const controller = new AbortController();
198
- const timeout = setTimeout(() => controller.abort(), timeoutMs ?? 5000);
199
- const res = await fetch(`${account.serverUrl}/health`, { signal: controller.signal });
200
- clearTimeout(timeout);
201
- const data = (await res.json()) as { ok?: boolean };
202
- return {
203
- ok: data.ok === true,
204
- elapsedMs: Date.now() - start,
205
- };
206
- } catch (err) {
207
- return {
208
- ok: false,
209
- error: err instanceof Error ? err.message : String(err),
210
- elapsedMs: Date.now() - start,
211
- };
212
- }
184
+ return {
185
+ ok: account.configured && Boolean(account.clientToken),
186
+ elapsedMs: Date.now() - start,
187
+ };
213
188
  },
214
189
  buildAccountSnapshot: ({ account, runtime, probe }) => ({
215
190
  accountId: account.accountId,
216
191
  name: account.name,
217
192
  enabled: account.enabled,
218
193
  configured: account.configured,
219
- baseUrl: account.serverUrl,
194
+ wsUrl: account.wsUrl,
220
195
  running: runtime?.running ?? false,
221
196
  lastStartAt: runtime?.lastStartAt ?? null,
222
197
  lastStopAt: runtime?.lastStopAt ?? null,
@@ -227,61 +202,20 @@ export const adpOpenclawPlugin: ChannelPlugin<ResolvedAdpOpenclawAccount> = {
227
202
  gateway: {
228
203
  startAccount: async (ctx) => {
229
204
  const account = ctx.account;
230
- ctx.setStatus({ accountId: account.accountId, baseUrl: account.serverUrl });
231
- ctx.log?.info(`[adp-openclaw] starting poll loop → ${account.serverUrl}`);
205
+ ctx.setStatus({ accountId: account.accountId, wsUrl: account.wsUrl });
206
+ ctx.log?.info(`[adp-openclaw] starting WebSocket connection → ${account.wsUrl}`);
232
207
 
233
208
  const { monitorAdpOpenclaw } = await import("./monitor.js");
234
209
  return monitorAdpOpenclaw({
235
- serverUrl: account.serverUrl,
210
+ wsUrl: account.wsUrl,
236
211
  clientToken: account.clientToken,
237
- hmacKey: account.hmacKey,
238
- pollIntervalMs: account.pollIntervalMs,
212
+ signKey: account.signKey,
239
213
  abortSignal: ctx.abortSignal,
240
214
  log: ctx.log,
241
215
  cfg: ctx.cfg,
242
216
  });
243
217
  },
244
218
  },
245
- outbound: {
246
- send: async ({ target, message, cfg, context }: {
247
- target: string;
248
- message: string;
249
- cfg: unknown;
250
- context?: {
251
- conversationId?: string;
252
- userId?: string;
253
- username?: string;
254
- tenantId?: string;
255
- };
256
- }) => {
257
- const account = resolveAccount(cfg as ClawdbotConfig);
258
- const conversationId = context?.conversationId || "";
259
-
260
- // Extract user info from context for response tracing
261
- const userInfo = context?.userId ? {
262
- userId: context.userId,
263
- username: context.username,
264
- tenantId: context.tenantId,
265
- } : undefined;
266
-
267
- const res = await fetch(`${account.serverUrl}/send`, {
268
- method: "POST",
269
- headers: {
270
- "Content-Type": "application/json",
271
- Authorization: `Bearer ${account.clientToken}`,
272
- },
273
- body: JSON.stringify({
274
- to: target,
275
- text: message,
276
- conversationId,
277
- user: userInfo,
278
- }),
279
- });
280
- const data = (await res.json()) as { ok?: boolean; message?: { id?: string } };
281
- return {
282
- ok: data.ok === true,
283
- messageId: data.message?.id,
284
- };
285
- },
286
- },
219
+ // Note: outbound.send is not available in WebSocket-only architecture
220
+ // All message sending is done through the WebSocket connection in monitor.ts
287
221
  };
@@ -2,8 +2,7 @@ import { z } from "zod";
2
2
 
3
3
  export const AdpOpenclawConfigSchema = z.object({
4
4
  enabled: z.boolean().optional(),
5
- serverUrl: z.string().optional(),
5
+ wsUrl: z.string().optional(), // WebSocket URL (optional, default: wss://wss.lke.cloud.tencent.com/bot/gateway/ws/)
6
6
  clientToken: z.string().optional(),
7
- hmacKey: z.string().optional(),
8
- pollIntervalMs: z.number().optional(),
7
+ signKey: z.string().optional(),
9
8
  });
package/src/monitor.ts CHANGED
@@ -5,11 +5,13 @@ import type { PluginLogger, ClawdbotConfig } from "openclaw/plugin-sdk";
5
5
  import { getAdpOpenclawRuntime } from "./runtime.js";
6
6
  import crypto from "crypto";
7
7
 
8
+ // WebSocket reconnect delay (fixed at 1 second)
9
+ const RECONNECT_DELAY_MS = 1000;
10
+
8
11
  export type MonitorParams = {
9
- serverUrl: string;
12
+ wsUrl: string; // WebSocket URL (direct, no conversion needed)
10
13
  clientToken: string;
11
- hmacKey?: string; // HMAC key for signature generation
12
- pollIntervalMs: number; // Used as reconnect delay
14
+ signKey?: string; // HMAC key for signature generation
13
15
  abortSignal?: AbortSignal;
14
16
  log?: PluginLogger;
15
17
  cfg?: ClawdbotConfig; // OpenClaw config for model settings
@@ -52,6 +54,7 @@ type UserInfo = {
52
54
  type InboundMessage = {
53
55
  id: string;
54
56
  conversationId: string;
57
+ recordId?: string; // Record ID from server for tracking message pairs
55
58
  clientId: string;
56
59
  from: string;
57
60
  to: string;
@@ -67,10 +70,10 @@ type AuthResultPayload = {
67
70
  };
68
71
 
69
72
  // Generate HMAC-SHA256 signature for authentication (includes timestamp for anti-replay)
70
- // Uses hmacKey as the HMAC key, and "token:nonce:timestamp" as the message
71
- function generateSignature(hmacKey: string, token: string, nonce: string, timestamp: number): string {
72
- // Use HMAC-SHA256 with hmacKey as the key, and "token:nonce:timestamp" as the message
73
- return crypto.createHmac("sha256", hmacKey).update(`${token}:${nonce}:${timestamp}`).digest("hex");
73
+ // Uses signKey as the HMAC key, and "token:nonce:timestamp" as the message
74
+ function generateSignature(signKey: string, token: string, nonce: string, timestamp: number): string {
75
+ // Use HMAC-SHA256 with signKey as the key, and "token:nonce:timestamp" as the message
76
+ return crypto.createHmac("sha256", signKey).update(`${token}:${nonce}:${timestamp}`).digest("hex");
74
77
  }
75
78
 
76
79
  // Generate random nonce
@@ -84,12 +87,9 @@ function generateRequestId(): string {
84
87
  }
85
88
 
86
89
  export async function monitorAdpOpenclaw(params: MonitorParams): Promise<void> {
87
- const { serverUrl, clientToken, hmacKey, pollIntervalMs, abortSignal, log, cfg } = params;
90
+ const { wsUrl, clientToken, signKey, abortSignal, log, cfg } = params;
88
91
  const runtime = getAdpOpenclawRuntime();
89
92
 
90
- // Convert HTTP URL to WebSocket URL
91
- const wsUrl = serverUrl.replace(/^http/, "ws") + "/ws";
92
-
93
93
  log?.info(`[adp-openclaw] WebSocket monitor started, connecting to ${wsUrl}`);
94
94
 
95
95
  while (!abortSignal?.aborted) {
@@ -97,8 +97,7 @@ export async function monitorAdpOpenclaw(params: MonitorParams): Promise<void> {
97
97
  await connectAndHandle({
98
98
  wsUrl,
99
99
  clientToken,
100
- hmacKey,
101
- serverUrl,
100
+ signKey,
102
101
  abortSignal,
103
102
  log,
104
103
  runtime,
@@ -111,8 +110,8 @@ export async function monitorAdpOpenclaw(params: MonitorParams): Promise<void> {
111
110
 
112
111
  // Wait before reconnecting
113
112
  if (!abortSignal?.aborted) {
114
- log?.info(`[adp-openclaw] Reconnecting in ${pollIntervalMs}ms...`);
115
- await sleep(pollIntervalMs, abortSignal);
113
+ log?.info(`[adp-openclaw] Reconnecting in ${RECONNECT_DELAY_MS}ms...`);
114
+ await sleep(RECONNECT_DELAY_MS, abortSignal);
116
115
  }
117
116
  }
118
117
 
@@ -122,8 +121,7 @@ export async function monitorAdpOpenclaw(params: MonitorParams): Promise<void> {
122
121
  type ConnectParams = {
123
122
  wsUrl: string;
124
123
  clientToken: string;
125
- hmacKey?: string;
126
- serverUrl: string;
124
+ signKey?: string;
127
125
  abortSignal?: AbortSignal;
128
126
  log?: PluginLogger;
129
127
  runtime: ReturnType<typeof getAdpOpenclawRuntime>;
@@ -131,7 +129,7 @@ type ConnectParams = {
131
129
  };
132
130
 
133
131
  async function connectAndHandle(params: ConnectParams): Promise<void> {
134
- const { wsUrl, clientToken, hmacKey, serverUrl, abortSignal, log, runtime, cfg } = params;
132
+ const { wsUrl, clientToken, signKey, abortSignal, log, runtime, cfg } = params;
135
133
 
136
134
  // Dynamic import for WebSocket (works in both Node.js and browser)
137
135
  const WebSocket = (await import("ws")).default;
@@ -154,17 +152,17 @@ async function connectAndHandle(params: ConnectParams): Promise<void> {
154
152
  // Send authentication message with signature (includes timestamp for anti-replay)
155
153
  const nonce = generateNonce();
156
154
  const timestamp = Date.now();
157
- // Generate signature only if hmacKey is provided
158
- const signature = hmacKey ? generateSignature(hmacKey, clientToken, nonce, timestamp) : "";
155
+ // Generate signature only if signKey is provided
156
+ const signature = signKey ? generateSignature(signKey, clientToken, nonce, timestamp) : "";
159
157
 
160
158
  const authMsg: WSMessage = {
161
159
  type: MsgType.Auth,
162
160
  requestId: generateRequestId(),
163
161
  payload: {
164
162
  token: clientToken,
165
- nonce: hmacKey ? nonce : undefined,
166
- signature: hmacKey ? signature : undefined,
167
- timestamp: hmacKey ? timestamp : undefined, // Include timestamp in payload for server verification
163
+ nonce: signKey ? nonce : undefined,
164
+ signature: signKey ? signature : undefined,
165
+ timestamp: signKey ? timestamp : undefined, // Include timestamp in payload for server verification
168
166
  },
169
167
  timestamp: Date.now(),
170
168
  };
@@ -208,7 +206,7 @@ async function connectAndHandle(params: ConnectParams): Promise<void> {
208
206
  if (!authenticated) break;
209
207
 
210
208
  const inMsg = msg.payload as InboundMessage;
211
- log?.info(`[adp-openclaw] Received: ${inMsg.from}: ${inMsg.text} (conv=${inMsg.conversationId}, user=${JSON.stringify(inMsg.user || {})})`);
209
+ log?.info(`[adp-openclaw] Received: ${inMsg.from}: ${inMsg.text} (conv=${inMsg.conversationId}, rec=${inMsg.recordId || 'none'}, user=${JSON.stringify(inMsg.user || {})})`);
212
210
 
213
211
  // Process the message with full user identity
214
212
  try {
@@ -288,6 +286,7 @@ async function connectAndHandle(params: ConnectParams): Promise<void> {
288
286
  to: inMsg.from,
289
287
  text: text,
290
288
  conversationId: inMsg.conversationId,
289
+ recordId: inMsg.recordId, // Pass recordId back to server
291
290
  streamId: streamId,
292
291
  totalChunks: chunkIndex,
293
292
  user: inMsg.user,
@@ -305,6 +304,7 @@ async function connectAndHandle(params: ConnectParams): Promise<void> {
305
304
  to: inMsg.from,
306
305
  text: text,
307
306
  conversationId: inMsg.conversationId,
307
+ recordId: inMsg.recordId, // Pass recordId back to server
308
308
  user: inMsg.user,
309
309
  },
310
310
  timestamp: Date.now(),
@@ -313,6 +313,7 @@ async function connectAndHandle(params: ConnectParams): Promise<void> {
313
313
  }
314
314
  };
315
315
 
316
+ log?.info(`[adp-openclaw] Starting dispatchReplyWithBufferedBlockDispatcher for ${displayName}`);
316
317
  await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
317
318
  ctx,
318
319
  cfg: cfg ?? {},
@@ -349,6 +350,7 @@ async function connectAndHandle(params: ConnectParams): Promise<void> {
349
350
  to: inMsg.from,
350
351
  chunk: delta, // Send only the new delta, not cumulative
351
352
  conversationId: inMsg.conversationId,
353
+ recordId: inMsg.recordId, // Pass recordId back to server
352
354
  streamId: streamId,
353
355
  index: chunkIndex,
354
356
  isPartial: true, // Mark as incremental delta
@@ -366,10 +368,10 @@ async function connectAndHandle(params: ConnectParams): Promise<void> {
366
368
  // SDK calls this with info.kind = "block" for streaming chunks, "final" for complete response
367
369
  deliver: async (payload: { text?: string }, info?: { kind?: string }) => {
368
370
  const text = payload.text || "";
369
- const kind = info?.kind || "final";
371
+ const kind = info?.kind;
370
372
 
371
- // Debug log for all deliver calls
372
- log?.debug?.(`[adp-openclaw] deliver called with kind=${kind}, text length=${text.length}`);
373
+ // Debug log for all deliver calls - log the actual info object
374
+ log?.info(`[adp-openclaw] deliver called: kind=${kind}, text.length=${text.length}, info=${JSON.stringify(info)}`);
373
375
 
374
376
  // Handle streaming block - send chunk via WebSocket
375
377
  if (kind === "block" && text) {
@@ -382,6 +384,7 @@ async function connectAndHandle(params: ConnectParams): Promise<void> {
382
384
  to: inMsg.from,
383
385
  chunk: text,
384
386
  conversationId: inMsg.conversationId,
387
+ recordId: inMsg.recordId, // Pass recordId back to server
385
388
  streamId: streamId,
386
389
  index: chunkIndex,
387
390
  user: inMsg.user,
@@ -400,9 +403,11 @@ async function connectAndHandle(params: ConnectParams): Promise<void> {
400
403
  return;
401
404
  }
402
405
 
403
- // Handle final reply - send outbound_end
404
- if (kind === "final") {
405
- sendOutboundEnd(text);
406
+ // Handle final reply or undefined kind - send outbound_end
407
+ // SDK may call deliver without kind when streaming ends
408
+ if (kind === "final" || kind === undefined) {
409
+ log?.info(`[adp-openclaw] deliver triggering sendOutboundEnd (kind=${kind})`);
410
+ sendOutboundEnd(text || lastPartialText);
406
411
  }
407
412
  },
408
413
  onError: (err: Error) => {
@@ -411,6 +416,8 @@ async function connectAndHandle(params: ConnectParams): Promise<void> {
411
416
  },
412
417
  });
413
418
 
419
+ log?.info(`[adp-openclaw] dispatchReplyWithBufferedBlockDispatcher returned (finalSent=${finalSent}, chunkIndex=${chunkIndex})`);
420
+
414
421
  // IMPORTANT: After dispatchReplyWithBufferedBlockDispatcher completes,
415
422
  // ensure outbound_end is sent even if "final" deliver was not called.
416
423
  // This handles cases where the SDK only sends blocks without a final callback.
package/src/runtime.ts CHANGED
@@ -7,9 +7,7 @@ let adpOpenclawRuntime: PluginRuntime | null = null;
7
7
  export type PluginConfig = {
8
8
  wsUrl?: string;
9
9
  clientToken?: string;
10
- hmacKey?: string;
11
- serverUrl?: string;
12
- pollIntervalMs?: number;
10
+ signKey?: string;
13
11
  };
14
12
 
15
13
  let pluginConfig: PluginConfig = {};
package/e2e_test.cjs DELETED
@@ -1,98 +0,0 @@
1
- const WebSocket = require('ws');
2
-
3
- // 测试配置
4
- const token = '334b209ed0833464202cc4438e7ce801583fc74c13384cbac60d02949f33e09c';
5
- const host = '101.32.33.231';
6
- const port = 9876;
7
-
8
- console.log('🧪 OpenClaw 发送消息测试');
9
- console.log(`📍 目标服务器: ${host}:${port}`);
10
- console.log(`🔑 Token: ${token.slice(0, 16)}...\n`);
11
-
12
- // Step 1: 测试 WebSocket 连接
13
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
14
- console.log('1️⃣ 连接 WebSocket...');
15
- const ws = new WebSocket(`ws://${host}:${port}/ws`);
16
-
17
- ws.on('open', () => {
18
- console.log(' ✅ WebSocket 连接成功');
19
- console.log(' 📤 发送认证请求...');
20
- ws.send(JSON.stringify({ type: 'auth', payload: { token, name: 'e2e-test-client' } }));
21
- });
22
-
23
- ws.on('message', (data) => {
24
- const msg = JSON.parse(data.toString());
25
- console.log(' 📥 收到消息:', msg.type);
26
-
27
- if (msg.type === 'auth_result') {
28
- console.log(' 完整响应:', JSON.stringify(msg.payload, null, 2));
29
- if (msg.payload.success) {
30
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
31
- console.log('2️⃣ 认证成功');
32
- console.log(' ClientID:', msg.payload.clientId);
33
-
34
- // Step 3: 发送 outbound 消息 (Bot 回复)
35
- setTimeout(() => {
36
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
37
- console.log('3️⃣ 发送 outbound 消息...');
38
-
39
- const requestId = 'req-' + Date.now() + '-test';
40
- const testMessage = {
41
- type: 'outbound',
42
- requestId: requestId,
43
- payload: {
44
- to: 'test-user',
45
- text: '🎉 这是来自 E2E 测试的消息!时间: ' + new Date().toLocaleString('zh-CN'),
46
- conversationId: 'conv-test-' + Date.now()
47
- },
48
- timestamp: Date.now()
49
- };
50
-
51
- console.log(' 发送内容:', JSON.stringify(testMessage, null, 2));
52
- ws.send(JSON.stringify(testMessage));
53
-
54
- console.log(' 📤 消息已发送,等待服务器确认 (ack)...');
55
- }, 500);
56
- } else {
57
- console.log(' ❌ 认证失败:', msg.payload.message || msg.payload.error);
58
- ws.close();
59
- }
60
- }
61
-
62
- // 处理服务器的响应
63
- if (msg.type === 'ack') {
64
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
65
- console.log('4️⃣ 收到服务器确认 (ack)');
66
- console.log(' 确认内容:', JSON.stringify(msg.payload, null, 2));
67
- if (msg.payload?.ok) {
68
- console.log(' ✅ 消息发送成功!');
69
- console.log(' 消息ID:', msg.payload.message?.id);
70
- } else {
71
- console.log(' ❌ 消息发送失败');
72
- }
73
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
74
- console.log('✅ 测试完成!');
75
- ws.close();
76
- }
77
-
78
- if (msg.type === 'error') {
79
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
80
- console.log('❌ 服务器错误:', JSON.stringify(msg.payload, null, 2));
81
- ws.close();
82
- }
83
- });
84
-
85
- ws.on('error', (err) => {
86
- console.log(' ❌ WebSocket 错误:', err.message);
87
- });
88
-
89
- ws.on('close', () => {
90
- console.log('\n📴 连接已关闭');
91
- process.exit(0);
92
- });
93
-
94
- // 超时处理
95
- setTimeout(() => {
96
- console.log('\n⏱️ 测试超时 (10秒)');
97
- ws.close();
98
- }, 10000);