@newbase-clawchat/openclaw-clawchat 2026.4.24 → 2026.4.30

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.
Files changed (56) hide show
  1. package/README.md +66 -16
  2. package/dist/index.js +27 -0
  3. package/dist/src/api-client.js +156 -0
  4. package/dist/src/api-types.js +17 -0
  5. package/dist/src/buffered-stream.js +177 -0
  6. package/dist/src/channel.js +191 -0
  7. package/dist/src/client.js +176 -0
  8. package/dist/src/commands.js +35 -0
  9. package/dist/src/config.js +214 -0
  10. package/dist/src/inbound.js +133 -0
  11. package/dist/src/login.runtime.js +130 -0
  12. package/dist/src/media-runtime.js +85 -0
  13. package/dist/src/message-mapper.js +82 -0
  14. package/dist/src/outbound.js +181 -0
  15. package/dist/src/protocol.js +38 -0
  16. package/dist/src/reply-dispatcher.js +440 -0
  17. package/dist/src/runtime.js +288 -0
  18. package/dist/src/streaming.js +65 -0
  19. package/dist/src/tools-schema.js +38 -0
  20. package/dist/src/tools.js +287 -0
  21. package/index.ts +2 -1
  22. package/openclaw.plugin.json +81 -1
  23. package/package.json +21 -9
  24. package/skills/clawchat-account-tools/SKILL.md +26 -0
  25. package/skills/clawchat-activate/SKILL.md +47 -0
  26. package/src/api-client.test.ts +6 -5
  27. package/src/api-client.ts +8 -3
  28. package/src/buffered-stream.test.ts +14 -4
  29. package/src/buffered-stream.ts +19 -11
  30. package/src/channel.outbound.test.ts +49 -35
  31. package/src/channel.test.ts +45 -10
  32. package/src/channel.ts +26 -17
  33. package/src/client.test.ts +9 -1
  34. package/src/client.ts +48 -21
  35. package/src/commands.test.ts +39 -0
  36. package/src/commands.ts +41 -0
  37. package/src/config.test.ts +40 -3
  38. package/src/config.ts +60 -4
  39. package/src/inbound.test.ts +9 -6
  40. package/src/inbound.ts +51 -16
  41. package/src/login.runtime.test.ts +142 -3
  42. package/src/login.runtime.ts +59 -26
  43. package/src/manifest.test.ts +183 -5
  44. package/src/outbound.test.ts +10 -7
  45. package/src/outbound.ts +8 -7
  46. package/src/plugin-entry.test.ts +27 -0
  47. package/src/protocol.ts +5 -0
  48. package/src/reply-dispatcher.test.ts +420 -3
  49. package/src/reply-dispatcher.ts +137 -12
  50. package/src/runtime.test.ts +23 -7
  51. package/src/runtime.ts +13 -1
  52. package/src/streaming.test.ts +12 -9
  53. package/src/streaming.ts +22 -12
  54. package/src/tools-schema.ts +28 -19
  55. package/src/tools.test.ts +181 -40
  56. package/src/tools.ts +107 -95
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,26 @@
1
1
  {
2
2
  "id": "openclaw-clawchat",
3
3
  "channels": ["openclaw-clawchat"],
4
+ "skills": ["./skills"],
5
+ "activation": {
6
+ "onStartup": true,
7
+ "onChannels": ["openclaw-clawchat"],
8
+ "onCommands": ["clawchat-login"]
9
+ },
10
+ "commandAliases": [
11
+ { "name": "clawchat-login", "kind": "runtime-slash" }
12
+ ],
13
+ "contracts": {
14
+ "tools": [
15
+ "clawchat_activate",
16
+ "clawchat_get_account_profile",
17
+ "clawchat_get_user_profile",
18
+ "clawchat_list_account_friends",
19
+ "clawchat_update_account_profile",
20
+ "clawchat_upload_avatar_image",
21
+ "clawchat_upload_media_file"
22
+ ]
23
+ },
4
24
  "configSchema": {
5
25
  "type": "object",
6
26
  "additionalProperties": false,
@@ -9,8 +29,10 @@
9
29
  "websocketUrl": { "type": "string" },
10
30
  "baseUrl": { "type": "string" },
11
31
  "token": { "type": "string" },
32
+ "refreshToken": { "type": "string" },
12
33
  "userId": { "type": "string" },
13
34
  "replyMode": { "type": "string", "enum": ["static", "stream"] },
35
+ "groupMode": { "type": "string", "enum": ["mention", "all"] },
14
36
  "forwardThinking": { "type": "boolean" },
15
37
  "forwardToolCalls": { "type": "boolean" },
16
38
  "stream": {
@@ -28,7 +50,8 @@
28
50
  "properties": {
29
51
  "initialDelay": { "type": "integer", "minimum": 100 },
30
52
  "maxDelay": { "type": "integer", "minimum": 100 },
31
- "jitterRatio": { "type": "number", "minimum": 0 }
53
+ "jitterRatio": { "type": "number", "minimum": 0 },
54
+ "maxRetries": { "type": "integer", "minimum": 0 }
32
55
  }
33
56
  },
34
57
  "heartbeat": {
@@ -48,5 +71,62 @@
48
71
  }
49
72
  }
50
73
  }
