@newbase-clawchat/openclaw-clawchat 2026.5.4 → 2026.5.12-13

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.
Files changed (85) hide show
  1. package/INSTALL.md +64 -0
  2. package/README.md +121 -19
  3. package/dist/index.js +10 -19
  4. package/dist/setup-entry.js +3 -0
  5. package/dist/src/api-client.js +78 -10
  6. package/dist/src/api-types.test-d.js +10 -0
  7. package/dist/src/channel.js +25 -156
  8. package/dist/src/channel.setup.js +120 -0
  9. package/dist/src/client.js +37 -41
  10. package/dist/src/config.js +75 -17
  11. package/dist/src/inbound.js +79 -61
  12. package/dist/src/login.runtime.js +84 -19
  13. package/dist/src/media-runtime.js +8 -8
  14. package/dist/src/message-mapper.js +1 -1
  15. package/dist/src/mock-transport.js +31 -0
  16. package/dist/src/outbound.js +410 -26
  17. package/dist/src/protocol-types.js +63 -0
  18. package/dist/src/protocol-types.typecheck.js +1 -0
  19. package/dist/src/protocol.js +2 -7
  20. package/dist/src/reply-dispatcher.js +157 -54
  21. package/dist/src/runtime.js +795 -119
  22. package/dist/src/storage.js +689 -0
  23. package/dist/src/tools-schema.js +98 -16
  24. package/dist/src/tools.js +422 -135
  25. package/dist/src/ws-alignment.js +178 -0
  26. package/dist/src/ws-client.js +588 -0
  27. package/dist/src/ws-log.js +19 -0
  28. package/index.ts +10 -22
  29. package/openclaw.plugin.json +37 -2
  30. package/package.json +17 -4
  31. package/setup-entry.ts +4 -0
  32. package/skills/clawchat/SKILL.md +88 -0
  33. package/src/api-client.test.ts +274 -14
  34. package/src/api-client.ts +138 -23
  35. package/src/api-types.test-d.ts +12 -0
  36. package/src/api-types.ts +90 -4
  37. package/src/buffered-stream.test.ts +14 -12
  38. package/src/buffered-stream.ts +1 -1
  39. package/src/channel.outbound.test.ts +269 -60
  40. package/src/channel.setup.ts +146 -0
  41. package/src/channel.test.ts +130 -24
  42. package/src/channel.ts +30 -186
  43. package/src/client.test.ts +197 -11
  44. package/src/client.ts +50 -57
  45. package/src/config.test.ts +108 -6
  46. package/src/config.ts +95 -24
  47. package/src/inbound.test.ts +288 -37
  48. package/src/inbound.ts +96 -84
  49. package/src/login.runtime.test.ts +347 -13
  50. package/src/login.runtime.ts +105 -23
  51. package/src/manifest.test.ts +146 -74
  52. package/src/media-runtime.test.ts +57 -2
  53. package/src/media-runtime.ts +26 -17
  54. package/src/message-mapper.test.ts +2 -2
  55. package/src/message-mapper.ts +2 -2
  56. package/src/mock-transport.test.ts +35 -0
  57. package/src/mock-transport.ts +38 -0
  58. package/src/outbound.test.ts +694 -73
  59. package/src/outbound.ts +484 -31
  60. package/src/plugin-entry.test.ts +1 -0
  61. package/src/protocol-types.test.ts +69 -0
  62. package/src/protocol-types.ts +296 -0
  63. package/src/protocol-types.typecheck.ts +89 -0
  64. package/src/protocol.test.ts +1 -6
  65. package/src/protocol.ts +2 -7
  66. package/src/reply-dispatcher.test.ts +819 -119
  67. package/src/reply-dispatcher.ts +202 -60
  68. package/src/runtime.test.ts +2120 -41
  69. package/src/runtime.ts +935 -142
  70. package/src/scripts.test.ts +85 -0
  71. package/src/storage.test.ts +793 -0
  72. package/src/storage.ts +1095 -0
  73. package/src/streaming.test.ts +9 -8
  74. package/src/streaming.ts +1 -1
  75. package/src/tools-schema.ts +148 -20
  76. package/src/tools.test.ts +377 -50
  77. package/src/tools.ts +574 -154
  78. package/src/ws-alignment.test.ts +103 -0
  79. package/src/ws-alignment.ts +275 -0
  80. package/src/ws-client.test.ts +1218 -0
  81. package/src/ws-client.ts +662 -0
  82. package/src/ws-log.test.ts +32 -0
  83. package/src/ws-log.ts +31 -0
  84. package/skills/clawchat-account-tools/SKILL.md +0 -26
  85. package/skills/clawchat-activate/SKILL.md +0 -47
