@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.
- package/INSTALL.md +64 -0
- package/README.md +121 -19
- package/dist/index.js +10 -19
- package/dist/setup-entry.js +3 -0
- package/dist/src/api-client.js +78 -10
- package/dist/src/api-types.test-d.js +10 -0
- package/dist/src/channel.js +25 -156
- package/dist/src/channel.setup.js +120 -0
- package/dist/src/client.js +37 -41
- package/dist/src/config.js +75 -17
- package/dist/src/inbound.js +79 -61
- package/dist/src/login.runtime.js +84 -19
- package/dist/src/media-runtime.js +8 -8
- package/dist/src/message-mapper.js +1 -1
- package/dist/src/mock-transport.js +31 -0
- package/dist/src/outbound.js +410 -26
- package/dist/src/protocol-types.js +63 -0
- package/dist/src/protocol-types.typecheck.js +1 -0
- package/dist/src/protocol.js +2 -7
- package/dist/src/reply-dispatcher.js +157 -54
- package/dist/src/runtime.js +795 -119
- package/dist/src/storage.js +689 -0
- package/dist/src/tools-schema.js +98 -16
- package/dist/src/tools.js +422 -135
- package/dist/src/ws-alignment.js +178 -0
- package/dist/src/ws-client.js +588 -0
- package/dist/src/ws-log.js +19 -0
- package/index.ts +10 -22
- package/openclaw.plugin.json +37 -2
- package/package.json +17 -4
- package/setup-entry.ts +4 -0
- package/skills/clawchat/SKILL.md +88 -0
- package/src/api-client.test.ts +274 -14
- package/src/api-client.ts +138 -23
- package/src/api-types.test-d.ts +12 -0
- package/src/api-types.ts +90 -4
- package/src/buffered-stream.test.ts +14 -12
- package/src/buffered-stream.ts +1 -1
- package/src/channel.outbound.test.ts +269 -60
- package/src/channel.setup.ts +146 -0
- package/src/channel.test.ts +130 -24
- package/src/channel.ts +30 -186
- package/src/client.test.ts +197 -11
- package/src/client.ts +50 -57
- package/src/config.test.ts +108 -6
- package/src/config.ts +95 -24
- package/src/inbound.test.ts +288 -37
- package/src/inbound.ts +96 -84
- package/src/login.runtime.test.ts +347 -13
- package/src/login.runtime.ts +105 -23
- package/src/manifest.test.ts +146 -74
- package/src/media-runtime.test.ts +57 -2
- package/src/media-runtime.ts +26 -17
- package/src/message-mapper.test.ts +2 -2
- package/src/message-mapper.ts +2 -2
- package/src/mock-transport.test.ts +35 -0
- package/src/mock-transport.ts +38 -0
- package/src/outbound.test.ts +694 -73
- package/src/outbound.ts +484 -31
- package/src/plugin-entry.test.ts +1 -0
- package/src/protocol-types.test.ts +69 -0
- package/src/protocol-types.ts +296 -0
- package/src/protocol-types.typecheck.ts +89 -0
- package/src/protocol.test.ts +1 -6
- package/src/protocol.ts +2 -7
- package/src/reply-dispatcher.test.ts +819 -119
- package/src/reply-dispatcher.ts +202 -60
- package/src/runtime.test.ts +2120 -41
- package/src/runtime.ts +935 -142
- package/src/scripts.test.ts +85 -0
- package/src/storage.test.ts +793 -0
- package/src/storage.ts +1095 -0
- package/src/streaming.test.ts +9 -8
- package/src/streaming.ts +1 -1
- package/src/tools-schema.ts +148 -20
- package/src/tools.test.ts +377 -50
- package/src/tools.ts +574 -154
- package/src/ws-alignment.test.ts +103 -0
- package/src/ws-alignment.ts +275 -0
- package/src/ws-client.test.ts +1218 -0
- package/src/ws-client.ts +662 -0
- package/src/ws-log.test.ts +32 -0
- package/src/ws-log.ts +31 -0
- package/skills/clawchat-account-tools/SKILL.md +0 -26
- package/skills/clawchat-activate/SKILL.md +0 -47
package/src/client.test.ts
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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: {
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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:
|
|
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
|
|
80
|
-
* `chat_id` routing without
|
|
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
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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:
|
|
101
|
+
trace_id: client.nextTraceId(),
|
|
103
102
|
emitted_at: Date.now(),
|
|
104
103
|
chat_id: routing.chatId,
|
|
105
104
|
payload,
|
|
106
105
|
};
|
|
107
|
-
|
|
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
|
-
*
|
|
223
|
-
*
|
|
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
|
|
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
|
-
|
|
293
|
-
reason,
|
|
294
|
-
...reasonFragment,
|
|
287
|
+
fragments: reasonText ? [{ kind: "text", text: reasonText }] : [],
|
|
295
288
|
streaming: {
|
|
296
289
|
status: "failed",
|
|
297
290
|
sequence: params.sequence,
|
package/src/config.test.ts
CHANGED
|
@@ -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": {
|
|
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("
|
|
144
|
-
expect(DEFAULT_WEBSOCKET_URL).toBe("
|
|
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.
|
|
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"
|
|
273
|
+
expect(cfg.tools.allow).toEqual(["bash"]);
|
|
172
274
|
expect(cfg.tools.deny).toEqual(["exec"]);
|
|
173
|
-
expect(cfg.tools.alsoAllow).
|
|
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", () => {
|