74
+ },
75
+ "channelConfigs": {
76
+ "openclaw-clawchat": {
77
+ "label": "Clawling Chat",
78
+ "description": "Clawling Protocol v2 over WebSocket (chat-sdk).",
79
+ "schema": {
80
+ "type": "object",
81
+ "additionalProperties": false,
82
+ "properties": {
83
+ "enabled": { "type": "boolean" },
84
+ "websocketUrl": { "type": "string" },
85
+ "baseUrl": { "type": "string" },
86
+ "token": { "type": "string" },
87
+ "refreshToken": { "type": "string" },
88
+ "userId": { "type": "string" },
89
+ "replyMode": { "type": "string", "enum": ["static", "stream"] },
90
+ "groupMode": { "type": "string", "enum": ["mention", "all"] },
91
+ "forwardThinking": { "type": "boolean" },
92
+ "forwardToolCalls": { "type": "boolean" },
93
+ "stream": {
94
+ "type": "object",
95
+ "additionalProperties": false,
96
+ "properties": {
97
+ "flushIntervalMs": { "type": "integer", "minimum": 10 },
98
+ "minChunkChars": { "type": "integer", "minimum": 1 },
99
+ "maxBufferChars": { "type": "integer", "minimum": 1 }
100
+ }
101
+ },
102
+ "reconnect": {
103
+ "type": "object",
104
+ "additionalProperties": false,
105
+ "properties": {
106
+ "initialDelay": { "type": "integer", "minimum": 100 },
107
+ "maxDelay": { "type": "integer", "minimum": 100 },
108
+ "jitterRatio": { "type": "number", "minimum": 0 },
109
+ "maxRetries": { "type": "integer", "minimum": 0 }
110
+ }
111
+ },
112
+ "heartbeat": {
113
+ "type": "object",
114
+ "additionalProperties": false,
115
+ "properties": {
116
+ "interval": { "type": "integer", "minimum": 1000 },
117
+ "timeout": { "type": "integer", "minimum": 1000 }
118
+ }
119
+ },
120
+ "ack": {
121
+ "type": "object",
122
+ "additionalProperties": false,
123
+ "properties": {
124
+ "timeout": { "type": "integer", "minimum": 100 },
125
+ "autoResendOnTimeout": { "type": "boolean" }
126
+ }
127
+ }
128
+ }
129
+ }
130
+ }
51
131
  }
52
132
  }