@@ -1,12 +1,14 @@
1
- import { MockTransport } from "@newbase-clawchat/sdk";
2
1
  import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
3
- import { describe, expect, it } from "vitest";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { MockTransport } from "./mock-transport.ts";
4
+ import type { ClawlingChatClient } from "./ws-client.ts";
4
5
  import {
5
6
  createOpenclawClawlingClient,
6
7
  emitStreamCreated,
7
8
  emitStreamAdd,
8
9
  emitStreamDone,
9
10
  emitStreamFailed,
11
+ emitFinalStreamReply,
10
12
  } from "./client.ts";
11
13
  import type { ResolvedOpenclawClawlingAccount } from "./config.ts";
12
14
 
@@ -20,6 +22,7 @@ function baseAccount(): ResolvedOpenclawClawlingAccount {
20
22
  baseUrl: "",
21
23
  token: "t",
22
24
  userId: "agent-1",
25
+ ownerUserId: "owner-1",
23
26
  replyMode: "static",
24
27
  forwardThinking: true,
25
28
  forwardToolCalls: false,
@@ -37,11 +40,7 @@ function baseAccount(): ResolvedOpenclawClawlingAccount {
37
40
  }
38
41
 
39
42
  async function authenticate(transport: MockTransport) {
40
- // Yield to allow MockTransport.connect's internal Promise.resolve() to fire
41
- // onOpen (which transitions state: connecting -> challenging) before we send
42
- // the challenge envelope.
43
43
  await Promise.resolve();
44
- // Challenge from server
45
44
  transport.emitInbound(
46
45
  JSON.stringify({
47
46
  version: "2",
@@ -51,12 +50,14 @@ async function authenticate(transport: MockTransport) {
51
50
  payload: { nonce: "nonce-1" },
52
51
  }),
53
52
  );
54
- // hello-ok
53
+ const connectFrame = transport.sent
54
+ .map((raw) => JSON.parse(raw))
55
+ .find((env) => env.event === "connect");
55
56
  transport.emitInbound(
56
57
  JSON.stringify({
57
58
  version: "2",
58
59
  event: "hello-ok",
59
- trace_id: "t-hello",
60
+ trace_id: connectFrame.trace_id,
60
61
  emitted_at: Date.now(),
61
62
  payload: {},
62
63
  }),
@@ -74,6 +75,103 @@ describe("openclaw-clawchat client", () => {
74
75
  client.close();
75
76
  });
76
77
 
78
+ it("sends msghub connect payload without signature fields", async () => {
79
+ const transport = new MockTransport();
80
+ const client = createOpenclawClawlingClient(baseAccount(), { transport });
81
+ const p = client.connect();
82
+
83
+ await Promise.resolve();
84
+ transport.emitInbound(
85
+ JSON.stringify({
86
+ version: "2",
87
+ event: "connect.challenge",
88
+ trace_id: "challenge-1",
89
+ emitted_at: Date.now(),
90
+ payload: { nonce: "nonce-1" },
91
+ }),
92
+ );
93
+
94
+ const connectFrame = transport.sent
95
+ .map((raw) => JSON.parse(raw))
96
+ .find((env) => env.event === "connect");
97
+ expect(connectFrame.payload).toMatchObject({
98
+ token: "t",
99
+ nonce: "nonce-1",
100
+ });
101
+ expect(connectFrame.payload).not.toHaveProperty("sign");
102
+ expect(connectFrame.payload).not.toHaveProperty("signature");
103
+ expect(connectFrame.payload).not.toHaveProperty("client_id");
104
+ expect(connectFrame.payload).not.toHaveProperty("client_version");
105
+
106
+ transport.emitInbound(
107
+ JSON.stringify({
108
+ version: "2",
109
+ event: "hello-ok",
110
+ trace_id: connectFrame.trace_id,
111
+ emitted_at: Date.now(),
112
+ payload: {},
113
+ }),
114
+ );
115
+ await p;
116
+ client.close();
117
+ });
118
+
119
+ it("reports connect frames after the transport records them", async () => {
120
+ const transport = new MockTransport();
121
+ const onConnectFrameSent = vi.fn((env: { trace_id?: unknown }) => {
122
+ const sentConnect = transport.sent
123
+ .map((raw) => JSON.parse(raw))
124
+ .find((frame) => frame.event === "connect");
125
+ expect(sentConnect?.trace_id).toBe(env.trace_id);
126
+ });
127
+ const client = createOpenclawClawlingClient(baseAccount(), {
128
+ transport,
129
+ wsLifecycle: { onConnectFrameSent },
130
+ });
131
+
132
+ const connected = client.connect();
133
+ await authenticate(transport);
134
+ await connected;
135
+
136
+ expect(onConnectFrameSent).toHaveBeenCalledTimes(1);
137
+ client.close();
138
+ });
139
+
140
+ it("reports connect frames only after they are sent", async () => {
141
+ const transport = new MockTransport();
142
+ const onConnectFrameSent = vi.fn();
143
+ const originalSend = transport.send.bind(transport);
144
+ transport.send = vi.fn((wire: string) => {
145
+ const env = JSON.parse(wire);
146
+ if (env.event === "connect") throw new Error("send failed");
147
+ originalSend(wire);
148
+ });
149
+ const client = createOpenclawClawlingClient(baseAccount(), {
150
+ transport,
151
+ wsLifecycle: { onConnectFrameSent },
152
+ });
153
+
154
+ const connected = client.connect();
155
+ await Promise.resolve();
156
+ transport.emitInbound(
157
+ JSON.stringify({
158
+ version: "2",
159
+ event: "connect.challenge",
160
+ trace_id: "challenge-1",
161
+ emitted_at: Date.now(),
162
+ payload: { nonce: "nonce-1" },
163
+ }),
164
+ );
165
+
166
+ await expect(connected).rejects.toThrow(/send failed/);
167
+ expect(onConnectFrameSent).not.toHaveBeenCalled();
168
+ });
169
+
170
+ it("creates a local production transport when no override is supplied", () => {
171
+ const client = createOpenclawClawlingClient(baseAccount());
172
+ expect(client.transportState).toBe("closed");
173
+ });
174
+
77
175
  it("emitStreamCreated emits a minimal message.created envelope with just message_id", async () => {
78
176
  const transport = new MockTransport();
79
177
  const client = createOpenclawClawlingClient(baseAccount(), { transport });
@@ -85,7 +183,7 @@ describe("openclaw-clawchat client", () => {
85
183
  emitStreamCreated(client, {
86
184
  messageId: "msg-1",
87
185
  to: { id: "user-1", type: "direct" },
88
- sender: { sender_id: "agent-1", type: "direct", display_name: "Clawling Assistant" },
186
+ sender: { id: "agent-1", type: "direct", nick_name: "Clawling Assistant" },
89
187
  });
90
188
 
91
189
  expect(transport.sent).toHaveLength(1);
@@ -100,6 +198,66 @@ describe("openclaw-clawchat client", () => {
100
198
  client.close();
101
199
  });
102
200
 
201
+ it("uses local emitRaw with chat_id routing for stream lifecycle frames", () => {
202
+ const transportSend = vi.fn();
203
+ const emitRaw = vi.fn();
204
+ const client = {
205
+ opts: {
206
+ transport: { send: transportSend },
207
+ traceIdFactory: () => "trace-raw",
208
+ },
209
+ emitRaw,
210
+ } as unknown as ClawlingChatClient;
211
+
212
+ emitStreamCreated(client, {
213
+ messageId: "msg-1",
214
+ routing: { chatId: "chat-1", chatType: "group" },
215
+ });
216
+
217
+ expect(emitRaw).toHaveBeenCalledWith(
218
+ "message.created",
219
+ { message_id: "msg-1" },
220
+ { chat_id: "chat-1" },
221
+ );
222
+ expect(transportSend).not.toHaveBeenCalled();
223
+ });
224
+
225
+ it("uses local emitRaw with chat_id routing when raw transport is unavailable", () => {
226
+ const emitRaw = vi.fn();
227
+ const client = { emitRaw } as unknown as ClawlingChatClient;
228
+
229
+ emitStreamCreated(client, {
230
+ messageId: "msg-1",
231
+ routing: { chatId: "chat-1", chatType: "direct" },
232
+ });
233
+
234
+ expect(emitRaw).toHaveBeenCalledWith(
235
+ "message.created",
236
+ { message_id: "msg-1" },
237
+ { chat_id: "chat-1" },
238
+ );
239
+ });
240
+
241
+ it("requires raw transport for final stream replies that carry a client message_id", () => {
242
+ const emitRaw = vi.fn();
243
+ const client = { emitRaw } as unknown as ClawlingChatClient;
244
+
245
+ expect(() =>
246
+ emitFinalStreamReply(client, {
247
+ messageId: "agent-stream-1",
248
+ routing: { chatId: "chat-1", chatType: "direct" },
249
+ replyTo: {
250
+ msgId: "user-msg-1",
251
+ previewId: "user-1",
252
+ nickName: "User 1",
253
+ fragments: [{ kind: "text", text: "hello" }],
254
+ },
255
+ body: { fragments: [{ kind: "text", text: "reply" }] },
256
+ }),
257
+ ).toThrow(/raw transport/);
258
+ expect(emitRaw).not.toHaveBeenCalled();
259
+ });
260
+
103
261
  it("emitStreamAdd emits message.add with fragments: [{ text: full, delta: new }]", async () => {
104
262
  const transport = new MockTransport();
105
263
  const client = createOpenclawClawlingClient(baseAccount(), { transport });
@@ -154,7 +312,7 @@ describe("openclaw-clawchat client", () => {
154
312
  client.close();
155
313
  });
156
314
 
157
- it("emitStreamFailed emits message.failed with reason", async () => {
315
+ it("emitStreamFailed emits message.failed with StreamDonePayload shape", async () => {
158
316
  const transport = new MockTransport();
159
317
  const client = createOpenclawClawlingClient(baseAccount(), { transport });
160
318
  const p = client.connect();
@@ -172,11 +330,39 @@ describe("openclaw-clawchat client", () => {
172
330
  const env = JSON.parse(transport.sent[0]!);
173
331
  expect(env.event).toBe("message.failed");
174
332
  expect(env.payload.message_id).toBe("msg-1");
175
- expect(env.payload.reason).toBe("upstream_error");
333
+ expect(env.payload).not.toHaveProperty("reason");
334
+ expect(env.payload).not.toHaveProperty("sequence");
176
335
  expect(env.payload.fragments).toEqual([{ kind: "text", text: "upstream_error" }]);
177
336
  expect(env.payload.streaming.status).toBe("failed");
337
+ expect(env.payload.streaming.sequence).toBe(4);
178
338
  expect(env.payload.streaming.completed_at).toBe(env.payload.completed_at);
179
339
  expect(env.payload).not.toHaveProperty("failed_at");
180
340
  client.close();
181
341
  });
342
+
343
+ it("emitStreamFailed emits empty fragments when reason is omitted", async () => {
344
+ const transport = new MockTransport();
345
+ const client = createOpenclawClawlingClient(baseAccount(), { transport });
346
+ const p = client.connect();
347
+ await authenticate(transport);
348
+ await p;
349
+ transport.sent.length = 0;
350
+
351
+ emitStreamFailed(client, {
352
+ messageId: "msg-1",
353
+ to: { id: "user-1", type: "direct" },
354
+ sequence: 4,
355
+ });
356
+
357
+ const env = JSON.parse(transport.sent[0]!);
358
+ expect(env.event).toBe("message.failed");
359
+ expect(env.payload).toMatchObject({
360
+ message_id: "msg-1",
361
+ fragments: [],
362
+ completed_at: expect.any(Number),
363
+ });
364
+ expect(env.payload).not.toHaveProperty("reason");
365
+ expect(env.payload).not.toHaveProperty("sequence");
366
+ client.close();
367
+ });
182
368
  });
package/src/client.ts CHANGED
@@ -1,38 +1,36 @@
1
- import {
2
- createWSClient,
3
- type ClawlingChatClient,
4
- type CreateWSClientOptions,
5
- type Fragment,
6
- type Transport,
7
- } from "@newbase-clawchat/sdk";
1
+ import type { ChatType, Envelope, Fragment, Transport } from "./protocol-types.ts";
2
+ import { createClawChatClient, type ClawlingChatClient } from "./ws-client.ts";
8
3
  import type { ResolvedOpenclawClawlingAccount } from "./config.ts";
9
4
 
5
+ export type { ChatType } from "./protocol-types.ts";
6
+
10
7
  export interface CreateClientOverrides {
11
8
  /** Transport override — only intended for tests (e.g. MockTransport). */
12
9
  transport?: Transport;
13
- logger?: CreateWSClientOptions["logger"];
10
+ wsLifecycle?: {
11
+ onConnectFrameSent?: (env: {
12
+ trace_id?: unknown;
13
+ payload?: { device_id?: unknown };
14
+ }) => void;
15
+ };
14
16
  }
15
17
 
16
18
  export function createOpenclawClawlingClient(
17
19
  account: ResolvedOpenclawClawlingAccount,
18
20
  overrides: CreateClientOverrides = {},
19
21
  ): ClawlingChatClient {
20
- // Only forward a finite `maxRetries` to the SDK — the SDK's own default
21
- // is already unbounded, so omitting the field keeps that behavior. This
22
- // avoids forcing the SDK to special-case `Infinity`.
23
- const maxRetries = account.reconnect.maxRetries;
24
- const reconnect: CreateWSClientOptions["reconnect"] = {
25
- enabled: true,
26
- initialDelay: account.reconnect.initialDelay,
27
- maxDelay: account.reconnect.maxDelay,
28
- jitterRatio: account.reconnect.jitterRatio,
29
- ...(Number.isFinite(maxRetries) ? { maxRetries } : {}),
30
- };
31
-
32
- const options: CreateWSClientOptions = {
22
+ const client = createClawChatClient({
33
23
  url: account.websocketUrl,
34
24
  token: account.token,
35
- reconnect,
25
+ deviceId: account.userId,
26
+ ...(overrides.transport ? { transport: overrides.transport } : {}),
27
+ reconnect: {
28
+ enabled: true,
29
+ initialDelay: account.reconnect.initialDelay,
30
+ maxDelay: account.reconnect.maxDelay,
31
+ jitterRatio: account.reconnect.jitterRatio,
32
+ maxRetries: account.reconnect.maxRetries,
33
+ },
36
34
  heartbeat: {
37
35
  enabled: true,
38
36
  interval: account.heartbeat.interval,
@@ -42,20 +40,24 @@ export function createOpenclawClawlingClient(
42
40
  timeout: account.ack.timeout,
43
41
  autoResendOnTimeout: account.ack.autoResendOnTimeout,
44
42
  },
45
- // Buffer outbound sends during the tiny reconnect window so an inbound
46
- // message isn't silently dropped while the socket is flapping.
47
- queueWhileReconnecting: true,
48
- ...(overrides.transport ? { transport: overrides.transport } : {}),
49
- ...(overrides.logger ? { logger: overrides.logger } : {}),
50
- };
51
- return createWSClient(options);
43
+ });
44
+ if (overrides.wsLifecycle?.onConnectFrameSent) {
45
+ const sendRawEnvelope = client.sendRawEnvelope.bind(client);
46
+ client.sendRawEnvelope = (env: Envelope) => {
47
+ sendRawEnvelope(env);
48
+ if (env.event === "connect") {
49
+ overrides.wsLifecycle?.onConnectFrameSent?.(
50
+ env as { trace_id?: unknown; payload?: { device_id?: unknown } },
51
+ );
52
+ }
53
+ };
54
+ }
55
+ return client;
52
56
  }
53
57
 
54
- export type ChatType = "direct" | "group";
55
-
56
58
  export interface StreamSender {
57
59
  id: string;
58
- type: ChatType;
60
+ type: "direct";
59
61
  nick_name: string;
60
62
  }
61
63
 
@@ -76,35 +78,32 @@ function normalizeRouting(params: {
76
78
  }
77
79
 
78
80
  /**
79
- * Emit a raw v2 envelope directly over the transport so we can carry top-level
80
- * `chat_id` routing without SDK-injected `to` metadata.
81
+ * Emit a raw v2 envelope through the local client so stream helpers carry
82
+ * top-level `chat_id` routing without legacy `to` metadata.
81
83
  */
82
84
  function emitEnvelope(
83
85
  client: ClawlingChatClient,
84
86
  event: string,
85
87
  payload: object,
86
88
  routing: EnvelopeRouting,
89
+ options: { forceRawTransport?: boolean } = {},
87
90
  ): void {
88
- const inner = client as unknown as {
89
- opts?: {
90
- transport: { send: (data: string) => void };
91
- traceIdFactory: () => string;
92
- };
93
- emitRaw?: (event: string, payload: object, routing?: { to?: { id: string; type: ChatType } }) => void;
94
- };
95
- if (!inner.opts?.transport) {
96
- inner.emitRaw?.(event, payload, { to: { id: routing.chatId, type: routing.chatType } });
91
+ if (!options.forceRawTransport) {
92
+ client.emitRaw(event, payload, { chat_id: routing.chatId });
97
93
  return;
98
94
  }
99
- const env = {
95
+ if (typeof client.nextTraceId !== "function" || typeof client.sendRawEnvelope !== "function") {
96
+ throw new Error("openclaw-clawchat streaming emit requires local raw transport");
97
+ }
98
+ const env: Envelope = {
100
99
  version: "2" as const,
101
100
  event,
102
- trace_id: inner.opts.traceIdFactory(),
101
+ trace_id: client.nextTraceId(),
103
102
  emitted_at: Date.now(),
104
103
  chat_id: routing.chatId,
105
104
  payload,
106
105
  };
107
- inner.opts.transport.send(JSON.stringify(env));
106
+ client.sendRawEnvelope(env);
108
107
  }
109
108
 
110
109
  /**
@@ -219,10 +218,8 @@ export function emitStreamDone(
219
218
  * the same `payload.message_id` as the preceding `message.created` /
220
219
  * `message.add` / `message.done` frames.
221
220
  *
222
- * The SDK's high-level `client.replyMessage()` disallows `payload.message_id`
223
- * on outbound replies (the server normally assigns one via ack); for the
224
- * streaming-finalize use case the backend expects the correlated id, so we
225
- * bypass the SDK validator and write directly to the transport.
221
+ * Final stream replies include the correlated `payload.message_id`, so they
222
+ * use the local raw-envelope API instead of any higher-level ackable send.
226
223
  */
227
224
  export function emitFinalStreamReply(
228
225
  client: ClawlingChatClient,
@@ -265,6 +262,7 @@ export function emitFinalStreamReply(
265
262
  },
266
263
  },
267
264
  routing,
265
+ { forceRawTransport: true },
268
266
  );
269
267
  }
270
268
 
@@ -280,18 +278,13 @@ export function emitStreamFailed(
280
278
  ): void {
281
279
  const now = Date.now();
282
280
  const routing = normalizeRouting(params);
283
- const reason = params.reason ?? "unknown";
284
- const reasonFragment = params.reason?.trim()
285
- ? { fragments: [{ kind: "text", text: params.reason.trim() }] }
286
- : {};
281
+ const reasonText = params.reason?.trim();
287
282
  emitEnvelope(
288
283
  client,
289
284
  "message.failed",
290
285
  {
291
286
  message_id: params.messageId,
292
- sequence: params.sequence,
293
- reason,
294
- ...reasonFragment,
287
+ fragments: reasonText ? [{ kind: "text", text: reasonText }] : [],
295
288
  streaming: {
296
289
  status: "failed",
297
290
  sequence: params.sequence,
@@ -5,6 +5,7 @@ import {
5
5
  DEFAULT_BASE_URL,
6
6
  DEFAULT_WEBSOCKET_URL,
7
7
  DEFAULT_STREAM,
8
+ effectiveGroupMode,
8
9
  mergeOpenclawClawchatToolAllow,
9
10
  resolveOpenclawClawlingAccount,
10
11
  listOpenclawClawlingAccountIds,
@@ -21,6 +22,7 @@ describe("openclaw-clawchat config", () => {
21
22
  expect(account.enabled).toBe(true);
22
23
  expect(account.configured).toBe(false);
23
24
  expect(account.replyMode).toBe("static");
25
+ expect(account.groupMode).toBe("all");
24
26
  expect(account.forwardThinking).toBe(true);
25
27
  expect(account.forwardToolCalls).toBe(false);
26
28
  expect(account.richInteractions).toBe(false);
@@ -34,6 +36,7 @@ describe("openclaw-clawchat config", () => {
34
36
  websocketUrl: "wss://chat.example.com/ws",
35
37
  token: "secret",
36
38
  userId: "agent-1",
39
+ ownerUserId: "owner-1",
37
40
  replyMode: "stream",
38
41
  forwardThinking: false,
39
42
  forwardToolCalls: true,
@@ -47,6 +50,7 @@ describe("openclaw-clawchat config", () => {
47
50
  expect(account.websocketUrl).toBe("wss://chat.example.com/ws");
48
51
  expect(account.token).toBe("secret");
49
52
  expect(account.userId).toBe("agent-1");
53
+ expect(account.ownerUserId).toBe("owner-1");
50
54
  expect(account.replyMode).toBe("stream");
51
55
  expect(account.forwardThinking).toBe(false);
52
56
  expect(account.forwardToolCalls).toBe(true);
@@ -62,6 +66,7 @@ describe("openclaw-clawchat config", () => {
62
66
  {
63
67
  CLAWCHAT_TOKEN: "env-token",
64
68
  CLAWCHAT_USER_ID: "env-user",
69
+ CLAWCHAT_OWNER_USER_ID: "env-owner",
65
70
  CLAWCHAT_BASE_URL: "https://api.env.example",
66
71
  CLAWCHAT_WEBSOCKET_URL: "wss://ws.env.example/ws",
67
72
  },
@@ -70,6 +75,7 @@ describe("openclaw-clawchat config", () => {
70
75
  expect(account.configured).toBe(true);
71
76
  expect(account.token).toBe("env-token");
72
77
  expect(account.userId).toBe("env-user");
78
+ expect(account.ownerUserId).toBe("env-owner");
73
79
  expect(account.baseUrl).toBe("https://api.env.example");
74
80
  expect(account.websocketUrl).toBe("wss://ws.env.example/ws");
75
81
  });
@@ -81,6 +87,7 @@ describe("openclaw-clawchat config", () => {
81
87
  "openclaw-clawchat": {
82
88
  token: "config-token",
83
89
  userId: "config-user",
90
+ ownerUserId: "config-owner",
84
91
  baseUrl: "https://api.config.example",
85
92
  websocketUrl: "wss://ws.config.example/ws",
86
93
  },
@@ -89,6 +96,7 @@ describe("openclaw-clawchat config", () => {
89
96
  {
90
97
  CLAWCHAT_TOKEN: "env-token",
91
98
  CLAWCHAT_USER_ID: "env-user",
99
+ CLAWCHAT_OWNER_USER_ID: "env-owner",
92
100
  CLAWCHAT_BASE_URL: "https://api.env.example",
93
101
  CLAWCHAT_WEBSOCKET_URL: "wss://ws.env.example/ws",
94
102
  },
@@ -96,6 +104,7 @@ describe("openclaw-clawchat config", () => {
96
104
 
97
105
  expect(account.token).toBe("config-token");
98
106
  expect(account.userId).toBe("config-user");
107
+ expect(account.ownerUserId).toBe("config-owner");
99
108
  expect(account.baseUrl).toBe("https://api.config.example");
100
109
  expect(account.websocketUrl).toBe("wss://ws.config.example/ws");
101
110
  });
@@ -103,13 +112,104 @@ describe("openclaw-clawchat config", () => {
103
112
  it("falls back to static replyMode for unknown values", () => {
104
113
  const cfg = {
105
114
  channels: {
106
- "openclaw-clawchat": { websocketUrl: "w", token: "t", userId: "a", replyMode: "weird" },
115
+ "openclaw-clawchat": {
116
+ websocketUrl: "w",
117
+ token: "t",
118
+ userId: "a",
119
+ ownerUserId: "o",
120
+ replyMode: "weird",
121
+ },
107
122
  },
108
123
  };
109
124
  const account = resolveOpenclawClawlingAccount(cfg);
110
125
  expect(account.replyMode).toBe("static");
111
126
  });
112
127
 
128
+ it("preserves explicit mention groupMode", () => {
129
+ const cfg = {
130
+ channels: {
131
+ "openclaw-clawchat": {
132
+ websocketUrl: "w",
133
+ token: "t",
134
+ userId: "a",
135
+ ownerUserId: "o",
136
+ groupMode: "mention",
137
+ },
138
+ },
139
+ };
140
+ const account = resolveOpenclawClawlingAccount(cfg);
141
+ expect(account.groupMode).toBe("mention");
142
+ });
143
+
144
+ it("falls back to all groupMode for unknown values", () => {
145
+ const cfg = {
146
+ channels: {
147
+ "openclaw-clawchat": {
148
+ websocketUrl: "w",
149
+ token: "t",
150
+ userId: "a",
151
+ ownerUserId: "o",
152
+ groupMode: "weird",
153
+ },
154
+ },
155
+ };
156
+ const account = resolveOpenclawClawlingAccount(cfg);
157
+ expect(account.groupMode).toBe("all");
158
+ });
159
+
160
+ it("resolves per-group groupMode overrides", () => {
161
+ const account = resolveOpenclawClawlingAccount({
162
+ channels: {
163
+ "openclaw-clawchat": {
164
+ groups: {
165
+ "group-1": { groupMode: "mention" },
166
+ "*": { groupMode: "all" },
167
+ },
168
+ },
169
+ },
170
+ });
171
+
172
+ expect(account.groups).toEqual({
173
+ "group-1": { groupMode: "mention" },
174
+ "*": { groupMode: "all" },
175
+ });
176
+ expect(effectiveGroupMode(account, "group-1")).toBe("mention");
177
+ expect(effectiveGroupMode(account, "group-2")).toBe("all");
178
+ });
179
+
180
+ it("lets exact per-group groupMode win over wildcard and channel defaults", () => {
181
+ const account = resolveOpenclawClawlingAccount({
182
+ channels: {
183
+ "openclaw-clawchat": {
184
+ groupMode: "mention",
185
+ groups: {
186
+ "group-open": { groupMode: "all" },
187
+ "*": { groupMode: "mention" },
188
+ },
189
+ },
190
+ },
191
+ });
192
+
193
+ expect(effectiveGroupMode(account, "group-open")).toBe("all");
194
+ expect(effectiveGroupMode(account, "group-other")).toBe("mention");
195
+ });
196
+
197
+ it("normalizes invalid per-group groupMode values to all", () => {
198
+ const account = resolveOpenclawClawlingAccount({
199
+ channels: {
200
+ "openclaw-clawchat": {
201
+ groupMode: "mention",
202
+ groups: {
203
+ "group-weird": { groupMode: "weird" },
204
+ },
205
+ },
206
+ },
207
+ });
208
+
209
+ expect(account.groups["group-weird"]?.groupMode).toBe("all");
210
+ expect(effectiveGroupMode(account, "group-weird")).toBe("all");
211
+ });
212
+
113
213
  it("lists the default account id", () => {
114
214
  expect(listOpenclawClawlingAccountIds()).toEqual([DEFAULT_ACCOUNT_ID]);
115
215
  });
@@ -121,6 +221,7 @@ describe("openclaw-clawchat config", () => {
121
221
  websocketUrl: "wss://w",
122
222
  token: "t",
123
223
  userId: "u",
224
+ ownerUserId: "o",
124
225
  baseUrl: "https://api.example.com",
125
226
  },
126
227
  },
@@ -140,8 +241,8 @@ describe("openclaw-clawchat config", () => {
140
241
  });
141
242
 
142
243
  it("uses the production ClawChat service as the built-in fallback endpoint", () => {
143
- expect(DEFAULT_BASE_URL).toBe("http://company.newbaselab.com:10086");
144
- expect(DEFAULT_WEBSOCKET_URL).toBe("ws://company.newbaselab.com:10086/ws");
244
+ expect(DEFAULT_BASE_URL).toBe("https://app.clawling.com");
245
+ expect(DEFAULT_WEBSOCKET_URL).toBe("wss://app.clawling.com/ws");
145
246
  });
146
247
 
147
248
  it("does NOT include baseUrl in the configured predicate (channel still works without it)", async () => {
@@ -151,6 +252,7 @@ describe("openclaw-clawchat config", () => {
151
252
  websocketUrl: "wss://w",
152
253
  token: "t",
153
254
  userId: "u",
255
+ ownerUserId: "o",
154
256
  // no baseUrl — resolver uses default
155
257
  },
156
258
  },
@@ -160,7 +262,7 @@ describe("openclaw-clawchat config", () => {
160
262
  expect(account.baseUrl).toBe(DEFAULT_BASE_URL);
161
263
  });
162
264
 
163
- it("adds the plugin to tools.allow when an explicit allowlist is already in use", () => {
265
+ it("adds the plugin to tools.alsoAllow even when an explicit allowlist is already in use", () => {
164
266
  const cfg = mergeOpenclawClawchatToolAllow({
165
267
  tools: {
166
268
  allow: ["bash"],
@@ -168,9 +270,9 @@ describe("openclaw-clawchat config", () => {
168
270
  },
169
271
  } as never) as { tools: Record<string, unknown> };
170
272
 
171
- expect(cfg.tools.allow).toEqual(["bash", "openclaw-clawchat"]);
273
+ expect(cfg.tools.allow).toEqual(["bash"]);
172
274
  expect(cfg.tools.deny).toEqual(["exec"]);
173
- expect(cfg.tools.alsoAllow).toBeUndefined();
275
+ expect(cfg.tools.alsoAllow).toEqual(["openclaw-clawchat"]);
174
276
  });
175
277
 
176
278
  it("adds the plugin to tools.alsoAllow when no explicit allowlist is in use", () => {