@newbase-clawchat/openclaw-clawchat 2026.4.29 → 2026.5.4
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 +37 -11
- 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 +200 -0
- package/dist/src/client.js +176 -0
- package/dist/src/commands.js +35 -0
- package/dist/src/config.js +226 -0
- package/dist/src/inbound.js +133 -0
- package/dist/src/login.runtime.js +132 -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 +21 -0
- package/package.json +27 -5
- package/skills/clawchat-activate/SKILL.md +18 -9
- 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.test.ts +7 -1
- package/src/channel.ts +27 -8
- 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 +47 -0
- package/src/config.ts +28 -5
- 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 +57 -27
- package/src/manifest.test.ts +156 -30
- package/src/outbound.test.ts +6 -5
- package/src/outbound.ts +8 -7
- package/src/plugin-entry.test.ts +7 -1
- 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 +65 -74
package/openclaw.plugin.json
CHANGED
|
@@ -3,12 +3,33 @@
|
|
|
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
|
},
|
|
10
|
+
"channelEnvVars": {
|
|
11
|
+
"openclaw-clawchat": [
|
|
12
|
+
"CLAWCHAT_TOKEN",
|
|
13
|
+
"CLAWCHAT_USER_ID",
|
|
14
|
+
"CLAWCHAT_REFRESH_TOKEN",
|
|
15
|
+
"CLAWCHAT_BASE_URL",
|
|
16
|
+
"CLAWCHAT_WEBSOCKET_URL"
|
|
17
|
+
]
|
|
18
|
+
},
|
|
9
19
|
"commandAliases": [
|
|
10
20
|
{ "name": "clawchat-login", "kind": "runtime-slash" }
|
|
11
21
|
],
|
|
22
|
+
"contracts": {
|
|
23
|
+
"tools": [
|
|
24
|
+
"clawchat_activate",
|
|
25
|
+
"clawchat_get_account_profile",
|
|
26
|
+
"clawchat_get_user_profile",
|
|
27
|
+
"clawchat_list_account_friends",
|
|
28
|
+
"clawchat_update_account_profile",
|
|
29
|
+
"clawchat_upload_avatar_image",
|
|
30
|
+
"clawchat_upload_media_file"
|
|
31
|
+
]
|
|
32
|
+
},
|
|
12
33
|
"configSchema": {
|
|
13
34
|
"type": "object",
|
|
14
35
|
"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.5.4",
|
|
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,14 @@
|
|
|
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:agent": "bash .e2e/run-install-clawchat-plugin-agent-e2e.sh",
|
|
19
|
+
"test:e2e:install-clawchat-plugin:agent:smoke": "node --test .e2e/run-install-clawchat-plugin-agent-e2e.test.mjs",
|
|
20
|
+
"test:e2e:install-clawchat-plugin:smoke": "node --test .e2e/run-install-clawchat-plugin-e2e.test.mjs",
|
|
21
|
+
"dev:openclaw-source": "test -d tmp/openclaw || git clone --depth=1 https://github.com/openclaw/openclaw.git tmp/openclaw",
|
|
22
|
+
"prepack": "npm run build",
|
|
14
23
|
"typecheck": "tsc --noEmit",
|
|
15
24
|
"prepublishOnly": "npm run typecheck",
|
|
16
25
|
"release": "npm run prepublishOnly && npm publish"
|
|
@@ -21,11 +30,12 @@
|
|
|
21
30
|
},
|
|
22
31
|
"devDependencies": {
|
|
23
32
|
"@types/node": "^25.5.0",
|
|
24
|
-
"openclaw": "
|
|
25
|
-
"typescript": "^5.4.0"
|
|
33
|
+
"openclaw": "2026.5.4",
|
|
34
|
+
"typescript": "^5.4.0",
|
|
35
|
+
"vitest": "^4.1.5"
|
|
26
36
|
},
|
|
27
37
|
"peerDependencies": {
|
|
28
|
-
"openclaw": "
|
|
38
|
+
"openclaw": ">=2026.5.4"
|
|
29
39
|
},
|
|
30
40
|
"peerDependenciesMeta": {
|
|
31
41
|
"openclaw": {
|
|
@@ -39,10 +49,22 @@
|
|
|
39
49
|
"extensions": [
|
|
40
50
|
"./index.ts"
|
|
41
51
|
],
|
|
52
|
+
"runtimeExtensions": [
|
|
53
|
+
"./dist/index.js"
|
|
54
|
+
],
|
|
55
|
+
"channel": {
|
|
56
|
+
"id": "openclaw-clawchat",
|
|
57
|
+
"label": "Clawling Chat",
|
|
58
|
+
"selectionLabel": "Clawling Chat",
|
|
59
|
+
"docsPath": "/channels/openclaw-clawchat",
|
|
60
|
+
"docsLabel": "openclaw-clawchat",
|
|
61
|
+
"blurb": "Clawling Protocol v2 over WebSocket (chat-sdk).",
|
|
62
|
+
"order": 110
|
|
63
|
+
},
|
|
42
64
|
"install": {
|
|
43
65
|
"npmSpec": "@newbase-clawchat/openclaw-clawchat",
|
|
44
66
|
"defaultChoice": "npm",
|
|
45
|
-
"minHostVersion": ">=2026.
|
|
67
|
+
"minHostVersion": ">=2026.5.4"
|
|
46
68
|
}
|
|
47
69
|
}
|
|
48
70
|
}
|
|
@@ -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 add for first-time CLI activation. 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 add command with the invite code:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
openclaw channels add --channel openclaw-clawchat --token "$CLAWCHAT_INVITE_CODE"
|
|
22
|
+
```
|
|
23
|
+
5. `channels add --token` is the first-time CLI activation path; the token argument is the ClawChat invite code on this setup path.
|
|
24
|
+
6. Use `openclaw channels login --channel openclaw-clawchat` only to refresh credentials later after the channel already exists.
|
|
25
|
+
7. After activation succeeds, rely on OpenClaw's `channels.*` config hot reload when the plugin is already loaded. Verify channel status if available:
|
|
15
26
|
|
|
16
27
|
```bash
|
|
17
|
-
openclaw channels
|
|
28
|
+
openclaw channels status --probe
|
|
18
29
|
```
|
|
19
|
-
|
|
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:
|
|
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 channel add/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
|
|
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 add --channel openclaw-clawchat --token "$CLAWCHAT_INVITE_CODE"` 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` or falling back to channel add.
|
|
@@ -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.test.ts
CHANGED
|
@@ -28,9 +28,12 @@ describe("openclaw-clawchat plugin", () => {
|
|
|
28
28
|
expect(
|
|
29
29
|
validate!({ cfg: {}, accountId: "default", input: { code: " " } }),
|
|
30
30
|
).toMatch(/invite code is required/i);
|
|
31
|
+
expect(
|
|
32
|
+
validate!({ cfg: {}, accountId: "default", input: { token: " " } }),
|
|
33
|
+
).toMatch(/invite code is required/i);
|
|
31
34
|
});
|
|
32
35
|
|
|
33
|
-
it("setup.validateInput passes when code is present", () => {
|
|
36
|
+
it("setup.validateInput passes when code or token is present", () => {
|
|
34
37
|
const validate = openclawClawlingPlugin.setup?.validateInput as (args: {
|
|
35
38
|
cfg: unknown;
|
|
36
39
|
accountId: string;
|
|
@@ -39,6 +42,9 @@ describe("openclaw-clawchat plugin", () => {
|
|
|
39
42
|
expect(
|
|
40
43
|
validate({ cfg: {}, accountId: "default", input: { code: "INV-XXXX" } }),
|
|
41
44
|
).toBeNull();
|
|
45
|
+
expect(
|
|
46
|
+
validate({ cfg: {}, accountId: "default", input: { token: "INV-XXXX" } }),
|
|
47
|
+
).toBeNull();
|
|
42
48
|
});
|
|
43
49
|
|
|
44
50
|
it("setup.applyAccountConfig marks the channel enabled without touching credentials", () => {
|
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,
|
|
@@ -48,18 +50,24 @@ const configAdapter = createTopLevelChannelConfigAdapter<ResolvedOpenclawClawlin
|
|
|
48
50
|
* one-shot setup metadata because current hosts do not discover channels from
|
|
49
51
|
* `plugins.load.paths`.
|
|
50
52
|
*
|
|
51
|
-
* Setup takes
|
|
52
|
-
* userId come from the login flow
|
|
53
|
-
* `afterAccountConfigWritten`.
|
|
53
|
+
* Setup takes an invite code from `code` or from OpenClaw's generic
|
|
54
|
+
* `channels add --token` input. URL + token + userId come from the login flow
|
|
55
|
+
* which is triggered automatically in `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,
|
|
61
63
|
validateInput: ({ input }: { cfg: unknown; accountId: string; input: ChannelSetupInput }) => {
|
|
62
|
-
|
|
64
|
+
const inviteCode =
|
|
65
|
+
typeof input.code === "string" && input.code.trim()
|
|
66
|
+
? input.code.trim()
|
|
67
|
+
: typeof input.token === "string"
|
|
68
|
+
? input.token.trim()
|
|
69
|
+
: "";
|
|
70
|
+
if (!inviteCode) {
|
|
63
71
|
return "ClawChat invite code is required.";
|
|
64
72
|
}
|
|
65
73
|
return null;
|
|
@@ -94,7 +102,12 @@ const setupAdapter = {
|
|
|
94
102
|
runtime: { log: (message: string) => void };
|
|
95
103
|
previousCfg: OpenClawConfig;
|
|
96
104
|
}) => {
|
|
97
|
-
const code =
|
|
105
|
+
const code =
|
|
106
|
+
typeof input.code === "string" && input.code.trim()
|
|
107
|
+
? input.code.trim()
|
|
108
|
+
: typeof input.token === "string"
|
|
109
|
+
? input.token.trim()
|
|
110
|
+
: "";
|
|
98
111
|
if (!code) return;
|
|
99
112
|
// Lazy-import the login runtime to keep @clack/prompts / readline /
|
|
100
113
|
// config-runtime off the plugin's cold-start path. `readInviteCode`
|
|
@@ -105,6 +118,9 @@ const setupAdapter = {
|
|
|
105
118
|
accountId: null,
|
|
106
119
|
runtime: { log: (message: string) => runtime.log(message) },
|
|
107
120
|
readInviteCode: async () => code,
|
|
121
|
+
mutateConfigFile: (getOpenclawClawlingRuntime().config as unknown as {
|
|
122
|
+
mutateConfigFile: OpenclawClawchatMutateConfigFile;
|
|
123
|
+
}).mutateConfigFile,
|
|
108
124
|
});
|
|
109
125
|
},
|
|
110
126
|
};
|
|
@@ -177,6 +193,9 @@ export const openclawClawlingPlugin: ChannelPlugin<ResolvedOpenclawClawlingAccou
|
|
|
177
193
|
cfg,
|
|
178
194
|
accountId: accountId ?? null,
|
|
179
195
|
runtime: { log: (message: string) => runtime.log(message) },
|
|
196
|
+
mutateConfigFile: (getOpenclawClawlingRuntime().config as unknown as {
|
|
197
|
+
mutateConfigFile: OpenclawClawchatMutateConfigFile;
|
|
198
|
+
}).mutateConfigFile,
|
|
180
199
|
});
|
|
181
200
|
},
|
|
182
201
|
},
|
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,11 +50,56 @@ 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);
|
|
54
57
|
});
|
|
55
58
|
|
|
59
|
+
it("resolves credentials from ClawChat environment variables when config omits them", () => {
|
|
60
|
+
const account = resolveOpenclawClawlingAccount(
|
|
61
|
+
{},
|
|
62
|
+
{
|
|
63
|
+
CLAWCHAT_TOKEN: "env-token",
|
|
64
|
+
CLAWCHAT_USER_ID: "env-user",
|
|
65
|
+
CLAWCHAT_BASE_URL: "https://api.env.example",
|
|
66
|
+
CLAWCHAT_WEBSOCKET_URL: "wss://ws.env.example/ws",
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
expect(account.configured).toBe(true);
|
|
71
|
+
expect(account.token).toBe("env-token");
|
|
72
|
+
expect(account.userId).toBe("env-user");
|
|
73
|
+
expect(account.baseUrl).toBe("https://api.env.example");
|
|
74
|
+
expect(account.websocketUrl).toBe("wss://ws.env.example/ws");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("prefers explicit channel config over ClawChat environment variables", () => {
|
|
78
|
+
const account = resolveOpenclawClawlingAccount(
|
|
79
|
+
{
|
|
80
|
+
channels: {
|
|
81
|
+
"openclaw-clawchat": {
|
|
82
|
+
token: "config-token",
|
|
83
|
+
userId: "config-user",
|
|
84
|
+
baseUrl: "https://api.config.example",
|
|
85
|
+
websocketUrl: "wss://ws.config.example/ws",
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
CLAWCHAT_TOKEN: "env-token",
|
|
91
|
+
CLAWCHAT_USER_ID: "env-user",
|
|
92
|
+
CLAWCHAT_BASE_URL: "https://api.env.example",
|
|
93
|
+
CLAWCHAT_WEBSOCKET_URL: "wss://ws.env.example/ws",
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
expect(account.token).toBe("config-token");
|
|
98
|
+
expect(account.userId).toBe("config-user");
|
|
99
|
+
expect(account.baseUrl).toBe("https://api.config.example");
|
|
100
|
+
expect(account.websocketUrl).toBe("wss://ws.config.example/ws");
|
|
101
|
+
});
|
|
102
|
+
|
|
56
103
|
it("falls back to static replyMode for unknown values", () => {
|
|
57
104
|
const cfg = {
|
|
58
105
|
channels: {
|