@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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @newbase-clawchat/openclaw-clawchat
2
2
 
3
- OpenClaw channel plugin that connects an agent to ClawChat over the ClawChat Protocol v2, using [`@newbase-clawchat/sdk`](https://www.npmjs.com/package/@newbase-clawchat/sdk) for the WebSocket transport plus a small REST surface under `/v1/*` for profile / social / media operations.
3
+ OpenClaw channel plugin that connects an agent to ClawChat over the ClawChat Protocol v2, using [`@newbase-clawchat/sdk`](https://www.npmjs.com/package/@newbase-clawchat/sdk) for the WebSocket transport plus a small REST surface for profile / social / media operations (`/v1/*` plus unversioned `/media/upload`).
4
4
 
5
5
  ## Features
6
6
 
@@ -10,31 +10,52 @@ OpenClaw channel plugin that connects an agent to ClawChat over the ClawChat Pro
10
10
  - Outbound text replies in `static` or `stream` mode, with a consolidated final `message.reply`
11
11
  - Typing indicators and filtered forwarding for thinking / tool-call content
12
12
  - Media fragments (image / file / audio / video) in either direction
13
- - `clawchat_*` agent tools for profile, friends, media upload, and self-activation
13
+ - Channel-login onboarding plus `clawchat_*` agent tools for the configured ClawChat account profile, friends, and media upload
14
14
 
15
15
  ## Install
16
16
 
17
17
  ```bash
18
- # Via the OpenClaw CLI (recommended)
19
- openclaw channels setup --channel openclaw-clawchat --code INV-ABC123
20
-
21
- # Or add as a local extension
18
+ # Add as an OpenClaw extension
22
19
  npm i @newbase-clawchat/openclaw-clawchat
23
20
  ```
24
21
 
25
22
  Requires `openclaw >= 2026.3.23` as a peer host.
26
23
 
24
+ For the OpenClaw plugin install/update flow, see [`INSTALL.md`](./INSTALL.md).
25
+
26
+ Example LLM prompt:
27
+
28
+ ```text
29
+ Use https://raw.githubusercontent.com/clawling/openclaw-clawchat/refs/heads/main/INSTALL.md to install and activate the ClawChat plugin. The invite code is XXXXXX.
30
+ ```
31
+
27
32
  ## Quick start
28
33
 
34
+ Send one of these in chat:
35
+
36
+ ```text
37
+ activate ClawChat with invite code A1B2C3
38
+ ```
39
+
40
+ The activation skill executes OpenClaw channel login:
41
+
29
42
  ```bash
30
- # One-shot: apply config + exchange invite code for an access token
31
- openclaw channels setup --channel openclaw-clawchat --code INV-ABC123
43
+ openclaw channels login --channel openclaw-clawchat
44
+ openclaw gateway restart
45
+ ```
46
+
47
+ If you run the gateway manually instead of as a service, start it after login:
32
48
 
33
- # Run the gateway
49
+ ```bash
34
50
  openclaw gateway run
35
51
  ```
36
52
 
37
- Minimal `~/.openclaw/openclaw.json`:
53
+ The invite code is not a token; token fields are written only after login. Before
54
+ login, the plugin registers no ClawChat account tools; after activation/login,
55
+ the channel is enabled and the `clawchat_*` account tools register on config
56
+ reload.
57
+
58
+ After activation/login, the channel section is enabled and has credentials:
38
59
 
39
60
  ```json5
40
61
  {
@@ -44,7 +65,9 @@ Minimal `~/.openclaw/openclaw.json`:
44
65
  replyMode: "stream",
45
66
  forwardThinking: true,
46
67
  forwardToolCalls: false
47
- // token / userId / refreshToken are written by the login flow.
68
+ token: "...",
69
+ userId: "...",
70
+ refreshToken: "..."
48
71
  }
49
72
  }
50
73
  }
@@ -55,10 +78,11 @@ Minimal `~/.openclaw/openclaw.json`:
55
78
  A minimal browser test harness is bundled under `tools/`:
56
79
 
57
80
  ```bash
58
- npm run test-ui
81
+ node tools/standalone-webchat-server.mjs
82
+ # Options: --host (default 127.0.0.1), --port (default 4318), --default-ws-url
59
83
  ```
60
84
 
61
- Then open the printed URL it mounts `tools/standalone-webchat.html` against a local relay so you can exercise the plugin end to end without a full ClawChat backend.
85
+ Then open the printed URL (default `http://127.0.0.1:4318`) to exercise the plugin end to end against a WebSocket relay.
62
86
 
63
87
  ## Layout
64
88
 
@@ -103,6 +127,9 @@ See [`docs/openclaw-clawchat.md`](./docs/openclaw-clawchat.md) for:
103
127
  ```bash
104
128
  # Tests
105
129
  npx vitest run
130
+
131
+ # Typecheck
132
+ npm run typecheck
106
133
  ```
107
134
 
108
135
  Tests live next to the source they cover (`*.test.ts`). The plugin is pure TypeScript and is consumed via the OpenClaw host's extension loader — no bundling step is required.
package/index.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  // import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
2
2
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
3
3
  import { openclawClawlingPlugin } from "./src/channel.ts";
4
+ import { registerOpenclawClawlingCommands } from "./src/commands.ts";
4
5
  import { setOpenclawClawlingRuntime } from "./src/runtime.ts";
5
6
  import { registerOpenclawClawlingTools } from "./src/tools.ts";
6
7
  import { openclawClawlingConfigSchema } from "./src/config.ts";
7
8
 
8
-
9
9
  export default {
10
10
  id: "openclaw-clawchat",
11
11
  name: "Clawling Chat",
@@ -14,6 +14,7 @@ export default {
14
14
  register(api: OpenClawPluginApi) {
15
15
  setOpenclawClawlingRuntime(api.runtime);
16
16
  api.registerChannel({ plugin: openclawClawlingPlugin });
17
+ registerOpenclawClawlingCommands(api);
17
18
  registerOpenclawClawlingTools(api);
18
19
  }
19
20
  }
@@ -1,6 +1,14 @@
1
1
  {
2
2
  "id": "openclaw-clawchat",
3
3
  "channels": ["openclaw-clawchat"],
4
+ "skills": ["./skills"],
5
+ "activation": {
6
+ "onChannels": ["openclaw-clawchat"],
7
+ "onCommands": ["clawchat-login"]
8
+ },
9
+ "commandAliases": [
10
+ { "name": "clawchat-login", "kind": "runtime-slash" }
11
+ ],
4
12
  "configSchema": {
5
13
  "type": "object",
6
14
  "additionalProperties": false,
@@ -9,8 +17,10 @@
9
17
  "websocketUrl": { "type": "string" },
10
18
  "baseUrl": { "type": "string" },
11
19
  "token": { "type": "string" },
20
+ "refreshToken": { "type": "string" },
12
21
  "userId": { "type": "string" },
13
22
  "replyMode": { "type": "string", "enum": ["static", "stream"] },
23
+ "groupMode": { "type": "string", "enum": ["mention", "all"] },
14
24
  "forwardThinking": { "type": "boolean" },
15
25
  "forwardToolCalls": { "type": "boolean" },
16
26
  "stream": {
@@ -28,7 +38,8 @@
28
38
  "properties": {
29
39
  "initialDelay": { "type": "integer", "minimum": 100 },
30
40
  "maxDelay": { "type": "integer", "minimum": 100 },
31
- "jitterRatio": { "type": "number", "minimum": 0 }
41
+ "jitterRatio": { "type": "number", "minimum": 0 },
42
+ "maxRetries": { "type": "integer", "minimum": 0 }
32
43
  }
33
44
  },
34
45
  "heartbeat": {
@@ -48,5 +59,62 @@
48
59
  }
49
60
  }
50
61
  }
62
+ },
63
+ "channelConfigs": {
64
+ "openclaw-clawchat": {
65
+ "label": "Clawling Chat",
66
+ "description": "Clawling Protocol v2 over WebSocket (chat-sdk).",
67
+ "schema": {
68
+ "type": "object",
69
+ "additionalProperties": false,
70
+ "properties": {
71
+ "enabled": { "type": "boolean" },
72
+ "websocketUrl": { "type": "string" },
73
+ "baseUrl": { "type": "string" },
74
+ "token": { "type": "string" },
75
+ "refreshToken": { "type": "string" },
76
+ "userId": { "type": "string" },
77
+ "replyMode": { "type": "string", "enum": ["static", "stream"] },
78
+ "groupMode": { "type": "string", "enum": ["mention", "all"] },
79
+ "forwardThinking": { "type": "boolean" },
80
+ "forwardToolCalls": { "type": "boolean" },
81
+ "stream": {
82
+ "type": "object",
83
+ "additionalProperties": false,
84
+ "properties": {
85
+ "flushIntervalMs": { "type": "integer", "minimum": 10 },
86
+ "minChunkChars": { "type": "integer", "minimum": 1 },
87
+ "maxBufferChars": { "type": "integer", "minimum": 1 }
88
+ }
89
+ },
90
+ "reconnect": {
91
+ "type": "object",
92
+ "additionalProperties": false,
93
+ "properties": {
94
+ "initialDelay": { "type": "integer", "minimum": 100 },
95
+ "maxDelay": { "type": "integer", "minimum": 100 },
96
+ "jitterRatio": { "type": "number", "minimum": 0 },
97
+ "maxRetries": { "type": "integer", "minimum": 0 }
98
+ }
99
+ },
100
+ "heartbeat": {
101
+ "type": "object",
102
+ "additionalProperties": false,
103
+ "properties": {
104
+ "interval": { "type": "integer", "minimum": 1000 },
105
+ "timeout": { "type": "integer", "minimum": 1000 }
106
+ }
107
+ },
108
+ "ack": {
109
+ "type": "object",
110
+ "additionalProperties": false,
111
+ "properties": {
112
+ "timeout": { "type": "integer", "minimum": 100 },
113
+ "autoResendOnTimeout": { "type": "boolean" }
114
+ }
115
+ }
116
+ }
117
+ }
118
+ }
51
119
  }
52
120
  }
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@newbase-clawchat/openclaw-clawchat",
3
- "version": "2026.4.24",
3
+ "version": "2026.4.29",
4
4
  "description": "OpenClaw ClawChat channel plugin",
5
5
  "files": [
6
6
  "index.ts",
7
7
  "src",
8
+ "skills",
8
9
  "openclaw.plugin.json",
9
10
  "README.md"
10
11
  ],
@@ -24,7 +25,7 @@
24
25
  "typescript": "^5.4.0"
25
26
  },
