adp-openclaw 0.0.4 → 0.0.6

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,6 +1,6 @@
1
1
  {
2
2
  "name": "adp-openclaw",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "OpenClaw demo channel plugin (Go WebSocket backend)",
5
5
  "type": "module",
6
6
  "dependencies": {
package/server.tar ADDED
Binary file
package/src/channel.ts CHANGED
@@ -12,7 +12,6 @@ export type AdpOpenclawChannelConfig = {
12
12
  enabled?: boolean;
13
13
  serverUrl?: string;
14
14
  wsUrl?: string;
15
- apiToken?: string;
16
15
  clientToken?: string;
17
16
  pollIntervalMs?: number;
18
17
  };
@@ -23,13 +22,13 @@ export type ResolvedAdpOpenclawAccount = {
23
22
  enabled: boolean;
24
23
  configured: boolean;
25
24
  serverUrl: string;
26
- apiToken: string;
25
+ clientToken: string;
27
26
  pollIntervalMs: number;
28
27
  };
29
28
 
30
29
  function resolveAdpOpenclawCredentials(channelCfg?: AdpOpenclawChannelConfig): {
31
30
  serverUrl: string;
32
- apiToken: string;
31
+ clientToken: string;
33
32
  pollIntervalMs: number;
34
33
  } | null {
35
34
  // Get serverUrl: support both serverUrl and wsUrl
@@ -45,23 +44,20 @@ function resolveAdpOpenclawCredentials(channelCfg?: AdpOpenclawChannelConfig): {
45
44
  serverUrl = process.env.ADP_OPENCLAW_SERVER_URL || "";
46
45
  }
47
46
 
48
- // Get apiToken: support both apiToken and clientToken
49
- let apiToken = channelCfg?.apiToken?.trim();
50
- if (!apiToken && channelCfg?.clientToken) {
51
- apiToken = channelCfg.clientToken.trim();
52
- }
53
- if (!apiToken) {
54
- apiToken = process.env.ADP_OPENCLAW_API_TOKEN || "";
47
+ // Get clientToken from config or env
48
+ let clientToken = channelCfg?.clientToken?.trim();
49
+ if (!clientToken) {
50
+ clientToken = process.env.ADP_OPENCLAW_CLIENT_TOKEN || "";
55
51
  }
56
52
 
57
53
  // Both are required for configured status
58
- if (!serverUrl || !apiToken) {
54
+ if (!serverUrl || !clientToken) {
59
55
  return null;
60
56
  }
61
57
 
62
58
  const pollIntervalMs = channelCfg?.pollIntervalMs ?? 1000;
63
59
 
64
- return { serverUrl, apiToken, pollIntervalMs };
60
+ return { serverUrl, clientToken, pollIntervalMs };
65
61
  }
66
62
 
67
63
  function resolveAccount(cfg: ClawdbotConfig, accountId?: string): ResolvedAdpOpenclawAccount {
@@ -75,7 +71,7 @@ function resolveAccount(cfg: ClawdbotConfig, accountId?: string): ResolvedAdpOpe
75
71
  enabled,
76
72
  configured: Boolean(creds),
77
73
  serverUrl: creds?.serverUrl || "http://localhost:9876",
78
- apiToken: creds?.apiToken || "",
74
+ clientToken: creds?.clientToken || "",
79
75
  pollIntervalMs: creds?.pollIntervalMs || 1000,
80
76
  };
81
77
  }
@@ -96,6 +92,11 @@ export const adpOpenclawPlugin: ChannelPlugin<ResolvedAdpOpenclawAccount> = {
96
92
  reactions: false,
97
93
  threads: false,
98
94
  media: false,
95
+ /**
96
+ * blockStreaming: true 启用 SDK 的块流式功能
97
+ * SDK 会通过 deliver 回调的 info.kind="block" 传递流式块
98
+ */
99
+ blockStreaming: true,
99
100
  },
100
101
  reload: { configPrefixes: ["channels.adp-openclaw"] },
101
102
  configSchema: {
@@ -106,7 +107,6 @@ export const adpOpenclawPlugin: ChannelPlugin<ResolvedAdpOpenclawAccount> = {
106
107
  enabled: { type: "boolean" },
107
108
  serverUrl: { type: "string" },
108
109
  wsUrl: { type: "string" },
109
- apiToken: { type: "string" },
110
110
  clientToken: { type: "string" },
111
111
  pollIntervalMs: { type: "integer", minimum: 100 },
112
112
  },
@@ -222,7 +222,7 @@ export const adpOpenclawPlugin: ChannelPlugin<ResolvedAdpOpenclawAccount> = {
222
222
  const { monitorAdpOpenclaw } = await import("./monitor.js");
223
223
  return monitorAdpOpenclaw({
224
224
  serverUrl: account.serverUrl,
225
- apiToken: account.apiToken,
225
+ clientToken: account.clientToken,
226
226
  pollIntervalMs: account.pollIntervalMs,
227
227
  abortSignal: ctx.abortSignal,
228
228
  log: ctx.log,
@@ -256,7 +256,7 @@ export const adpOpenclawPlugin: ChannelPlugin<ResolvedAdpOpenclawAccount> = {
256
256
  method: "POST",
257
257
  headers: {
258
258
  "Content-Type": "application/json",
259
- Authorization: `Bearer ${account.apiToken}`,
259
+ Authorization: `Bearer ${account.clientToken}`,
260
260
  },
261
261
  body: JSON.stringify({
262
262
  to: target,
@@ -3,6 +3,6 @@ import { z } from "zod";
3
3
  export const AdpOpenclawConfigSchema = z.object({
4
4
  enabled: z.boolean().optional(),
5
5
  serverUrl: z.string().optional(),
6
- apiToken: z.string().optional(),
6
+ clientToken: z.string().optional(),
7
7
  pollIntervalMs: z.number().optional(),
8
8
  });
package/src/monitor.ts CHANGED
@@ -7,7 +7,7 @@ import crypto from "crypto";
7
7
 
8
8
  export type MonitorParams = {
9
9
  serverUrl: string;
10
- apiToken: string;
10
+ clientToken: string;
11
11
  pollIntervalMs: number; // Used as reconnect delay
12
12
  abortSignal?: AbortSignal;
13
13
  log?: PluginLogger;
@@ -22,6 +22,8 @@ const MsgType = {
22
22
  Pong: "pong",
23
23
  Inbound: "inbound",
24
24
  Outbound: "outbound",
25
+ OutboundChunk: "outbound_chunk",
26
+ OutboundEnd: "outbound_end",
25
27
  Ack: "ack",
26
28
  Error: "error",
27
29
  ConvHistory: "conv_history",
@@ -63,9 +65,10 @@ type AuthResultPayload = {
63
65
  message?: string;
64
66
  };
65
67
 
66
- // Generate HMAC-SHA256 signature for extra security
67
- function generateSignature(token: string, nonce: string): string {
68
- return crypto.createHash("sha256").update(`${token}:${nonce}`).digest("hex");
68
+ // Generate HMAC-SHA256 signature for authentication (includes timestamp for anti-replay)
69
+ function generateSignature(token: string, nonce: string, timestamp: number): string {
70
+ // Use HMAC-SHA256 with token as the key, and "nonce:timestamp" as the message
71
+ return crypto.createHmac("sha256", token).update(`${nonce}:${timestamp}`).digest("hex");
69
72
  }
70
73
 
71
74
  // Generate random nonce
@@ -79,7 +82,7 @@ function generateRequestId(): string {
79
82
  }
80
83
 
81
84
  export async function monitorAdpOpenclaw(params: MonitorParams): Promise<void> {
82
- const { serverUrl, apiToken, pollIntervalMs, abortSignal, log, cfg } = params;
85
+ const { serverUrl, clientToken, pollIntervalMs, abortSignal, log, cfg } = params;
83
86
  const runtime = getAdpOpenclawRuntime();
84
87
 
85
88
  // Convert HTTP URL to WebSocket URL
@@ -91,7 +94,7 @@ export async function monitorAdpOpenclaw(params: MonitorParams): Promise<void> {
91
94
  try {
92
95
  await connectAndHandle({
93
96
  wsUrl,
94
- apiToken,
97
+ clientToken,
95
98
  serverUrl,
96
99
  abortSignal,
97
100
  log,
@@ -115,7 +118,7 @@ export async function monitorAdpOpenclaw(params: MonitorParams): Promise<void> {
115
118
 
116
119
  type ConnectParams = {
117
120
  wsUrl: string;
118
- apiToken: string;
121
+ clientToken: string;
119
122
  serverUrl: string;
120
123
  abortSignal?: AbortSignal;
121
124
  log?: PluginLogger;
@@ -124,7 +127,7 @@ type ConnectParams = {
124
127
  };
125
128
 
126
129
  async function connectAndHandle(params: ConnectParams): Promise<void> {
127
- const { wsUrl, apiToken, serverUrl, abortSignal, log, runtime, cfg } = params;
130
+ const { wsUrl, clientToken, serverUrl, abortSignal, log, runtime, cfg } = params;
128
131
 
129
132
  // Dynamic import for WebSocket (works in both Node.js and browser)
130
133
  const WebSocket = (await import("ws")).default;
@@ -144,19 +147,21 @@ async function connectAndHandle(params: ConnectParams): Promise<void> {
144
147
  ws.on("open", () => {
145
148
  log?.info(`[adp-openclaw] WebSocket connected, authenticating...`);
146
149
 
147
- // Send authentication message with signature
150
+ // Send authentication message with signature (includes timestamp for anti-replay)
148
151
  const nonce = generateNonce();
149
- const signature = generateSignature(apiToken, nonce);
152
+ const timestamp = Date.now();
153
+ const signature = generateSignature(clientToken, nonce, timestamp);
150
154
 
151
155
  const authMsg: WSMessage = {
152
156
  type: MsgType.Auth,
153
157
  requestId: generateRequestId(),
154
158
  payload: {
155
- token: apiToken,
159
+ token: clientToken,
156
160
  nonce,
157
161
  signature,
162
+ timestamp, // Include timestamp in payload for server verification
158
163
  },
159
- timestamp: Date.now(),
164
+ timestamp,
160
165
  };
161
166
 
162
167
  ws.send(JSON.stringify(authMsg));
@@ -257,29 +262,89 @@ async function connectAndHandle(params: ConnectParams): Promise<void> {
257
262
  ...userMetadata,
258
263
  });
259
264
 
265
+ // Generate unique stream ID for this response
266
+ const streamId = `stream-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
267
+ let chunkIndex = 0;
268
+ const displayName = inMsg.user?.username || inMsg.from;
269
+
260
270
  await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
261
271
  ctx,
262
272
  cfg: cfg ?? {},
263
273
  dispatcherOptions: {
264
- deliver: async (payload: { text: string }) => {
265
- const displayName = inMsg.user?.username || inMsg.from;
266
- log?.info(`[adp-openclaw] Sending reply to ${displayName}: ${payload.text.slice(0, 50)}...`);
267
-
268
- // Send via WebSocket
269
- const outMsg: WSMessage = {
270
- type: MsgType.Outbound,
271
- requestId: generateRequestId(),
272
- payload: {
273
- to: inMsg.from,
274
- text: payload.text,
275
- conversationId: inMsg.conversationId,
276
- // Include user info in response for tracing
277
- user: inMsg.user,
278
- },
279
- timestamp: Date.now(),
280
- };
281
-
282
- ws.send(JSON.stringify(outMsg));
274
+ // Unified deliver callback - handles both streaming blocks and final reply
275
+ // SDK calls this with info.kind = "block" for streaming chunks, "final" for complete response
276
+ deliver: async (payload: { text?: string }, info?: { kind?: string }) => {
277
+ const text = payload.text || "";
278
+ const kind = info?.kind || "final";
279
+
280
+ // Handle streaming block - send chunk via WebSocket
281
+ if (kind === "block" && text) {
282
+ log?.debug?.(`[adp-openclaw] Streaming block[${chunkIndex}] to ${displayName}: ${text.slice(0, 30)}...`);
283
+
284
+ const chunkMsg: WSMessage = {
285
+ type: MsgType.OutboundChunk,
286
+ requestId: generateRequestId(),
287
+ payload: {
288
+ to: inMsg.from,
289
+ chunk: text,
290
+ conversationId: inMsg.conversationId,
291
+ streamId: streamId,
292
+ index: chunkIndex,
293
+ user: inMsg.user,
294
+ },
295
+ timestamp: Date.now(),
296
+ };
297
+
298
+ ws.send(JSON.stringify(chunkMsg));
299
+ chunkIndex++;
300
+ return;
301
+ }
302
+
303
+ // Handle tool result - log but don't send separately
304
+ if (kind === "tool") {
305
+ log?.debug?.(`[adp-openclaw] Tool result received for ${displayName}`);
306
+ return;
307
+ }
308
+
309
+ // Handle final reply
310
+ if (kind === "final") {
311
+ log?.info(`[adp-openclaw] Sending final reply to ${displayName}: ${text.slice(0, 50)}...`);
312
+
313
+ // If we have streaming chunks, send stream end message
314
+ if (chunkIndex > 0) {
315
+ const endMsg: WSMessage = {
316
+ type: MsgType.OutboundEnd,
317
+ requestId: generateRequestId(),
318
+ payload: {
319
+ to: inMsg.from,
320
+ text: text,
321
+ conversationId: inMsg.conversationId,
322
+ streamId: streamId,
323
+ totalChunks: chunkIndex,
324
+ user: inMsg.user,
325
+ },
326
+ timestamp: Date.now(),
327
+ };
328
+
329
+ ws.send(JSON.stringify(endMsg));
330
+ } else {
331
+ // No streaming chunks were sent, send as regular outbound message
332
+ const outMsg: WSMessage = {
333
+ type: MsgType.Outbound,
334
+ requestId: generateRequestId(),
335
+ payload: {
336
+ to: inMsg.from,
337
+ text: text,
338
+ conversationId: inMsg.conversationId,
339
+ // Include user info in response for tracing
340
+ user: inMsg.user,
341
+ },
342
+ timestamp: Date.now(),
343
+ };
344
+
345
+ ws.send(JSON.stringify(outMsg));
346
+ }
347
+ }
283
348
  },
284
349
  onError: (err: Error) => {
285
350
  log?.error(`[adp-openclaw] Reply error: ${err.message}`);
package/src/runtime.ts CHANGED
@@ -8,7 +8,6 @@ export type PluginConfig = {
8
8
  wsUrl?: string;
9
9
  clientToken?: string;
10
10
  serverUrl?: string;
11
- apiToken?: string;
12
11
  pollIntervalMs?: number;
13
12
  };
14
13