@newbase-clawchat/openclaw-clawchat 2026.4.21 → 2026.4.23

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.
@@ -166,6 +166,8 @@ describe("openclaw-clawchat runtime media ingest", () => {
166
166
  event: "message.send",
167
167
  trace_id: "ti",
168
168
  emitted_at: Date.now(),
169
+ chat_id: "chat-1",
170
+ chat_type: "direct",
169
171
  to: { id: "u", type: "direct" },
170
172
  sender: { sender_id: "user-1", type: "direct", display_name: "User" },
171
173
  payload: {
@@ -198,6 +200,302 @@ describe("openclaw-clawchat runtime media ingest", () => {
198
200
  expect(fetched).toEqual([{ url: "https://cdn/a.png" }]);
199
201
  expect(capturedCtx?.MediaPath).toBe("/cache/1.png");
200
202
  expect(capturedCtx?.MediaPaths).toEqual(["/cache/1.png"]);
203
+ expect(capturedCtx?.From).toBe("openclaw-clawchat:chat-1");
204
+ expect(capturedCtx?.ConversationLabel).toBe("chat-1");
205
+ expect(capturedCtx?.SenderId).toBe("user-1");
206
+ });
207
+
208
+ it("uses group chat_id as the canonical conversation identity", async () => {
209
+ let capturedCtx: Record<string, unknown> | undefined;
210
+ const finalizeInboundContext = vi.fn((ctx: Record<string, unknown>) => {
211
+ capturedCtx = ctx;
212
+ return ctx;
213
+ });
214
+
215
+ const runtime = {
216
+ channel: {
217
+ routing: {
218
+ resolveAgentRoute: vi.fn(() => ({
219
+ agentId: "u",
220
+ accountId: "default",
221
+ sessionKey: "s",
222
+ })),
223
+ },
224
+ session: {
225
+ resolveStorePath: vi.fn(() => "/tmp/sessions.json"),
226
+ recordInboundSession: vi.fn().mockResolvedValue(undefined),
227
+ },
228
+ reply: {
229
+ formatAgentEnvelope: vi.fn((p: { body: string }) => p.body),
230
+ resolveEnvelopeFormatOptions: vi.fn(() => ({})),
231
+ finalizeInboundContext,
232
+ resolveHumanDelayConfig: vi.fn(() => ({ enabled: false })),
233
+ createReplyDispatcherWithTyping: vi.fn(() => ({
234
+ dispatcher: {},
235
+ replyOptions: {},
236
+ markDispatchIdle: vi.fn(),
237
+ markRunComplete: vi.fn(),
238
+ })),
239
+ withReplyDispatcher: vi.fn(async (opts: { run: () => Promise<unknown> }) => {
240
+ await opts.run();
241
+ }),
242
+ dispatchReplyFromConfig: vi.fn().mockResolvedValue(undefined),
243
+ },
244
+ media: {
245
+ fetchRemoteMedia: vi.fn(),
246
+ saveMediaBuffer: vi.fn(),
247
+ loadWebMedia: vi.fn(),
248
+ },
249
+ },
250
+ } as unknown as import("openclaw/plugin-sdk/core").PluginRuntime;
251
+
252
+ setOpenclawClawlingRuntime(runtime);
253
+
254
+ const { startOpenclawClawlingGateway } = await import("./runtime.ts");
255
+ const transport = new MockTransport();
256
+ const abortController = new AbortController();
257
+
258
+ const startPromise = startOpenclawClawlingGateway({
259
+ cfg: {} as import("openclaw/plugin-sdk/core").OpenClawConfig,
260
+ account: {
261
+ accountId: "default",
262
+ name: "openclaw-clawchat",
263
+ enabled: true,
264
+ configured: true,
265
+ websocketUrl: "ws://t",
266
+ baseUrl: "https://api.example.com",
267
+ token: "tk",
268
+ userId: "u",
269
+ replyMode: "static",
270
+ groupMode: "all",
271
+ forwardThinking: true,
272
+ forwardToolCalls: false,
273
+ allowFrom: [],
274
+ stream: { flushIntervalMs: 250, minChunkChars: 40, maxBufferChars: 2000 },
275
+ reconnect: {
276
+ initialDelay: 1000,
277
+ maxDelay: 30000,
278
+ jitterRatio: 0.3,
279
+ maxRetries: Number.POSITIVE_INFINITY,
280
+ },
281
+ heartbeat: { interval: 25000, timeout: 10000 },
282
+ ack: { timeout: 10000, autoResendOnTimeout: false },
283
+ },
284
+ abortSignal: abortController.signal,
285
+ setStatus: vi.fn(),
286
+ getStatus: vi.fn(() => ({ accountId: "default" })),
287
+ log: { info: () => {}, error: () => {} },
288
+ transport,
289
+ });
290
+
291
+ await new Promise((r) => setTimeout(r, 0));
292
+ transport.emitInbound(
293
+ JSON.stringify({
294
+ version: "2",
295
+ event: "connect.challenge",
296
+ trace_id: "tc",
297
+ emitted_at: Date.now(),
298
+ payload: { nonce: "n" },
299
+ }),
300
+ );
301
+ transport.emitInbound(
302
+ JSON.stringify({
303
+ version: "2",
304
+ event: "hello-ok",
305
+ trace_id: "th",
306
+ emitted_at: Date.now(),
307
+ payload: {},
308
+ }),
309
+ );
310
+ await new Promise((r) => setTimeout(r, 5));
311
+
312
+ transport.emitInbound(
313
+ JSON.stringify({
314
+ version: "2",
315
+ event: "message.send",
316
+ trace_id: "tg",
317
+ emitted_at: Date.now(),
318
+ chat_id: "grp-1",
319
+ chat_type: "group",
320
+ to: { id: "u", type: "group" },
321
+ sender: { sender_id: "user-1", type: "group", display_name: "Alice" },
322
+ payload: {
323
+ message_id: "m-group",
324
+ message_mode: "normal",
325
+ message: {
326
+ body: {
327
+ fragments: [{ kind: "text", text: "hello group" }],
328
+ },
329
+ context: { mentions: [], reply: null },
330
+ streaming: {
331
+ status: "static",
332
+ sequence: 0,
333
+ mutation_policy: "sealed",
334
+ started_at: null,
335
+ completed_at: null,
336
+ },
337
+ },
338
+ },
339
+ }),
340
+ );
341
+ await new Promise((r) => setTimeout(r, 30));
342
+ abortController.abort();
343
+ await startPromise;
344
+
345
+ expect(capturedCtx?.From).toBe("openclaw-clawchat:group:grp-1");
346
+ expect(capturedCtx?.ConversationLabel).toBe("group:grp-1");
347
+ expect(capturedCtx?.SenderId).toBe("user-1");
348
+ expect(capturedCtx?.ChatType).toBe("group");
349
+ });
350
+ });
351
+
352
+ describe("openclaw-clawchat runtime reply dispatch lifecycle", () => {
353
+ it("marks dispatch idle when reply dispatch fails", async () => {
354
+ const markDispatchIdle = vi.fn();
355
+ const withReplyDispatcher = vi.fn(
356
+ async (opts: { run: () => Promise<unknown>; onSettled?: () => void | Promise<void> }) => {
357
+ try {
358
+ await opts.run();
359
+ } finally {
360
+ await opts.onSettled?.();
361
+ }
362
+ },
363
+ );
364
+ const dispatchReplyFromConfig = vi.fn().mockRejectedValue(new Error("dispatch boom"));
365
+ const logError = vi.fn();
366
+
367
+ const runtime = {
368
+ channel: {
369
+ routing: {
370
+ resolveAgentRoute: vi.fn(() => ({
371
+ agentId: "u",
372
+ accountId: "default",
373
+ sessionKey: "s",
374
+ })),
375
+ },
376
+ session: {
377
+ resolveStorePath: vi.fn(() => "/tmp/sessions.json"),
378
+ recordInboundSession: vi.fn().mockResolvedValue(undefined),
379
+ },
380
+ reply: {
381
+ formatAgentEnvelope: vi.fn((p: { body: string }) => p.body),
382
+ resolveEnvelopeFormatOptions: vi.fn(() => ({})),
383
+ finalizeInboundContext: vi.fn((ctx: Record<string, unknown>) => ctx),
384
+ resolveHumanDelayConfig: vi.fn(() => ({ enabled: false })),
385
+ createReplyDispatcherWithTyping: vi.fn(() => ({
386
+ dispatcher: {},
387
+ replyOptions: {},
388
+ markDispatchIdle,
389
+ })),
390
+ withReplyDispatcher,
391
+ dispatchReplyFromConfig,
392
+ },
393
+ media: {
394
+ fetchRemoteMedia: vi.fn(),
395
+ saveMediaBuffer: vi.fn(),
396
+ loadWebMedia: vi.fn(),
397
+ },
398
+ },
399
+ } as unknown as PluginRuntime;
400
+
401
+ setOpenclawClawlingRuntime(runtime);
402
+
403
+ const { startOpenclawClawlingGateway } = await import("./runtime.ts");
404
+ const transport = new MockTransport();
405
+ const abortController = new AbortController();
406
+
407
+ const startPromise = startOpenclawClawlingGateway({
408
+ cfg: {} as OpenClawConfig,
409
+ account: {
410
+ accountId: "default",
411
+ name: "openclaw-clawchat",
412
+ enabled: true,
413
+ configured: true,
414
+ websocketUrl: "ws://t",
415
+ baseUrl: "https://api.example.com",
416
+ token: "tk",
417
+ userId: "u",
418
+ replyMode: "static",
419
+ forwardThinking: true,
420
+ forwardToolCalls: false,
421
+ allowFrom: [],
422
+ stream: { flushIntervalMs: 250, minChunkChars: 40, maxBufferChars: 2000 },
423
+ reconnect: {
424
+ initialDelay: 1000,
425
+ maxDelay: 30000,
426
+ jitterRatio: 0.3,
427
+ maxRetries: Number.POSITIVE_INFINITY,
428
+ },
429
+ heartbeat: { interval: 25000, timeout: 10000 },
430
+ ack: { timeout: 10000, autoResendOnTimeout: false },
431
+ },
432
+ abortSignal: abortController.signal,
433
+ setStatus: vi.fn(),
434
+ getStatus: vi.fn(() => ({ accountId: "default" })),
435
+ log: { info: vi.fn(), error: logError },
436
+ transport,
437
+ });
438
+
439
+ await new Promise((r) => setTimeout(r, 0));
440
+ transport.emitInbound(
441
+ JSON.stringify({
442
+ version: "2",
443
+ event: "connect.challenge",
444
+ trace_id: "tc",
445
+ emitted_at: Date.now(),
446
+ payload: { nonce: "n" },
447
+ }),
448
+ );
449
+ transport.emitInbound(
450
+ JSON.stringify({
451
+ version: "2",
452
+ event: "hello-ok",
453
+ trace_id: "th",
454
+ emitted_at: Date.now(),
455
+ payload: {},
456
+ }),
457
+ );
458
+ await new Promise((r) => setTimeout(r, 5));
459
+
460
+ transport.emitInbound(
461
+ JSON.stringify({
462
+ version: "2",
463
+ event: "message.send",
464
+ trace_id: "tm",
465
+ emitted_at: Date.now(),
466
+ chat_id: "chat-1",
467
+ chat_type: "direct",
468
+ to: { id: "u", type: "direct" },
469
+ sender: { sender_id: "user-1", type: "direct", display_name: "User" },
470
+ payload: {
471
+ message_id: "m-fail",
472
+ message_mode: "normal",
473
+ message: {
474
+ body: {
475
+ fragments: [{ kind: "text", text: "hello" }],
476
+ },
477
+ context: { mentions: [], reply: null },
478
+ streaming: {
479
+ status: "static",
480
+ sequence: 0,
481
+ mutation_policy: "sealed",
482
+ started_at: null,
483
+ completed_at: null,
484
+ },
485
+ },
486
+ },
487
+ }),
488
+ );
489
+
490
+ await new Promise((r) => setTimeout(r, 30));
491
+ abortController.abort();
492
+ await startPromise;
493
+
494
+ expect(dispatchReplyFromConfig).toHaveBeenCalledTimes(1);
495
+ expect(markDispatchIdle).toHaveBeenCalledTimes(1);
496
+ expect(logError).toHaveBeenCalledWith(
497
+ expect.stringContaining("openclaw-clawchat message handler error:"),
498
+ );
201
499
  });
202
500
  });
203
501
 
package/src/runtime.ts CHANGED
@@ -19,6 +19,7 @@ import { dispatchOpenclawClawlingInbound } from "./inbound.ts";
19
19
  import { fetchInboundMedia } from "./media-runtime.ts";
20
20
  import { createOpenclawClawlingReplyDispatcher } from "./reply-dispatcher.ts";
21
21
  import { sendStreamingText } from "./streaming.ts";
22
+ import { sendOpenclawClawlingText } from "./outbound.ts";
22
23
 
23
24
  type Log = { info?: (m: string) => void; error?: (m: string) => void };
24
25
 
@@ -33,6 +34,28 @@ export function getOpenclawClawlingClient(accountId: string): ClawlingChatClient
33
34
  return activeClients.get(accountId);
34
35
  }
