@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/api-client.test.ts
CHANGED
|
@@ -55,12 +55,28 @@ describe("openclaw-clawchat api-client", () => {
|
|
|
55
55
|
expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/users/u%2F2");
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
it("
|
|
58
|
+
it("getUserProfile aliases GET /v1/users/{id}", async () => {
|
|
59
|
+
const fetchImpl = vi.fn().mockResolvedValue(
|
|
60
|
+
jsonResponse({ code: 0, message: "ok", data: { id: "u/2", nickname: "Bob" } }),
|
|
61
|
+
);
|
|
62
|
+
const client = createOpenclawClawlingApiClient({
|
|
63
|
+
baseUrl: "https://api.example.com",
|
|
64
|
+
token: "tk",
|
|
65
|
+
fetchImpl,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const profile = await client.getUserProfile("u/2");
|
|
69
|
+
|
|
70
|
+
expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/users/u%2F2");
|
|
71
|
+
expect(profile).toEqual({ id: "u/2", nickname: "Bob" });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("getAgentDetail calls GET /v1/agents/{agent_id}", async () => {
|
|
59
75
|
const fetchImpl = vi.fn().mockResolvedValue(
|
|
60
76
|
jsonResponse({
|
|
61
77
|
code: 0,
|
|
62
78
|
message: "ok",
|
|
63
|
-
data: {
|
|
79
|
+
data: { agent: { user_id: "agt/1", owner_id: "owner", type: "agent" } },
|
|
64
80
|
}),
|
|
65
81
|
);
|
|
66
82
|
const client = createOpenclawClawlingApiClient({
|
|
@@ -68,10 +84,221 @@ describe("openclaw-clawchat api-client", () => {
|
|
|
68
84
|
token: "tk",
|
|
69
85
|
fetchImpl,
|
|
70
86
|
});
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
87
|
+
|
|
88
|
+
await client.getAgentDetail("agt/1");
|
|
89
|
+
|
|
90
|
+
expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/agents/agt%2F1");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("metadata REST helpers", () => {
|
|
94
|
+
it("patchAgent sends only nickname/avatar_url/bio/behavior to PATCH /v1/agents/{agent_id}", async () => {
|
|
95
|
+
const fetchImpl = vi.fn().mockResolvedValue(
|
|
96
|
+
jsonResponse({
|
|
97
|
+
code: 0,
|
|
98
|
+
message: "ok",
|
|
99
|
+
data: {
|
|
100
|
+
agent: {
|
|
101
|
+
id: "agt/1",
|
|
102
|
+
owner_id: "owner",
|
|
103
|
+
user_id: "agent-user",
|
|
104
|
+
type: "bot",
|
|
105
|
+
nickname: "OpenClaw Bot",
|
|
106
|
+
avatar_url: "https://cdn/agent.png",
|
|
107
|
+
bio: "metadata bio",
|
|
108
|
+
visibility: "public",
|
|
109
|
+
status: "active",
|
|
110
|
+
platform: "openclaw",
|
|
111
|
+
created_at: "2026-05-24T00:00:00Z",
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
}),
|
|
115
|
+
);
|
|
116
|
+
const client = createOpenclawClawlingApiClient({
|
|
117
|
+
baseUrl: "https://api.example.com",
|
|
118
|
+
token: "tk",
|
|
119
|
+
fetchImpl,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const result = await client.patchAgent("agt/1", {
|
|
123
|
+
nickname: "OpenClaw Bot",
|
|
124
|
+
avatar_url: "https://cdn/agent.png",
|
|
125
|
+
bio: "metadata bio",
|
|
126
|
+
behavior: "do not forward",
|
|
127
|
+
filePath: "owner.md",
|
|
128
|
+
} as never);
|
|
129
|
+
|
|
130
|
+
expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/agents/agt%2F1");
|
|
131
|
+
const init = fetchImpl.mock.calls[0]![1] as RequestInit;
|
|
132
|
+
expect(init.method).toBe("PATCH");
|
|
133
|
+
expect(JSON.parse(init.body as string)).toEqual({
|
|
134
|
+
nickname: "OpenClaw Bot",
|
|
135
|
+
avatar_url: "https://cdn/agent.png",
|
|
136
|
+
bio: "metadata bio",
|
|
137
|
+
behavior: "do not forward",
|
|
138
|
+
});
|
|
139
|
+
expect((init.headers as Record<string, string>)["content-type"]).toBe("application/json");
|
|
140
|
+
expect(result.agent.nickname).toBe("OpenClaw Bot");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("patchConversation sends only title/description to PATCH /v1/conversations/{id}", async () => {
|
|
144
|
+
const fetchImpl = vi.fn().mockResolvedValue(
|
|
145
|
+
jsonResponse({
|
|
146
|
+
code: 0,
|
|
147
|
+
message: "ok",
|
|
148
|
+
data: {
|
|
149
|
+
conversation: {
|
|
150
|
+
id: "cnv/1",
|
|
151
|
+
type: "group",
|
|
152
|
+
title: "Metadata Team",
|
|
153
|
+
description: "Group metadata",
|
|
154
|
+
creator_id: "usr_creator",
|
|
155
|
+
created_at: "2026-05-20T00:00:00Z",
|
|
156
|
+
updated_at: "2026-05-24T00:00:00Z",
|
|
157
|
+
participants: [],
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
}),
|
|
161
|
+
);
|
|
162
|
+
const client = createOpenclawClawlingApiClient({
|
|
163
|
+
baseUrl: "https://api.example.com",
|
|
164
|
+
token: "tk",
|
|
165
|
+
fetchImpl,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const result = await client.patchConversation("cnv/1", {
|
|
169
|
+
title: "Metadata Team",
|
|
170
|
+
description: "Group metadata",
|
|
171
|
+
filePath: "groups/cnv_1.md",
|
|
172
|
+
} as never);
|
|
173
|
+
|
|
174
|
+
expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/conversations/cnv%2F1");
|
|
175
|
+
const init = fetchImpl.mock.calls[0]![1] as RequestInit;
|
|
176
|
+
expect(init.method).toBe("PATCH");
|
|
177
|
+
expect(JSON.parse(init.body as string)).toEqual({
|
|
178
|
+
title: "Metadata Team",
|
|
179
|
+
description: "Group metadata",
|
|
180
|
+
});
|
|
181
|
+
expect((init.headers as Record<string, string>)["content-type"]).toBe("application/json");
|
|
182
|
+
expect(result.conversation.description).toBe("Group metadata");
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("metadata user updates still use updateMyProfile with PATCH /v1/users/me", async () => {
|
|
186
|
+
const fetchImpl = vi.fn().mockResolvedValue(
|
|
187
|
+
jsonResponse({
|
|
188
|
+
code: 0,
|
|
189
|
+
message: "ok",
|
|
190
|
+
data: { id: "usr_1", nickname: "Alice", avatar_url: "https://cdn/a.png", bio: "hello" },
|
|
191
|
+
}),
|
|
192
|
+
);
|
|
193
|
+
const client = createOpenclawClawlingApiClient({
|
|
194
|
+
baseUrl: "https://api.example.com",
|
|
195
|
+
token: "tk",
|
|
196
|
+
fetchImpl,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
await client.updateMyProfile({
|
|
200
|
+
nickname: "Alice",
|
|
201
|
+
avatar_url: "https://cdn/a.png",
|
|
202
|
+
bio: "hello",
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/users/me");
|
|
206
|
+
const init = fetchImpl.mock.calls[0]![1] as RequestInit;
|
|
207
|
+
expect(init.method).toBe("PATCH");
|
|
208
|
+
expect(JSON.parse(init.body as string)).toEqual({
|
|
209
|
+
nickname: "Alice",
|
|
210
|
+
avatar_url: "https://cdn/a.png",
|
|
211
|
+
bio: "hello",
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("metadata getConversation preserves participants for caller mapping", async () => {
|
|
216
|
+
const participants = [
|
|
217
|
+
{
|
|
218
|
+
conversation_id: "cnv_1",
|
|
219
|
+
user_id: "usr_1",
|
|
220
|
+
role: "owner",
|
|
221
|
+
joined_at: "2026-05-20T00:00:00Z",
|
|
222
|
+
user: { id: "usr_1", nickname: "Alice", avatar_url: "https://cdn/a.png" },
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
conversation_id: "cnv_1",
|
|
226
|
+
user_id: "usr_2",
|
|
227
|
+
role: "member",
|
|
228
|
+
joined_at: "2026-05-21T00:00:00Z",
|
|
229
|
+
user: { id: "usr_2", nickname: "Bob", avatar_url: "https://cdn/b.png" },
|
|
230
|
+
},
|
|
231
|
+
];
|
|
232
|
+
const fetchImpl = vi.fn().mockResolvedValue(
|
|
233
|
+
jsonResponse({
|
|
234
|
+
code: 0,
|
|
235
|
+
message: "ok",
|
|
236
|
+
data: {
|
|
237
|
+
conversation: {
|
|
238
|
+
id: "cnv_1",
|
|
239
|
+
type: "group",
|
|
240
|
+
title: "Team",
|
|
241
|
+
creator_id: "usr_1",
|
|
242
|
+
created_at: "2026-05-20T00:00:00Z",
|
|
243
|
+
updated_at: "2026-05-24T00:00:00Z",
|
|
244
|
+
participants,
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
}),
|
|
248
|
+
);
|
|
249
|
+
const client = createOpenclawClawlingApiClient({
|
|
250
|
+
baseUrl: "https://api.example.com",
|
|
251
|
+
token: "tk",
|
|
252
|
+
fetchImpl,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const result = await client.getConversation("cnv_1");
|
|
256
|
+
|
|
257
|
+
expect(result.conversation.participants).toEqual(participants);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("metadata patch helpers reject blank ids and empty allowed patches before sending", async () => {
|
|
261
|
+
const fetchImpl = vi.fn();
|
|
262
|
+
const client = createOpenclawClawlingApiClient({
|
|
263
|
+
baseUrl: "https://api.example.com",
|
|
264
|
+
token: "tk",
|
|
265
|
+
fetchImpl,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
await expect(client.patchAgent(" ", { nickname: "Bot" })).rejects.toMatchObject({
|
|
269
|
+
kind: "validation",
|
|
270
|
+
});
|
|
271
|
+
await expect(client.patchAgent("agt_1", {})).rejects.toMatchObject({
|
|
272
|
+
kind: "validation",
|
|
273
|
+
});
|
|
274
|
+
await expect(client.patchConversation("", { title: "Team" })).rejects.toMatchObject({
|
|
275
|
+
kind: "validation",
|
|
276
|
+
});
|
|
277
|
+
await expect(
|
|
278
|
+
client.patchConversation("cnv_1", { filePath: "groups/cnv_1.md" } as never),
|
|
279
|
+
).rejects.toMatchObject({
|
|
280
|
+
kind: "validation",
|
|
281
|
+
});
|
|
282
|
+
expect(fetchImpl).not.toHaveBeenCalled();
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("listFriends uses the friendships endpoint", async () => {
|
|
287
|
+
const fetchImpl = vi.fn().mockResolvedValue(
|
|
288
|
+
jsonResponse({
|
|
289
|
+
code: 0,
|
|
290
|
+
message: "ok",
|
|
291
|
+
data: { friends: [{ id: "u1", nickname: "Alice", type: "user" }] },
|
|
292
|
+
}),
|
|
74
293
|
);
|
|
294
|
+
const client = createOpenclawClawlingApiClient({
|
|
295
|
+
baseUrl: "https://api.example.com",
|
|
296
|
+
token: "tk",
|
|
297
|
+
fetchImpl,
|
|
298
|
+
});
|
|
299
|
+
const result = await client.listFriends();
|
|
300
|
+
expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/friendships");
|
|
301
|
+
expect(result).toEqual({ friends: [{ id: "u1", nickname: "Alice", type: "user" }] });
|
|
75
302
|
});
|
|
76
303
|
|
|
77
304
|
it("searchUsers sends q + limit as query string", async () => {
|
|
@@ -217,7 +444,7 @@ describe("openclaw-clawchat api-client", () => {
|
|
|
217
444
|
expect((fetchImpl.mock.calls[0]![1] as RequestInit).method).toBe("DELETE");
|
|
218
445
|
});
|
|
219
446
|
|
|
220
|
-
it("updateMyProfile sends PATCH /me with JSON body", async () => {
|
|
447
|
+
it("updateMyProfile sends PATCH /me with JSON body without requiring configured userId", async () => {
|
|
221
448
|
const fetchImpl = vi.fn().mockResolvedValue(
|
|
222
449
|
jsonResponse({
|
|
223
450
|
code: 0,
|
|
@@ -228,7 +455,6 @@ describe("openclaw-clawchat api-client", () => {
|
|
|
228
455
|
const client = createOpenclawClawlingApiClient({
|
|
229
456
|
baseUrl: "https://api.example.com",
|
|
230
457
|
token: "tk",
|
|
231
|
-
userId: "agent-1",
|
|
232
458
|
fetchImpl,
|
|
233
459
|
});
|
|
234
460
|
await client.updateMyProfile({ display_name: "Alice2" });
|
|
@@ -258,18 +484,91 @@ describe("openclaw-clawchat api-client", () => {
|
|
|
258
484
|
expect(JSON.parse(init.body as string)).toEqual({ bio: "hello there" });
|
|
259
485
|
});
|
|
260
486
|
|
|
261
|
-
it("
|
|
262
|
-
const fetchImpl = vi.fn()
|
|
487
|
+
it("listConversations sends before + limit as query string", async () => {
|
|
488
|
+
const fetchImpl = vi.fn().mockResolvedValue(
|
|
489
|
+
jsonResponse({
|
|
490
|
+
code: 0,
|
|
491
|
+
message: "ok",
|
|
492
|
+
data: {
|
|
493
|
+
conversations: [
|
|
494
|
+
{
|
|
495
|
+
id: "cnv_1",
|
|
496
|
+
type: "direct",
|
|
497
|
+
title: "Alice",
|
|
498
|
+
created_at: "2026-05-20T00:00:00Z",
|
|
499
|
+
updated_at: "2026-05-20T00:01:00Z",
|
|
500
|
+
peer: { id: "usr_1", type: "user", nickname: "Alice", avatar_url: "https://cdn/a.png" },
|
|
501
|
+
},
|
|
502
|
+
],
|
|
503
|
+
next_before: "cursor-2",
|
|
504
|
+
},
|
|
505
|
+
}),
|
|
506
|
+
);
|
|
263
507
|
const client = createOpenclawClawlingApiClient({
|
|
264
508
|
baseUrl: "https://api.example.com",
|
|
265
509
|
token: "tk",
|
|
266
|
-
// userId intentionally omitted
|
|
267
510
|
fetchImpl,
|
|
268
511
|
});
|
|
269
|
-
await
|
|
270
|
-
|
|
512
|
+
const result = await client.listConversations({ before: "cursor-1", limit: 10 });
|
|
513
|
+
expect(fetchImpl.mock.calls[0]![0]).toBe(
|
|
514
|
+
"https://api.example.com/v1/conversations?before=cursor-1&limit=10",
|
|
515
|
+
);
|
|
516
|
+
expect(result.conversations[0]!.id).toBe("cnv_1");
|
|
517
|
+
expect(result.conversations[0]!.created_at).toBe("2026-05-20T00:00:00Z");
|
|
518
|
+
expect(result.conversations[0]!.peer.nickname).toBe("Alice");
|
|
519
|
+
expect(result.next_before).toBe("cursor-2");
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it("getConversation sends GET /v1/conversations/{id}", async () => {
|
|
523
|
+
const fetchImpl = vi.fn().mockResolvedValue(
|
|
524
|
+
jsonResponse({
|
|
525
|
+
code: 0,
|
|
526
|
+
message: "ok",
|
|
527
|
+
data: {
|
|
528
|
+
conversation: {
|
|
529
|
+
id: "cnv_1",
|
|
530
|
+
type: "group",
|
|
531
|
+
title: "Team",
|
|
532
|
+
creator_id: "usr_creator",
|
|
533
|
+
created_at: "2026-05-20T00:00:00Z",
|
|
534
|
+
updated_at: "2026-05-20T00:01:00Z",
|
|
535
|
+
participants: [
|
|
536
|
+
{
|
|
537
|
+
conversation_id: "cnv_1",
|
|
538
|
+
user_id: "usr_1",
|
|
539
|
+
role: "owner",
|
|
540
|
+
joined_at: "2026-05-20T00:00:00Z",
|
|
541
|
+
},
|
|
542
|
+
],
|
|
543
|
+
},
|
|
544
|
+
},
|
|
545
|
+
}),
|
|
546
|
+
);
|
|
547
|
+
const client = createOpenclawClawlingApiClient({
|
|
548
|
+
baseUrl: "https://api.example.com",
|
|
549
|
+
token: "tk",
|
|
550
|
+
fetchImpl,
|
|
271
551
|
});
|
|
272
|
-
|
|
552
|
+
const result = await client.getConversation("cnv_1");
|
|
553
|
+
expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/conversations/cnv_1");
|
|
554
|
+
expect(result.conversation.title).toBe("Team");
|
|
555
|
+
expect(result.conversation.creator_id).toBe("usr_creator");
|
|
556
|
+
expect(result.conversation.participants[0]!.user_id).toBe("usr_1");
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it("does not expose conversation admin mutation methods", () => {
|
|
560
|
+
const client = createOpenclawClawlingApiClient({
|
|
561
|
+
baseUrl: "https://api.example.com",
|
|
562
|
+
token: "tk",
|
|
563
|
+
fetchImpl: vi.fn(),
|
|
564
|
+
});
|
|
565
|
+
expect(client).not.toHaveProperty("createConversation");
|
|
566
|
+
expect(client).not.toHaveProperty("updateConversation");
|
|
567
|
+
expect(client).not.toHaveProperty("leaveConversation");
|
|
568
|
+
expect(client).not.toHaveProperty("dissolveConversation");
|
|
569
|
+
expect(client).not.toHaveProperty("addConversationUsers");
|
|
570
|
+
expect(client).not.toHaveProperty("removeConversationUsers");
|
|
571
|
+
expect(client).not.toHaveProperty("listConversationUsers");
|
|
273
572
|
});
|
|
274
573
|
|
|
275
574
|
it("uploadMedia POSTs multipart with field name 'file'", async () => {
|
|
@@ -277,7 +576,7 @@ describe("openclaw-clawchat api-client", () => {
|
|
|
277
576
|
jsonResponse({
|
|
278
577
|
code: 0,
|
|
279
578
|
message: "ok",
|
|
280
|
-
data: { url: "https://cdn/x.png", size: 12, mime: "image/png" },
|
|
579
|
+
data: { kind: "image", url: "https://cdn/x.png", name: "x.png", size: 12, mime: "image/png" },
|
|
281
580
|
}),
|
|
282
581
|
);
|
|
283
582
|
const client = createOpenclawClawlingApiClient({
|
|
@@ -301,7 +600,51 @@ describe("openclaw-clawchat api-client", () => {
|
|
|
301
600
|
expect(file).toBeInstanceOf(File);
|
|
302
601
|
expect(file.name).toBe("x.png");
|
|
303
602
|
expect(file.type).toBe("image/png");
|
|
304
|
-
expect(result).toEqual({ url: "https://cdn/x.png", size: 12, mime: "image/png" });
|
|
603
|
+
expect(result).toEqual({ kind: "image", url: "https://cdn/x.png", name: "x.png", size: 12, mime: "image/png" });
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it.each([
|
|
607
|
+
["kind", { url: "https://cdn/x.png", name: "x.png", size: 12, mime: "image/png" }],
|
|
608
|
+
["url", { kind: "image", name: "x.png", size: 12, mime: "image/png" }],
|
|
609
|
+
["name", { kind: "image", url: "https://cdn/x.png", size: 12, mime: "image/png" }],
|
|
610
|
+
["mime", { kind: "image", url: "https://cdn/x.png", name: "x.png", size: 12 }],
|
|
611
|
+
["size", { kind: "image", url: "https://cdn/x.png", name: "x.png", mime: "image/png" }],
|
|
612
|
+
])("uploadMedia treats missing %s in response data as protocol error", async (_field, data) => {
|
|
613
|
+
const fetchImpl = vi.fn().mockResolvedValue(jsonResponse({ code: 0, message: "ok", data }));
|
|
614
|
+
const client = createOpenclawClawlingApiClient({
|
|
615
|
+
baseUrl: "https://api.example.com",
|
|
616
|
+
token: "tk",
|
|
617
|
+
fetchImpl,
|
|
618
|
+
});
|
|
619
|
+
await expect(
|
|
620
|
+
client.uploadMedia({
|
|
621
|
+
buffer: Buffer.from("hi-bytes-12!"),
|
|
622
|
+
filename: "x.png",
|
|
623
|
+
mime: "image/png",
|
|
624
|
+
}),
|
|
625
|
+
).rejects.toMatchObject({ kind: "api" });
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
it("uploadMedia treats unsupported kind in response data as protocol error", async () => {
|
|
629
|
+
const fetchImpl = vi.fn().mockResolvedValue(
|
|
630
|
+
jsonResponse({
|
|
631
|
+
code: 0,
|
|
632
|
+
message: "ok",
|
|
633
|
+
data: { kind: "bogus", url: "https://cdn/x.png", name: "x.png", size: 12, mime: "image/png" },
|
|
634
|
+
}),
|
|
635
|
+
);
|
|
636
|
+
const client = createOpenclawClawlingApiClient({
|
|
637
|
+
baseUrl: "https://api.example.com",
|
|
638
|
+
token: "tk",
|
|
639
|
+
fetchImpl,
|
|
640
|
+
});
|
|
641
|
+
await expect(
|
|
642
|
+
client.uploadMedia({
|
|
643
|
+
buffer: Buffer.from("hi-bytes-12!"),
|
|
644
|
+
filename: "x.png",
|
|
645
|
+
mime: "image/png",
|
|
646
|
+
}),
|
|
647
|
+
).rejects.toMatchObject({ kind: "api" });
|
|
305
648
|
});
|
|
306
649
|
|
|
307
650
|
it("uploadAvatar POSTs multipart to /v1/files/upload-url", async () => {
|
|
@@ -333,6 +676,8 @@ describe("openclaw-clawchat api-client", () => {
|
|
|
333
676
|
expect(file.type).toBe("image/png");
|
|
334
677
|
// X-Device-Id is present on avatar uploads too.
|
|
335
678
|
expect((init.headers as Record<string, string>)["x-device-id"]).toBe("openclaw-clawchat");
|
|
679
|
+
expect(result).not.toHaveProperty("kind");
|
|
680
|
+
expect(result).not.toHaveProperty("name");
|
|
336
681
|
expect(result).toEqual({ url: "https://cdn/avatars/a.png", size: 99, mime: "image/png" });
|
|
337
682
|
});
|
|
338
683
|
|
package/src/api-client.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ClawlingApiError,
|
|
3
|
+
type AgentMetadataPatch,
|
|
4
|
+
type AgentProfile,
|
|
3
5
|
type AgentConnectResult,
|
|
6
|
+
type AvatarUploadResult,
|
|
7
|
+
type ConversationDetails,
|
|
8
|
+
type ConversationListResult,
|
|
9
|
+
type ConversationMetadataPatch,
|
|
4
10
|
type FriendList,
|
|
5
11
|
type MomentComment,
|
|
6
12
|
type MomentReaction,
|
|
@@ -14,11 +20,7 @@ import { CHANNEL_ID } from "./config.ts";
|
|
|
14
20
|
export interface ApiClientOptions {
|
|
15
21
|
baseUrl: string;
|
|
16
22
|
token: string;
|
|
17
|
-
/**
|
|
18
|
-
* Logged-in agent's `user_id`. Required for `updateMyProfile`, which
|
|
19
|
-
* targets `/v1/agents/{userId}`. Safe to omit for unauthenticated /
|
|
20
|
-
* pre-login calls (e.g. `agentsConnect`).
|
|
21
|
-
*/
|
|
23
|
+
/** Logged-in agent's `user_id`, used by WebSocket setup but not REST `/me` calls. */
|
|
22
24
|
userId?: string;
|
|
23
25
|
/** Test override only. Defaults to global `fetch`. */
|
|
24
26
|
fetchImpl?: typeof fetch;
|
|
@@ -26,8 +28,12 @@ export interface ApiClientOptions {
|
|
|
26
28
|
|
|
27
29
|
export interface OpenclawClawlingApiClient {
|
|
28
30
|
getMyProfile(): Promise<Profile>;
|
|
31
|
+
getAgentProfile(agentId: string): Promise<{ agent: AgentProfile }>;
|
|
32
|
+
getAgentDetail(agentId: string): Promise<{ agent: AgentProfile }>;
|
|
33
|
+
patchAgent(agentId: string, patch: AgentMetadataPatch): Promise<{ agent: AgentProfile }>;
|
|
29
34
|
getUserInfo(userId: string): Promise<Profile>;
|
|
30
|
-
|
|
35
|
+
getUserProfile(userId: string): Promise<Profile>;
|
|
36
|
+
listFriends(): Promise<FriendList>;
|
|
31
37
|
searchUsers(params: { q?: string; limit?: number }): Promise<{ users: UserSearchHit[] }>;
|
|
32
38
|
listMoments(params: { before?: number; limit?: number }): Promise<{ moments: MomentView[] }>;
|
|
33
39
|
createMoment(body: { text?: string; images?: string[] }): Promise<{ moment: MomentView }>;
|
|
@@ -46,6 +52,12 @@ export interface OpenclawClawlingApiClient {
|
|
|
46
52
|
text: string;
|
|
47
53
|
}): Promise<{ comment: MomentComment }>;
|
|
48
54
|
deleteMomentComment(params: { momentId: number; commentId: number }): Promise<{ ok: boolean }>;
|
|
55
|
+
listConversations(params: { before?: string; limit?: number }): Promise<ConversationListResult>;
|
|
56
|
+
getConversation(conversationId: string): Promise<{ conversation: ConversationDetails }>;
|
|
57
|
+
patchConversation(
|
|
58
|
+
conversationId: string,
|
|
59
|
+
patch: ConversationMetadataPatch,
|
|
60
|
+
): Promise<{ conversation: ConversationDetails }>;
|
|
49
61
|
updateMyProfile(patch: { nickname?: string; avatar_url?: string; bio?: string }): Promise<Profile>;
|
|
50
62
|
uploadMedia(params: { buffer: Buffer; filename: string; mime?: string }): Promise<UploadResult>;
|
|
51
63
|
/**
|
|
@@ -61,15 +73,14 @@ export interface OpenclawClawlingApiClient {
|
|
|
61
73
|
type: string;
|
|
62
74
|
}): Promise<AgentConnectResult>;
|
|
63
75
|
/**
|
|
64
|
-
* Upload an avatar image via `POST /v1/files/upload-url`.
|
|
65
|
-
*
|
|
66
|
-
* what you then pass to `updateMyProfile({ avatar: url })`.
|
|
76
|
+
* Upload an avatar image via `POST /v1/files/upload-url`. The resulting
|
|
77
|
+
* `url` is what you then pass to `updateMyProfile({ avatar_url: url })`.
|
|
67
78
|
*/
|
|
68
79
|
uploadAvatar(params: {
|
|
69
80
|
buffer: Buffer;
|
|
70
81
|
filename: string;
|
|
71
82
|
mime?: string;
|
|
72
|
-
}): Promise<
|
|
83
|
+
}): Promise<AvatarUploadResult>;
|
|
73
84
|
}
|
|
74
85
|
|
|
75
86
|
export function createOpenclawClawlingApiClient(opts: ApiClientOptions): OpenclawClawlingApiClient {
|
|
@@ -173,6 +184,50 @@ export function createOpenclawClawlingApiClient(opts: ApiClientOptions): Opencla
|
|
|
173
184
|
return await readEnvelope<T>(res, path);
|
|
174
185
|
}
|
|
175
186
|
|
|
187
|
+
function parseUploadResult(data: unknown, path: string): UploadResult {
|
|
188
|
+
const obj = data as Partial<UploadResult> | null;
|
|
189
|
+
const validKind =
|
|
190
|
+
obj?.kind === "image" || obj?.kind === "file" || obj?.kind === "audio" || obj?.kind === "video";
|
|
191
|
+
if (
|
|
192
|
+
!obj ||
|
|
193
|
+
!validKind ||
|
|
194
|
+
typeof obj.url !== "string" ||
|
|
195
|
+
typeof obj.name !== "string" ||
|
|
196
|
+
typeof obj.mime !== "string" ||
|
|
197
|
+
typeof obj.size !== "number"
|
|
198
|
+
) {
|
|
199
|
+
throw new ClawlingApiError(
|
|
200
|
+
"api",
|
|
201
|
+
"invalid upload response: missing required media fields",
|
|
202
|
+
{ path },
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
return obj as UploadResult;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function assertNonBlankId(value: string, label: string): void {
|
|
209
|
+
if (!value.trim()) {
|
|
210
|
+
throw new ClawlingApiError("validation", `${label} is required`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function pickPatch<T extends object>(
|
|
215
|
+
patch: Record<string, unknown>,
|
|
216
|
+
keys: readonly (keyof T & string)[],
|
|
217
|
+
label: string,
|
|
218
|
+
): T {
|
|
219
|
+
const body: Record<string, unknown> = {};
|
|
220
|
+
for (const key of keys) {
|
|
221
|
+
if (Object.prototype.hasOwnProperty.call(patch, key) && patch[key] !== undefined) {
|
|
222
|
+
body[key] = patch[key];
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (Object.keys(body).length === 0) {
|
|
226
|
+
throw new ClawlingApiError("validation", `${label} patch must include at least one mutable field`);
|
|
227
|
+
}
|
|
228
|
+
return body as T;
|
|
229
|
+
}
|
|
230
|
+
|
|
176
231
|
// All JSON API endpoints live under `/v1/...`. Media upload is the one
|
|
177
232
|
// intentional exception — the upstream server mounts it at `/media/upload`
|
|
178
233
|
// without the version prefix.
|
|
@@ -180,15 +235,32 @@ export function createOpenclawClawlingApiClient(opts: ApiClientOptions): Opencla
|
|
|
180
235
|
async getMyProfile(): Promise<Profile> {
|
|
181
236
|
return await call<Profile>("GET", "/v1/users/me");
|
|
182
237
|
},
|
|
238
|
+
async getAgentProfile(agentId: string): Promise<{ agent: AgentProfile }> {
|
|
239
|
+
return await call<{ agent: AgentProfile }>("GET", `/v1/agents/${encodeURIComponent(agentId)}`);
|
|
240
|
+
},
|
|
241
|
+
async getAgentDetail(agentId: string): Promise<{ agent: AgentProfile }> {
|
|
242
|
+
return await call<{ agent: AgentProfile }>("GET", `/v1/agents/${encodeURIComponent(agentId)}`);
|
|
243
|
+
},
|
|
244
|
+
async patchAgent(agentId: string, patch: AgentMetadataPatch): Promise<{ agent: AgentProfile }> {
|
|
245
|
+
assertNonBlankId(agentId, "patchAgent: agentId");
|
|
246
|
+
const body = pickPatch<AgentMetadataPatch>(
|
|
247
|
+
patch as Record<string, unknown>,
|
|
248
|
+
["nickname", "avatar_url", "bio", "behavior"],
|
|
249
|
+
"patchAgent",
|
|
250
|
+
);
|
|
251
|
+
return await call<{ agent: AgentProfile }>("PATCH", `/v1/agents/${encodeURIComponent(agentId)}`, {
|
|
252
|
+
body: JSON.stringify(body),
|
|
253
|
+
headers: { "content-type": "application/json" },
|
|
254
|
+
});
|
|
255
|
+
},
|
|
183
256
|
async getUserInfo(userId: string): Promise<Profile> {
|
|
184
257
|
return await call<Profile>("GET", `/v1/users/${encodeURIComponent(userId)}`);
|
|
185
258
|
},
|
|
186
|
-
async
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
return await call<FriendList>("GET", q ? `/v1/friends?${q}` : "/v1/friends");
|
|
259
|
+
async getUserProfile(userId: string): Promise<Profile> {
|
|
260
|
+
return await call<Profile>("GET", `/v1/users/${encodeURIComponent(userId)}`);
|
|
261
|
+
},
|
|
262
|
+
async listFriends(): Promise<FriendList> {
|
|
263
|
+
return await call<FriendList>("GET", "/v1/friendships");
|
|
192
264
|
},
|
|
193
265
|
async searchUsers(params): Promise<{ users: UserSearchHit[] }> {
|
|
194
266
|
const sp = new URLSearchParams();
|
|
@@ -255,13 +327,42 @@ export function createOpenclawClawlingApiClient(opts: ApiClientOptions): Opencla
|
|
|
255
327
|
`/v1/moments/${encodeURIComponent(String(params.momentId))}/comments/${encodeURIComponent(String(params.commentId))}`,
|
|
256
328
|
);
|
|
257
329
|
},
|
|
330
|
+
async listConversations(params): Promise<ConversationListResult> {
|
|
331
|
+
const sp = new URLSearchParams();
|
|
332
|
+
if (typeof params.before === "string") sp.set("before", params.before);
|
|
333
|
+
if (typeof params.limit === "number") sp.set("limit", String(params.limit));
|
|
334
|
+
const q = sp.toString();
|
|
335
|
+
return await call<ConversationListResult>(
|
|
336
|
+
"GET",
|
|
337
|
+
q ? `/v1/conversations?${q}` : "/v1/conversations",
|
|
338
|
+
);
|
|
339
|
+
},
|
|
340
|
+
async getConversation(conversationId): Promise<{ conversation: ConversationDetails }> {
|
|
341
|
+
return await call<{ conversation: ConversationDetails }>(
|
|
342
|
+
"GET",
|
|
343
|
+
`/v1/conversations/${encodeURIComponent(conversationId)}`,
|
|
344
|
+
);
|
|
345
|
+
},
|
|
346
|
+
async patchConversation(
|
|
347
|
+
conversationId: string,
|
|
348
|
+
patch: ConversationMetadataPatch,
|
|
349
|
+
): Promise<{ conversation: ConversationDetails }> {
|
|
350
|
+
assertNonBlankId(conversationId, "patchConversation: conversationId");
|
|
351
|
+
const body = pickPatch<ConversationMetadataPatch>(
|
|
352
|
+
patch as Record<string, unknown>,
|
|
353
|
+
["title", "description"],
|
|
354
|
+
"patchConversation",
|
|
355
|
+
);
|
|
356
|
+
return await call<{ conversation: ConversationDetails }>(
|
|
357
|
+
"PATCH",
|
|
358
|
+
`/v1/conversations/${encodeURIComponent(conversationId)}`,
|
|
359
|
+
{
|
|
360
|
+
body: JSON.stringify(body),
|
|
361
|
+
headers: { "content-type": "application/json" },
|
|
362
|
+
},
|
|
363
|
+
);
|
|
364
|
+
},
|
|
258
365
|
async updateMyProfile(patch): Promise<Profile> {
|
|
259
|
-
if (!opts.userId?.trim()) {
|
|
260
|
-
throw new ClawlingApiError(
|
|
261
|
-
"validation",
|
|
262
|
-
"updateMyProfile: userId is required to target /v1/agents/{userId}",
|
|
263
|
-
);
|
|
264
|
-
}
|
|
265
366
|
return await call<Profile>(
|
|
266
367
|
"PATCH",
|
|
267
368
|
`/v1/users/me`,
|
|
@@ -300,9 +401,10 @@ export function createOpenclawClawlingApiClient(opts: ApiClientOptions): Opencla
|
|
|
300
401
|
});
|
|
301
402
|
const fd = new FormData();
|
|
302
403
|
fd.set("file", file);
|
|
303
|
-
|
|
404
|
+
const data = await call<unknown>("POST", "/media/upload", { body: fd });
|
|
405
|
+
return parseUploadResult(data, "/media/upload");
|
|
304
406
|
},
|
|
305
|
-
async uploadAvatar(params): Promise<
|
|
407
|
+
async uploadAvatar(params): Promise<AvatarUploadResult> {
|
|
306
408
|
const blob = new Blob([new Uint8Array(params.buffer)], {
|
|
307
409
|
type: params.mime ?? "application/octet-stream",
|
|
308
410
|
});
|
|
@@ -311,7 +413,7 @@ export function createOpenclawClawlingApiClient(opts: ApiClientOptions): Opencla
|
|
|
311
413
|
});
|
|
312
414
|
const fd = new FormData();
|
|
313
415
|
fd.set("file", file);
|
|
314
|
-
return await call<
|
|
416
|
+
return await call<AvatarUploadResult>("POST", "/v1/files/upload-url", { body: fd });
|
|
315
417
|
},
|
|
316
418
|
};
|
|
317
419
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ConversationListItem } from "./api-types.ts";
|
|
2
|
+
|
|
3
|
+
const listItemWithSoftDeletedPeer: ConversationListItem = {
|
|
4
|
+
id: "cnv_1",
|
|
5
|
+
type: "direct",
|
|
6
|
+
title: "Deleted peer",
|
|
7
|
+
created_at: "2026-05-20T00:00:00Z",
|
|
8
|
+
updated_at: "2026-05-20T00:01:00Z",
|
|
9
|
+
peer: null,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
void listItemWithSoftDeletedPeer;
|