@newbase-clawchat/openclaw-clawchat 2026.4.30 → 2026.5.4-2
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 +64 -12
- package/dist/index.js +9 -18
- package/dist/setup-entry.js +3 -0
- package/dist/src/channel.js +20 -142
- package/dist/src/channel.setup.js +133 -0
- package/dist/src/client.js +11 -4
- package/dist/src/config.js +17 -5
- package/dist/src/login.runtime.js +11 -2
- package/dist/src/media-runtime.js +6 -5
- package/dist/src/outbound.js +12 -2
- package/dist/src/reply-dispatcher.js +4 -9
- package/dist/src/runtime.js +12 -6
- package/dist/src/tools.js +2 -2
- package/index.ts +9 -21
- package/openclaw.plugin.json +10 -0
- package/package.json +20 -5
- package/setup-entry.ts +4 -0
- package/skills/clawchat-activate/SKILL.md +15 -10
- package/src/channel.outbound.test.ts +6 -0
- package/src/channel.setup.ts +156 -0
- package/src/channel.test.ts +29 -1
- package/src/channel.ts +24 -171
- package/src/client.test.ts +63 -2
- package/src/client.ts +12 -3
- package/src/config.test.ts +44 -0
- package/src/config.ts +21 -5
- package/src/login.runtime.test.ts +5 -2
- package/src/login.runtime.ts +13 -2
- package/src/manifest.test.ts +114 -26
- package/src/media-runtime.test.ts +26 -0
- package/src/media-runtime.ts +19 -7
- package/src/outbound.test.ts +1 -1
- package/src/outbound.ts +11 -2
- package/src/plugin-entry.test.ts +8 -1
- package/src/reply-dispatcher.test.ts +100 -2
- package/src/reply-dispatcher.ts +4 -9
- package/src/runtime.test.ts +2 -0
- package/src/runtime.ts +14 -6
- package/src/scripts.test.ts +42 -0
- package/src/tools.ts +2 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { interactiveReplyToPresentation, renderMessagePresentationFallbackText, } from "openclaw/plugin-sdk/interactive-runtime";
|
|
2
|
+
import { resolveOutboundMediaUrls } from "openclaw/plugin-sdk/reply-payload";
|
|
2
3
|
import { createOpenclawClawlingApiClient } from "./api-client.js";
|
|
3
4
|
import { openBufferedStreamingSession, mergeStreamingText, } from "./buffered-stream.js";
|
|
4
5
|
import { emitFinalStreamReply } from "./client.js";
|
|
@@ -256,7 +257,7 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
256
257
|
routing,
|
|
257
258
|
replyTo: {
|
|
258
259
|
msgId: inboundMessageId ?? streamingMessageId,
|
|
259
|
-
previewId: inboundForFinalReply?.
|
|
260
|
+
previewId: inboundForFinalReply?.senderId ?? target.chatId,
|
|
260
261
|
nickName: inboundForFinalReply?.senderNickName ?? inboundForFinalReply?.senderId ?? target.chatId,
|
|
261
262
|
fragments: inboundForFinalReply?.bodyText
|
|
262
263
|
? [{ kind: "text", text: inboundForFinalReply.bodyText }]
|
|
@@ -271,10 +272,7 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
271
272
|
}
|
|
272
273
|
if (text)
|
|
273
274
|
streamText = mergeStreamingText(streamText, text);
|
|
274
|
-
const urls =
|
|
275
|
-
...(payload.mediaUrl ? [payload.mediaUrl] : []),
|
|
276
|
-
...(payload.mediaUrls ?? []),
|
|
277
|
-
].filter((u) => Boolean(u));
|
|
275
|
+
const urls = resolveOutboundMediaUrls(payload).filter(Boolean);
|
|
278
276
|
for (const url of urls) {
|
|
279
277
|
if (!accumulatedMediaUrls.includes(url))
|
|
280
278
|
accumulatedMediaUrls.push(url);
|
|
@@ -313,10 +311,7 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
313
311
|
deliver: async (payload, info) => {
|
|
314
312
|
const richFragment = buildRichInteractionFragment(payload);
|
|
315
313
|
const text = richFragment && account.richInteractions ? "" : resolvePayloadText(payload);
|
|
316
|
-
const urls =
|
|
317
|
-
...(payload.mediaUrl ? [payload.mediaUrl] : []),
|
|
318
|
-
...(payload.mediaUrls ?? []),
|
|
319
|
-
].filter((u) => Boolean(u));
|
|
314
|
+
const urls = resolveOutboundMediaUrls(payload).filter(Boolean);
|
|
320
315
|
log?.info?.(`[${account.accountId}] openclaw-clawchat deliver kind=${info?.kind ?? "unknown"} text_len=${text.length} media_urls=${urls.length} reasoning=${payload.isReasoning === true}`);
|
|
321
316
|
if (payload.isReasoning) {
|
|
322
317
|
if (!account.forwardThinking)
|
package/dist/src/runtime.js
CHANGED
|
@@ -76,9 +76,11 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
76
76
|
// Obtain PluginRuntime from the stored runtime set via setOpenclawClawlingRuntime.
|
|
77
77
|
const runtime = getOpenclawClawlingRuntime();
|
|
78
78
|
const accountId = account.accountId;
|
|
79
|
+
log?.info?.(`[${accountId}] openclaw-clawchat runtime start entered configured=${account.configured} enabled=${account.enabled} hasToken=${Boolean(account.token)} hasUserId=${Boolean(account.userId)} websocketUrl=${account.websocketUrl || "(empty)"}`);
|
|
79
80
|
const client = createOpenclawClawlingClient(account, {
|
|
80
81
|
...(params.transport ? { transport: params.transport } : {}),
|
|
81
82
|
});
|
|
83
|
+
log?.info?.(`[${accountId}] openclaw-clawchat runtime client created`);
|
|
82
84
|
client.on("state", ({ from, to }) => {
|
|
83
85
|
log?.info?.(`[${accountId}] openclaw-clawchat state ${from} -> ${to}`);
|
|
84
86
|
const next = { ...getStatus(), ...mapClawlingStateToStatus(to) };
|
|
@@ -138,16 +140,16 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
138
140
|
timestamp: turn.timestamp,
|
|
139
141
|
...rt.reply.resolveEnvelopeFormatOptions(cfg),
|
|
140
142
|
});
|
|
143
|
+
const conversationTarget = `${CHANNEL_ID}:${formatConversationSubject(turn.peer)}`;
|
|
141
144
|
const ctxPayload = rt.reply.finalizeInboundContext({
|
|
142
145
|
Body: body,
|
|
143
146
|
BodyForAgent: turn.rawBody,
|
|
144
147
|
RawBody: turn.rawBody,
|
|
145
148
|
CommandBody: turn.rawBody,
|
|
146
|
-
// Clawling v2 routes by chat_id. `
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
|
|
150
|
-
From: `${CHANNEL_ID}:${formatConversationSubject(turn.peer)}`,
|
|
149
|
+
// Clawling v2 routes by chat_id. `OriginatingTo` is what the
|
|
150
|
+
// message tool uses as the implicit current-chat target, so keep it
|
|
151
|
+
// on the conversation id rather than the agent account user id.
|
|
152
|
+
From: conversationTarget,
|
|
151
153
|
To: `${CHANNEL_ID}:${account.userId}`,
|
|
152
154
|
SessionKey: route.sessionKey,
|
|
153
155
|
AccountId: route.accountId ?? accountId,
|
|
@@ -160,7 +162,7 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
160
162
|
MessageSidFull: turn.messageId,
|
|
161
163
|
Timestamp: turn.timestamp,
|
|
162
164
|
OriginatingChannel: CHANNEL_ID,
|
|
163
|
-
OriginatingTo:
|
|
165
|
+
OriginatingTo: conversationTarget,
|
|
164
166
|
});
|
|
165
167
|
// Fetch any inbound media attachments and populate MediaPath/MediaPaths in context.
|
|
166
168
|
const inboundPaths = turn.mediaItems.length > 0
|
|
@@ -250,7 +252,9 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
250
252
|
// return without throwing (which would make the gateway supervisor
|
|
251
253
|
// restart us immediately in a tight loop).
|
|
252
254
|
try {
|
|
255
|
+
log?.info?.(`[${accountId}] openclaw-clawchat runtime calling client.connect()`);
|
|
253
256
|
await client.connect();
|
|
257
|
+
log?.info?.(`[${accountId}] openclaw-clawchat runtime client.connect() resolved`);
|
|
254
258
|
}
|
|
255
259
|
catch (err) {
|
|
256
260
|
const classified = classifyClawlingClientError(err);
|
|
@@ -265,6 +269,7 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
265
269
|
return;
|
|
266
270
|
}
|
|
267
271
|
activeClients.set(accountId, client);
|
|
272
|
+
log?.info?.(`[${accountId}] openclaw-clawchat runtime active client registered`);
|
|
268
273
|
setStatus({
|
|
269
274
|
...getStatus(),
|
|
270
275
|
connected: true,
|
|
@@ -273,6 +278,7 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
273
278
|
});
|
|
274
279
|
log?.info?.(`[${accountId}] openclaw-clawchat connected`);
|
|
275
280
|
await waitUntilAbort(abortSignal, async () => {
|
|
281
|
+
log?.info?.(`[${accountId}] openclaw-clawchat runtime abort received; closing client`);
|
|
276
282
|
activeClients.delete(accountId);
|
|
277
283
|
client.close();
|
|
278
284
|
setStatus({
|
package/dist/src/tools.js
CHANGED
|
@@ -118,8 +118,8 @@ export function registerOpenclawClawlingTools(api) {
|
|
|
118
118
|
function buildClient() {
|
|
119
119
|
const acct = resolveCurrent();
|
|
120
120
|
// `baseUrl` always resolves via the built-in default in config.ts, so we
|
|
121
|
-
// only need to gate on `token` here (which is populated by
|
|
122
|
-
//
|
|
121
|
+
// only need to gate on `token` here (which is populated by ClawChat
|
|
122
|
+
// activation/login).
|
|
123
123
|
if (!acct.token) {
|
|
124
124
|
return { ok: false, error: configError("openclaw-clawchat: token is required") };
|
|
125
125
|
}
|
package/index.ts
CHANGED
|
@@ -1,31 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
1
|
+
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/channel-core";
|
|
3
2
|
import { openclawClawlingPlugin } from "./src/channel.ts";
|
|
4
3
|
import { registerOpenclawClawlingCommands } from "./src/commands.ts";
|
|
4
|
+
import { openclawClawlingConfigSchema } from "./src/config.ts";
|
|
5
5
|
import { setOpenclawClawlingRuntime } from "./src/runtime.ts";
|
|
6
6
|
import { registerOpenclawClawlingTools } from "./src/tools.ts";
|
|
7
|
-
import { openclawClawlingConfigSchema } from "./src/config.ts";
|
|
8
7
|
|
|
9
|
-
export default {
|
|
8
|
+
export default defineChannelPluginEntry({
|
|
10
9
|
id: "openclaw-clawchat",
|
|
11
10
|
name: "Clawling Chat",
|
|
12
11
|
description: "Clawling Chat Protocol v2 channel plugin (chat-sdk)",
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
plugin: openclawClawlingPlugin,
|
|
13
|
+
configSchema: { schema: openclawClawlingConfigSchema },
|
|
14
|
+
setRuntime: setOpenclawClawlingRuntime,
|
|
15
|
+
registerFull(api) {
|
|
17
16
|
registerOpenclawClawlingCommands(api);
|
|
18
17
|
registerOpenclawClawlingTools(api);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// export default defineChannelPluginEntry({
|
|
23
|
-
// id: "openclaw-clawchat",
|
|
24
|
-
// name: "Clawling Chat",
|
|
25
|
-
// description: "Clawling Chat Protocol v2 channel plugin (chat-sdk)",
|
|
26
|
-
// plugin: openclawClawlingPlugin,
|
|
27
|
-
// setRuntime: setOpenclawClawlingRuntime,
|
|
28
|
-
// registerFull(api) {
|
|
29
|
-
// registerOpenclawClawlingTools(api);
|
|
30
|
-
// },
|
|
31
|
-
// });
|
|
18
|
+
},
|
|
19
|
+
});
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "openclaw-clawchat",
|
|
3
|
+
"kind": "channel",
|
|
3
4
|
"channels": ["openclaw-clawchat"],
|
|
4
5
|
"skills": ["./skills"],
|
|
5
6
|
"activation": {
|
|
@@ -7,6 +8,15 @@
|
|
|
7
8
|
"onChannels": ["openclaw-clawchat"],
|
|
8
9
|
"onCommands": ["clawchat-login"]
|
|
9
10
|
},
|
|
11
|
+
"channelEnvVars": {
|
|
12
|
+
"openclaw-clawchat": [
|
|
13
|
+
"CLAWCHAT_TOKEN",
|
|
14
|
+
"CLAWCHAT_USER_ID",
|
|
15
|
+
"CLAWCHAT_REFRESH_TOKEN",
|
|
16
|
+
"CLAWCHAT_BASE_URL",
|
|
17
|
+
"CLAWCHAT_WEBSOCKET_URL"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
10
20
|
"commandAliases": [
|
|
11
21
|
{ "name": "clawchat-login", "kind": "runtime-slash" }
|
|
12
22
|
],
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@newbase-clawchat/openclaw-clawchat",
|
|
3
|
-
"version": "2026.4
|
|
3
|
+
"version": "2026.5.4-2",
|
|
4
4
|
"description": "OpenClaw ClawChat channel plugin",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
7
7
|
"index.ts",
|
|
8
|
+
"setup-entry.ts",
|
|
8
9
|
"src",
|
|
9
10
|
"skills",
|
|
10
11
|
"openclaw.plugin.json",
|
|
@@ -15,6 +16,8 @@
|
|
|
15
16
|
"build": "tsc -p tsconfig.build.json",
|
|
16
17
|
"test": "vitest",
|
|
17
18
|
"test:e2e:install-clawchat-plugin": "bash .e2e/run-install-clawchat-plugin-e2e.sh",
|
|
19
|
+
"test:e2e:install-clawchat-plugin:agent": "bash .e2e/run-install-clawchat-plugin-agent-e2e.sh",
|
|
20
|
+
"test:e2e:install-clawchat-plugin:agent:smoke": "node --test .e2e/run-install-clawchat-plugin-agent-e2e.test.mjs",
|
|
18
21
|
"test:e2e:install-clawchat-plugin:smoke": "node --test .e2e/run-install-clawchat-plugin-e2e.test.mjs",
|
|
19
22
|
"dev:openclaw-source": "test -d tmp/openclaw || git clone --depth=1 https://github.com/openclaw/openclaw.git tmp/openclaw",
|
|
20
23
|
"prepack": "npm run build",
|
|
@@ -28,12 +31,12 @@
|
|
|
28
31
|
},
|
|
29
32
|
"devDependencies": {
|
|
30
33
|
"@types/node": "^25.5.0",
|
|
31
|
-
"openclaw": "2026.4
|
|
34
|
+
"openclaw": "2026.5.4",
|
|
32
35
|
"typescript": "^5.4.0",
|
|
33
36
|
"vitest": "^4.1.5"
|
|
34
37
|
},
|
|
35
38
|
"peerDependencies": {
|
|
36
|
-
"openclaw": "
|
|
39
|
+
"openclaw": ">=2026.5.4"
|
|
37
40
|
},
|
|
38
41
|
"peerDependenciesMeta": {
|
|
39
42
|
"openclaw": {
|
|
@@ -50,6 +53,12 @@
|
|
|
50
53
|
"runtimeExtensions": [
|
|
51
54
|
"./dist/index.js"
|
|
52
55
|
],
|
|
56
|
+
"setupEntry": "./setup-entry.ts",
|
|
57
|
+
"runtimeSetupEntry": "./dist/setup-entry.js",
|
|
58
|
+
"plugin": {
|
|
59
|
+
"id": "openclaw-clawchat",
|
|
60
|
+
"label": "Clawling Chat"
|
|
61
|
+
},
|
|
53
62
|
"channel": {
|
|
54
63
|
"id": "openclaw-clawchat",
|
|
55
64
|
"label": "Clawling Chat",
|
|
@@ -57,12 +66,18 @@
|
|
|
57
66
|
"docsPath": "/channels/openclaw-clawchat",
|
|
58
67
|
"docsLabel": "openclaw-clawchat",
|
|
59
68
|
"blurb": "Clawling Protocol v2 over WebSocket (chat-sdk).",
|
|
60
|
-
"order": 110
|
|
69
|
+
"order": 110,
|
|
70
|
+
"cliAddOptions": [
|
|
71
|
+
{
|
|
72
|
+
"flags": "--token <invite-code>",
|
|
73
|
+
"description": "ClawChat invite code"
|
|
74
|
+
}
|
|
75
|
+
]
|
|
61
76
|
},
|
|
62
77
|
"install": {
|
|
63
78
|
"npmSpec": "@newbase-clawchat/openclaw-clawchat",
|
|
64
79
|
"defaultChoice": "npm",
|
|
65
|
-
"minHostVersion": ">=2026.4
|
|
80
|
+
"minHostVersion": ">=2026.5.4"
|
|
66
81
|
}
|
|
67
82
|
}
|
|
68
83
|
}
|
package/setup-entry.ts
ADDED
|
@@ -8,31 +8,36 @@ description: |
|
|
|
8
8
|
|
|
9
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
10
|
|
|
11
|
-
If the tool is not available because the Gateway has not loaded this plugin yet,
|
|
11
|
+
If the tool is not available because the Gateway has not loaded this plugin yet, restart the Gateway after install/update and retry from a new turn. Do not edit config files manually. On OpenClaw 2026.5.5, npm-installed third-party channels may be loaded by the Gateway but still missing from the `channels add` CLI catalog, which fails with `Unknown channel: openclaw-clawchat`. In that case, ask the user to send the runtime slash command `/clawchat-login A1B2C3` in the chat where OpenClaw is running.
|
|
12
12
|
|
|
13
13
|
## Workflow
|
|
14
14
|
|
|
15
15
|
1. Check whether the user is trying to activate or log in ClawChat.
|
|
16
16
|
2. If `clawchat_activate` is available and the invite code is known, call it with the invite code.
|
|
17
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,
|
|
18
|
+
4. If `clawchat_activate` is not available but the plugin is loaded in a chat session, ask the user to send this runtime slash command in that chat:
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
/clawchat-login A1B2C3
|
|
22
|
+
```
|
|
23
|
+
5. If using a host where `openclaw channels add --help` lists `openclaw-clawchat`, terminal activation can also use:
|
|
19
24
|
|
|
20
25
|
```bash
|
|
21
|
-
openclaw channels
|
|
26
|
+
openclaw channels add --channel openclaw-clawchat --token "$CLAWCHAT_INVITE_CODE"
|
|
22
27
|
```
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
6. The token argument is the ClawChat invite code on this setup path. Do not use `channels add` on OpenClaw 2026.5.5 when it reports `Unknown channel: openclaw-clawchat`.
|
|
29
|
+
7. Use `openclaw channels login --channel openclaw-clawchat` only to refresh credentials later after the channel already exists and the host recognizes the channel.
|
|
30
|
+
8. After activation succeeds, verify channel status if available:
|
|
26
31
|
|
|
27
32
|
```bash
|
|
28
33
|
openclaw channels status --probe
|
|
29
34
|
```
|
|
30
|
-
|
|
35
|
+
9. Execute the Gateway restart command 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
36
|
|
|
32
37
|
```bash
|
|
33
38
|
openclaw gateway restart
|
|
34
39
|
```
|
|
35
|
-
|
|
40
|
+
10. Tell the user activation completes after `clawchat_activate`, `/clawchat-login`, or supported channel add/login succeeds and the status probe succeeds after any required Gateway restart.
|
|
36
41
|
|
|
37
42
|
## Trigger Examples
|
|
38
43
|
|
|
@@ -42,6 +47,6 @@ openclaw gateway restart
|
|
|
42
47
|
- `绑定 ClawChat,邀请码 A1B2C3`
|
|
43
48
|
- `激活 ClawChat`
|
|
44
49
|
|
|
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,
|
|
50
|
+
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 but the runtime slash command is available in chat, ask the user to send `/clawchat-login A1B2C3`. If the host's CLI channel catalog supports `openclaw-clawchat`, `openclaw channels add --channel openclaw-clawchat --token "$CLAWCHAT_INVITE_CODE"` is also acceptable; then probe channel status and perform a real Gateway restart if the channel is configured but stopped or disconnected.
|
|
46
51
|
|
|
47
|
-
When the user asks to activate ClawChat without including a code, ask for the invite code before calling `clawchat_activate
|
|
52
|
+
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.
|
|
@@ -88,6 +88,8 @@ describe("openclaw-clawchat channel outbound", () => {
|
|
|
88
88
|
uploadOutboundMediaMock.mockResolvedValue([
|
|
89
89
|
{ kind: "image", url: "https://cdn/uploaded.png", mime: "image/png" },
|
|
90
90
|
]);
|
|
91
|
+
const mediaReadFile = vi.fn(async () => Buffer.from("host-read"));
|
|
92
|
+
const mediaAccess = { localRoots: ["/tmp"], workspaceDir: "/workspace" };
|
|
91
93
|
|
|
92
94
|
const { openclawClawlingOutbound } = await import("./outbound.ts");
|
|
93
95
|
const result = await openclawClawlingOutbound.sendMedia!({
|
|
@@ -105,7 +107,9 @@ describe("openclaw-clawchat channel outbound", () => {
|
|
|
105
107
|
to: "cc:group:room-1",
|
|
106
108
|
text: "caption",
|
|
107
109
|
mediaUrl: "/tmp/photo.png",
|
|
110
|
+
mediaAccess,
|
|
108
111
|
mediaLocalRoots: ["/tmp"],
|
|
112
|
+
mediaReadFile,
|
|
109
113
|
});
|
|
110
114
|
|
|
111
115
|
expect(createApiClientMock).toHaveBeenCalledWith({
|
|
@@ -116,7 +120,9 @@ describe("openclaw-clawchat channel outbound", () => {
|
|
|
116
120
|
expect(uploadOutboundMediaMock).toHaveBeenCalledWith(["/tmp/photo.png"], {
|
|
117
121
|
apiClient,
|
|
118
122
|
runtime,
|
|
123
|
+
mediaAccess,
|
|
119
124
|
mediaLocalRoots: ["/tmp"],
|
|
125
|
+
mediaReadFile,
|
|
120
126
|
});
|
|
121
127
|
expect(client.sendMessage).toHaveBeenCalledWith(
|
|
122
128
|
expect.objectContaining({
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { createTopLevelChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers";
|
|
2
|
+
import type { ChannelSetupInput } from "openclaw/plugin-sdk/channel-setup";
|
|
3
|
+
import type { ChannelPlugin, OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
4
|
+
import { mutateConfigFile } from "openclaw/plugin-sdk/config-mutation";
|
|
5
|
+
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
|
|
6
|
+
import {
|
|
7
|
+
createComputedAccountStatusAdapter,
|
|
8
|
+
createDefaultChannelRuntimeState,
|
|
9
|
+
} from "openclaw/plugin-sdk/status-helpers";
|
|
10
|
+
import {
|
|
11
|
+
CHANNEL_ID,
|
|
12
|
+
listOpenclawClawlingAccountIds,
|
|
13
|
+
mergeOpenclawClawchatToolAllow,
|
|
14
|
+
openclawClawlingConfigSchema,
|
|
15
|
+
resolveOpenclawClawlingAccount,
|
|
16
|
+
type ResolvedOpenclawClawlingAccount,
|
|
17
|
+
} from "./config.ts";
|
|
18
|
+
import type { OpenclawClawchatMutateConfigFile } from "./login.runtime.ts";
|
|
19
|
+
|
|
20
|
+
const configAdapter = createTopLevelChannelConfigAdapter<ResolvedOpenclawClawlingAccount>({
|
|
21
|
+
sectionKey: CHANNEL_ID,
|
|
22
|
+
resolveAccount: (cfg) => resolveOpenclawClawlingAccount(cfg),
|
|
23
|
+
listAccountIds: () => listOpenclawClawlingAccountIds(),
|
|
24
|
+
defaultAccountId: () => DEFAULT_ACCOUNT_ID,
|
|
25
|
+
deleteMode: "clear-fields",
|
|
26
|
+
clearBaseFields: [
|
|
27
|
+
"websocketUrl",
|
|
28
|
+
"baseUrl",
|
|
29
|
+
"token",
|
|
30
|
+
"userId",
|
|
31
|
+
"replyMode",
|
|
32
|
+
"forwardThinking",
|
|
33
|
+
"forwardToolCalls",
|
|
34
|
+
"richInteractions",
|
|
35
|
+
"enabled",
|
|
36
|
+
],
|
|
37
|
+
resolveAllowFrom: (account) => account.allowFrom,
|
|
38
|
+
formatAllowFrom: () => [],
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Invite-code setup adapter used by OpenClaw setup surfaces.
|
|
43
|
+
*
|
|
44
|
+
* `channels add --token` passes the invite code as setup input. The first
|
|
45
|
+
* config write only enables the channel; `afterAccountConfigWritten` exchanges
|
|
46
|
+
* the invite code and persists token/userId through the host runtime mutator.
|
|
47
|
+
*/
|
|
48
|
+
const setupAdapter: NonNullable<ChannelPlugin<ResolvedOpenclawClawlingAccount>["setup"]> = {
|
|
49
|
+
resolveAccountId: () => DEFAULT_ACCOUNT_ID,
|
|
50
|
+
validateInput: ({ input }: { cfg: OpenClawConfig; accountId: string; input: ChannelSetupInput }) => {
|
|
51
|
+
const inviteCode =
|
|
52
|
+
typeof input.code === "string" && input.code.trim()
|
|
53
|
+
? input.code.trim()
|
|
54
|
+
: typeof input.token === "string"
|
|
55
|
+
? input.token.trim()
|
|
56
|
+
: "";
|
|
57
|
+
if (!inviteCode) {
|
|
58
|
+
return "ClawChat invite code is required.";
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
},
|
|
62
|
+
applyAccountConfig: ({ cfg }: { cfg: OpenClawConfig; accountId: string; input: ChannelSetupInput }) => {
|
|
63
|
+
const channels = (cfg.channels ?? {}) as Record<string, unknown>;
|
|
64
|
+
const current = (channels[CHANNEL_ID] ?? {}) as Record<string, unknown>;
|
|
65
|
+
return mergeOpenclawClawchatToolAllow({
|
|
66
|
+
...cfg,
|
|
67
|
+
channels: {
|
|
68
|
+
...channels,
|
|
69
|
+
[CHANNEL_ID]: { ...current, enabled: true },
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
afterAccountConfigWritten: async ({ cfg, input, runtime }) => {
|
|
74
|
+
runtime.log("[default] openclaw-clawchat setup afterAccountConfigWritten invoked");
|
|
75
|
+
const code =
|
|
76
|
+
typeof input.code === "string" && input.code.trim()
|
|
77
|
+
? input.code.trim()
|
|
78
|
+
: typeof input.token === "string"
|
|
79
|
+
? input.token.trim()
|
|
80
|
+
: "";
|
|
81
|
+
if (!code) {
|
|
82
|
+
runtime.log("[default] openclaw-clawchat setup afterAccountConfigWritten skipped: empty invite code");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const { runOpenclawClawlingLogin } = await import("./login.runtime.ts");
|
|
86
|
+
await runOpenclawClawlingLogin({
|
|
87
|
+
cfg,
|
|
88
|
+
accountId: null,
|
|
89
|
+
runtime: { log: (message: string) => runtime.log(message) },
|
|
90
|
+
readInviteCode: async () => code,
|
|
91
|
+
mutateConfigFile: mutateConfigFile as OpenclawClawchatMutateConfigFile,
|
|
92
|
+
});
|
|
93
|
+
runtime.log("[default] openclaw-clawchat setup afterAccountConfigWritten completed");
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
type OpenclawClawlingSetupPlugin = Pick<
|
|
98
|
+
ChannelPlugin<ResolvedOpenclawClawlingAccount>,
|
|
99
|
+
"id" | "meta" | "capabilities" | "reload" | "configSchema" | "config" | "setup" | "status"
|
|
100
|
+
>;
|
|
101
|
+
|
|
102
|
+
export const openclawClawlingSetupPlugin: OpenclawClawlingSetupPlugin = {
|
|
103
|
+
id: CHANNEL_ID,
|
|
104
|
+
meta: {
|
|
105
|
+
id: CHANNEL_ID,
|
|
106
|
+
label: "Clawling Chat",
|
|
107
|
+
selectionLabel: "Clawling Chat",
|
|
108
|
+
docsPath: "/channels/openclaw-clawchat",
|
|
109
|
+
docsLabel: "openclaw-clawchat",
|
|
110
|
+
blurb: "Clawling Protocol v2 over WebSocket (chat-sdk).",
|
|
111
|
+
order: 110,
|
|
112
|
+
},
|
|
113
|
+
capabilities: {
|
|
114
|
+
chatTypes: ["direct", "group"],
|
|
115
|
+
media: true,
|
|
116
|
+
reactions: false,
|
|
117
|
+
threads: false,
|
|
118
|
+
polls: false,
|
|
119
|
+
blockStreaming: true,
|
|
120
|
+
},
|
|
121
|
+
reload: {
|
|
122
|
+
configPrefixes: [`channels.${CHANNEL_ID}`],
|
|
123
|
+
},
|
|
124
|
+
configSchema: {
|
|
125
|
+
schema: openclawClawlingConfigSchema,
|
|
126
|
+
},
|
|
127
|
+
config: {
|
|
128
|
+
...configAdapter,
|
|
129
|
+
isConfigured: (account) => account.configured,
|
|
130
|
+
describeAccount: (account) => ({
|
|
131
|
+
accountId: account.accountId,
|
|
132
|
+
name: account.name,
|
|
133
|
+
enabled: account.enabled,
|
|
134
|
+
configured: account.configured,
|
|
135
|
+
}),
|
|
136
|
+
},
|
|
137
|
+
setup: setupAdapter,
|
|
138
|
+
status: createComputedAccountStatusAdapter<ResolvedOpenclawClawlingAccount>({
|
|
139
|
+
defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID, {
|
|
140
|
+
connected: false,
|
|
141
|
+
lastInboundAt: null,
|
|
142
|
+
lastOutboundAt: null,
|
|
143
|
+
}),
|
|
144
|
+
resolveAccountSnapshot: ({ account }) => ({
|
|
145
|
+
accountId: account.accountId,
|
|
146
|
+
name: account.name,
|
|
147
|
+
enabled: account.enabled,
|
|
148
|
+
configured: account.configured,
|
|
149
|
+
extra: {
|
|
150
|
+
websocketUrl: account.websocketUrl || null,
|
|
151
|
+
baseUrl: account.baseUrl || null,
|
|
152
|
+
userId: account.userId || null,
|
|
153
|
+
},
|
|
154
|
+
}),
|
|
155
|
+
}),
|
|
156
|
+
};
|
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", () => {
|
|
@@ -117,10 +123,32 @@ describe("openclaw-clawchat plugin", () => {
|
|
|
117
123
|
expect(normalized).toBe("usr_01KPN6SQFQEGM9HR11CHRHPMMT");
|
|
118
124
|
});
|
|
119
125
|
|
|
126
|
+
it("declares ClawChat target prefixes for OpenClaw channel selection", () => {
|
|
127
|
+
expect(openclawClawlingPlugin.messaging?.targetPrefixes).toEqual([
|
|
128
|
+
"cc",
|
|
129
|
+
"clawchat",
|
|
130
|
+
"openclaw-clawchat",
|
|
131
|
+
]);
|
|
132
|
+
});
|
|
133
|
+
|
|
120
134
|
it("parses openclaw-clawchat target prefix as a direct recipient", () => {
|
|
121
135
|
expect(parseOpenclawRecipient("openclaw-clawchat:usr_01KPN6SQFQEGM9HR11CHRHPMMT")).toEqual({
|
|
122
136
|
chatId: "usr_01KPN6SQFQEGM9HR11CHRHPMMT",
|
|
123
137
|
chatType: "direct",
|
|
124
138
|
});
|
|
125
139
|
});
|
|
140
|
+
|
|
141
|
+
it("parses host-normalized group targets as group recipients", () => {
|
|
142
|
+
expect(parseOpenclawRecipient("group:cnv_01KR2NBGTKEQ0S0CAYCEQP3YPW")).toEqual({
|
|
143
|
+
chatId: "cnv_01KR2NBGTKEQ0S0CAYCEQP3YPW",
|
|
144
|
+
chatType: "group",
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("parses host-normalized direct targets as direct recipients", () => {
|
|
149
|
+
expect(parseOpenclawRecipient("direct:cnv_01KR2NBGTKEQ0S0CAYCEQP3YPW")).toEqual({
|
|
150
|
+
chatId: "cnv_01KR2NBGTKEQ0S0CAYCEQP3YPW",
|
|
151
|
+
chatType: "direct",
|
|
152
|
+
});
|
|
153
|
+
});
|
|
126
154
|
});
|