35
36
 
37
+ export async function waitForOpenclawClawlingClient(
38
+ accountId: string,
39
+ options: { timeoutMs?: number; pollMs?: number } = {},
40
+ ): Promise<ClawlingChatClient> {
41
+ const timeoutMs = options.timeoutMs ?? 15_000;
42
+ const pollMs = options.pollMs ?? 100;
43
+ const deadline = Date.now() + timeoutMs;
44
+
45
+ for (;;) {
46
+ const client = activeClients.get(accountId);
47
+ if (client && (client as { state?: string }).state === "connected") {
48
+ return client;
49
+ }
50
+ if (Date.now() >= deadline) {
51
+ throw new Error(
52
+ `openclaw-clawchat client did not activate within ${timeoutMs}ms for account ${accountId}`,
53
+ );
54
+ }
55
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
56
+ }
57
+ }
58
+
36
59
  export type ClawlingState =
37
60
  | "idle"
38
61
  | "connecting"
@@ -80,6 +103,10 @@ export function classifyClawlingClientError(err: unknown): {
80
103
  };
81
104
  }
82
105
 
106
+ function formatConversationSubject(peer: { kind: "direct" | "group"; id: string }): string {
107
+ return peer.kind === "group" ? `group:${peer.id}` : peer.id;
108
+ }
109
+
83
110
  export interface StartGatewayParams {
84
111
  cfg: OpenClawConfig;
85
112
  account: ResolvedOpenclawClawlingAccount;
@@ -134,8 +161,9 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
134
161
  }