package/package.json CHANGED
@@ -1,15 +1,23 @@
1
1
  {
2
2
  "name": "@newbase-clawchat/openclaw-clawchat",
3
- "version": "2026.4.24",
3
+ "version": "2026.4.30",
4
4
  "description": "OpenClaw ClawChat channel plugin",
5
5
  "files": [
6
+ "dist",
6
7
  "index.ts",
7
8
  "src",
9
+ "skills",
8
10
  "openclaw.plugin.json",
9
11
  "README.md"
10
12
  ],
11
13
  "type": "module",
12
14
  "scripts": {
15
+ "build": "tsc -p tsconfig.build.json",
16
+ "test": "vitest",
17
+ "test:e2e:install-clawchat-plugin": "bash .e2e/run-install-clawchat-plugin-e2e.sh",
18
+ "test:e2e:install-clawchat-plugin:smoke": "node --test .e2e/run-install-clawchat-plugin-e2e.test.mjs",
19
+ "dev:openclaw-source": "test -d tmp/openclaw || git clone --depth=1 https://github.com/openclaw/openclaw.git tmp/openclaw",
20
+ "prepack": "npm run build",
13
21
  "typecheck": "tsc --noEmit",
14
22
  "prepublishOnly": "npm run typecheck",
15
23
  "release": "npm run prepublishOnly && npm publish"
@@ -20,11 +28,12 @@
20
28
  },
21
29
  "devDependencies": {
22
30
  "@types/node": "^25.5.0",
23
- "openclaw": "^2026.3.23",
24
- "typescript": "^5.4.0"
31
+ "openclaw": "2026.4.29",
32
+ "typescript": "^5.4.0",
33
+ "vitest": "^4.1.5"
25
34
  },
26
35
  "peerDependencies": {
27
- "openclaw": ">=2026.3.23"
36
+ "openclaw": "^2026.4.29"
28
37
  },
29
38
  "peerDependenciesMeta": {
30
39
  "openclaw": {
@@ -38,19 +47,22 @@
38
47
  "extensions": [
39
48
  "./index.ts"
40
49
  ],
50
+ "runtimeExtensions": [
51
+ "./dist/index.js"
52
+ ],
41
53
  "channel": {
42
54
  "id": "openclaw-clawchat",
43
- "label": "openclaw-clawchat",
44
- "selectionLabel": "openclaw-clawchat",
55
+ "label": "Clawling Chat",
56
+ "selectionLabel": "Clawling Chat",
45
57
  "docsPath": "/channels/openclaw-clawchat",
46
58
  "docsLabel": "openclaw-clawchat",
47
- "blurb": "OpenClaw ClawChat channel plugin",
48
- "order": 70
59
+ "blurb": "Clawling Protocol v2 over WebSocket (chat-sdk).",
60
+ "order": 110
49
61
  },
50
62
  "install": {
51
63
  "npmSpec": "@newbase-clawchat/openclaw-clawchat",
52
64
  "defaultChoice": "npm",
53
- "minHostVersion": ">=2026.3.23"
65
+ "minHostVersion": ">=2026.4.29"
54
66
  }
55
67
  }
56
68
  }
@@ -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,47 @@
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 should use the `clawchat_activate` tool when that tool is available. The tool runs the same invite-code login runtime as OpenClaw channel login and persists credentials into `channels.openclaw-clawchat`.
10
+
11
+ If the tool is not available because the Gateway has not loaded this plugin yet, fall back to OpenClaw channel login. Do not edit config files manually and do not try to execute slash commands from this skill.
12
+
13
+ ## Workflow
14
+
15
+ 1. Check whether the user is trying to activate or log in ClawChat.
16
+ 2. If `clawchat_activate` is available and the invite code is known, call it with the invite code.
17
+ 3. If the invite code is not known, ask the user for it before calling `clawchat_activate`.
18
+ 4. If `clawchat_activate` is not available, execute the channel login command:
19
+
20
+ ```bash
21
+ openclaw channels login --channel openclaw-clawchat
22
+ ```
23
+ 5. Do not append the invite code to the command; `openclaw channels login` has no code argument.
24
+ 6. 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.
25
+ 7. After activation succeeds, rely on OpenClaw's `channels.*` config hot reload when the plugin is already loaded. Verify channel status if available:
26
+
27
+ ```bash
28
+ openclaw channels status --probe
29
+ ```
30
+ 8. Execute the Gateway restart command only when the plugin was just installed/updated, the tool was unavailable because the running Gateway had not loaded the plugin, config reload is disabled, or the probe does not become healthy:
31
+
32
+ ```bash
33
+ openclaw gateway restart
34
+ ```
35
+ 9. Tell the user activation completes after login succeeds and either config hot reload/probe succeeds or Gateway restart succeeds.
36
+
37
+ ## Trigger Examples
38
+
39
+ - `activate ClawChat with invite code A1B2C3`
40
+ - `login to ClawChat with invite code A1B2C3`
41
+ - `connect ClawChat using invite code A1B2C3`
42
+ - `绑定 ClawChat,邀请码 A1B2C3`
43
+ - `激活 ClawChat`
44
+
45
+ Do not ask the user to enter a bare ClawChat command. If activation is requested and `clawchat_activate` is available, call that tool yourself. If the tool is unavailable, execute `openclaw channels login --channel openclaw-clawchat` yourself, then probe channel status and restart the Gateway only when needed.
46
+
47
+ When the user asks to activate ClawChat without including a code, ask for the invite code before calling `clawchat_activate`; if falling back to channel login, provide the 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,8 @@ 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
+ expect((client.typing as ReturnType<typeof vi.fn>).mock.calls).toEqual([["u1", true]]);
56
57
  expect(sent.map((s) => s.event)).toEqual(["message.created"]);
57
58
  });
