@newbase-clawchat/openclaw-clawchat 2026.4.29 → 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.
- package/README.md +33 -10
- package/dist/index.js +27 -0
- package/dist/src/api-client.js +156 -0
- package/dist/src/api-types.js +17 -0
- package/dist/src/buffered-stream.js +177 -0
- package/dist/src/channel.js +191 -0
- package/dist/src/client.js +176 -0
- package/dist/src/commands.js +35 -0
- package/dist/src/config.js +214 -0
- package/dist/src/inbound.js +133 -0
- package/dist/src/login.runtime.js +130 -0
- package/dist/src/media-runtime.js +85 -0
- package/dist/src/message-mapper.js +82 -0
- package/dist/src/outbound.js +181 -0
- package/dist/src/protocol.js +38 -0
- package/dist/src/reply-dispatcher.js +440 -0
- package/dist/src/runtime.js +288 -0
- package/dist/src/streaming.js +65 -0
- package/dist/src/tools-schema.js +38 -0
- package/dist/src/tools.js +287 -0
- package/openclaw.plugin.json +12 -0
- package/package.json +25 -5
- package/skills/clawchat-activate/SKILL.md +17 -8
- package/src/buffered-stream.test.ts +10 -0
- package/src/buffered-stream.ts +6 -6
- package/src/channel.outbound.test.ts +3 -3
- package/src/channel.ts +11 -3
- package/src/client.test.ts +8 -1
- package/src/client.ts +11 -10
- package/src/commands.test.ts +6 -0
- package/src/commands.ts +5 -1
- package/src/config.test.ts +3 -0
- package/src/config.ts +7 -0
- package/src/inbound.test.ts +4 -1
- package/src/inbound.ts +11 -10
- package/src/login.runtime.test.ts +36 -0
- package/src/login.runtime.ts +54 -26
- package/src/manifest.test.ts +98 -22
- package/src/outbound.test.ts +6 -5
- package/src/outbound.ts +8 -7
- package/src/reply-dispatcher.test.ts +418 -3
- package/src/reply-dispatcher.ts +137 -12
- package/src/runtime.ts +1 -0
- package/src/streaming.test.ts +12 -9
- package/src/streaming.ts +6 -6
- package/src/tools.test.ts +81 -18
- package/src/tools.ts +63 -72
package/openclaw.plugin.json
CHANGED
|
@@ -3,12 +3,24 @@
|
|
|
3
3
|
"channels": ["openclaw-clawchat"],
|
|
4
4
|
"skills": ["./skills"],
|
|
5
5
|
"activation": {
|
|
6
|
+
"onStartup": true,
|
|
6
7
|
"onChannels": ["openclaw-clawchat"],
|
|
7
8
|
"onCommands": ["clawchat-login"]
|
|
8
9
|
},
|
|
9
10
|
"commandAliases": [
|
|
10
11
|
{ "name": "clawchat-login", "kind": "runtime-slash" }
|
|
11
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
|
+
},
|
|
12
24
|
"configSchema": {
|
|
13
25
|
"type": "object",
|
|
14
26
|
"additionalProperties": false,
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@newbase-clawchat/openclaw-clawchat",
|
|
3
|
-
"version": "2026.4.
|
|
3
|
+
"version": "2026.4.30",
|
|
4
4
|
"description": "OpenClaw ClawChat channel plugin",
|
|
5
5
|
"files": [
|
|
6
|
+
"dist",
|
|
6
7
|
"index.ts",
|
|
7
8
|
"src",
|
|
8
9
|
"skills",
|
|
@@ -11,6 +12,12 @@
|
|
|
11
12
|
],
|
|
12
13
|
"type": "module",
|
|
13
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",
|
|
14
21
|
"typecheck": "tsc --noEmit",
|
|
15
22
|
"prepublishOnly": "npm run typecheck",
|
|
16
23
|
"release": "npm run prepublishOnly && npm publish"
|
|
@@ -21,11 +28,12 @@
|
|
|
21
28
|
},
|
|
22
29
|
"devDependencies": {
|
|
23
30
|
"@types/node": "^25.5.0",
|
|
24
|
-
"openclaw": "
|
|
25
|
-
"typescript": "^5.4.0"
|
|
31
|
+
"openclaw": "2026.4.29",
|
|
32
|
+
"typescript": "^5.4.0",
|
|
33
|
+
"vitest": "^4.1.5"
|
|
26
34
|
},
|
|
27
35
|
"peerDependencies": {
|
|
28
|
-
"openclaw": "^2026.
|
|
36
|
+
"openclaw": "^2026.4.29"
|
|
29
37
|
},
|
|
30
38
|
"peerDependenciesMeta": {
|
|
31
39
|
"openclaw": {
|
|
@@ -39,10 +47,22 @@
|
|
|
39
47
|
"extensions": [
|
|
40
48
|
"./index.ts"
|
|
41
49
|
],
|
|
50
|
+
"runtimeExtensions": [
|
|
51
|
+
"./dist/index.js"
|
|
52
|
+
],
|
|
53
|
+
"channel": {
|
|
54
|
+
"id": "openclaw-clawchat",
|
|
55
|
+
"label": "Clawling Chat",
|
|
56
|
+
"selectionLabel": "Clawling Chat",
|
|
57
|
+
"docsPath": "/channels/openclaw-clawchat",
|
|
58
|
+
"docsLabel": "openclaw-clawchat",
|
|
59
|
+
"blurb": "Clawling Protocol v2 over WebSocket (chat-sdk).",
|
|
60
|
+
"order": 110
|
|
61
|
+
},
|
|
42
62
|
"install": {
|
|
43
63
|
"npmSpec": "@newbase-clawchat/openclaw-clawchat",
|
|
44
64
|
"defaultChoice": "npm",
|
|
45
|
-
"minHostVersion": ">=2026.
|
|
65
|
+
"minHostVersion": ">=2026.4.29"
|
|
46
66
|
}
|
|
47
67
|
}
|
|
48
68
|
}
|
|
@@ -6,24 +6,33 @@ description: |
|
|
|
6
6
|
|
|
7
7
|
# ClawChat Activation
|
|
8
8
|
|
|
9
|
-
Activation
|
|
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.
|
|
10
12
|
|
|
11
13
|
## Workflow
|
|
12
14
|
|
|
13
15
|
1. Check whether the user is trying to activate or log in ClawChat.
|
|
14
|
-
2.
|
|
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:
|
|
15
19
|
|
|
16
20
|
```bash
|
|
17
21
|
openclaw channels login --channel openclaw-clawchat
|
|
18
22
|
```
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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:
|
|
22
31
|
|
|
23
32
|
```bash
|
|
24
33
|
openclaw gateway restart
|
|
25
34
|
```
|
|
26
|
-
|
|
35
|
+
9. Tell the user activation completes after login succeeds and either config hot reload/probe succeeds or Gateway restart succeeds.
|
|
27
36
|
|
|
28
37
|
## Trigger Examples
|
|
29
38
|
|
|
@@ -33,6 +42,6 @@ openclaw gateway restart
|
|
|
33
42
|
- `绑定 ClawChat,邀请码 A1B2C3`
|
|
34
43
|
- `激活 ClawChat`
|
|
35
44
|
|
|
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
|
|
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.
|
|
37
46
|
|
|
38
|
-
When the user asks to activate ClawChat without including a code,
|
|
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.
|
|
@@ -53,6 +53,7 @@ describe("openBufferedStreamingSession", () => {
|
|
|
53
53
|
maxBufferChars: 1000,
|
|
54
54
|
});
|
|
55
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
|
|
|
@@ -125,6 +127,10 @@ describe("openBufferedStreamingSession", () => {
|
|
|
125
127
|
["u1", true],
|
|
126
128
|
["u1", false],
|
|
127
129
|
]);
|
|
130
|
+
expect((client.typing as ReturnType<typeof vi.fn>).mock.calls).toEqual([
|
|
131
|
+
["u1", true],
|
|
132
|
+
["u1", false],
|
|
133
|
+
]);
|
|
128
134
|
});
|
|
129
135
|
|
|
130
136
|
it("done() is idempotent", async () => {
|
|
@@ -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");
|
|
167
|
+
expect(failed.payload.sequence).toBe(0);
|
|
168
|
+
expect(failed.payload).toHaveProperty("completed_at");
|
|
169
|
+
expect(failed.payload).not.toHaveProperty("failed_at");
|
|
161
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 () => {
|
package/src/buffered-stream.ts
CHANGED
|
@@ -93,7 +93,7 @@ export function openBufferedStreamingSession(
|
|
|
93
93
|
const routing = resolveRouting(options);
|
|
94
94
|
const emitTyping = options.emitTyping !== false;
|
|
95
95
|
if (emitTyping)
|
|
96
|
-
options.client.typing(routing.chatId, true
|
|
96
|
+
options.client.typing(routing.chatId, true);
|
|
97
97
|
emitStreamCreated(options.client, {
|
|
98
98
|
messageId: options.messageId,
|
|
99
99
|
routing,
|
|
@@ -101,7 +101,7 @@ export function openBufferedStreamingSession(
|
|
|
101
101
|
|
|
102
102
|
let bufferedSnapshot = "";
|
|
103
103
|
let flushedSnapshot = "";
|
|
104
|
-
let sequence =
|
|
104
|
+
let sequence = -1;
|
|
105
105
|
let flushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
106
106
|
let pendingFlush: Promise<void> = Promise.resolve();
|
|
107
107
|
let closed = false;
|
|
@@ -177,11 +177,11 @@ export function openBufferedStreamingSession(
|
|
|
177
177
|
emitStreamDone(options.client, {
|
|
178
178
|
messageId: options.messageId,
|
|
179
179
|
routing,
|
|
180
|
-
finalSequence: sequence,
|
|
180
|
+
finalSequence: Math.max(sequence, 0),
|
|
181
181
|
finalText: bufferedSnapshot,
|
|
182
182
|
});
|
|
183
183
|
if (emitTyping)
|
|
184
|
-
options.client.typing(routing.chatId, false
|
|
184
|
+
options.client.typing(routing.chatId, false);
|
|
185
185
|
};
|
|
186
186
|
|
|
187
187
|
const fail = async (reason?: string): Promise<void> => {
|
|
@@ -191,11 +191,11 @@ export function openBufferedStreamingSession(
|
|
|
191
191
|
emitStreamFailed(options.client, {
|
|
192
192
|
messageId: options.messageId,
|
|
193
193
|
routing,
|
|
194
|
-
sequence: sequence
|
|
194
|
+
sequence: Math.max(sequence, 0),
|
|
195
195
|
...(reason !== undefined ? { reason } : {}),
|
|
196
196
|
});
|
|
197
197
|
if (emitTyping)
|
|
198
|
-
options.client.typing(routing.chatId, false
|
|
198
|
+
options.client.typing(routing.chatId, false);
|
|
199
199
|
};
|
|
200
200
|
|
|
201
201
|
return {
|
|
@@ -62,10 +62,10 @@ describe("openclaw-clawchat channel outbound", () => {
|
|
|
62
62
|
expect(client.sendMessage).toHaveBeenCalledWith(
|
|
63
63
|
expect.objectContaining({
|
|
64
64
|
chat_id: "user-1",
|
|
65
|
-
chat_type: "direct",
|
|
66
65
|
body: { fragments: [{ kind: "text", text: "hello" }] },
|
|
67
66
|
}),
|
|
68
67
|
);
|
|
68
|
+
expect(client.sendMessage.mock.calls[0][0]).not.toHaveProperty("chat_type");
|
|
69
69
|
expect(result).toEqual({
|
|
70
70
|
channel: "openclaw-clawchat",
|
|
71
71
|
to: "cc:user-1",
|
|
@@ -121,7 +121,6 @@ describe("openclaw-clawchat channel outbound", () => {
|
|
|
121
121
|
expect(client.sendMessage).toHaveBeenCalledWith(
|
|
122
122
|
expect.objectContaining({
|
|
123
123
|
chat_id: "room-1",
|
|
124
|
-
chat_type: "group",
|
|
125
124
|
body: {
|
|
126
125
|
fragments: [
|
|
127
126
|
{ kind: "text", text: "caption" },
|
|
@@ -130,6 +129,7 @@ describe("openclaw-clawchat channel outbound", () => {
|
|
|
130
129
|
},
|
|
131
130
|
}),
|
|
132
131
|
);
|
|
132
|
+
expect(client.sendMessage.mock.calls[0][0]).not.toHaveProperty("chat_type");
|
|
133
133
|
expect(result).toEqual({
|
|
134
134
|
channel: "openclaw-clawchat",
|
|
135
135
|
to: "cc:group:room-1",
|
|
@@ -199,7 +199,6 @@ describe("openclaw-clawchat channel outbound", () => {
|
|
|
199
199
|
expect(client.sendMessage).toHaveBeenCalledWith(
|
|
200
200
|
expect.objectContaining({
|
|
201
201
|
chat_id: "room-1",
|
|
202
|
-
chat_type: "group",
|
|
203
202
|
body: {
|
|
204
203
|
fragments: [
|
|
205
204
|
{ kind: "text", text: "caption" },
|
|
@@ -208,6 +207,7 @@ describe("openclaw-clawchat channel outbound", () => {
|
|
|
208
207
|
},
|
|
209
208
|
}),
|
|
210
209
|
);
|
|
210
|
+
expect(client.sendMessage.mock.calls[0][0]).not.toHaveProperty("chat_type");
|
|
211
211
|
expect(result).toEqual({
|
|
212
212
|
channel: "openclaw-clawchat",
|
|
213
213
|
to: "cc:group:room-1",
|
package/src/channel.ts
CHANGED
|
@@ -19,8 +19,9 @@ import {
|
|
|
19
19
|
resolveOpenclawClawlingAccount,
|
|
20
20
|
type ResolvedOpenclawClawlingAccount,
|
|
21
21
|
} from "./config.ts";
|
|
22
|
+
import type { OpenclawClawchatMutateConfigFile } from "./login.runtime.ts";
|
|
22
23
|
import { openclawClawlingOutbound } from "./outbound.ts";
|
|
23
|
-
import { startOpenclawClawlingGateway } from "./runtime.ts";
|
|
24
|
+
import { getOpenclawClawlingRuntime, startOpenclawClawlingGateway } from "./runtime.ts";
|
|
24
25
|
|
|
25
26
|
const configAdapter = createTopLevelChannelConfigAdapter<ResolvedOpenclawClawlingAccount>({
|
|
26
27
|
sectionKey: CHANNEL_ID,
|
|
@@ -36,6 +37,7 @@ const configAdapter = createTopLevelChannelConfigAdapter<ResolvedOpenclawClawlin
|
|
|
36
37
|
"replyMode",
|
|
37
38
|
"forwardThinking",
|
|
38
39
|
"forwardToolCalls",
|
|
40
|
+
"richInteractions",
|
|
39
41
|
"enabled",
|
|
40
42
|
],
|
|
41
43
|
resolveAllowFrom: (account) => account.allowFrom,
|
|
@@ -53,8 +55,8 @@ const configAdapter = createTopLevelChannelConfigAdapter<ResolvedOpenclawClawlin
|
|
|
53
55
|
* `afterAccountConfigWritten`.
|
|
54
56
|
*
|
|
55
57
|
* `applyAccountConfig` itself only marks the section `enabled: true`;
|
|
56
|
-
* credentials are written by `runOpenclawClawlingLogin`
|
|
57
|
-
*
|
|
58
|
+
* credentials are written by `runOpenclawClawlingLogin` via the runtime config
|
|
59
|
+
* mutator after the `/v1/agents/connect` response lands.
|
|
58
60
|
*/
|
|
59
61
|
const setupAdapter = {
|
|
60
62
|
resolveAccountId: () => DEFAULT_ACCOUNT_ID,
|
|
@@ -105,6 +107,9 @@ const setupAdapter = {
|
|
|
105
107
|
accountId: null,
|
|
106
108
|
runtime: { log: (message: string) => runtime.log(message) },
|
|
107
109
|
readInviteCode: async () => code,
|
|
110
|
+
mutateConfigFile: (getOpenclawClawlingRuntime().config as unknown as {
|
|
111
|
+
mutateConfigFile: OpenclawClawchatMutateConfigFile;
|
|
112
|
+
}).mutateConfigFile,
|
|
108
113
|
});
|
|
109
114
|
},
|
|
110
115
|
};
|
|
@@ -177,6 +182,9 @@ export const openclawClawlingPlugin: ChannelPlugin<ResolvedOpenclawClawlingAccou
|
|
|
177
182
|
cfg,
|
|
178
183
|
accountId: accountId ?? null,
|
|
179
184
|
runtime: { log: (message: string) => runtime.log(message) },
|
|
185
|
+
mutateConfigFile: (getOpenclawClawlingRuntime().config as unknown as {
|
|
186
|
+
mutateConfigFile: OpenclawClawchatMutateConfigFile;
|
|
187
|
+
}).mutateConfigFile,
|
|
180
188
|
});
|
|
181
189
|
},
|
|
182
190
|
},
|
package/src/client.test.ts
CHANGED
|
@@ -92,7 +92,8 @@ describe("openclaw-clawchat client", () => {
|
|
|
92
92
|
const env = JSON.parse(transport.sent[0]!);
|
|
93
93
|
expect(env.event).toBe("message.created");
|
|
94
94
|
expect(env.chat_id).toBe("user-1");
|
|
95
|
-
expect(env
|
|
95
|
+
expect(env).not.toHaveProperty("chat_type");
|
|
96
|
+
expect(env).not.toHaveProperty("sender");
|
|
96
97
|
// Payload is intentionally minimal: just message_id, no message body /
|
|
97
98
|
// context / sender / streaming metadata.
|
|
98
99
|
expect(env.payload).toEqual({ message_id: "msg-1" });
|
|
@@ -118,6 +119,9 @@ describe("openclaw-clawchat client", () => {
|
|
|
118
119
|
const env = JSON.parse(transport.sent[0]!);
|
|
119
120
|
expect(env.event).toBe("message.add");
|
|
120
121
|
expect(env.payload.sequence).toBe(3);
|
|
122
|
+
expect(env.chat_id).toBe("user-1");
|
|
123
|
+
expect(env).not.toHaveProperty("chat_type");
|
|
124
|
+
expect(env).not.toHaveProperty("sender");
|
|
121
125
|
expect(env.payload.fragments).toEqual([
|
|
122
126
|
{ kind: "text", text: "Hello, wor", delta: "wor" },
|
|
123
127
|
]);
|
|
@@ -169,7 +173,10 @@ describe("openclaw-clawchat client", () => {
|
|
|
169
173
|
expect(env.event).toBe("message.failed");
|
|
170
174
|
expect(env.payload.message_id).toBe("msg-1");
|
|
171
175
|
expect(env.payload.reason).toBe("upstream_error");
|
|
176
|
+
expect(env.payload.fragments).toEqual([{ kind: "text", text: "upstream_error" }]);
|
|
172
177
|
expect(env.payload.streaming.status).toBe("failed");
|
|
178
|
+
expect(env.payload.streaming.completed_at).toBe(env.payload.completed_at);
|
|
179
|
+
expect(env.payload).not.toHaveProperty("failed_at");
|
|
173
180
|
client.close();
|
|
174
181
|
});
|
|
175
182
|
});
|
package/src/client.ts
CHANGED
|
@@ -76,11 +76,8 @@ function normalizeRouting(params: {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
/**
|
|
79
|
-
* Emit a raw v2 envelope directly over the transport so we can carry
|
|
80
|
-
* `chat_id`
|
|
81
|
-
* `emitRaw` can't express `chat_type` and always writes `to`; we bypass it
|
|
82
|
-
* entirely for the events we construct ourselves (streaming lifecycle +
|
|
83
|
-
* message.reply finalize).
|
|
79
|
+
* Emit a raw v2 envelope directly over the transport so we can carry top-level
|
|
80
|
+
* `chat_id` routing without SDK-injected `to` metadata.
|
|
84
81
|
*/
|
|
85
82
|
function emitEnvelope(
|
|
86
83
|
client: ClawlingChatClient,
|
|
@@ -105,7 +102,6 @@ function emitEnvelope(
|
|
|
105
102
|
trace_id: inner.opts.traceIdFactory(),
|
|
106
103
|
emitted_at: Date.now(),
|
|
107
104
|
chat_id: routing.chatId,
|
|
108
|
-
chat_type: routing.chatType,
|
|
109
105
|
payload,
|
|
110
106
|
};
|
|
111
107
|
inner.opts.transport.send(JSON.stringify(env));
|
|
@@ -238,7 +234,7 @@ export function emitFinalStreamReply(
|
|
|
238
234
|
/** The user message this stream is a reply to (usually the inbound turn). */
|
|
239
235
|
replyTo: {
|
|
240
236
|
msgId: string;
|
|
241
|
-
|
|
237
|
+
previewId: string;
|
|
242
238
|
nickName: string;
|
|
243
239
|
fragments: Fragment[];
|
|
244
240
|
};
|
|
@@ -260,7 +256,7 @@ export function emitFinalStreamReply(
|
|
|
260
256
|
reply: {
|
|
261
257
|
reply_to_msg_id: params.replyTo.msgId,
|
|
262
258
|
reply_preview: {
|
|
263
|
-
id: params.replyTo.
|
|
259
|
+
id: params.replyTo.previewId,
|
|
264
260
|
nick_name: params.replyTo.nickName,
|
|
265
261
|
fragments: params.replyTo.fragments,
|
|
266
262
|
},
|
|
@@ -284,13 +280,18 @@ export function emitStreamFailed(
|
|
|
284
280
|
): void {
|
|
285
281
|
const now = Date.now();
|
|
286
282
|
const routing = normalizeRouting(params);
|
|
283
|
+
const reason = params.reason ?? "unknown";
|
|
284
|
+
const reasonFragment = params.reason?.trim()
|
|
285
|
+
? { fragments: [{ kind: "text", text: params.reason.trim() }] }
|
|
286
|
+
: {};
|
|
287
287
|
emitEnvelope(
|
|
288
288
|
client,
|
|
289
289
|
"message.failed",
|
|
290
290
|
{
|
|
291
291
|
message_id: params.messageId,
|
|
292
292
|
sequence: params.sequence,
|
|
293
|
-
reason
|
|
293
|
+
reason,
|
|
294
|
+
...reasonFragment,
|
|
294
295
|
streaming: {
|
|
295
296
|
status: "failed",
|
|
296
297
|
sequence: params.sequence,
|
|
@@ -298,7 +299,7 @@ export function emitStreamFailed(
|
|
|
298
299
|
started_at: null,
|
|
299
300
|
completed_at: now,
|
|
300
301
|
},
|
|
301
|
-
|
|
302
|
+
completed_at: now,
|
|
302
303
|
},
|
|
303
304
|
routing,
|
|
304
305
|
);
|
package/src/commands.test.ts
CHANGED
|
@@ -13,6 +13,11 @@ describe("registerOpenclawClawlingCommands", () => {
|
|
|
13
13
|
const commands: Array<{ name: string; acceptsArgs?: boolean; handler: (ctx: unknown) => Promise<{ text: string }> }> = [];
|
|
14
14
|
const api = {
|
|
15
15
|
registerCommand: (command: (typeof commands)[number]) => commands.push(command),
|
|
16
|
+
runtime: {
|
|
17
|
+
config: {
|
|
18
|
+
mutateConfigFile: vi.fn(),
|
|
19
|
+
},
|
|
20
|
+
},
|
|
16
21
|
} as never;
|
|
17
22
|
|
|
18
23
|
registerOpenclawClawlingCommands(api);
|
|
@@ -27,6 +32,7 @@ describe("registerOpenclawClawlingCommands", () => {
|
|
|
27
32
|
|
|
28
33
|
expect(loginRuntime.runOpenclawClawlingLogin).toHaveBeenCalledTimes(1);
|
|
29
34
|
const params = loginRuntime.runOpenclawClawlingLogin.mock.calls[0]?.[0];
|
|
35
|
+
expect(params.mutateConfigFile).toBe(api.runtime.config.mutateConfigFile);
|
|
30
36
|
await expect(params.readInviteCode()).resolves.toBe("A1B2C3");
|
|
31
37
|
expect(result.text).toMatch(/activated successfully/i);
|
|
32
38
|
});
|
package/src/commands.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import type { OpenclawClawchatMutateConfigFile } from "./login.runtime.ts";
|
|
2
3
|
|
|
3
4
|
function extractInviteCode(value: unknown): string {
|
|
4
5
|
const raw = typeof value === "string" ? value.trim() : "";
|
|
@@ -9,7 +10,7 @@ function errorMessage(err: unknown): string {
|
|
|
9
10
|
return err instanceof Error ? err.message : String(err);
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
export function registerOpenclawClawlingCommands(api: Pick<OpenClawPluginApi, "registerCommand" | "logger">): void {
|
|
13
|
+
export function registerOpenclawClawlingCommands(api: Pick<OpenClawPluginApi, "registerCommand" | "logger" | "runtime">): void {
|
|
13
14
|
api.registerCommand({
|
|
14
15
|
name: "clawchat-login",
|
|
15
16
|
description: "Activate ClawChat with an invite code, e.g. /clawchat-login A1B2C3.",
|
|
@@ -27,6 +28,9 @@ export function registerOpenclawClawlingCommands(api: Pick<OpenClawPluginApi, "r
|
|
|
27
28
|
accountId: ctx.accountId ?? null,
|
|
28
29
|
runtime: { log: (message: string) => api.logger?.info?.(message) },
|
|
29
30
|
readInviteCode: async () => code,
|
|
31
|
+
mutateConfigFile: (api.runtime.config as unknown as {
|
|
32
|
+
mutateConfigFile: OpenclawClawchatMutateConfigFile;
|
|
33
|
+
}).mutateConfigFile,
|
|
30
34
|
});
|
|
31
35
|
return { text: "✅ ClawChat activated successfully." };
|
|
32
36
|
} catch (err) {
|
package/src/config.test.ts
CHANGED
|
@@ -23,6 +23,7 @@ describe("openclaw-clawchat config", () => {
|
|
|
23
23
|
expect(account.replyMode).toBe("static");
|
|
24
24
|
expect(account.forwardThinking).toBe(true);
|
|
25
25
|
expect(account.forwardToolCalls).toBe(false);
|
|
26
|
+
expect(account.richInteractions).toBe(false);
|
|
26
27
|
expect(account.stream).toEqual(DEFAULT_STREAM);
|
|
27
28
|
});
|
|
28
29
|
|
|
@@ -36,6 +37,7 @@ describe("openclaw-clawchat config", () => {
|
|
|
36
37
|
replyMode: "stream",
|
|
37
38
|
forwardThinking: false,
|
|
38
39
|
forwardToolCalls: true,
|
|
40
|
+
richInteractions: true,
|
|
39
41
|
stream: { flushIntervalMs: 500, minChunkChars: 50, maxBufferChars: 3000 },
|
|
40
42
|
},
|
|
41
43
|
},
|
|
@@ -48,6 +50,7 @@ describe("openclaw-clawchat config", () => {
|
|
|
48
50
|
expect(account.replyMode).toBe("stream");
|
|
49
51
|
expect(account.forwardThinking).toBe(false);
|
|
50
52
|
expect(account.forwardToolCalls).toBe(true);
|
|
53
|
+
expect(account.richInteractions).toBe(true);
|
|
51
54
|
expect(account.stream.flushIntervalMs).toBe(500);
|
|
52
55
|
expect(account.stream.minChunkChars).toBe(50);
|
|
53
56
|
expect(account.stream.maxBufferChars).toBe(3000);
|
package/src/config.ts
CHANGED
|
@@ -91,6 +91,8 @@ export type OpenclawClawlingConfig = {
|
|
|
91
91
|
groupMode?: GroupMode;
|
|
92
92
|
forwardThinking?: boolean;
|
|
93
93
|
forwardToolCalls?: boolean;
|
|
94
|
+
/** Emit approval/action rich fragments instead of plain fallback text. */
|
|
95
|
+
richInteractions?: boolean;
|
|
94
96
|
stream?: OpenclawClawlingStreamConfig;
|
|
95
97
|
reconnect?: OpenclawClawlingReconnectConfig;
|
|
96
98
|
heartbeat?: OpenclawClawlingHeartbeatConfig;
|
|
@@ -111,6 +113,7 @@ export const openclawClawlingConfigSchema = {
|
|
|
111
113
|
groupMode: { type: "string", enum: ["mention", "all"] },
|
|
112
114
|
forwardThinking: { type: "boolean" },
|
|
113
115
|
forwardToolCalls: { type: "boolean" },
|
|
116
|
+
richInteractions: { type: "boolean" },
|
|
114
117
|
stream: {
|
|
115
118
|
type: "object",
|
|
116
119
|
additionalProperties: false,
|
|
@@ -213,6 +216,7 @@ export type ResolvedOpenclawClawlingAccount = {
|
|
|
213
216
|
groupMode: GroupMode;
|
|
214
217
|
forwardThinking: boolean;
|
|
215
218
|
forwardToolCalls: boolean;
|
|
219
|
+
richInteractions: boolean;
|
|
216
220
|
allowFrom: string[];
|
|
217
221
|
stream: Required<OpenclawClawlingStreamConfig>;
|
|
218
222
|
reconnect: Required<OpenclawClawlingReconnectConfig>;
|
|
@@ -299,6 +303,8 @@ export function resolveOpenclawClawlingAccount(
|
|
|
299
303
|
typeof channel.forwardThinking === "boolean" ? channel.forwardThinking : true;
|
|
300
304
|
const forwardToolCalls =
|
|
301
305
|
typeof channel.forwardToolCalls === "boolean" ? channel.forwardToolCalls : false;
|
|
306
|
+
const richInteractions =
|
|
307
|
+
typeof channel.richInteractions === "boolean" ? channel.richInteractions : false;
|
|
302
308
|
|
|
303
309
|
return {
|
|
304
310
|
accountId: DEFAULT_ACCOUNT_ID,
|
|
@@ -313,6 +319,7 @@ export function resolveOpenclawClawlingAccount(
|
|
|
313
319
|
groupMode,
|
|
314
320
|
forwardThinking,
|
|
315
321
|
forwardToolCalls,
|
|
322
|
+
richInteractions,
|
|
316
323
|
allowFrom: [],
|
|
317
324
|
stream: readStream(channel.stream),
|
|
318
325
|
reconnect: readReconnect(channel.reconnect),
|
package/src/inbound.test.ts
CHANGED
|
@@ -41,6 +41,7 @@ function buildSendEnvelope(
|
|
|
41
41
|
mentions: string[];
|
|
42
42
|
reply: unknown;
|
|
43
43
|
messageId: string;
|
|
44
|
+
chatId: string;
|
|
44
45
|
}> = {},
|
|
45
46
|
): Envelope<DownlinkMessageSendPayload> {
|
|
46
47
|
return {
|
|
@@ -48,6 +49,7 @@ function buildSendEnvelope(
|
|
|
48
49
|
event: overrides.event ?? "message.send",
|
|
49
50
|
trace_id: "trace-1",
|
|
50
51
|
emitted_at: 1776162600000,
|
|
52
|
+
chat_id: overrides.chatId,
|
|
51
53
|
to: { id: "agent-1", type: overrides.senderType ?? "direct" },
|
|
52
54
|
sender: {
|
|
53
55
|
sender_id: "user-1",
|
|
@@ -174,7 +176,7 @@ describe("openclaw-clawchat inbound", () => {
|
|
|
174
176
|
},
|
|
175
177
|
};
|
|
176
178
|
await dispatchOpenclawClawlingInbound({
|
|
177
|
-
envelope: buildSendEnvelope({ event: "message.reply", reply: replyRef }),
|
|
179
|
+
envelope: buildSendEnvelope({ event: "message.reply", reply: replyRef, chatId: "chat-1" }),
|
|
178
180
|
cfg: {},
|
|
179
181
|
runtime: {} as never,
|
|
180
182
|
account: baseAccount(),
|
|
@@ -183,6 +185,7 @@ describe("openclaw-clawchat inbound", () => {
|
|
|
183
185
|
const { replyCtx } = ingest.mock.calls[0]![0];
|
|
184
186
|
expect(replyCtx).toEqual({
|
|
185
187
|
replyToMessageId: "m-orig",
|
|
188
|
+
replyPreviewChatId: "chat-1",
|
|
186
189
|
replyPreviewSenderId: "user-2",
|
|
187
190
|
replyPreviewNickName: "User Two",
|
|
188
191
|
replyPreviewText: "original text",
|
package/src/inbound.ts
CHANGED
|
@@ -29,6 +29,7 @@ export interface IngestTurnParams {
|
|
|
29
29
|
mediaItems: MediaItem[];
|
|
30
30
|
replyCtx?: {
|
|
31
31
|
replyToMessageId: string;
|
|
32
|
+
replyPreviewChatId: string;
|
|
32
33
|
replyPreviewSenderId: string;
|
|
33
34
|
replyPreviewNickName: string;
|
|
34
35
|
replyPreviewText: string;
|
|
@@ -191,9 +192,19 @@ export async function dispatchOpenclawClawlingInbound(
|
|
|
191
192
|
return;
|
|
192
193
|
}
|
|
193
194
|
|
|
195
|
+
log?.info?.(
|
|
196
|
+
`[${account.accountId}] openclaw-clawchat inbound event=${envelope.event === EVENT.MESSAGE_REPLY ? "reply" : "send"} msg=${payload.message_id} from=${sender.id} text_len=${rawBody.length} mentioned=${wasMentioned}`,
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
// New protocol: `chat_id` is the routing primary; `to` is deprecated.
|
|
200
|
+
// Fall back to sender.id if neither is present (defensive).
|
|
201
|
+
const chatId =
|
|
202
|
+
(envelope as Envelope<DownlinkMessageSendPayload> & { chat_id?: string }).chat_id ??
|
|
203
|
+
sender.id;
|
|
194
204
|
const replyCtx = message.context.reply
|
|
195
205
|
? {
|
|
196
206
|
replyToMessageId: message.context.reply.reply_to_msg_id,
|
|
207
|
+
replyPreviewChatId: chatId,
|
|
197
208
|
replyPreviewSenderId:
|
|
198
209
|
message.context.reply.reply_preview.id ??
|
|
199
210
|
message.context.reply.reply_preview.sender_id ??
|
|
@@ -206,16 +217,6 @@ export async function dispatchOpenclawClawlingInbound(
|
|
|
206
217
|
}
|
|
207
218
|
: undefined;
|
|
208
219
|
|
|
209
|
-
log?.info?.(
|
|
210
|
-
`[${account.accountId}] openclaw-clawchat inbound event=${envelope.event === EVENT.MESSAGE_REPLY ? "reply" : "send"} msg=${payload.message_id} from=${sender.id} text_len=${rawBody.length} mentioned=${wasMentioned}`,
|
|
211
|
-
);
|
|
212
|
-
|
|
213
|
-
// New protocol: `chat_id` is the routing primary; `to` is deprecated.
|
|
214
|
-
// Fall back to sender.id if neither is present (defensive).
|
|
215
|
-
const chatId =
|
|
216
|
-
(envelope as Envelope<DownlinkMessageSendPayload> & { chat_id?: string }).chat_id ??
|
|
217
|
-
sender.id;
|
|
218
|
-
|
|
219
220
|
await params.ingest({
|
|
220
221
|
channel: "openclaw-clawchat",
|
|
221
222
|
accountId: account.accountId,
|
|
@@ -112,6 +112,42 @@ describe("runOpenclawClawlingLogin", () => {
|
|
|
112
112
|
expect(log).toHaveBeenCalledWith(expect.stringContaining("login succeeded"));
|
|
113
113
|
});
|
|
114
114
|
|
|
115
|
+
it("uses the runtime config mutator with auto reload intent for config writes", async () => {
|
|
116
|
+
const cfg = buildCfg({
|
|
117
|
+
baseUrl: "https://api.example.com",
|
|
118
|
+
websocketUrl: "wss://ws.example.com/v2/client",
|
|
119
|
+
});
|
|
120
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
121
|
+
agent: { user_id: "agent-123", nickname: "Bot" },
|
|
122
|
+
access_token: "access-tok",
|
|
123
|
+
refresh_token: "refresh-tok",
|
|
124
|
+
});
|
|
125
|
+
let mutatedCfg: OpenClawConfig | undefined;
|
|
126
|
+
const mutateConfigFile = vi.fn(async (params) => {
|
|
127
|
+
expect(params.afterWrite).toEqual({ mode: "auto" });
|
|
128
|
+
const draft = structuredClone(cfg) as OpenClawConfig;
|
|
129
|
+
await params.mutate(draft, { snapshot: {} as never, previousHash: "before" });
|
|
130
|
+
mutatedCfg = draft;
|
|
131
|
+
return { nextConfig: draft } as never;
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
await runOpenclawClawlingLogin({
|
|
135
|
+
cfg,
|
|
136
|
+
runtime: { log: vi.fn() },
|
|
137
|
+
readInviteCode: async () => "INV-ABC",
|
|
138
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
139
|
+
mutateConfigFile,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(mutateConfigFile).toHaveBeenCalledTimes(1);
|
|
143
|
+
const section = (mutatedCfg!.channels as Record<string, Record<string, unknown>>)[
|
|
144
|
+
CHANNEL_ID
|
|
145
|
+
]!;
|
|
146
|
+
expect(section.token).toBe("access-tok");
|
|
147
|
+
expect(section.refreshToken).toBe("refresh-tok");
|
|
148
|
+
expect(section.userId).toBe("agent-123");
|
|
149
|
+
});
|
|
150
|
+
|
|
115
151
|
it("preserves other configured channels when persisting ClawChat credentials", async () => {
|
|
116
152
|
const cfg = {
|
|
117
153
|
channels: {
|