135
162
  });
136
163
 
137
- client.on("message", (env: Envelope) => {
138
- dispatchOpenclawClawlingInbound({
164
+ client.on("message", async (env: Envelope) => {
165
+ try {
166
+ await dispatchOpenclawClawlingInbound({
139
167
  envelope: env as Envelope<DownlinkMessageSendPayload>,
140
168
  cfg,
141
169
  runtime,
@@ -152,7 +180,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
152
180
  });
153
181
  const body = rt.reply.formatAgentEnvelope({
154
182
  channel: "Clawling Chat",
155
- from: turn.senderNickName || turn.senderId,
183
+ from: formatConversationSubject(turn.peer),
156
184
  body: turn.rawBody,
157
185
  timestamp: turn.timestamp,
158
186
  ...rt.reply.resolveEnvelopeFormatOptions(cfg),
@@ -162,12 +190,16 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
162
190
  BodyForAgent: turn.rawBody,
163
191
  RawBody: turn.rawBody,
164
192
  CommandBody: turn.rawBody,
165
- From: `${CHANNEL_ID}:${turn.senderId}`,
193
+ // Clawling v2 routes by chat_id. `senderId` is still preserved as
194
+ // structured metadata, but the conversation target must be based on
195
+ // `peer.id` so follow-up sends address the active chat, not merely
196
+ // the human sender identity.
197
+ From: `${CHANNEL_ID}:${formatConversationSubject(turn.peer)}`,
166
198
  To: `${CHANNEL_ID}:${account.userId}`,
167
199
  SessionKey: route.sessionKey,
168
200
  AccountId: route.accountId ?? accountId,
169
201
  ChatType: turn.peer.kind,
170
- ConversationLabel: turn.senderNickName || turn.senderId,
202
+ ConversationLabel: formatConversationSubject(turn.peer),
171
203
  SenderId: turn.senderId,
172
204
  Provider: CHANNEL_ID,
173
205
  Surface: CHANNEL_ID,
@@ -230,9 +262,11 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
230
262
  log?.info?.(
231
263
  `[${accountId}] openclaw-clawchat dispatching reply msg=${turn.messageId} session=${ctxPayload.SessionKey ?? route.sessionKey} agent=${route.agentId} agentsConfigured=[${agentsConfigured.join(",")}]`,
232
264
  );
265
+
233
266
  try {
234
267
  const dispatchResult = await rt.reply.withReplyDispatcher({
235
268
  dispatcher,
269
+ onSettled: () => markDispatchIdle(),
236
270
  run: () =>
237
271
  rt.reply.dispatchReplyFromConfig({ ctx: ctxPayload, cfg, dispatcher, replyOptions }),
238
272
  });
@@ -254,16 +288,24 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
254
288
  log?.error?.(
255
289
  `[${accountId}] openclaw-clawchat dispatch failed msg=${turn.messageId}: ${String(err)}`,
256
290
  );
257
- throw err;
258
- } finally {
259
- markDispatchIdle();
291
+ await sendOpenclawClawlingText({
292
+ client,
293
+ account: turn.account,
294
+ to: {
295
+ chatId: turn.peer.id,
296
+ chatType: turn.peer.kind === "group" ? "group" : "direct",
297
+ },
298
+ text: String(err),
299
+ ...(turn.replyCtx ? { replyCtx: turn.replyCtx } : {}),
300
+ });
260
301
  }
261
302
  },
262
- }).catch((err) => {
303
+ })
304
+ } catch (err) {
263
305
  log?.error?.(
264
- `[${accountId}] openclaw-clawchat inbound dispatch error: ${err instanceof Error ? err.stack || err.message : String(err)}`,
306
+ `[${accountId}] openclaw-clawchat message handler error: ${err instanceof Error ? err.stack || err.message : String(err)}`,
265
307
  );
266
- });
308
+ }
267
309
  });
268
310
 
269
311
  // `client.connect()` resolves on `hello-ok` or rejects on `hello-fail`
@@ -22,9 +22,10 @@ export type ClawchatListFriendsParams = Static<typeof ClawchatListFriendsSchema>
22
22
 
23
23
  export const ClawchatUpdateMyProfileSchema = Type.Object({
24
24
  nickname: Type.Optional(Type.String({ description: "New Nick Name" })),
25
- avatar: Type.Optional(
26
- Type.String({ description: "Avatar URL (use clawchat_upload_file first to obtain)" }),
25
+ avatar_url: Type.Optional(
26
+ Type.String({ description: "Avatar URL (use clawchat_upload_avatar first to obtain)" }),
27
27
  ),
28
+ bio: Type.Optional(Type.String({ description: "New self-introduction / bio text" })),
28
29
  });
29
30
  export type ClawchatUpdateMyProfileParams = Static<typeof ClawchatUpdateMyProfileSchema>;
30
31
 
@@ -35,6 +36,13 @@ export const ClawchatUploadFileSchema = Type.Object({
35
36
  });
36
37
  export type ClawchatUploadFileParams = Static<typeof ClawchatUploadFileSchema>;
37
38
 
39
+ export const ClawchatUploadAvatarSchema = Type.Object({
40
+ filePath: Type.String({
41
+ description: "Absolute local path of the avatar image to upload (max 20MB)",
42
+ }),
43
+ });
44
+ export type ClawchatUploadAvatarParams = Static<typeof ClawchatUploadAvatarSchema>;
45
+
38
46
  export const ClawchatActivateSchema = Type.Object({
39
47
  code: Type.String({
40
48
  description:
package/src/tools.test.ts CHANGED
@@ -55,7 +55,7 @@ describe("registerOpenclawClawlingTools", () => {
55
55
  expect(registered).toHaveLength(0);
56
56
  });
57
57
 
58
- it("registers all six tools when configured (regardless of baseUrl)", () => {
58
+ it("registers all seven tools when configured (regardless of baseUrl)", () => {
59
59
  const { api, registered } = buildApi({
60
60
  configChannel: configuredChannel(/* no baseUrl */),
61
61
  });
@@ -67,6 +67,7 @@ describe("registerOpenclawClawlingTools", () => {
67
67
  "clawchat_get_user_info",
68
68
  "clawchat_list_friends",
69
69
  "clawchat_update_my_profile",
70
+ "clawchat_upload_avatar",
70
71
  "clawchat_upload_file",
71
72
  ]);
72
73
  });
@@ -91,7 +92,7 @@ describe("registerOpenclawClawlingTools", () => {
91
92
  expect(activate.description).toMatch(/verbatim/i);
92
93
  });
93
94
 
94
- it("clawchat_update_my_profile description names name + avatar triggers (EN + ZH)", () => {
95
+ it("clawchat_update_my_profile description names name + avatar + bio triggers (EN + ZH)", () => {
95
96
  const { api } = buildApi({ configChannel: configuredChannel() });
96
97
  const fullTools: Array<{ name: string; description?: string }> = [];
97
98
  api.registerTool = (tool: { name: string; description?: string }) => {
@@ -104,6 +105,34 @@ describe("registerOpenclawClawlingTools", () => {
104
105
  expect(update.description).toMatch(/你叫/);
105
106
  expect(update.description).toMatch(/avatar/i);
106
107
  expect(update.description).toMatch(/生成头像|换个头像/);
108
+ expect(update.description).toMatch(/clawchat_upload_avatar/);
109
+ expect(update.description).toMatch(/bio|self introduction/i);
110
+ expect(update.description).toMatch(/自我介绍|个人简介/);
111
+ });
112
+
113
+ it("clawchat_upload_avatar rejects oversized files before upload", async () => {
114
+ const fs = await import("node:fs/promises");
115
+ const path = await import("node:path");
116
+ const os = await import("node:os");
117
+ const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-clawchat-"));
118
+ const big = path.join(tmp, "avatar-big.bin");
119
+ const handle = await fs.open(big, "w");
120
+ await handle.truncate(21 * 1024 * 1024);
121
+ await handle.close();
122
+ try {
123
+ const { api, registered } = buildApi({
124
+ configChannel: configuredChannel({ baseUrl: "https://api.example.com" }),
125
+ });
126
+ registerOpenclawClawlingTools(api);
127
+ const tool = registered.find((t) => t.name === "clawchat_upload_avatar")!;
128
+ const result = await tool.execute("call-1", { filePath: big });
129
+ const text = (result as { content: { text: string }[] }).content[0]!.text;
130
+ const parsed = JSON.parse(text) as { error?: string; message?: string };
131
+ expect(parsed.error).toBe("validation");
132
+ expect(parsed.message).toMatch(/20 ?MB|too large/i);
133
+ } finally {
134
+ await fs.rm(tmp, { recursive: true, force: true });
135
+ }
107
136
  });
108
137
 
109
138
 
package/src/tools.ts CHANGED
@@ -11,11 +11,13 @@ import {
11
11
  ClawchatGetUserInfoSchema,
12
12
  ClawchatListFriendsSchema,
13
13
  ClawchatUpdateMyProfileSchema,
14
+ ClawchatUploadAvatarSchema,
14
15
  ClawchatUploadFileSchema,
15
16
  type ClawchatActivateParams,
16
17
  type ClawchatGetUserInfoParams,
17
18
  type ClawchatListFriendsParams,
18
19
  type ClawchatUpdateMyProfileParams,
20
+ type ClawchatUploadAvatarParams,
19
21
  type ClawchatUploadFileParams,
20
22
  } from "./tools-schema.ts";
21
23
 
@@ -188,7 +190,7 @@ export function registerOpenclawClawlingTools(api: OpenClawPluginApi): void {
188
190
  api.registerTool(
189
191
  {
190
192
  name: "clawchat_get_my_profile",
191
- label: "Clawling: Get My Profile",
193
+ label: "Get Profile",
192
194
  description: "Fetch the agent's own Clawling profile (id, display name, avatar, bio).",
193
195
  parameters: ClawchatGetMyProfileSchema,
194
196
  async execute(_callId, _params) {
@@ -201,7 +203,7 @@ export function registerOpenclawClawlingTools(api: OpenClawPluginApi): void {
201
203
  api.registerTool(
202
204
  {
203
205
  name: "clawchat_get_user_info",
204
- label: "Clawling: Get User Info",
206
+ label: "Get User Info",
205
207
  description: "Fetch a Clawling user's public profile by userId.",
206
208
  parameters: ClawchatGetUserInfoSchema,
207
209
  async execute(_callId, params) {
@@ -215,7 +217,7 @@ export function registerOpenclawClawlingTools(api: OpenClawPluginApi): void {
215
217
  api.registerTool(
216
218
  {
217
219
  name: "clawchat_list_friends",
218
- label: "Clawling: List Friends",
220
+ label: "List Friends",
219
221
  description: "List the agent's friends, paginated (page=1, pageSize=20 by default).",
220
222
  parameters: ClawchatListFriendsSchema,
221
223
  async execute(_callId, params) {
@@ -234,9 +236,9 @@ export function registerOpenclawClawlingTools(api: OpenClawPluginApi): void {
234
236
  api.registerTool(
235
237
  {
236
238
  name: "clawchat_update_my_profile",
237
- label: "Clawling: Update My Profile",
239
+ label: "Update Profile",
238
240
  description:
239
- "Update this agent's own ClawChat profile (nickname and/or avatar). " +
241
+ "Update this agent's own ClawChat profile (nickname and/or avatar and/or bio). " +
240
242
  "TRIGGER — invoke this tool whenever the user's message matches ANY of: " +
241
243
  "(1) nickname/name change: 'change your name to X', 'your name is X', 'rename yourself to X', " +
242
244
  "'I'll call you X', 'from now on you are X', '你叫 X', '改名为 X', '我叫你 X', " +
@@ -244,19 +246,23 @@ export function registerOpenclawClawlingTools(api: OpenClawPluginApi): void {
244
246
  "(2) avatar change or generation: 'change your avatar', 'update your profile picture', " +
245
247
  "'generate a new avatar', 'use this image as your avatar', '换个头像', '生成头像', " +
246
248
  "'把头像改为 …' → first obtain the avatar URL (generate + upload via " +
247
- "`clawchat_upload_file`, OR use a provided URL directly), then call this tool " +
248
- "with `avatar = <url>`. " +
249
- "You can pass `nickname` and `avatar` together in one call, or just one of them. " +
250
- "At least one of the two must be present.",
249
+ "`clawchat_upload_avatar`, OR use a provided URL directly), then call this tool " +
250
+ "with `avatar_url = <url>`; " +
251
+ "(3) bio/self-introduction change: 'update your bio', 'set your profile bio to X', " +
252
+ "'change your self introduction', '把简介改成 X', '更新自我介绍', '个人简介改为 X' " +
253
+ "→ call with `bio = X`. " +
254
+ "You can pass `nickname`, `avatar_url`, and `bio` together in one call, or just one of them. " +
255
+ "At least one of the three must be present.",
251
256
  parameters: ClawchatUpdateMyProfileSchema,
252
257
  async execute(_callId, params) {
253
258
  const p = (params ?? {}) as ClawchatUpdateMyProfileParams;
254
- const patch: { nick_name?: string; avatar?: string } = {};
255
- if (typeof p.nickname === "string") patch.nick_name = p.nickname;
256
- if (typeof p.avatar === "string") patch.avatar = p.avatar;
259
+ const patch: { nickname?: string; avatar_url?: string; bio?: string } = {};
260
+ if (typeof p.nickname === "string") patch.nickname = p.nickname;
261
+ if (typeof p.avatar_url === "string") patch.avatar_url = p.avatar_url;
262
+ if (typeof p.bio === "string") patch.bio = p.bio;
257
263
  if (Object.keys(patch).length === 0) {
258
264
  return validationError(
259
- "openclaw-clawchat: at least one of nickname / avatar is required",
265
+ "openclaw-clawchat: at least one of nickname / avatar / bio is required",
260
266
  );
261
267
  }
262
268
  return await withClient((c): Promise<Profile> => c.updateMyProfile(patch));
@@ -265,10 +271,48 @@ export function registerOpenclawClawlingTools(api: OpenClawPluginApi): void {
265
271
  { name: "clawchat_update_my_profile" },
266
272
  );
267
273
 
274
+ api.registerTool(
275
+ {
276
+ name: "clawchat_upload_avatar",
277
+ label: "Upload Avatar",
278
+ description:
279
+ "Upload a local avatar image to Clawling avatar storage (max 20MB) and return the public URL. " +
280
+ "Use this before `clawchat_update_my_profile` when changing the profile picture.",
281
+ parameters: ClawchatUploadAvatarSchema,
282
+ async execute(_callId, params) {
283
+ const p = params as ClawchatUploadAvatarParams;
284
+ if (!p.filePath || !path.isAbsolute(p.filePath)) {
285
+ return validationError("openclaw-clawchat: filePath must be an absolute local path");
286
+ }
287
+ let stat: fs.Stats;
288
+ try {
289
+ stat = fs.statSync(p.filePath);
290
+ } catch (err) {
291
+ return validationError(
292
+ `openclaw-clawchat: cannot stat ${p.filePath}: ${err instanceof Error ? err.message : String(err)}`,
293
+ );
294
+ }
295
+ if (!stat.isFile()) {
296
+ return validationError(`openclaw-clawchat: ${p.filePath} is not a regular file`);
297
+ }
298
+ if (stat.size > MAX_UPLOAD_BYTES) {
299
+ return validationError(
300
+ `openclaw-clawchat: file too large (${stat.size} bytes; max 20MB)`,
301
+ );
302
+ }
303
+ const buffer = fs.readFileSync(p.filePath);
304
+ const filename = path.basename(p.filePath);
305
+ const mime = inferMimeFromPath(p.filePath);
306
+ return await withClient((c) => c.uploadAvatar({ buffer, filename, mime }));
307
+ },
308
+ },
309
+ { name: "clawchat_upload_avatar" },
310
+ );
311
+
268
312
  api.registerTool(
269
313
  {
270
314
  name: "clawchat_upload_file",
271
- label: "Clawling: Upload File",
315
+ label: "Upload File",
272
316
  description:
273
317
  "Upload a local file to Clawling media storage (max 20MB) and return the public URL.",
274
318
  parameters: ClawchatUploadFileSchema,
@@ -303,6 +347,6 @@ export function registerOpenclawClawlingTools(api: OpenClawPluginApi): void {
303
347
  );
304
348
 
305
349
  api.logger.info?.(
306
- "openclaw-clawchat: registered 6 clawchat_* tools (activate, get_my_profile, get_user_info, list_friends, update_my_profile, upload_file)",
350
+ "openclaw-clawchat: registered 7 clawchat_* tools (activate, get_my_profile, get_user_info, list_friends, update_my_profile, upload_avatar, upload_file)",
307
351
  );
308
352
  }