@newbase-clawchat/openclaw-clawchat 2026.4.23 → 2026.4.29

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/src/runtime.ts CHANGED
@@ -107,6 +107,16 @@ function formatConversationSubject(peer: { kind: "direct" | "group"; id: string
107
107
  return peer.kind === "group" ? `group:${peer.id}` : peer.id;
108
108
  }
109
109
 
110
+ function withClawChatSessionScope(cfg: OpenClawConfig): OpenClawConfig {
111
+ return {
112
+ ...cfg,
113
+ session: {
114
+ ...(cfg.session ?? {}),
115
+ dmScope: "per-account-channel-peer",
116
+ },
117
+ };
118
+ }
119
+
110
120
  export interface StartGatewayParams {
111
121
  cfg: OpenClawConfig;
112
122
  account: ResolvedOpenclawClawlingAccount;
@@ -172,8 +182,9 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
172
182
  ingest: async (turn) => {
173
183
  const rt = runtime.channel;
174
184
  const storePath = rt.session.resolveStorePath(cfg.session?.store);
185
+ const routeCfg = withClawChatSessionScope(cfg);
175
186
  const route = rt.routing.resolveAgentRoute({
176
- cfg,
187
+ cfg: routeCfg,
177
188
  channel: CHANNEL_ID,
178
189
  accountId,
179
190
  peer: turn.peer,
@@ -37,8 +37,8 @@ describe("openclaw-clawchat streaming", () => {
37
37
  "message.done",
38
38
  ]);
39
39
  expect((client.typing as ReturnType<typeof vi.fn>).mock.calls).toEqual([
40
- [{ id: "u1", type: "direct" }, true],
41
- [{ id: "u1", type: "direct" }, false],
40
+ ["u1", true, "direct"],
41
+ ["u1", false, "direct"],
42
42
  ]);
43
43
  });
44
44
 
@@ -110,7 +110,7 @@ describe("openclaw-clawchat streaming", () => {
110
110
  expect(sent[0]!.payload.sequence).toBe(3);
111
111
  expect(sent[0]!.payload.reason).toBe("boom");
112
112
  expect((client.typing as ReturnType<typeof vi.fn>).mock.calls).toEqual([
113
- [{ id: "u1", type: "direct" }, false],
113
+ ["u1", false, "direct"],
114
114
  ]);
115
115
  });
116
116
  });
package/src/streaming.ts CHANGED
@@ -10,7 +10,8 @@ import {
10
10
 
11
11
  export interface StreamingSendParams {
12
12
  client: ClawlingChatClient;
13
- routing: EnvelopeRouting;
13
+ routing?: EnvelopeRouting;
14
+ to?: { id: string; type: "direct" | "group" };
14
15
  sender: StreamSender;
15
16
  /**
16
17
  * Pre-chunked text. Use `chunkMarkdownText` from
@@ -22,6 +23,12 @@ export interface StreamingSendParams {
22
23
  emitTyping?: boolean;
23
24
  }
24
25
 
26
+ function resolveRouting(params: { routing?: EnvelopeRouting; to?: { id: string; type: "direct" | "group" } }): EnvelopeRouting {
27
+ if (params.routing) return params.routing;
28
+ if (params.to) return { chatId: params.to.id, chatType: params.to.type };
29
+ throw new Error("openclaw-clawchat streaming requires routing");
30
+ }
31
+
25
32
  /**
26
33
  * Emit one full streaming lifecycle for a pre-chunked reply.
27
34
  *
@@ -35,13 +42,14 @@ export interface StreamingSendParams {
35
42
  * With zero chunks: typing(true) -> created -> done -> typing(false).
36
43
  */
37
44
  export async function sendStreamingText(params: StreamingSendParams): Promise<void> {
45
+ const routing = resolveRouting(params);
38
46
  const emitTyping = params.emitTyping !== false;
39
47
  if (emitTyping) {
40
- params.client.typing(params.routing.chatId, true, params.routing.chatType);
48
+ params.client.typing(routing.chatId, true, routing.chatType);
41
49
  }
42
50
  emitStreamCreated(params.client, {
43
51
  messageId: params.messageId,
44
- routing: params.routing,
52
+ routing,
45
53
  });
46
54
  let sequence = 0;
47
55
  let fullText = "";
@@ -50,7 +58,7 @@ export async function sendStreamingText(params: StreamingSendParams): Promise<vo
50
58
  fullText += chunk;
51
59
  emitStreamAdd(params.client, {
52
60
  messageId: params.messageId,
53
- routing: params.routing,
61
+ routing,
54
62
  sequence,
55
63
  fullText,
56
64
  textDelta: chunk,
@@ -58,18 +66,19 @@ export async function sendStreamingText(params: StreamingSendParams): Promise<vo
58
66
  }
59
67
  emitStreamDone(params.client, {
60
68
  messageId: params.messageId,
61
- routing: params.routing,
69
+ routing,
62
70
  finalSequence: sequence,
63
71
  finalText: fullText,
64
72
  });
65
73
  if (emitTyping) {
66
- params.client.typing(params.routing.chatId, false, params.routing.chatType);
74
+ params.client.typing(routing.chatId, false, routing.chatType);
67
75
  }
68
76
  }
69
77
 
70
78
  export interface StreamingFailureParams {
71
79
  client: ClawlingChatClient;
72
- routing: EnvelopeRouting;
80
+ routing?: EnvelopeRouting;
81
+ to?: { id: string; type: "direct" | "group" };
73
82
  messageId: string;
74
83
  currentSequence: number;
75
84
  reason?: string;
@@ -77,13 +86,14 @@ export interface StreamingFailureParams {
77
86
  }
78
87
 
79
88
  export async function sendStreamingFailure(params: StreamingFailureParams): Promise<void> {
89
+ const routing = resolveRouting(params);
80
90
  emitStreamFailed(params.client, {
81
91
  messageId: params.messageId,
82
- routing: params.routing,
92
+ routing,
83
93
  sequence: params.currentSequence + 1,
84
94
  reason: params.reason,
85
95
  });
86
96
  if (params.emitTyping !== false) {
87
- params.client.typing(params.routing.chatId, false, params.routing.chatType);
97
+ params.client.typing(routing.chatId, false, routing.chatType);
88
98
  }
89
99
  }
@@ -1,14 +1,16 @@
1
1
  import { Type, type Static } from "@sinclair/typebox";
2
2
 
3
- export const ClawchatGetMyProfileSchema = Type.Object({});
4
- export type ClawchatGetMyProfileParams = Static<typeof ClawchatGetMyProfileSchema>;
3
+ export const ClawchatGetAccountProfileSchema = Type.Object({});
4
+ export type ClawchatGetAccountProfileParams = Static<typeof ClawchatGetAccountProfileSchema>;
5
5
 
6
- export const ClawchatGetUserInfoSchema = Type.Object({
7
- userId: Type.String({ description: "Target user id (required)" }),
6
+ export const ClawchatGetUserProfileSchema = Type.Object({
7
+ userId: Type.String({
8
+ description: "Target ClawChat user id (required). Do not infer this from a nickname.",
9
+ }),
8
10
  });
9
- export type ClawchatGetUserInfoParams = Static<typeof ClawchatGetUserInfoSchema>;
11
+ export type ClawchatGetUserProfileParams = Static<typeof ClawchatGetUserProfileSchema>;
10
12
 
11
- export const ClawchatListFriendsSchema = Type.Object({
13
+ export const ClawchatListAccountFriendsSchema = Type.Object({
12
14
  page: Type.Optional(Type.Integer({ minimum: 1, description: "1-based page number (default 1)" })),
13
15
  pageSize: Type.Optional(
14
16
  Type.Integer({
@@ -18,35 +20,42 @@ export const ClawchatListFriendsSchema = Type.Object({
18
20
  }),
19
21
  ),
20
22
  });
21
- export type ClawchatListFriendsParams = Static<typeof ClawchatListFriendsSchema>;
23
+ export type ClawchatListAccountFriendsParams = Static<typeof ClawchatListAccountFriendsSchema>;
22
24
 
23
- export const ClawchatUpdateMyProfileSchema = Type.Object({
24
- nickname: Type.Optional(Type.String({ description: "New Nick Name" })),
25
+ export const ClawchatUpdateAccountProfileSchema = Type.Object({
26
+ nickname: Type.Optional(Type.String({ description: "New ClawChat account nickname" })),
25
27
  avatar_url: Type.Optional(
26
- Type.String({ description: "Avatar URL (use clawchat_upload_avatar first to obtain)" }),
28
+ Type.String({
29
+ description:
30
+ "Avatar URL for the ClawChat account profile (use clawchat_upload_avatar_image first to obtain one from a local image)",
31
+ }),
32
+ ),
33
+ bio: Type.Optional(
34
+ Type.String({ description: "New ClawChat account self-introduction / bio text" }),
27
35
  ),
28
- bio: Type.Optional(Type.String({ description: "New self-introduction / bio text" })),
29
36
  });
30
- export type ClawchatUpdateMyProfileParams = Static<typeof ClawchatUpdateMyProfileSchema>;
37
+ export type ClawchatUpdateAccountProfileParams = Static<
38
+ typeof ClawchatUpdateAccountProfileSchema
39
+ >;
31
40
 
32
- export const ClawchatUploadFileSchema = Type.Object({
41
+ export const ClawchatUploadMediaFileSchema = Type.Object({
33
42
  filePath: Type.String({
34
- description: "Absolute local path of the file to upload (max 20MB)",
43
+ description: "Absolute local path of the media/file to upload to ClawChat (max 20MB)",
35
44
  }),
36
45
  });
37
- export type ClawchatUploadFileParams = Static<typeof ClawchatUploadFileSchema>;
46
+ export type ClawchatUploadMediaFileParams = Static<typeof ClawchatUploadMediaFileSchema>;
38
47
 
39
- export const ClawchatUploadAvatarSchema = Type.Object({
48
+ export const ClawchatUploadAvatarImageSchema = Type.Object({
40
49
  filePath: Type.String({
41
- description: "Absolute local path of the avatar image to upload (max 20MB)",
50
+ description: "Absolute local path of the avatar image to upload to ClawChat (max 20MB)",
42
51
  }),
43
52
  });
44
- export type ClawchatUploadAvatarParams = Static<typeof ClawchatUploadAvatarSchema>;
53
+ export type ClawchatUploadAvatarImageParams = Static<typeof ClawchatUploadAvatarImageSchema>;
45
54
 
46
55
  export const ClawchatActivateSchema = Type.Object({
47
56
  code: Type.String({
48
57
  description:
49
- "The invite code (e.g. 'INV-ABC123') extracted from the user's message. " +
58
+ "The invite code (six uppercase letters/digits, e.g. 'A1B2C3') extracted from the user's message. " +
50
59
  "Whitespace is trimmed automatically.",
51
60
  }),
52
61
  });
package/src/tools.test.ts CHANGED
@@ -1,6 +1,12 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
2
  import { registerOpenclawClawlingTools } from "./tools.ts";
3
3
 
4
+ const loginRuntime = vi.hoisted(() => ({
5
+ runOpenclawClawlingLogin: vi.fn(),
6
+ }));
7
+
8
+ vi.mock("./login.runtime.ts", () => loginRuntime);
9
+
4
10
  interface RegisteredTool {
5
11
  name: string;
6
12
  execute: (callId: string, params: unknown) => Promise<unknown>;
@@ -8,6 +14,7 @@ interface RegisteredTool {
8
14
 
9
15
  function buildApi(opts: {
10
16
  configChannel?: Record<string, unknown> | null;
17
+ configTools?: Record<string, unknown>;
11
18
  registerTool?: (tool: { name: string }, options?: { name: string }) => void;
12
19
  }) {
13
20
  const registered: RegisteredTool[] = [];
@@ -15,7 +22,10 @@ function buildApi(opts: {
15
22
  config:
16
23
  opts.configChannel === null
17
24
  ? undefined
18
- : { channels: { "openclaw-clawchat": opts.configChannel ?? {} } },
25
+ : {
26
+ channels: { "openclaw-clawchat": opts.configChannel ?? {} },
27
+ ...(opts.configTools ? { tools: opts.configTools } : {}),
28
+ },
19
29
  logger: {
20
30
  info: vi.fn(),
21
31
  debug: vi.fn(),
@@ -39,14 +49,38 @@ function configuredChannel(extra: Record<string, unknown> = {}) {
39
49
  }
40
50
 
41
51
  describe("registerOpenclawClawlingTools", () => {
42
- it("registers ONLY clawchat_activate when account.configured is false (onboarding path)", () => {
52
+ it("registers no tools when account.configured is false", () => {
53
+ const { api, registered } = buildApi({
54
+ configChannel: { websocketUrl: "wss://w" /* token / userId missing */ },
55
+ });
56
+ registerOpenclawClawlingTools(api);
57
+ expect(registered.map((t) => t.name)).toEqual([]);
58
+ });
59
+
60
+ it("does not mutate tool policy during registration before account activation", () => {
61
+ const { api, registered } = buildApi({
62
+ configChannel: { websocketUrl: "wss://w" /* token / userId missing */ },
63
+ configTools: { profile: "coding", allow: [] },
64
+ });
65
+
66
+ registerOpenclawClawlingTools(api);
67
+
68
+ expect(registered.map((t) => t.name)).toEqual([]);
69
+ expect(api.config?.tools).toEqual({
70
+ profile: "coding",
71
+ allow: [],
72
+ });
73
+ });
74
+
75
+ it("does not register clawchat_activate for invite-code onboarding", async () => {
43
76
  const { api, registered } = buildApi({
44
77
  configChannel: { websocketUrl: "wss://w" /* token / userId missing */ },
45
78
  });
79
+
46
80
  registerOpenclawClawlingTools(api);
47
- // clawchat_activate must be available before a token exists — it's the
48
- // tool the agent calls to onboard.
49
- expect(registered.map((t) => t.name)).toEqual(["clawchat_activate"]);
81
+
82
+ expect(registered.some((t) => t.name === "clawchat_activate")).toBe(false);
83
+ expect(loginRuntime.runOpenclawClawlingLogin).not.toHaveBeenCalled();
50
84
  });
51
85
 
52
86
  it("skips registration when api.config is undefined", () => {
@@ -55,62 +89,106 @@ describe("registerOpenclawClawlingTools", () => {
55
89
  expect(registered).toHaveLength(0);
56
90
  });
57
91
 
58
- it("registers all seven tools when configured (regardless of baseUrl)", () => {
92
+ it("registers all six account tools when configured (regardless of baseUrl)", () => {
59
93
  const { api, registered } = buildApi({
60
94
  configChannel: configuredChannel(/* no baseUrl */),
61
95
  });
62
96
  registerOpenclawClawlingTools(api);
63
97
  const names = registered.map((t) => t.name).sort();
64
98
  expect(names).toEqual([
65
- "clawchat_activate",
66
- "clawchat_get_my_profile",
67
- "clawchat_get_user_info",
68
- "clawchat_list_friends",
69
- "clawchat_update_my_profile",
70
- "clawchat_upload_avatar",
71
- "clawchat_upload_file",
99
+ "clawchat_get_account_profile",
100
+ "clawchat_get_user_profile",
101
+ "clawchat_list_account_friends",
102
+ "clawchat_update_account_profile",
103
+ "clawchat_upload_avatar_image",
104
+ "clawchat_upload_media_file",
72
105
  ]);
73
106
  });
74
107
 
75
- it("clawchat_activate description names the `clawchat <code>` trigger", () => {
108
+ it("logs configured tool registration at debug level only", () => {
109
+ const { api } = buildApi({
110
+ configChannel: configuredChannel(),
111
+ });
112
+ const logger = api.logger as {
113
+ info: ReturnType<typeof vi.fn>;
114
+ debug: ReturnType<typeof vi.fn>;
115
+ };
116
+
117
+ registerOpenclawClawlingTools(api);
118
+
119
+ expect(logger.info).not.toHaveBeenCalled();
120
+ expect(logger.debug).toHaveBeenCalledWith(
121
+ "openclaw-clawchat: registered 6 clawchat_* tools (get_account_profile, get_user_profile, list_account_friends, update_account_profile, upload_avatar_image, upload_media_file)",
122
+ );
123
+ });
124
+
125
+ it("does not register clawchat_activate when configured", () => {
76
126
  const { api, registered } = buildApi({
77
127
  configChannel: configuredChannel(),
78
128
  });
79
- // Re-capture full tool objects so we can see description.
129
+ registerOpenclawClawlingTools(api);
130
+ expect(registered.some((t) => t.name === "clawchat_activate")).toBe(false);
131
+ });
132
+
133
+ it("clawchat_update_account_profile description names account profile triggers (EN + ZH)", () => {
134
+ const { api } = buildApi({ configChannel: configuredChannel() });
80
135
  const fullTools: Array<{ name: string; description?: string }> = [];
81
136
  api.registerTool = (tool: { name: string; description?: string }) => {
82
137
  fullTools.push(tool);
83
138
  };
84
139
  registerOpenclawClawlingTools(api);
85
- const activate = fullTools.find((t) => t.name === "clawchat_activate")!;
86
- expect(activate).toBeDefined();
87
- // Must tell the LLM exactly how to spot + parse the trigger.
88
- expect(activate.description).toMatch(/clawchat\s*<code>/i);
89
- expect(activate.description).toMatch(/INV-/);
90
- // Must spell out the verbatim-extraction rule so the model doesn't
91
- // re-case / prefix the code.
92
- expect(activate.description).toMatch(/verbatim/i);
140
+ const update = fullTools.find((t) => t.name === "clawchat_update_account_profile")!;
141
+ expect(update).toBeDefined();
142
+ expect(update.description).toMatch(/configured ClawChat account|logged-in ClawChat account/i);
143
+ expect(update.description).toMatch(/ClawChat (account )?(nickname|name)/i);
144
+ expect(update.description).toMatch(/ClawChat 昵称|账号昵称|账号名字/);
145
+ expect(update.description).toMatch(/avatar|profile picture/i);
146
+ expect(update.description).toMatch(/ClawChat 头像|账号头像/);
147
+ expect(update.description).toMatch(/clawchat_upload_avatar_image/);
148
+ expect(update.description).toMatch(/bio|self-introduction/i);
149
+ expect(update.description).toMatch(/ClawChat 简介|账号简介|个人简介/);
150
+ expect(update.description).not.toMatch(
151
+ new RegExp(["agent's own", "rename " + "yourself", "this agent's own"].join("|"), "i"),
152
+ );
93
153
  });
94
154
 
95
- it("clawchat_update_my_profile description names name + avatar + bio triggers (EN + ZH)", () => {
155
+ it("account query and upload tool descriptions include precise trigger semantics", () => {
96
156
  const { api } = buildApi({ configChannel: configuredChannel() });
97
157
  const fullTools: Array<{ name: string; description?: string }> = [];
98
158
  api.registerTool = (tool: { name: string; description?: string }) => {
99
159
  fullTools.push(tool);
100
160
  };
101
161
  registerOpenclawClawlingTools(api);
102
- const update = fullTools.find((t) => t.name === "clawchat_update_my_profile")!;
103
- expect(update).toBeDefined();
104
- expect(update.description).toMatch(/change your name/i);
105
- expect(update.description).toMatch(/你叫/);
106
- expect(update.description).toMatch(/avatar/i);
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(/自我介绍|个人简介/);
162
+
163
+ const accountProfile = fullTools.find((t) => t.name === "clawchat_get_account_profile")!;
164
+ const userProfile = fullTools.find((t) => t.name === "clawchat_get_user_profile")!;
165
+ const friends = fullTools.find((t) => t.name === "clawchat_list_account_friends")!;
166
+ const avatar = fullTools.find((t) => t.name === "clawchat_upload_avatar_image")!;
167
+ const media = fullTools.find((t) => t.name === "clawchat_upload_media_file")!;
168
+
169
+ expect(accountProfile.description).toMatch(/configured ClawChat account|logged-in ClawChat account/i);
170
+ expect(accountProfile.description).toMatch(/profile|nickname|avatar|bio/i);
171
+ expect(accountProfile.description).not.toMatch(/agent's own|this agent's/i);
172
+
173
+ expect(userProfile.description).toMatch(/userId/);
174
+ expect(userProfile.description).toMatch(/public profile/i);
175
+ expect(userProfile.description).toMatch(/do not guess|do not infer/i);
176
+
177
+ expect(friends.description).toMatch(/configured ClawChat account|logged-in ClawChat account/i);
178
+ expect(friends.description).toMatch(/friends|contacts/i);
179
+ expect(friends.description).toMatch(/page=1|pageSize=20/);
180
+
181
+ expect(avatar.description).toMatch(/local image/i);
182
+ expect(avatar.description).toMatch(/avatar URL|hosted avatar URL|public URL/i);
183
+ expect(avatar.description).toMatch(/clawchat_update_account_profile/);
184
+ expect(avatar.description).toMatch(/does not update|does not set/i);
185
+
186
+ expect(media.description).toMatch(/local file|media file/i);
187
+ expect(media.description).toMatch(/public URL|shareable URL/i);
188
+ expect(media.description).toMatch(/not.*avatar|do not use.*avatar/i);
111
189
  });
112
190
 
113
- it("clawchat_upload_avatar rejects oversized files before upload", async () => {
191
+ it("clawchat_upload_avatar_image rejects oversized files before upload", async () => {
114
192
  const fs = await import("node:fs/promises");
115
193
  const path = await import("node:path");
116
194
  const os = await import("node:os");
@@ -124,7 +202,7 @@ describe("registerOpenclawClawlingTools", () => {
124
202
  configChannel: configuredChannel({ baseUrl: "https://api.example.com" }),
125
203
  });
126
204
  registerOpenclawClawlingTools(api);
127
- const tool = registered.find((t) => t.name === "clawchat_upload_avatar")!;
205
+ const tool = registered.find((t) => t.name === "clawchat_upload_avatar_image")!;
128
206
  const result = await tool.execute("call-1", { filePath: big });
129
207
  const text = (result as { content: { text: string }[] }).content[0]!.text;
130
208
  const parsed = JSON.parse(text) as { error?: string; message?: string };
@@ -136,7 +214,7 @@ describe("registerOpenclawClawlingTools", () => {
136
214
  });
137
215
 
138
216
 
139
- it("clawchat_upload_file rejects oversized files before upload", async () => {
217
+ it("clawchat_upload_media_file rejects oversized files before upload", async () => {
140
218
  const fs = await import("node:fs/promises");
141
219
  const path = await import("node:path");
142
220
  const os = await import("node:os");
@@ -151,7 +229,7 @@ describe("registerOpenclawClawlingTools", () => {
151
229
  configChannel: configuredChannel({ baseUrl: "https://api.example.com" }),
152
230
  });
153
231
  registerOpenclawClawlingTools(api);
154
- const tool = registered.find((t) => t.name === "clawchat_upload_file")!;
232
+ const tool = registered.find((t) => t.name === "clawchat_upload_media_file")!;
155
233
  const result = await tool.execute("call-1", { filePath: big });
156
234
  const text = (result as { content: { text: string }[] }).content[0]!.text;
157
235
  const parsed = JSON.parse(text) as { error?: string; message?: string };