26
27
  "peerDependencies": {
27
- "openclaw": ">=2026.3.23"
28
+ "openclaw": "^2026.3.23"
28
29
  },
29
30
  "peerDependenciesMeta": {
30
31
  "openclaw": {
@@ -38,15 +39,6 @@
38
39
  "extensions": [
39
40
  "./index.ts"
40
41
  ],
41
- "channel": {
42
- "id": "openclaw-clawchat",
43
- "label": "openclaw-clawchat",
44
- "selectionLabel": "openclaw-clawchat",
45
- "docsPath": "/channels/openclaw-clawchat",
46
- "docsLabel": "openclaw-clawchat",
47
- "blurb": "OpenClaw ClawChat channel plugin",
48
- "order": 70
49
- },
50
42
  "install": {
51
43
  "npmSpec": "@newbase-clawchat/openclaw-clawchat",
52
44
  "defaultChoice": "npm",
@@ -0,0 +1,26 @@
1
+ ---
2
+ name: clawchat-account-tools
3
+ description: Use when a user asks to view or update the configured ClawChat account profile, inspect a ClawChat user profile, list account friends, change avatar/bio/nickname, or upload/share ClawChat media files.
4
+ ---
5
+
6
+ # ClawChat Account Tools
7
+
8
+ Use these tools for ClawChat account/profile, friends, avatar, and media-file requests. The configured ClawChat account is not the OpenClaw agent persona.
9
+
10
+ ## Tool Selection
11
+
12
+ | User intent | Tool | Notes |
13
+ | --- | --- | --- |
14
+ | View the configured ClawChat account profile | `clawchat_get_account_profile` | Returns account user id, nickname/display name, avatar, and bio. |
15
+ | Inspect another ClawChat user profile | `clawchat_get_user_profile` | Requires a concrete `userId`; do not infer it from nicknames. |
16
+ | List account friends or contacts | `clawchat_list_account_friends` | Use `page` and `pageSize` when paging through results. |
17
+ | Update account nickname, avatar URL, or bio | `clawchat_update_account_profile` | Pass at least one of `nickname`, `avatar_url`, or `bio`. |
18
+ | Upload a local avatar image | `clawchat_upload_avatar_image` | Returns a hosted avatar URL; then call `clawchat_update_account_profile` with `avatar_url` to set it. |
19
+ | Upload/share a non-avatar local media file | `clawchat_upload_media_file` | Returns a public URL. If the file should be sent in chat, use the message tool with `media`. |
20
+
21
+ ## Boundaries
22
+
23
+ - Do not treat ClawChat account profile changes as OpenClaw agent persona changes.
24
+ - Use avatar upload only for account avatar/profile-picture changes.
25
+ - Use media upload for non-avatar local files.
26
+ - For activation, login, or invite-code requests, use the activation skill instead.
@@ -0,0 +1,38 @@
1
+ ---
2
+ name: clawchat-activate
3
+ description: |
4
+ Activate or log in OpenClaw ClawChat. Use when the user asks to activate, connect, bind, or log in ClawChat, or provides a ClawChat invite code (six uppercase letters/digits, e.g. A1B2C3).
5
+ ---
6
+
7
+ # ClawChat Activation
8
+
9
+ Activation must execute the OpenClaw channel login command. Do not edit config files manually, do not call tools for activation, and do not try to execute slash commands from this skill.
10
+
11
+ ## Workflow
12
+
13
+ 1. Check whether the user is trying to activate or log in ClawChat.
14
+ 2. Execute the channel login command:
15
+
16
+ ```bash
17
+ openclaw channels login --channel openclaw-clawchat
18
+ ```
19
+ 3. Do not append the invite code to the command; `openclaw channels login` has no code argument.
20
+ 4. When the command prompts for an invite code, provide the user's invite code if one was already present, otherwise ask the user for it.
21
+ 5. After channel login succeeds, execute the Gateway restart command:
22
+
23
+ ```bash
24
+ openclaw gateway restart
25
+ ```
26
+ 6. Tell the user activation completes after channel login and Gateway restart both succeed.
27
+
28
+ ## Trigger Examples
29
+
30
+ - `activate ClawChat with invite code A1B2C3`
31
+ - `login to ClawChat with invite code A1B2C3`
32
+ - `connect ClawChat using invite code A1B2C3`
33
+ - `绑定 ClawChat,邀请码 A1B2C3`
34
+ - `激活 ClawChat`
35
+
36
+ Do not ask the user to enter a bare ClawChat command. If activation is requested, execute `openclaw channels login --channel openclaw-clawchat` yourself, then execute `openclaw gateway restart`.
37
+
38
+ When the user asks to activate ClawChat without including a code, run channel login and ask for the invite code when the command needs it.
@@ -89,7 +89,7 @@ describe("openclaw-clawchat api-client", () => {
89
89
  fetchImpl,
90
90
  });
91
91
  await client.updateMyProfile({ display_name: "Alice2" });
92
- expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/agents/agent-1");
92
+ expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/users/me");
93
93
  const init = fetchImpl.mock.calls[0]![1] as RequestInit;
94
94
  expect(init.method).toBe("PATCH");
95
95
  expect(JSON.parse(init.body as string)).toEqual({ display_name: "Alice2" });
@@ -196,6 +196,7 @@ describe("openclaw-clawchat api-client", () => {
196
196
  it("agentsConnect POSTs /agents/connect with { code, platform, type } body", async () => {
197
197
  const fetchImpl = vi.fn().mockResolvedValue(
198
198
  jsonResponse({
199
+ code: 0,
199
200
  msg: "ok",
200
201
  data: {
201
202
  agent: {
@@ -222,7 +223,7 @@ describe("openclaw-clawchat api-client", () => {
222
223
  fetchImpl,
223
224
  });
224
225
  const result = await client.agentsConnect({
225
- inviteCode: "INV-1",
226
+ code: "INV-1",
226
227
  platform: "openclaw",
227
228
  type: "bot",
228
229
  });
@@ -252,13 +253,13 @@ describe("openclaw-clawchat api-client", () => {
252
253
  fetchImpl,
253
254
  });
254
255
  await expect(
255
- client.agentsConnect({ inviteCode: " ", platform: "openclaw", type: "bot" }),
256
+ client.agentsConnect({ code: " ", platform: "openclaw", type: "bot" }),
256
257
  ).rejects.toMatchObject({ kind: "validation" });
257
258
  await expect(
258
- client.agentsConnect({ inviteCode: "INV", platform: "", type: "bot" }),
259
+ client.agentsConnect({ code: "INV", platform: "", type: "bot" }),
259
260
  ).rejects.toMatchObject({ kind: "validation" });
260
261
  await expect(
261
- client.agentsConnect({ inviteCode: "INV", platform: "openclaw", type: "" }),
262
+ client.agentsConnect({ code: "INV", platform: "openclaw", type: "" }),
262
263
  ).rejects.toMatchObject({ kind: "validation" });
263
264
  expect(fetchImpl).not.toHaveBeenCalled();
264
265
  });
package/src/api-client.ts CHANGED
@@ -24,7 +24,7 @@ export interface OpenclawClawlingApiClient {
24
24
  getMyProfile(): Promise<Profile>;
25
25
  getUserInfo(userId: string): Promise<Profile>;
26
26
  listFriends(params: { page?: number; pageSize?: number }): Promise<FriendList>;
27
- updateMyProfile(patch: { nick_name?: string; avatar_url?: string; bio?: string }): Promise<Profile>;
27
+ updateMyProfile(patch: { nickname?: string; avatar_url?: string; bio?: string }): Promise<Profile>;
28
28
  uploadMedia(params: { buffer: Buffer; filename: string; mime?: string }): Promise<UploadResult>;
29
29
  /**
30
30
  * Exchange an invite code for an agent token.
@@ -102,9 +102,14 @@ export function createOpenclawClawlingApiClient(opts: ApiClientOptions): Opencla
102
102
  // Unified envelope: `{ code: number, msg: string, data: T }`.
103
103
  // `code === 0` means success; any other value is a business error whose
104
104
  // `msg` is surfaced to callers and `code` is preserved on the error meta.
105
- const env = parsed as { code?: unknown; msg?: unknown; data?: T };
105
+ const env = parsed as { code?: unknown; msg?: unknown; message?: unknown; data?: T };
106
106
  const code = typeof env.code === "number" ? env.code : Number.NaN;
107
- const msg = typeof env.msg === "string" ? env.msg : "";
107
+ const msg =
108
+ typeof env.msg === "string"
109
+ ? env.msg
110
+ : typeof env.message === "string"
111
+ ? env.message
112
+ : "";
108
113
  if (!Number.isFinite(code)) {
109
114
  throw new ClawlingApiError("transport", "invalid envelope: missing numeric `code`", {
110
115
  status: res.status,
@@ -52,7 +52,7 @@ describe("openBufferedStreamingSession", () => {
52
52
  minChunkChars: 4,
53
53
  maxBufferChars: 1000,
54
54
  });
55
- expect(typing).toEqual([[{ id: "u1", type: "direct" }, true]]);
55
+ expect(typing).toEqual([["u1", true]]);
56
56
  expect(sent.map((s) => s.event)).toEqual(["message.created"]);
57
57
  });
58
58
 
@@ -122,8 +122,8 @@ describe("openBufferedStreamingSession", () => {
122
122
  const events = sent.map((s) => s.event);
123
123
  expect(events).toEqual(["message.created", "message.add", "message.done"]);
124
124
  expect(typing).toEqual([
125
- [{ id: "u1", type: "direct" }, true],
126
- [{ id: "u1", type: "direct" }, false],
125
+ ["u1", true],
126
+ ["u1", false],
127
127
  ]);
128
128
  });
129
129
 
@@ -158,7 +158,7 @@ describe("openBufferedStreamingSession", () => {
158
158
  await session.fail("boom");
159
159
  const failed = sent.find((s) => s.event === "message.failed")!;
160
160
  expect(failed.payload.reason).toBe("boom");
161
- expect(typing.at(-1)).toEqual([{ id: "u1", type: "direct" }, false]);
161
+ expect(typing.at(-1)).toEqual(["u1", false]);
162
162
  });
163
163
 
164
164
  it("deduplicates a snapshot that is a substring of the buffered snapshot", async () => {
@@ -43,7 +43,8 @@ export function mergeStreamingText(
43
43
 
44
44
  export interface BufferedStreamOptions {
45
45
  client: ClawlingChatClient;
46
- routing: EnvelopeRouting;
46
+ routing?: EnvelopeRouting;
47
+ to?: { id: string; type: "direct" | "group" };
47
48
  sender: StreamSender;
48
49
  messageId: string;
49
50
  flushIntervalMs: number;
@@ -53,6 +54,12 @@ export interface BufferedStreamOptions {
53
54
  emitTyping?: boolean;
54
55
  }
55
56
 
57
+ function resolveRouting(options: BufferedStreamOptions): EnvelopeRouting {
58
+ if (options.routing) return options.routing;
59
+ if (options.to) return { chatId: options.to.id, chatType: options.to.type };
60
+ throw new Error("openclaw-clawchat buffered stream requires routing");
61
+ }
62
+
56
63
  export interface BufferedStreamSession {
57
64
  /** The full accumulated text (even if not yet flushed to the wire). */
58
65
  readonly currentText: string;
@@ -83,12 +90,13 @@ export interface BufferedStreamSession {
83
90
  export function openBufferedStreamingSession(
84
91
  options: BufferedStreamOptions,
85
92
  ): BufferedStreamSession {
93
+ const routing = resolveRouting(options);
86
94
  const emitTyping = options.emitTyping !== false;
87
95
  if (emitTyping)
88
- options.client.typing(options.routing.chatId, true, options.routing.chatType);
96
+ options.client.typing(routing.chatId, true, routing.chatType);
89
97
  emitStreamCreated(options.client, {
90
98
  messageId: options.messageId,
91
- routing: options.routing,
99
+ routing,
92
100
  });
93
101
 
94
102
  let bufferedSnapshot = "";
@@ -115,7 +123,7 @@ export function openBufferedStreamingSession(
115
123
  sequence += 1;
116
124
  emitStreamAdd(options.client, {
117
125
  messageId: options.messageId,
118
- routing: options.routing,
126
+ routing,
119
127
  sequence,
120
128
  fullText: snapshot,
121
129
  textDelta: delta,
@@ -168,12 +176,12 @@ export function openBufferedStreamingSession(
168
176
  clearTimer();
169
177
  emitStreamDone(options.client, {
170
178
  messageId: options.messageId,
171
- routing: options.routing,
179
+ routing,
172
180
  finalSequence: sequence,
173
181
  finalText: bufferedSnapshot,
174
182
  });
175
183
  if (emitTyping)
176
- options.client.typing(options.routing.chatId, false, options.routing.chatType);
184
+ options.client.typing(routing.chatId, false, routing.chatType);
177
185
  };
178
186
 
179
187
  const fail = async (reason?: string): Promise<void> => {
@@ -182,12 +190,12 @@ export function openBufferedStreamingSession(
182
190
  clearTimer();
183
191
  emitStreamFailed(options.client, {
184
192
  messageId: options.messageId,
185
- routing: options.routing,
193
+ routing,
186
194
  sequence: sequence + 1,
187
195
  ...(reason !== undefined ? { reason } : {}),
188
196
  });
189
197
  if (emitTyping)
190
- options.client.typing(options.routing.chatId, false, options.routing.chatType);
198
+ options.client.typing(routing.chatId, false, routing.chatType);
191
199
  };
192
200
 
193
201
  return {
@@ -5,8 +5,6 @@ const getRuntimeMock = vi.hoisted(() => vi.fn());
5
5
  const waitForClientMock = vi.hoisted(() => vi.fn());
6
6
  const uploadOutboundMediaMock = vi.hoisted(() => vi.fn());
7
7
  const createApiClientMock = vi.hoisted(() => vi.fn());
8
- const sendTextMock = vi.hoisted(() => vi.fn());
9
- const sendMediaMock = vi.hoisted(() => vi.fn());
10
8
 
11
9
  vi.mock("./runtime.ts", () => ({
12
10
  getOpenclawClawlingClient: getClientMock,
@@ -23,11 +21,6 @@ vi.mock("./api-client.ts", () => ({
23
21
  createOpenclawClawlingApiClient: createApiClientMock,
24
22
  }));
25
23
 
26
- vi.mock("./outbound.ts", () => ({
27
- sendOpenclawClawlingText: sendTextMock,
28
- sendOpenclawClawlingMedia: sendMediaMock,
29
- }));
30
-
31
24
  describe("openclaw-clawchat channel outbound", () => {
32
25
  beforeEach(() => {
33
26
  vi.resetModules();
@@ -36,15 +29,17 @@ describe("openclaw-clawchat channel outbound", () => {
36
29
  waitForClientMock.mockReset();
37
30
  uploadOutboundMediaMock.mockReset();
38
31
  createApiClientMock.mockReset();
39
- sendTextMock.mockReset();
40
- sendMediaMock.mockReset();
41
32
  });
42
33
 
43
34
  it("sendText waits for client activation when no active client exists yet", async () => {
44
- const client = { sendMessage: vi.fn() };
35
+ const client = {
36
+ sendMessage: vi.fn().mockResolvedValue({
37
+ payload: { message_id: "m-2", accepted_at: 456 },
38
+ trace_id: "trace-2",
39
+ }),
40
+ };
45
41
  getClientMock.mockReturnValue(undefined);
46
42
  waitForClientMock.mockResolvedValue(client);
47
- sendTextMock.mockResolvedValue({ messageId: "m-2", acceptedAt: 456 });
48
43
 
49
44
  const { openclawClawlingOutbound } = await import("./outbound.ts");
50
45
  const result = await openclawClawlingOutbound.sendText!({
@@ -64,12 +59,13 @@ describe("openclaw-clawchat channel outbound", () => {
64
59
  });
65
60
 
66
61
  expect(waitForClientMock).toHaveBeenCalledWith("default");
67
- expect(sendTextMock).toHaveBeenCalledWith({
68
- client,
69
- account: expect.objectContaining({ userId: "agent-1" }),
70
- to: { chatId: "user-1", chatType: "direct" },
71
- text: "hello",
72
- });
62
+ expect(client.sendMessage).toHaveBeenCalledWith(
63
+ expect.objectContaining({
64
+ chat_id: "user-1",
65
+ chat_type: "direct",
66
+ body: { fragments: [{ kind: "text", text: "hello" }] },
67
+ }),
68
+ );
73
69
  expect(result).toEqual({
74
70
  channel: "openclaw-clawchat",
75
71
  to: "cc:user-1",
@@ -78,7 +74,12 @@ describe("openclaw-clawchat channel outbound", () => {
78
74
  });
79
75
 
80
76
  it("sendMedia uploads mediaUrl and sends resulting fragments", async () => {
81
- const client = { sendMessage: vi.fn() };
77
+ const client = {
78
+ sendMessage: vi.fn().mockResolvedValue({
79
+ payload: { message_id: "m-1", accepted_at: 123 },
80
+ trace_id: "trace-1",
81
+ }),
82
+ };
82
83
  const runtime = { media: { loadWebMedia: vi.fn() } };
83
84
  const apiClient = { uploadMedia: vi.fn() };
84
85
  getClientMock.mockReturnValue(client);
@@ -87,7 +88,6 @@ describe("openclaw-clawchat channel outbound", () => {
87
88
  uploadOutboundMediaMock.mockResolvedValue([
88
89
  { kind: "image", url: "https://cdn/uploaded.png", mime: "image/png" },
89
90
  ]);
90
- sendMediaMock.mockResolvedValue({ messageId: "m-1", acceptedAt: 123 });
91
91
 
92
92
  const { openclawClawlingOutbound } = await import("./outbound.ts");
93
93
  const result = await openclawClawlingOutbound.sendMedia!({
@@ -118,13 +118,18 @@ describe("openclaw-clawchat channel outbound", () => {
118
118
  runtime,
119
119
  mediaLocalRoots: ["/tmp"],
120
120
  });
121
- expect(sendMediaMock).toHaveBeenCalledWith({
122
- client,
123
- account: expect.objectContaining({ userId: "agent-1", baseUrl: "https://api.example.com" }),
124
- to: { chatId: "room-1", chatType: "group" },
125
- text: "caption",
126
- mediaFragments: [{ kind: "image", url: "https://cdn/uploaded.png", mime: "image/png" }],
127
- });
121
+ expect(client.sendMessage).toHaveBeenCalledWith(
122
+ expect.objectContaining({
123
+ chat_id: "room-1",
124
+ chat_type: "group",
125
+ body: {
126
+ fragments: [
127
+ { kind: "text", text: "caption" },
128
+ { kind: "image", url: "https://cdn/uploaded.png", mime: "image/png" },
129
+ ],
130
+ },
131
+ }),
132
+ );
128
133
  expect(result).toEqual({
129
134
  channel: "openclaw-clawchat",
130
135
  to: "cc:group:room-1",
@@ -155,7 +160,12 @@ describe("openclaw-clawchat channel outbound", () => {
155
160
  });
156
161
 
157
162
  it("sendMedia waits for client activation when no active client exists yet", async () => {
158
- const client = { sendMessage: vi.fn() };
163
+ const client = {
164
+ sendMessage: vi.fn().mockResolvedValue({
165
+ payload: { message_id: "m-3", accepted_at: 789 },
166
+ trace_id: "trace-3",
167
+ }),
168
+ };
159
169
  const runtime = { media: { loadWebMedia: vi.fn() } };
160
170
  const apiClient = { uploadMedia: vi.fn() };
161
171
  getClientMock.mockReturnValue(undefined);
@@ -165,7 +175,6 @@ describe("openclaw-clawchat channel outbound", () => {
165
175
  uploadOutboundMediaMock.mockResolvedValue([
166
176
  { kind: "image", url: "https://cdn/uploaded.png", mime: "image/png" },
167
177
  ]);
168
- sendMediaMock.mockResolvedValue({ messageId: "m-3", acceptedAt: 789 });
169
178
 
170
179
  const { openclawClawlingOutbound } = await import("./outbound.ts");
171
180
  const result = await openclawClawlingOutbound.sendMedia!({
@@ -187,13 +196,18 @@ describe("openclaw-clawchat channel outbound", () => {
187
196
  });
188
197
 
189
198
  expect(waitForClientMock).toHaveBeenCalledWith("default");
190
- expect(sendMediaMock).toHaveBeenCalledWith({
191
- client,
192
- account: expect.objectContaining({ userId: "agent-1", baseUrl: "https://api.example.com" }),
193
- to: { chatId: "room-1", chatType: "group" },
194
- text: "caption",
195
- mediaFragments: [{ kind: "image", url: "https://cdn/uploaded.png", mime: "image/png" }],
196
- });
199
+ expect(client.sendMessage).toHaveBeenCalledWith(
200
+ expect.objectContaining({
201
+ chat_id: "room-1",
202
+ chat_type: "group",
203
+ body: {
204
+ fragments: [
205
+ { kind: "text", text: "caption" },
206
+ { kind: "image", url: "https://cdn/uploaded.png", mime: "image/png" },
207
+ ],
208
+ },
209
+ }),
210
+ );
197
211
  expect(result).toEqual({
198
212
  channel: "openclaw-clawchat",
199
213
  to: "cc:group:room-1",