58
59
 
@@ -101,6 +102,7 @@ describe("openBufferedStreamingSession", () => {
101
102
  [{ kind: "text", text: "Hello ", delta: "Hello " }],
102
103
  [{ kind: "text", text: "Hello world", delta: "world" }],
103
104
  ]);
105
+ expect(adds.map((a) => a.payload.sequence)).toEqual([0, 1]);
104
106
  expect(session.currentText).toBe("Hello world");
105
107
  });
106
108
 
@@ -122,8 +124,12 @@ describe("openBufferedStreamingSession", () => {
122
124
  const events = sent.map((s) => s.event);
123
125
  expect(events).toEqual(["message.created", "message.add", "message.done"]);
124
126
  expect(typing).toEqual([
125
- [{ id: "u1", type: "direct" }, true],
126
- [{ id: "u1", type: "direct" }, false],
127
+ ["u1", true],
128
+ ["u1", false],
129
+ ]);
130
+ expect((client.typing as ReturnType<typeof vi.fn>).mock.calls).toEqual([
131
+ ["u1", true],
132
+ ["u1", false],
127
133
  ]);
128
134
  });
129
135
 
@@ -158,7 +164,11 @@ describe("openBufferedStreamingSession", () => {
158
164
  await session.fail("boom");
159
165
  const failed = sent.find((s) => s.event === "message.failed")!;
160
166
  expect(failed.payload.reason).toBe("boom");
161
- expect(typing.at(-1)).toEqual([{ id: "u1", type: "direct" }, false]);
167
+ expect(failed.payload.sequence).toBe(0);
168
+ expect(failed.payload).toHaveProperty("completed_at");
169
+ expect(failed.payload).not.toHaveProperty("failed_at");
170
+ expect(typing.at(-1)).toEqual(["u1", false]);
171
+ expect((client.typing as ReturnType<typeof vi.fn>).mock.calls.at(-1)).toEqual(["u1", false]);
162
172
  });
163
173
 
164
174
  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,17 +90,18 @@ 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);
89
97
  emitStreamCreated(options.client, {
90
98
  messageId: options.messageId,
91
- routing: options.routing,
99
+ routing,
92
100
  });
93
101
 
94
102
  let bufferedSnapshot = "";
95
103
  let flushedSnapshot = "";
96
- let sequence = 0;
104
+ let sequence = -1;
97
105
  let flushTimer: ReturnType<typeof setTimeout> | null = null;
98
106
  let pendingFlush: Promise<void> = Promise.resolve();
99
107
  let closed = false;
@@ -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,
172
- finalSequence: sequence,
179
+ routing,
180
+ finalSequence: Math.max(sequence, 0),
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);
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,
186
- sequence: sequence + 1,
193
+ routing,
194
+ sequence: Math.max(sequence, 0),
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);
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
+ body: { fragments: [{ kind: "text", text: "hello" }] },
66
+ }),
67
+ );
68
+ expect(client.sendMessage.mock.calls[0][0]).not.toHaveProperty("chat_type");
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
+ body: {
125
+ fragments: [
126
+ { kind: "text", text: "caption" },
127
+ { kind: "image", url: "https://cdn/uploaded.png", mime: "image/png" },
128
+ ],
129
+ },
130
+ }),
131
+ );
132
+ expect(client.sendMessage.mock.calls[0][0]).not.toHaveProperty("chat_type");
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
+ body: {
203
+ fragments: [
204
+ { kind: "text", text: "caption" },
205
+ { kind: "image", url: "https://cdn/uploaded.png", mime: "image/png" },
206
+ ],
207
+ },
208
+ }),
209
+ );
210
+ expect(client.sendMessage.mock.calls[0][0]).not.toHaveProperty("chat_type");
197
211
  expect(result).toEqual({
198
212
  channel: "openclaw-clawchat",
199
213
  to: "cc:group:room-1",