@newbase-clawchat/openclaw-clawchat 2026.5.12-2 → 2026.5.12-21
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/README.md +39 -17
- package/dist/index.js +3 -1
- package/dist/src/api-client.js +71 -12
- package/dist/src/api-types.test-d.js +10 -0
- package/dist/src/channel.js +5 -5
- package/dist/src/channel.setup.js +4 -17
- package/dist/src/clawchat-memory.js +290 -0
- package/dist/src/clawchat-metadata.js +235 -0
- package/dist/src/client.js +31 -93
- package/dist/src/commands.js +3 -3
- package/dist/src/config.js +58 -3
- package/dist/src/group-message-coalescer.js +107 -0
- package/dist/src/inbound.js +24 -28
- package/dist/src/login.runtime.js +82 -19
- package/dist/src/media-runtime.js +2 -3
- package/dist/src/message-mapper.js +1 -1
- package/dist/src/mock-transport.js +31 -0
- package/dist/src/outbound.js +281 -56
- package/dist/src/plugin-prompts.js +76 -0
- package/dist/src/profile-prompt.js +150 -0
- package/dist/src/profile-sync.js +169 -0
- package/dist/src/prompt-injection.js +25 -0
- package/dist/src/protocol-types.js +63 -0
- package/dist/src/protocol-types.typecheck.js +1 -0
- package/dist/src/protocol.js +2 -2
- package/dist/src/reply-dispatcher.js +143 -40
- package/dist/src/runtime.js +813 -109
- package/dist/src/storage.js +636 -0
- package/dist/src/tools-schema.js +70 -10
- package/dist/src/tools.js +600 -112
- package/dist/src/ws-alignment.js +8 -0
- package/dist/src/ws-client.js +588 -0
- package/index.ts +6 -1
- package/openclaw.plugin.json +44 -4
- package/package.json +4 -3
- package/prompts/platform.md +7 -0
- package/skills/clawchat/SKILL.md +90 -0
- package/src/api-client.test.ts +360 -15
- package/src/api-client.ts +127 -25
- package/src/api-types.test-d.ts +12 -0
- package/src/api-types.ts +71 -4
- package/src/buffered-stream.test.ts +1 -1
- package/src/buffered-stream.ts +1 -1
- package/src/channel.outbound.test.ts +270 -60
- package/src/channel.setup.ts +9 -18
- package/src/channel.test.ts +33 -25
- package/src/channel.ts +5 -7
- package/src/clawchat-memory.test.ts +372 -0
- package/src/clawchat-memory.ts +363 -0
- package/src/clawchat-metadata.test.ts +350 -0
- package/src/clawchat-metadata.ts +352 -0
- package/src/client.test.ts +57 -48
- package/src/client.ts +37 -129
- package/src/commands.test.ts +2 -2
- package/src/commands.ts +3 -3
- package/src/config.test.ts +169 -4
- package/src/config.ts +86 -6
- package/src/group-message-coalescer.test.ts +223 -0
- package/src/group-message-coalescer.ts +154 -0
- package/src/inbound.test.ts +106 -19
- package/src/inbound.ts +31 -35
- package/src/login.runtime.test.ts +294 -11
- package/src/login.runtime.ts +90 -21
- package/src/manifest.test.ts +86 -14
- package/src/media-runtime.test.ts +31 -2
- package/src/media-runtime.ts +7 -10
- 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 +811 -95
- package/src/outbound.ts +332 -65
- package/src/plugin-entry.test.ts +3 -1
- package/src/plugin-prompts.test.ts +78 -0
- package/src/plugin-prompts.ts +92 -0
- package/src/profile-prompt.test.ts +435 -0
- package/src/profile-prompt.ts +208 -0
- package/src/profile-sync.test.ts +611 -0
- package/src/profile-sync.ts +268 -0
- package/src/prompt-injection.test.ts +39 -0
- package/src/prompt-injection.ts +45 -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.ts +2 -2
- package/src/reply-dispatcher.test.ts +720 -135
- package/src/reply-dispatcher.ts +174 -42
- package/src/runtime.test.ts +3884 -337
- package/src/runtime.ts +956 -128
- package/src/storage.test.ts +692 -0
- package/src/storage.ts +989 -0
- package/src/streaming.test.ts +1 -1
- package/src/streaming.ts +1 -1
- package/src/tools-schema.ts +115 -13
- package/src/tools.test.ts +501 -10
- package/src/tools.ts +739 -133
- package/src/ws-alignment.ts +9 -0
- package/src/ws-client.test.ts +1218 -0
- package/src/ws-client.ts +662 -0
package/src/config.test.ts
CHANGED
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
DEFAULT_BASE_URL,
|
|
6
6
|
DEFAULT_WEBSOCKET_URL,
|
|
7
7
|
DEFAULT_STREAM,
|
|
8
|
+
effectiveGroupCommandMode,
|
|
9
|
+
effectiveGroupMode,
|
|
8
10
|
mergeOpenclawClawchatToolAllow,
|
|
9
11
|
resolveOpenclawClawlingAccount,
|
|
10
12
|
listOpenclawClawlingAccountIds,
|
|
@@ -20,7 +22,9 @@ describe("openclaw-clawchat config", () => {
|
|
|
20
22
|
expect(account.accountId).toBe(DEFAULT_ACCOUNT_ID);
|
|
21
23
|
expect(account.enabled).toBe(true);
|
|
22
24
|
expect(account.configured).toBe(false);
|
|
23
|
-
expect(account.replyMode).toBe("
|
|
25
|
+
expect(account.replyMode).toBe("stream");
|
|
26
|
+
expect(account.groupMode).toBe("all");
|
|
27
|
+
expect(account.groupCommandMode).toBe("owner");
|
|
24
28
|
expect(account.forwardThinking).toBe(true);
|
|
25
29
|
expect(account.forwardToolCalls).toBe(false);
|
|
26
30
|
expect(account.richInteractions).toBe(false);
|
|
@@ -33,7 +37,9 @@ describe("openclaw-clawchat config", () => {
|
|
|
33
37
|
"openclaw-clawchat": {
|
|
34
38
|
websocketUrl: "wss://chat.example.com/ws",
|
|
35
39
|
token: "secret",
|
|
40
|
+
agentId: "agt-1",
|
|
36
41
|
userId: "agent-1",
|
|
42
|
+
ownerUserId: "owner-1",
|
|
37
43
|
replyMode: "stream",
|
|
38
44
|
forwardThinking: false,
|
|
39
45
|
forwardToolCalls: true,
|
|
@@ -46,7 +52,9 @@ describe("openclaw-clawchat config", () => {
|
|
|
46
52
|
expect(account.configured).toBe(true);
|
|
47
53
|
expect(account.websocketUrl).toBe("wss://chat.example.com/ws");
|
|
48
54
|
expect(account.token).toBe("secret");
|
|
55
|
+
expect(account.agentId).toBe("agt-1");
|
|
49
56
|
expect(account.userId).toBe("agent-1");
|
|
57
|
+
expect(account.ownerUserId).toBe("owner-1");
|
|
50
58
|
expect(account.replyMode).toBe("stream");
|
|
51
59
|
expect(account.forwardThinking).toBe(false);
|
|
52
60
|
expect(account.forwardToolCalls).toBe(true);
|
|
@@ -56,12 +64,29 @@ describe("openclaw-clawchat config", () => {
|
|
|
56
64
|
expect(account.stream.maxBufferChars).toBe(3000);
|
|
57
65
|
});
|
|
58
66
|
|
|
67
|
+
it("does not use userId as an ownerUserId fallback", () => {
|
|
68
|
+
const account = resolveOpenclawClawlingAccount({
|
|
69
|
+
channels: {
|
|
70
|
+
"openclaw-clawchat": {
|
|
71
|
+
token: "secret",
|
|
72
|
+
userId: "agent-user-id",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
expect(account.userId).toBe("agent-user-id");
|
|
78
|
+
expect(account.ownerUserId).not.toBe("agent-user-id");
|
|
79
|
+
expect(account.configured).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
|
|
59
82
|
it("resolves credentials from ClawChat environment variables when config omits them", () => {
|
|
60
83
|
const account = resolveOpenclawClawlingAccount(
|
|
61
84
|
{},
|
|
62
85
|
{
|
|
63
86
|
CLAWCHAT_TOKEN: "env-token",
|
|
87
|
+
CLAWCHAT_AGENT_ID: "env-agent",
|
|
64
88
|
CLAWCHAT_USER_ID: "env-user",
|
|
89
|
+
CLAWCHAT_OWNER_USER_ID: "env-owner",
|
|
65
90
|
CLAWCHAT_BASE_URL: "https://api.env.example",
|
|
66
91
|
CLAWCHAT_WEBSOCKET_URL: "wss://ws.env.example/ws",
|
|
67
92
|
},
|
|
@@ -69,7 +94,9 @@ describe("openclaw-clawchat config", () => {
|
|
|
69
94
|
|
|
70
95
|
expect(account.configured).toBe(true);
|
|
71
96
|
expect(account.token).toBe("env-token");
|
|
97
|
+
expect(account.agentId).toBe("env-agent");
|
|
72
98
|
expect(account.userId).toBe("env-user");
|
|
99
|
+
expect(account.ownerUserId).toBe("env-owner");
|
|
73
100
|
expect(account.baseUrl).toBe("https://api.env.example");
|
|
74
101
|
expect(account.websocketUrl).toBe("wss://ws.env.example/ws");
|
|
75
102
|
});
|
|
@@ -80,7 +107,9 @@ describe("openclaw-clawchat config", () => {
|
|
|
80
107
|
channels: {
|
|
81
108
|
"openclaw-clawchat": {
|
|
82
109
|
token: "config-token",
|
|
110
|
+
agentId: "config-agent",
|
|
83
111
|
userId: "config-user",
|
|
112
|
+
ownerUserId: "config-owner",
|
|
84
113
|
baseUrl: "https://api.config.example",
|
|
85
114
|
websocketUrl: "wss://ws.config.example/ws",
|
|
86
115
|
},
|
|
@@ -88,26 +117,160 @@ describe("openclaw-clawchat config", () => {
|
|
|
88
117
|
},
|
|
89
118
|
{
|
|
90
119
|
CLAWCHAT_TOKEN: "env-token",
|
|
120
|
+
CLAWCHAT_AGENT_ID: "env-agent",
|
|
91
121
|
CLAWCHAT_USER_ID: "env-user",
|
|
122
|
+
CLAWCHAT_OWNER_USER_ID: "env-owner",
|
|
92
123
|
CLAWCHAT_BASE_URL: "https://api.env.example",
|
|
93
124
|
CLAWCHAT_WEBSOCKET_URL: "wss://ws.env.example/ws",
|
|
94
125
|
},
|
|
95
126
|
);
|
|
96
127
|
|
|
97
128
|
expect(account.token).toBe("config-token");
|
|
129
|
+
expect(account.agentId).toBe("config-agent");
|
|
98
130
|
expect(account.userId).toBe("config-user");
|
|
131
|
+
expect(account.ownerUserId).toBe("config-owner");
|
|
99
132
|
expect(account.baseUrl).toBe("https://api.config.example");
|
|
100
133
|
expect(account.websocketUrl).toBe("wss://ws.config.example/ws");
|
|
101
134
|
});
|
|
102
135
|
|
|
103
|
-
it("falls back to
|
|
136
|
+
it("falls back to stream replyMode for unknown values", () => {
|
|
104
137
|
const cfg = {
|
|
105
138
|
channels: {
|
|
106
|
-
"openclaw-clawchat": {
|
|
139
|
+
"openclaw-clawchat": {
|
|
140
|
+
websocketUrl: "w",
|
|
141
|
+
token: "t",
|
|
142
|
+
userId: "a",
|
|
143
|
+
ownerUserId: "o",
|
|
144
|
+
replyMode: "weird",
|
|
145
|
+
},
|
|
107
146
|
},
|
|
108
147
|
};
|
|
109
148
|
const account = resolveOpenclawClawlingAccount(cfg);
|
|
110
|
-
expect(account.replyMode).toBe("
|
|
149
|
+
expect(account.replyMode).toBe("stream");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("preserves explicit mention groupMode", () => {
|
|
153
|
+
const cfg = {
|
|
154
|
+
channels: {
|
|
155
|
+
"openclaw-clawchat": {
|
|
156
|
+
websocketUrl: "w",
|
|
157
|
+
token: "t",
|
|
158
|
+
userId: "a",
|
|
159
|
+
ownerUserId: "o",
|
|
160
|
+
groupMode: "mention",
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
const account = resolveOpenclawClawlingAccount(cfg);
|
|
165
|
+
expect(account.groupMode).toBe("mention");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("falls back to all groupMode for unknown values", () => {
|
|
169
|
+
const cfg = {
|
|
170
|
+
channels: {
|
|
171
|
+
"openclaw-clawchat": {
|
|
172
|
+
websocketUrl: "w",
|
|
173
|
+
token: "t",
|
|
174
|
+
userId: "a",
|
|
175
|
+
ownerUserId: "o",
|
|
176
|
+
groupMode: "weird",
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
const account = resolveOpenclawClawlingAccount(cfg);
|
|
181
|
+
expect(account.groupMode).toBe("all");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("resolves per-group groupMode overrides", () => {
|
|
185
|
+
const account = resolveOpenclawClawlingAccount({
|
|
186
|
+
channels: {
|
|
187
|
+
"openclaw-clawchat": {
|
|
188
|
+
groups: {
|
|
189
|
+
"group-1": { groupMode: "mention" },
|
|
190
|
+
"*": { groupMode: "all" },
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
expect(account.groups).toEqual({
|
|
197
|
+
"group-1": { groupMode: "mention", groupCommandMode: "owner" },
|
|
198
|
+
"*": { groupMode: "all", groupCommandMode: "owner" },
|
|
199
|
+
});
|
|
200
|
+
expect(effectiveGroupMode(account, "group-1")).toBe("mention");
|
|
201
|
+
expect(effectiveGroupMode(account, "group-2")).toBe("all");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("resolves groupCommandMode and per-group overrides", () => {
|
|
205
|
+
const account = resolveOpenclawClawlingAccount({
|
|
206
|
+
channels: {
|
|
207
|
+
"openclaw-clawchat": {
|
|
208
|
+
groupCommandMode: "off",
|
|
209
|
+
groups: {
|
|
210
|
+
"group-open": { groupCommandMode: "all" },
|
|
211
|
+
"*": { groupCommandMode: "owner" },
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
expect(account.groupCommandMode).toBe("off");
|
|
218
|
+
expect(account.groups).toEqual({
|
|
219
|
+
"group-open": { groupMode: "all", groupCommandMode: "all" },
|
|
220
|
+
"*": { groupMode: "all", groupCommandMode: "owner" },
|
|
221
|
+
});
|
|
222
|
+
expect(effectiveGroupCommandMode(account, "group-open")).toBe("all");
|
|
223
|
+
expect(effectiveGroupCommandMode(account, "group-other")).toBe("owner");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("normalizes invalid groupCommandMode values to owner", () => {
|
|
227
|
+
const account = resolveOpenclawClawlingAccount({
|
|
228
|
+
channels: {
|
|
229
|
+
"openclaw-clawchat": {
|
|
230
|
+
groupCommandMode: "weird",
|
|
231
|
+
groups: {
|
|
232
|
+
"group-weird": { groupCommandMode: "weird" },
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
expect(account.groupCommandMode).toBe("owner");
|
|
239
|
+
expect(account.groups["group-weird"]?.groupCommandMode).toBe("owner");
|
|
240
|
+
expect(effectiveGroupCommandMode(account, "group-weird")).toBe("owner");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("lets exact per-group groupMode win over wildcard and channel defaults", () => {
|
|
244
|
+
const account = resolveOpenclawClawlingAccount({
|
|
245
|
+
channels: {
|
|
246
|
+
"openclaw-clawchat": {
|
|
247
|
+
groupMode: "mention",
|
|
248
|
+
groups: {
|
|
249
|
+
"group-open": { groupMode: "all" },
|
|
250
|
+
"*": { groupMode: "mention" },
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
expect(effectiveGroupMode(account, "group-open")).toBe("all");
|
|
257
|
+
expect(effectiveGroupMode(account, "group-other")).toBe("mention");
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("normalizes invalid per-group groupMode values to all", () => {
|
|
261
|
+
const account = resolveOpenclawClawlingAccount({
|
|
262
|
+
channels: {
|
|
263
|
+
"openclaw-clawchat": {
|
|
264
|
+
groupMode: "mention",
|
|
265
|
+
groups: {
|
|
266
|
+
"group-weird": { groupMode: "weird" },
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
expect(account.groups["group-weird"]?.groupMode).toBe("all");
|
|
273
|
+
expect(effectiveGroupMode(account, "group-weird")).toBe("all");
|
|
111
274
|
});
|
|
112
275
|
|
|
113
276
|
it("lists the default account id", () => {
|
|
@@ -121,6 +284,7 @@ describe("openclaw-clawchat config", () => {
|
|
|
121
284
|
websocketUrl: "wss://w",
|
|
122
285
|
token: "t",
|
|
123
286
|
userId: "u",
|
|
287
|
+
ownerUserId: "o",
|
|
124
288
|
baseUrl: "https://api.example.com",
|
|
125
289
|
},
|
|
126
290
|
},
|
|
@@ -151,6 +315,7 @@ describe("openclaw-clawchat config", () => {
|
|
|
151
315
|
websocketUrl: "wss://w",
|
|
152
316
|
token: "t",
|
|
153
317
|
userId: "u",
|
|
318
|
+
ownerUserId: "o",
|
|
154
319
|
// no baseUrl — resolver uses default
|
|
155
320
|
},
|
|
156
321
|
},
|
package/src/config.ts
CHANGED
|
@@ -3,7 +3,9 @@ import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
|
|
|
3
3
|
|
|
4
4
|
export const CHANNEL_ID = "openclaw-clawchat" as const;
|
|
5
5
|
export const CLAWCHAT_TOKEN_ENV = "CLAWCHAT_TOKEN" as const;
|
|
6
|
+
export const CLAWCHAT_AGENT_ID_ENV = "CLAWCHAT_AGENT_ID" as const;
|
|
6
7
|
export const CLAWCHAT_USER_ID_ENV = "CLAWCHAT_USER_ID" as const;
|
|
8
|
+
export const CLAWCHAT_OWNER_USER_ID_ENV = "CLAWCHAT_OWNER_USER_ID" as const;
|
|
7
9
|
export const CLAWCHAT_REFRESH_TOKEN_ENV = "CLAWCHAT_REFRESH_TOKEN" as const;
|
|
8
10
|
export const CLAWCHAT_BASE_URL_ENV = "CLAWCHAT_BASE_URL" as const;
|
|
9
11
|
export const CLAWCHAT_WEBSOCKET_URL_ENV = "CLAWCHAT_WEBSOCKET_URL" as const;
|
|
@@ -21,11 +23,12 @@ export type ReplyMode = "static" | "stream";
|
|
|
21
23
|
|
|
22
24
|
/**
|
|
23
25
|
* Group-chat trigger policy.
|
|
24
|
-
* - "
|
|
25
|
-
*
|
|
26
|
-
*
|
|
26
|
+
* - "all" (default): trigger on every group message regardless of mentions (open listen).
|
|
27
|
+
* - "mention": only trigger a reply when the inbound `context.mentions`
|
|
28
|
+
* list includes our `userId` (i.e. the sender @-mentioned us).
|
|
27
29
|
*/
|
|
28
30
|
export type GroupMode = "mention" | "all";
|
|
31
|
+
export type GroupCommandMode = "owner" | "all" | "off";
|
|
29
32
|
|
|
30
33
|
export const DEFAULT_STREAM = {
|
|
31
34
|
flushIntervalMs: 250,
|
|
@@ -66,6 +69,11 @@ export type OpenclawClawlingStreamConfig = {
|
|
|
66
69
|
maxBufferChars?: number;
|
|
67
70
|
};
|
|
68
71
|
|
|
72
|
+
export type OpenclawClawlingGroupConfig = {
|
|
73
|
+
groupMode: GroupMode;
|
|
74
|
+
groupCommandMode: GroupCommandMode;
|
|
75
|
+
};
|
|
76
|
+
|
|
69
77
|
export type OpenclawClawlingReconnectConfig = {
|
|
70
78
|
initialDelay?: number;
|
|
71
79
|
maxDelay?: number;
|
|
@@ -91,9 +99,14 @@ export type OpenclawClawlingConfig = {
|
|
|
91
99
|
token?: string;
|
|
92
100
|
/** Refresh token persisted by ClawChat activation/login (paired with `token`). */
|
|
93
101
|
refreshToken?: string;
|
|
102
|
+
/** Server agent id used with `/v1/agents/{id}`. */
|
|
103
|
+
agentId?: string;
|
|
94
104
|
userId?: string;
|
|
105
|
+
ownerUserId?: string;
|
|
95
106
|
replyMode?: ReplyMode;
|
|
96
107
|
groupMode?: GroupMode;
|
|
108
|
+
groupCommandMode?: GroupCommandMode;
|
|
109
|
+
groups?: Record<string, Partial<OpenclawClawlingGroupConfig>>;
|
|
97
110
|
forwardThinking?: boolean;
|
|
98
111
|
forwardToolCalls?: boolean;
|
|
99
112
|
/** Emit approval/action rich fragments instead of plain fallback text. */
|
|
@@ -113,9 +126,23 @@ export const openclawClawlingConfigSchema = {
|
|
|
113
126
|
baseUrl: { type: "string" },
|
|
114
127
|
token: { type: "string" },
|
|
115
128
|
refreshToken: { type: "string" },
|
|
129
|
+
agentId: { type: "string" },
|
|
116
130
|
userId: { type: "string" },
|
|
131
|
+
ownerUserId: { type: "string" },
|
|
117
132
|
replyMode: { type: "string", enum: ["static", "stream"] },
|
|
118
133
|
groupMode: { type: "string", enum: ["mention", "all"] },
|
|
134
|
+
groupCommandMode: { type: "string", enum: ["owner", "all", "off"] },
|
|
135
|
+
groups: {
|
|
136
|
+
type: "object",
|
|
137
|
+
additionalProperties: {
|
|
138
|
+
type: "object",
|
|
139
|
+
additionalProperties: false,
|
|
140
|
+
properties: {
|
|
141
|
+
groupMode: { type: "string", enum: ["mention", "all"] },
|
|
142
|
+
groupCommandMode: { type: "string", enum: ["owner", "all", "off"] },
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
},
|
|
119
146
|
forwardThinking: { type: "boolean" },
|
|
120
147
|
forwardToolCalls: { type: "boolean" },
|
|
121
148
|
richInteractions: { type: "boolean" },
|
|
@@ -238,9 +265,13 @@ export type ResolvedOpenclawClawlingAccount = {
|
|
|
238
265
|
websocketUrl: string;
|
|
239
266
|
baseUrl: string;
|
|
240
267
|
token: string;
|
|
268
|
+
agentId: string;
|
|
241
269
|
userId: string;
|
|
270
|
+
ownerUserId: string;
|
|
242
271
|
replyMode: ReplyMode;
|
|
243
272
|
groupMode: GroupMode;
|
|
273
|
+
groupCommandMode: GroupCommandMode;
|
|
274
|
+
groups: Record<string, OpenclawClawlingGroupConfig>;
|
|
244
275
|
forwardThinking: boolean;
|
|
245
276
|
forwardToolCalls: boolean;
|
|
246
277
|
richInteractions: boolean;
|
|
@@ -266,11 +297,51 @@ function readEnvString(env: Record<string, string | undefined>, key: string): st
|
|
|
266
297
|
}
|
|
267
298
|
|
|
268
299
|
function readReplyMode(value: unknown): ReplyMode {
|
|
269
|
-
return value === "
|
|
300
|
+
return value === "static" ? "static" : "stream";
|
|
270
301
|
}
|
|
271
302
|
|
|
272
303
|
function readGroupMode(value: unknown): GroupMode {
|
|
273
|
-
return value === "
|
|
304
|
+
return value === "mention" ? "mention" : "all";
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function readGroupCommandMode(value: unknown): GroupCommandMode {
|
|
308
|
+
return value === "all" || value === "off" ? value : "owner";
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function readGroups(value: unknown): Record<string, OpenclawClawlingGroupConfig> {
|
|
312
|
+
const rawGroups = value && typeof value === "object" && !Array.isArray(value)
|
|
313
|
+
? (value as Record<string, unknown>)
|
|
314
|
+
: {};
|
|
315
|
+
const groups: Record<string, OpenclawClawlingGroupConfig> = {};
|
|
316
|
+
for (const [chatId, rawGroup] of Object.entries(rawGroups)) {
|
|
317
|
+
if (!chatId) continue;
|
|
318
|
+
const group = rawGroup && typeof rawGroup === "object" && !Array.isArray(rawGroup)
|
|
319
|
+
? (rawGroup as Record<string, unknown>)
|
|
320
|
+
: {};
|
|
321
|
+
groups[chatId] = {
|
|
322
|
+
groupMode: readGroupMode(group.groupMode),
|
|
323
|
+
groupCommandMode: readGroupCommandMode(group.groupCommandMode),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
return groups;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function effectiveGroupMode(
|
|
330
|
+
account: Pick<ResolvedOpenclawClawlingAccount, "groupMode" | "groups">,
|
|
331
|
+
chatId: string,
|
|
332
|
+
): GroupMode {
|
|
333
|
+
return account.groups[chatId]?.groupMode
|
|
334
|
+
?? account.groups["*"]?.groupMode
|
|
335
|
+
?? account.groupMode;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export function effectiveGroupCommandMode(
|
|
339
|
+
account: Pick<ResolvedOpenclawClawlingAccount, "groupCommandMode" | "groups">,
|
|
340
|
+
chatId: string,
|
|
341
|
+
): GroupCommandMode {
|
|
342
|
+
return account.groups[chatId]?.groupCommandMode
|
|
343
|
+
?? account.groups["*"]?.groupCommandMode
|
|
344
|
+
?? account.groupCommandMode;
|
|
274
345
|
}
|
|
275
346
|
|
|
276
347
|
function readStream(raw: unknown): Required<OpenclawClawlingStreamConfig> {
|
|
@@ -333,10 +404,15 @@ export function resolveOpenclawClawlingAccount(
|
|
|
333
404
|
readEnvString(env, CLAWCHAT_BASE_URL_ENV) ||
|
|
334
405
|
DEFAULT_BASE_URL;
|
|
335
406
|
const token = readOptionalString(channel.token) || readEnvString(env, CLAWCHAT_TOKEN_ENV);
|
|
407
|
+
const agentId = readOptionalString(channel.agentId) || readEnvString(env, CLAWCHAT_AGENT_ID_ENV);
|
|
336
408
|
const userId = readOptionalString(channel.userId) || readEnvString(env, CLAWCHAT_USER_ID_ENV);
|
|
409
|
+
const ownerUserId =
|
|
410
|
+
readOptionalString(channel.ownerUserId) || readEnvString(env, CLAWCHAT_OWNER_USER_ID_ENV);
|
|
337
411
|
const enabled = typeof channel.enabled === "boolean" ? channel.enabled : true;
|
|
338
412
|
const replyMode = readReplyMode(channel.replyMode);
|
|
339
413
|
const groupMode = readGroupMode(channel.groupMode);
|
|
414
|
+
const groupCommandMode = readGroupCommandMode(channel.groupCommandMode);
|
|
415
|
+
const groups = readGroups(channel.groups);
|
|
340
416
|
const forwardThinking =
|
|
341
417
|
typeof channel.forwardThinking === "boolean" ? channel.forwardThinking : true;
|
|
342
418
|
const forwardToolCalls =
|
|
@@ -348,13 +424,17 @@ export function resolveOpenclawClawlingAccount(
|
|
|
348
424
|
accountId: DEFAULT_ACCOUNT_ID,
|
|
349
425
|
name: CHANNEL_ID,
|
|
350
426
|
enabled,
|
|
351
|
-
configured: Boolean(websocketUrl && token && userId),
|
|
427
|
+
configured: Boolean(websocketUrl && token && userId && ownerUserId),
|
|
352
428
|
websocketUrl,
|
|
353
429
|
baseUrl,
|
|
354
430
|
token,
|
|
431
|
+
agentId,
|
|
355
432
|
userId,
|
|
433
|
+
ownerUserId,
|
|
356
434
|
replyMode,
|
|
357
435
|
groupMode,
|
|
436
|
+
groupCommandMode,
|
|
437
|
+
groups,
|
|
358
438
|
forwardThinking,
|
|
359
439
|
forwardToolCalls,
|
|
360
440
|
richInteractions,
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
createGroupMessageCoalescer,
|
|
4
|
+
formatCoalescedGroupBody,
|
|
5
|
+
type CoalescableGroupTurn,
|
|
6
|
+
} from "./group-message-coalescer.ts";
|
|
7
|
+
|
|
8
|
+
function turn(overrides: Partial<CoalescableGroupTurn> = {}): CoalescableGroupTurn {
|
|
9
|
+
return {
|
|
10
|
+
peer: { kind: "group", id: "room-1" },
|
|
11
|
+
senderId: "user-a",
|
|
12
|
+
senderNickName: "Alice",
|
|
13
|
+
rawBody: "hello",
|
|
14
|
+
messageId: "msg-1",
|
|
15
|
+
traceId: "trace-1",
|
|
16
|
+
timestamp: 1000,
|
|
17
|
+
wasMentioned: false,
|
|
18
|
+
mentionedUserIds: [],
|
|
19
|
+
mediaItems: [],
|
|
20
|
+
envelope: { event: "message.send", trace_id: "trace-1", emitted_at: 1000 },
|
|
21
|
+
...overrides,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
beforeEach(() => vi.useFakeTimers());
|
|
26
|
+
afterEach(() => vi.useRealTimers());
|
|
27
|
+
|
|
28
|
+
describe("group message coalescer", () => {
|
|
29
|
+
it("flushes after ten seconds of group inactivity", async () => {
|
|
30
|
+
const dispatched: CoalescableGroupTurn[] = [];
|
|
31
|
+
const coalescer = createGroupMessageCoalescer({
|
|
32
|
+
idleMs: 10000,
|
|
33
|
+
maxWaitMs: 30000,
|
|
34
|
+
dispatch: async (merged) => {
|
|
35
|
+
dispatched.push(merged);
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
coalescer.enqueue(turn({ messageId: "msg-1", rawBody: "one" }));
|
|
40
|
+
await vi.advanceTimersByTimeAsync(9999);
|
|
41
|
+
|
|
42
|
+
coalescer.enqueue(
|
|
43
|
+
turn({
|
|
44
|
+
messageId: "msg-2",
|
|
45
|
+
senderId: "user-b",
|
|
46
|
+
senderNickName: "Bob",
|
|
47
|
+
rawBody: "two",
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
await vi.advanceTimersByTimeAsync(9999);
|
|
52
|
+
expect(dispatched).toEqual([]);
|
|
53
|
+
|
|
54
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
55
|
+
expect(dispatched).toHaveLength(1);
|
|
56
|
+
expect(dispatched[0]?.messageId).toBe("msg-2");
|
|
57
|
+
expect(dispatched[0]?.rawBody).toContain("ClawChat group batch (2 messages, 10s idle, 30s max)");
|
|
58
|
+
expect(dispatched[0]?.rawBody).toContain("[message]\nsender_id: user-a\nsender_name: Alice\nsender_profile_type: user\nsender_is_owner: false\nmentions_current_agent: false\nmentioned_user_ids: -\ntext:\none");
|
|
59
|
+
expect(dispatched[0]?.rawBody).toContain("[message]\nsender_id: user-b\nsender_name: Bob\nsender_profile_type: user\nsender_is_owner: false\nmentions_current_agent: false\nmentioned_user_ids: -\ntext:\ntwo");
|
|
60
|
+
expect(dispatched[0]?.rawBody).not.toContain("sender_relation");
|
|
61
|
+
expect(dispatched[0]?.rawBody).not.toContain("[msg-");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("flushes at thirty seconds even while group messages keep arriving", async () => {
|
|
65
|
+
const dispatched: CoalescableGroupTurn[] = [];
|
|
66
|
+
const coalescer = createGroupMessageCoalescer({
|
|
67
|
+
idleMs: 10000,
|
|
68
|
+
maxWaitMs: 30000,
|
|
69
|
+
dispatch: async (merged) => {
|
|
70
|
+
dispatched.push(merged);
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
coalescer.enqueue(turn({ messageId: "msg-1", rawBody: "one" }));
|
|
75
|
+
await vi.advanceTimersByTimeAsync(9000);
|
|
76
|
+
coalescer.enqueue(turn({ messageId: "msg-2", rawBody: "two" }));
|
|
77
|
+
await vi.advanceTimersByTimeAsync(9000);
|
|
78
|
+
coalescer.enqueue(turn({ messageId: "msg-3", rawBody: "three" }));
|
|
79
|
+
await vi.advanceTimersByTimeAsync(9000);
|
|
80
|
+
coalescer.enqueue(turn({ messageId: "msg-4", rawBody: "four" }));
|
|
81
|
+
|
|
82
|
+
await vi.advanceTimersByTimeAsync(2999);
|
|
83
|
+
expect(dispatched).toEqual([]);
|
|
84
|
+
|
|
85
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
86
|
+
expect(dispatched).toHaveLength(1);
|
|
87
|
+
expect(dispatched[0]?.messageId).toBe("msg-4");
|
|
88
|
+
expect(dispatched[0]?.rawBody).toContain("ClawChat group batch (4 messages, 10s idle, 30s max)");
|
|
89
|
+
expect(dispatched[0]?.rawBody).toContain("[message]\nsender_id: user-a\nsender_name: Alice\nsender_profile_type: user\nsender_is_owner: false\nmentions_current_agent: false\nmentioned_user_ids: -\ntext:\nfour");
|
|
90
|
+
expect(dispatched[0]?.rawBody).not.toContain("[msg-");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("flushes one merged turn per group after the idle window", async () => {
|
|
94
|
+
const dispatched: CoalescableGroupTurn[] = [];
|
|
95
|
+
const coalescer = createGroupMessageCoalescer({
|
|
96
|
+
idleMs: 10000,
|
|
97
|
+
maxWaitMs: 30000,
|
|
98
|
+
dispatch: async (merged) => {
|
|
99
|
+
dispatched.push(merged);
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const latestEnvelope = { event: "message.send", trace_id: "trace-2", emitted_at: 2000 };
|
|
104
|
+
|
|
105
|
+
coalescer.enqueue(
|
|
106
|
+
turn({
|
|
107
|
+
messageId: "msg-1",
|
|
108
|
+
senderNickName: "Alice",
|
|
109
|
+
rawBody: "one",
|
|
110
|
+
mentionedUserIds: ["other-user"],
|
|
111
|
+
}),
|
|
112
|
+
);
|
|
113
|
+
coalescer.enqueue(
|
|
114
|
+
turn({
|
|
115
|
+
messageId: "msg-2",
|
|
116
|
+
senderId: "user-b",
|
|
117
|
+
senderNickName: "Bob",
|
|
118
|
+
rawBody: "two",
|
|
119
|
+
traceId: "trace-2",
|
|
120
|
+
timestamp: 2000,
|
|
121
|
+
envelope: latestEnvelope,
|
|
122
|
+
}),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
await vi.advanceTimersByTimeAsync(9999);
|
|
126
|
+
expect(dispatched).toEqual([]);
|
|
127
|
+
|
|
128
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
129
|
+
expect(dispatched).toHaveLength(1);
|
|
130
|
+
expect(dispatched[0]?.messageId).toBe("msg-2");
|
|
131
|
+
expect(dispatched[0]?.senderId).toBe("user-b");
|
|
132
|
+
expect(dispatched[0]?.traceId).toBe("trace-2");
|
|
133
|
+
expect(dispatched[0]?.timestamp).toBe(2000);
|
|
134
|
+
expect(dispatched[0]?.envelope).toBe(latestEnvelope);
|
|
135
|
+
expect(dispatched[0]?.mentionedUserIds).toEqual(["other-user"]);
|
|
136
|
+
expect(dispatched[0]?.rawBody).toContain("ClawChat group batch (2 messages, 10s idle, 30s max)");
|
|
137
|
+
expect(dispatched[0]?.rawBody).toContain("[message]\nsender_id: user-a\nsender_name: Alice\nsender_profile_type: user\nsender_is_owner: false\nmentions_current_agent: false\nmentioned_user_ids: other-user\ntext:\none");
|
|
138
|
+
expect(dispatched[0]?.rawBody).toContain("[message]\nsender_id: user-b\nsender_name: Bob\nsender_profile_type: user\nsender_is_owner: false\nmentions_current_agent: false\nmentioned_user_ids: -\ntext:\ntwo");
|
|
139
|
+
expect(dispatched[0]?.rawBody).not.toContain("[msg-");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("keeps independent timers per group", async () => {
|
|
143
|
+
const dispatched: CoalescableGroupTurn[] = [];
|
|
144
|
+
const coalescer = createGroupMessageCoalescer({
|
|
145
|
+
idleMs: 10000,
|
|
146
|
+
maxWaitMs: 30000,
|
|
147
|
+
dispatch: async (merged) => dispatched.push(merged),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
coalescer.enqueue(turn({ peer: { kind: "group", id: "room-1" }, messageId: "r1-a" }));
|
|
151
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
152
|
+
|
|
153
|
+
coalescer.enqueue(turn({ peer: { kind: "group", id: "room-2" }, messageId: "r2-a" }));
|
|
154
|
+
|
|
155
|
+
await vi.advanceTimersByTimeAsync(8999);
|
|
156
|
+
expect(dispatched).toEqual([]);
|
|
157
|
+
|
|
158
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
159
|
+
expect(dispatched.map((item) => item.peer.id)).toEqual(["room-1"]);
|
|
160
|
+
|
|
161
|
+
await vi.advanceTimersByTimeAsync(999);
|
|
162
|
+
expect(dispatched.map((item) => item.peer.id)).toEqual(["room-1"]);
|
|
163
|
+
|
|
164
|
+
await vi.advanceTimersByTimeAsync(8001);
|
|
165
|
+
expect(dispatched.map((item) => item.peer.id)).toEqual(["room-1", "room-2"]);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("drops pending batches on cancel without dispatching after shutdown", async () => {
|
|
169
|
+
const dispatched: CoalescableGroupTurn[] = [];
|
|
170
|
+
const coalescer = createGroupMessageCoalescer({
|
|
171
|
+
idleMs: 10000,
|
|
172
|
+
maxWaitMs: 30000,
|
|
173
|
+
dispatch: async (merged) => dispatched.push(merged),
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
coalescer.enqueue(turn());
|
|
177
|
+
coalescer.cancelAll();
|
|
178
|
+
await vi.advanceTimersByTimeAsync(30000);
|
|
179
|
+
|
|
180
|
+
expect(dispatched).toEqual([]);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("formatCoalescedGroupBody", () => {
|
|
185
|
+
it("keeps sender name and sender identity visible without message ids", () => {
|
|
186
|
+
expect(
|
|
187
|
+
formatCoalescedGroupBody([
|
|
188
|
+
turn({ messageId: "msg-1", senderId: "u1", senderNickName: "Ann", rawBody: "first" }),
|
|
189
|
+
turn({
|
|
190
|
+
messageId: "msg-2",
|
|
191
|
+
senderId: "u2",
|
|
192
|
+
senderNickName: "Ben",
|
|
193
|
+
senderIsOwner: true,
|
|
194
|
+
senderProfileType: "user",
|
|
195
|
+
rawBody: "second",
|
|
196
|
+
}),
|
|
197
|
+
]),
|
|
198
|
+
).toBe(
|
|
199
|
+
[
|
|
200
|
+
"ClawChat group batch (2 messages, 10s idle, 30s max):",
|
|
201
|
+
"[message]",
|
|
202
|
+
"sender_id: u1",
|
|
203
|
+
"sender_name: Ann",
|
|
204
|
+
"sender_profile_type: user",
|
|
205
|
+
"sender_is_owner: false",
|
|
206
|
+
"mentions_current_agent: false",
|
|
207
|
+
"mentioned_user_ids: -",
|
|
208
|
+
"text:",
|
|
209
|
+
"first",
|
|
210
|
+
"",
|
|
211
|
+
"[message]",
|
|
212
|
+
"sender_id: u2",
|
|
213
|
+
"sender_name: Ben",
|
|
214
|
+
"sender_profile_type: user",
|
|
215
|
+
"sender_is_owner: true",
|
|
216
|
+
"mentions_current_agent: false",
|
|
217
|
+
"mentioned_user_ids: -",
|
|
218
|
+
"text:",
|
|
219
|
+
"second",
|
|
220
|
+
].join("\n"),
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
});
|