@newbase-clawchat/openclaw-clawchat 2026.4.24 → 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.
@@ -95,7 +95,7 @@ describe("runOpenclawClawlingLogin", () => {
95
95
  });
96
96
 
97
97
  expect(agentsConnect).toHaveBeenCalledWith({
98
- inviteCode: "INV-ABC",
98
+ code: "INV-ABC",
99
99
  platform: "openclaw",
100
100
  type: "clawbot",
101
101
  });
@@ -112,6 +112,109 @@ describe("runOpenclawClawlingLogin", () => {
112
112
  expect(log).toHaveBeenCalledWith(expect.stringContaining("login succeeded"));
113
113
  });
114
114
 
115
+ it("preserves other configured channels when persisting ClawChat credentials", async () => {
116
+ const cfg = {
117
+ channels: {
118
+ telegram: {
119
+ enabled: true,
120
+ token: "telegram-token",
121
+ },
122
+ [CHANNEL_ID]: {
123
+ baseUrl: "https://api.example.com",
124
+ websocketUrl: "wss://ws.example.com/v2/client",
125
+ },
126
+ },
127
+ } as unknown as OpenClawConfig;
128
+ const agentsConnect = vi.fn().mockResolvedValue({
129
+ agent: { user_id: "agent-123" },
130
+ access_token: "access-tok",
131
+ refresh_token: "refresh-tok",
132
+ });
133
+ const persistConfig = vi.fn();
134
+
135
+ await runOpenclawClawlingLogin({
136
+ cfg,
137
+ runtime: { log: vi.fn() },
138
+ readInviteCode: async () => "INV-ABC",
139
+ apiClientFactory: () => makeApiClient({ agentsConnect }),
140
+ persistConfig,
141
+ });
142
+
143
+ const savedCfg = persistConfig.mock.calls[0]![0] as OpenClawConfig;
144
+ expect(savedCfg.channels?.telegram).toEqual({
145
+ enabled: true,
146
+ token: "telegram-token",
147
+ });
148
+ expect(savedCfg.channels?.[CHANNEL_ID]).toMatchObject({
149
+ baseUrl: "https://api.example.com",
150
+ websocketUrl: "wss://ws.example.com/v2/client",
151
+ token: "access-tok",
152
+ userId: "agent-123",
153
+ refreshToken: "refresh-tok",
154
+ });
155
+ });
156
+
157
+ it("enables the channel when login fills a pre-activation disabled skeleton", async () => {
158
+ const cfg = buildCfg({
159
+ enabled: false,
160
+ baseUrl: "https://api.example.com",
161
+ });
162
+ const agentsConnect = vi.fn().mockResolvedValue({
163
+ agent: { user_id: "agent-123" },
164
+ access_token: "access-tok",
165
+ refresh_token: "refresh-tok",
166
+ });
167
+ const persistConfig = vi.fn();
168
+
169
+ await runOpenclawClawlingLogin({
170
+ cfg,
171
+ runtime: { log: vi.fn() },
172
+ readInviteCode: async () => "INV-ABC",
173
+ apiClientFactory: () => makeApiClient({ agentsConnect }),
174
+ persistConfig,
175
+ });
176
+
177
+ const savedCfg = persistConfig.mock.calls[0]![0] as OpenClawConfig;
178
+ const section = (savedCfg.channels as Record<string, Record<string, unknown>>)[CHANNEL_ID]!;
179
+ expect(section.enabled).toBe(true);
180
+ expect(section.token).toBe("access-tok");
181
+ expect(section.userId).toBe("agent-123");
182
+ });
183
+
184
+ it("allows openclaw-clawchat plugin tools after successful login without replacing policy", async () => {
185
+ const cfg = {
186
+ ...buildCfg({ baseUrl: "https://api.example.com" }),
187
+ tools: {
188
+ profile: "coding",
189
+ allow: [],
190
+ deny: ["exec"],
191
+ alsoAllow: ["browser"],
192
+ },
193
+ } as unknown as OpenClawConfig;
194
+ const agentsConnect = vi.fn().mockResolvedValue({
195
+ agent: { user_id: "agent-123" },
196
+ access_token: "access-tok",
197
+ refresh_token: "refresh-tok",
198
+ });
199
+ const persistConfig = vi.fn();
200
+
201
+ await runOpenclawClawlingLogin({
202
+ cfg,
203
+ runtime: { log: vi.fn() },
204
+ readInviteCode: async () => "INV-ABC",
205
+ apiClientFactory: () => makeApiClient({ agentsConnect }),
206
+ persistConfig,
207
+ });
208
+
209
+ const savedCfg = persistConfig.mock.calls[0]![0] as OpenClawConfig;
210
+ expect(savedCfg.tools).toMatchObject({
211
+ profile: "coding",
212
+ allow: [],
213
+ deny: ["exec"],
214
+ alsoAllow: ["browser", "openclaw-clawchat"],
215
+ });
216
+ });
217
+
115
218
  it("surfaces agents/connect API errors with the kind and message", async () => {
116
219
  const cfg = buildCfg({ baseUrl: "https://api.example.com" });
117
220
  const agentsConnect = vi.fn().mockRejectedValue(
@@ -219,7 +322,7 @@ describe("runOpenclawClawlingLogin", () => {
219
322
  persistConfig: vi.fn(),
220
323
  });
221
324
  expect(agentsConnect).toHaveBeenCalledWith({
222
- inviteCode: "INV-TRIM",
325
+ code: "INV-TRIM",
223
326
  platform: "openclaw",
224
327
  type: "clawbot",
225
328
  });
@@ -243,7 +346,7 @@ describe("runOpenclawClawlingLogin (non-interactive via readInviteCode)", () =>
243
346
  persistConfig,
244
347
  });
245
348
  expect(agentsConnect).toHaveBeenCalledWith({
246
- inviteCode: "INV-PROGRAMMATIC",
349
+ code: "INV-PROGRAMMATIC",
247
350
  platform: "openclaw",
248
351
  type: "clawbot",
249
352
  });
@@ -3,7 +3,11 @@ import { writeConfigFile } from "openclaw/plugin-sdk/config-runtime";
3
3
  import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
4
4
  import { createOpenclawClawlingApiClient } from "./api-client.ts";
5
5
  import { ClawlingApiError } from "./api-types.ts";
6
- import { CHANNEL_ID, resolveOpenclawClawlingAccount } from "./config.ts";
6
+ import {
7
+ CHANNEL_ID,
8
+ mergeOpenclawClawchatToolAllow,
9
+ resolveOpenclawClawlingAccount,
10
+ } from "./config.ts";
7
11
 
8
12
  /**
9
13
  * Platform tag sent to `/v1/agents/connect`. Identifies the host of this
@@ -120,16 +124,17 @@ export async function runOpenclawClawlingLogin(params: LoginParams): Promise<voi
120
124
  const existing = (channels[CHANNEL_ID] ?? {}) as Record<string, unknown>;
121
125
  const nextSection: Record<string, unknown> = {
122
126
  ...existing,
127
+ enabled: true,
123
128
  token: result.access_token,
124
129
  userId: result.agent.user_id,
125
130
  };
126
131
  if (result.refresh_token) {
127
132
  nextSection.refreshToken = result.refresh_token;
128
133
  }
129
- const nextCfg: OpenClawConfig = {
134
+ const nextCfg: OpenClawConfig = mergeOpenclawClawchatToolAllow({
130
135
  ...cfg,
131
136
  channels: { ...channels, [CHANNEL_ID]: nextSection },
132
- };
137
+ });
133
138
 
134
139
  const tokenPreview = redactToken(result.access_token);
135
140
  runtime.log(
@@ -1,11 +1,15 @@
1
+ import fs from "node:fs";
1
2
  import { describe, expect, it } from "vitest";
2
3
  import pluginManifest from "../openclaw.plugin.json" with { type: "json" };
3
4
  import packageJson from "../package.json" with { type: "json" };
4
5
 
5
6
  interface PackageJsonWithOpenclaw {
6
7
  name: string;
8
+ files: string[];
7
9
  openclaw: {
8
- channel: { id: string };
10
+ extensions: string[];
11
+ setupEntry?: string;
12
+ channel?: { id: string; aliases?: string[]; cliAddOptions?: Array<{ flags: string }> };
9
13
  install: { npmSpec: string };
10
14
  };
11
15
  }
@@ -14,9 +18,107 @@ describe("openclaw-clawchat manifest", () => {
14
18
  it("keeps plugin id / channel id / package name aligned", () => {
15
19
  expect(pluginManifest.id).toBe("openclaw-clawchat");
16
20
  expect(pluginManifest.channels).toContain("openclaw-clawchat");
17
- expect(packageJson.name).toBe("openclaw-clawchat");
21
+ expect(pluginManifest.skills).toContain("./skills");
22
+ expect(pluginManifest.channelConfigs?.["openclaw-clawchat"]?.label).toBe(
23
+ "Clawling Chat",
24
+ );
25
+ expect(pluginManifest.channelConfigs?.["openclaw-clawchat"]?.schema?.properties).toHaveProperty(
26
+ "token",
27
+ );
28
+ expect(packageJson.name).toBe("@newbase-clawchat/openclaw-clawchat");
18
29
  const pkg = packageJson as PackageJsonWithOpenclaw;
19
- expect(pkg.openclaw.channel.id).toBe("openclaw-clawchat");
20
- expect(pkg.openclaw.install.npmSpec).toBe("openclaw-clawchat");
30
+ expect(pkg.openclaw.extensions).toContain("./index.ts");
31
+ expect(pkg.openclaw.install.npmSpec).toBe("@newbase-clawchat/openclaw-clawchat");
32
+ });
33
+
34
+ it("declares supported channel/command activation hints for plugin loading", () => {
35
+ expect(pluginManifest.activation).toEqual({
36
+ onChannels: ["openclaw-clawchat"],
37
+ onCommands: ["clawchat-login"],
38
+ });
39
+ expect(pluginManifest.activation).not.toHaveProperty("onStartup");
40
+ expect(pluginManifest.commandAliases).toEqual([
41
+ { name: "clawchat-login", kind: "runtime-slash" },
42
+ ]);
43
+ });
44
+
45
+ it("does not publish setup migration or setup-runtime channel metadata", () => {
46
+ const pkg = packageJson as PackageJsonWithOpenclaw;
47
+ expect(pkg.files).not.toContain("setup-api.ts");
48
+ expect(pkg.files).not.toContain("setup-entry.ts");
49
+ expect(pkg.openclaw.setupEntry).toBeUndefined();
50
+ expect(pkg.openclaw.channel).toBeUndefined();
51
+ expect(fs.existsSync(new URL("../setup-api.ts", import.meta.url))).toBe(false);
52
+ expect(fs.existsSync(new URL("../setup-entry.ts", import.meta.url))).toBe(false);
53
+ });
54
+
55
+ it("does not document channels add as an activation path", () => {
56
+ const readme = fs.readFileSync(new URL("../README.md", import.meta.url), "utf8");
57
+ const docs = fs.readFileSync(new URL("../docs/openclaw-clawchat.md", import.meta.url), "utf8");
58
+ expect(readme).not.toMatch(/channels add --channel openclaw-clawchat/i);
59
+ expect(docs).not.toMatch(/channels add --channel openclaw-clawchat/i);
60
+ });
61
+
62
+ it("publishes a ClawChat account tools skill for non-activation workflows", () => {
63
+ const skill = fs.readFileSync(new URL("../skills/clawchat-account-tools/SKILL.md", import.meta.url), "utf8");
64
+ expect(skill).toMatch(/^---\nname: clawchat-account-tools\n/m);
65
+ expect(skill).toMatch(/description: .*Use when/i);
66
+ expect(skill).toMatch(/clawchat_get_account_profile/);
67
+ expect(skill).toMatch(/clawchat_get_user_profile/);
68
+ expect(skill).toMatch(/clawchat_list_account_friends/);
69
+ expect(skill).toMatch(/clawchat_update_account_profile/);
70
+ expect(skill).toMatch(/clawchat_upload_avatar_image/);
71
+ expect(skill).toMatch(/clawchat_upload_media_file/);
72
+ expect(skill).toMatch(/configured ClawChat account/i);
73
+ expect(skill).not.toMatch(/clawchat_activate/);
74
+ });
75
+
76
+ it("keeps the activation skill on channel login without tool or slash-command dispatch", () => {
77
+ const skill = fs.readFileSync(new URL("../skills/clawchat-activate/SKILL.md", import.meta.url), "utf8");
78
+ expect(skill).toMatch(/name:\s*clawchat-activate/);
79
+ expect(skill).not.toMatch(/clawchat_activate/);
80
+ expect(skill).not.toMatch(/command-dispatch:\s*tool/);
81
+ expect(skill).not.toMatch(/command-tool:/);
82
+ expect(skill).not.toMatch(/command-dispatch:/);
83
+ expect(skill).not.toMatch(/command-command:/);
84
+ expect(skill).not.toMatch(/command-arg-mode:/);
85
+ expect(skill).not.toMatch(/user-invocable:/);
86
+ expect(skill).not.toMatch(/`clawchat\s+A1B2C3`/i);
87
+ expect(skill).not.toMatch(/`clawchat\s*<code>`/i);
88
+ expect(skill).not.toMatch(/\/clawchat_activate A1B2C3/);
89
+ expect(skill).not.toMatch(/\/clawchat-activate A1B2C3/);
90
+ expect(skill).not.toMatch(/\/clawchat-login A1B2C3/);
91
+ expect(skill).toMatch(/openclaw channels login --channel openclaw-clawchat/);
92
+ expect(skill).toMatch(/do not append/i);
93
+ expect(skill).toMatch(/prompt[^\n]+invite code[^\n]+provide/i);
94
+ expect(skill).toMatch(/channel login/i);
95
+ expect(skill).toMatch(/openclaw gateway restart/);
96
+ expect(skill).not.toMatch(/ask the user to send/i);
97
+ expect(skill).not.toMatch(/give the exact/i);
98
+ expect(skill).toMatch(/execute[^\n]+openclaw channels login --channel openclaw-clawchat/i);
99
+ expect(skill).toMatch(/execute[^\n]+openclaw gateway restart/i);
100
+ });
101
+
102
+ it("documents channel login as the natural-language activation path", () => {
103
+ const readme = fs.readFileSync(new URL("../README.md", import.meta.url), "utf8");
104
+ const docs = fs.readFileSync(new URL("../docs/openclaw-clawchat.md", import.meta.url), "utf8");
105
+ expect(readme).toMatch(/openclaw channels login --channel openclaw-clawchat/i);
106
+ expect(docs).toMatch(/openclaw channels login --channel openclaw-clawchat/i);
107
+ expect(readme).toMatch(/openclaw gateway restart/i);
108
+ expect(docs).toMatch(/openclaw gateway restart/i);
109
+ expect(readme).not.toMatch(/activation skill[^.]+\/clawchat-login/i);
110
+ expect(docs).not.toMatch(/natural-language activation requests[^.]+\/clawchat-login/i);
111
+ expect(readme).not.toMatch(/\/clawchat-activate\s+A1B2C3/i);
112
+ expect(docs).not.toMatch(/\/clawchat-activate\s+A1B2C3/i);
113
+ expect(readme).not.toMatch(/clawchat_activate/);
114
+ expect(docs).not.toMatch(/clawchat_activate/);
115
+ expect(readme).not.toMatch(/\/clawchat_activate\s+A1B2C3/i);
116
+ expect(docs).not.toMatch(/\/clawchat_activate\s+A1B2C3/i);
117
+ expect(readme).not.toMatch(/direct users to/i);
118
+ expect(docs).not.toMatch(/direct the\s+user/i);
119
+ expect(docs).not.toMatch(/call the tool with the extracted code/i);
120
+ expect(docs).not.toMatch(/AGT->>PLG: clawchat_activate/);
121
+ expect(readme).toMatch(/activation skill[^.]+execute/i);
122
+ expect(docs).toMatch(/natural-language activation requests[^.]+execute/i);
21
123
  });
22
124
  });
@@ -64,7 +64,8 @@ describe("openclaw-clawchat outbound", () => {
64
64
  text: "hello",
65
65
  });
66
66
  expect((client.sendMessage as ReturnType<typeof vi.fn>).mock.calls[0][0]).toMatchObject({
67
- to: { id: "user-1", type: "direct" },
67
+ chat_id: "user-1",
68
+ chat_type: "direct",
68
69
  body: { fragments: [{ kind: "text", text: "hello" }] },
69
70
  });
70
71
  expect(client.replyMessage).not.toHaveBeenCalled();
@@ -82,16 +83,17 @@ describe("openclaw-clawchat outbound", () => {
82
83
  replyCtx: {
83
84
  replyToMessageId: "m-orig",
84
85
  replyPreviewSenderId: "user-2",
85
- replyPreviewDisplayName: "Sender",
86
+ replyPreviewNickName: "Sender",
86
87
  replyPreviewText: "original",
87
88
  },
88
89
  });
89
90
  expect((client.replyMessage as ReturnType<typeof vi.fn>).mock.calls[0][0]).toMatchObject({
90
- to: { id: "user-1", type: "direct" },
91
+ chat_id: "user-1",
92
+ chat_type: "direct",
91
93
  replyTo: {
92
94
  msgId: "m-orig",
93
95
  senderId: "user-2",
94
- displayName: "Sender",
96
+ nickName: "Sender",
95
97
  fragments: [{ kind: "text", text: "original" }],
96
98
  },
97
99
  body: { fragments: [{ kind: "text", text: "reply" }] },
@@ -168,7 +170,7 @@ describe("openclaw-clawchat outbound", () => {
168
170
  replyCtx: {
169
171
  replyToMessageId: "m-orig",
170
172
  replyPreviewSenderId: "user-2",
171
- replyPreviewDisplayName: "Sender",
173
+ replyPreviewNickName: "Sender",
172
174
  replyPreviewText: "original",
173
175
  },
174
176
  mediaFragments: [{ kind: "image", url: "https://cdn/x.png" }],
@@ -0,0 +1,27 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import pluginEntry from "../index.ts";
3
+
4
+ describe("openclaw-clawchat plugin entry", () => {
5
+ it("registers the channel/tools and native activation command without bootstrap migration", () => {
6
+ const api = {
7
+ config: {},
8
+ runtime: {},
9
+ logger: { debug: vi.fn(), info: vi.fn(), error: vi.fn() },
10
+ registerChannel: vi.fn(),
11
+ registerCommand: vi.fn(),
12
+ registerConfigMigration: vi.fn(),
13
+ registerTool: vi.fn(),
14
+ } as never;
15
+
16
+ pluginEntry.register(api);
17
+
18
+ expect(api.registerChannel).toHaveBeenCalledTimes(1);
19
+ expect(api.registerConfigMigration).not.toHaveBeenCalled();
20
+ expect(api.registerCommand).toHaveBeenCalledTimes(1);
21
+ expect(api.registerCommand).toHaveBeenCalledWith(
22
+ expect.objectContaining({
23
+ name: "clawchat-login",
24
+ }),
25
+ );
26
+ });
27
+ });
package/src/protocol.ts CHANGED
@@ -40,3 +40,8 @@ export function hasRenderableText(message: {
40
40
  (f as { url: string }).url.trim().length > 0)),
41
41
  );
42
42
  }
43
+
44
+ export function isGroupSender(sender: unknown): boolean {
45
+ if (!sender || typeof sender !== "object") return false;
46
+ return (sender as { type?: unknown }).type === "group";
47
+ }
@@ -185,7 +185,8 @@ describe("openclaw-clawchat reply-dispatcher", () => {
185
185
 
186
186
  expect(client.sendMessage).toHaveBeenCalledWith(
187
187
  expect.objectContaining({
188
- to: { id: "chat-1", type: "direct" },
188
+ chat_id: "chat-1",
189
+ chat_type: "direct",
189
190
  body: { fragments: [{ kind: "text", text: "Error: boom" }] },
190
191
  }),
191
192
  );
@@ -244,7 +245,8 @@ describe("openclaw-clawchat reply-dispatcher", () => {
244
245
 
245
246
  expect(client.sendMessage).toHaveBeenCalledWith(
246
247
  expect.objectContaining({
247
- to: { id: "chat-1", type: "direct" },
248
+ chat_id: "chat-1",
249
+ chat_type: "direct",
248
250
  body: { fragments: [{ kind: "text", text: "Error: boom" }] },
249
251
  }),
250
252
  );
@@ -56,15 +56,19 @@ describe("openclaw-clawchat runtime media ingest", () => {
56
56
  capturedCtx = ctx;
57
57
  return ctx;
58
58
  });
59
+ const resolveAgentRoute = vi.fn(() => ({
60
+ agentId: "u",
61
+ accountId: "default",
62
+ sessionKey: "s",
63
+ }));
64
+ const cfg = {
65
+ session: { store: "/tmp/sessions.json", dmScope: "main" },
66
+ } as unknown as OpenClawConfig;
59
67
 
60
68
  const runtime = {
61
69
  channel: {
62
70
  routing: {
63
- resolveAgentRoute: vi.fn(() => ({
64
- agentId: "u",
65
- accountId: "default",
66
- sessionKey: "s",
67
- })),
71
+ resolveAgentRoute,
68
72
  },
69
73
  session: {
70
74
  resolveStorePath: vi.fn(() => "/tmp/sessions.json"),
@@ -108,7 +112,7 @@ describe("openclaw-clawchat runtime media ingest", () => {
108
112
  const abortController = new AbortController();
109
113
 
110
114
  const startPromise = startOpenclawClawlingGateway({
111
- cfg: {} as import("openclaw/plugin-sdk/core").OpenClawConfig,
115
+ cfg,
112
116
  account: {
113
117
  accountId: "default",
114
118
  name: "openclaw-clawchat",
@@ -203,6 +207,18 @@ describe("openclaw-clawchat runtime media ingest", () => {
203
207
  expect(capturedCtx?.From).toBe("openclaw-clawchat:chat-1");
204
208
  expect(capturedCtx?.ConversationLabel).toBe("chat-1");
205
209
  expect(capturedCtx?.SenderId).toBe("user-1");
210
+ expect(resolveAgentRoute).toHaveBeenCalledWith(
211
+ expect.objectContaining({
212
+ cfg: expect.objectContaining({
213
+ session: expect.objectContaining({
214
+ dmScope: "per-account-channel-peer",
215
+ store: "/tmp/sessions.json",
216
+ }),
217
+ }),
218
+ peer: { kind: "direct", id: "chat-1" },
219
+ }),
220
+ );
221
+ expect(cfg.session?.dmScope).toBe("main");
206
222
  });
207
223
 
208
224
  it("uses group chat_id as the canonical conversation identity", async () => {
@@ -494,7 +510,7 @@ describe("openclaw-clawchat runtime reply dispatch lifecycle", () => {
494
510
  expect(dispatchReplyFromConfig).toHaveBeenCalledTimes(1);
495
511
  expect(markDispatchIdle).toHaveBeenCalledTimes(1);
496
512
  expect(logError).toHaveBeenCalledWith(
497
- expect.stringContaining("openclaw-clawchat message handler error:"),
513
+ expect.stringContaining("openclaw-clawchat dispatch failed msg=m-fail"),
498
514
  );
499
515
  });
500
516
  });
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
  });