@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
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ OpenClaw channel plugin that connects an agent to ClawChat over the ClawChat Pro
|
|
|
10
10
|
- Outbound text replies in `static` or `stream` mode, with a consolidated final `message.reply`
|
|
11
11
|
- Typing indicators and filtered forwarding for thinking / tool-call content
|
|
12
12
|
- Media fragments (image / file / audio / video) in either direction
|
|
13
|
-
- Invite-code onboarding via `clawchat_activate` or
|
|
13
|
+
- Invite-code onboarding via `clawchat_activate` or `/clawchat-login`, plus always-registered `clawchat_*` account/media tools
|
|
14
14
|
|
|
15
15
|
## Install
|
|
16
16
|
|
|
@@ -19,9 +19,10 @@ OpenClaw channel plugin that connects an agent to ClawChat over the ClawChat Pro
|
|
|
19
19
|
npm i @newbase-clawchat/openclaw-clawchat
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
Requires `openclaw >= 2026.4
|
|
22
|
+
Requires `openclaw >= 2026.5.4` as a peer host.
|
|
23
23
|
|
|
24
|
-
For the OpenClaw plugin install/update flow,
|
|
24
|
+
For the OpenClaw plugin install/update flow, use the R2-hosted tarball install
|
|
25
|
+
command documented in [`INSTALL.md`](./INSTALL.md).
|
|
25
26
|
|
|
26
27
|
Example LLM prompt:
|
|
27
28
|
|
|
@@ -38,15 +39,31 @@ activate ClawChat with invite code A1B2C3
|
|
|
38
39
|
```
|
|
39
40
|
|
|
40
41
|
If the plugin is already loaded by the Gateway, the activation skill calls the
|
|
41
|
-
`clawchat_activate` tool and
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
`clawchat_activate` tool and persists credentials. If you are activating from
|
|
43
|
+
Telegram or another chat platform and the agent does not call the tool, send the
|
|
44
|
+
runtime slash command in that chat:
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
/clawchat-login A1B2C3
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The slash command is provided by the loaded plugin. It is not a shell command,
|
|
51
|
+
so `openclaw clawchat-login` is expected to fail.
|
|
52
|
+
|
|
53
|
+
On OpenClaw hosts where the CLI channel catalog includes `openclaw-clawchat`,
|
|
54
|
+
terminal activation can also use:
|
|
44
55
|
|
|
45
56
|
```bash
|
|
46
|
-
|
|
57
|
+
CLAWCHAT_INVITE_CODE="A1B2C3"
|
|
58
|
+
openclaw channels add --channel openclaw-clawchat --token "$CLAWCHAT_INVITE_CODE"
|
|
47
59
|
openclaw channels status --probe
|
|
48
60
|
```
|
|
49
61
|
|
|
62
|
+
OpenClaw 2026.5.5 can load an npm-installed third-party channel while still
|
|
63
|
+
omitting it from the `channels add` CLI catalog. If `channels add` fails with
|
|
64
|
+
`Unknown channel: openclaw-clawchat`, use `clawchat_activate` or
|
|
65
|
+
`/clawchat-login A1B2C3` after a real Gateway restart.
|
|
66
|
+
|
|
50
67
|
Restart the Gateway after installing/updating the plugin, when config reload is
|
|
51
68
|
disabled, or when the channel probe does not become healthy:
|
|
52
69
|
|
|
@@ -55,17 +72,20 @@ openclaw gateway restart
|
|
|
55
72
|
```
|
|
56
73
|
|
|
57
74
|
If you run the gateway manually instead of as a service and it is not already
|
|
58
|
-
running, start it after
|
|
75
|
+
running, start it after activation:
|
|
59
76
|
|
|
60
77
|
```bash
|
|
61
78
|
openclaw gateway run
|
|
62
79
|
```
|
|
63
80
|
|
|
64
|
-
The
|
|
65
|
-
|
|
66
|
-
|
|
81
|
+
The `--token` value above is the ClawChat invite code for OpenClaw's generic
|
|
82
|
+
`channels add` CLI surface on hosts that expose this plugin in the channel
|
|
83
|
+
catalog; persisted token fields are written only after the invite code exchange
|
|
84
|
+
succeeds. The plugin registers `clawchat_activate` and the six account/media tools at plugin
|
|
85
|
+
load time so they stay visible before activation. Before activation, account/media
|
|
67
86
|
tools return a config error instead of disappearing; after activation/login, the
|
|
68
|
-
channel is enabled and the same tools read the
|
|
87
|
+
channel is enabled and the same tools read the persisted token/userId after the
|
|
88
|
+
Gateway restarts.
|
|
69
89
|
|
|
70
90
|
After activation/login, the channel section is enabled and has credentials:
|
|
71
91
|
|
|
@@ -157,6 +177,38 @@ npm run dev:openclaw-source
|
|
|
157
177
|
This checkout is local-only. It is ignored by git and is not required to run the
|
|
158
178
|
plugin tests or publish the package.
|
|
159
179
|
|
|
180
|
+
## R2 package scripts
|
|
181
|
+
|
|
182
|
+
Create and upload the OpenClaw plugin tarball to the R2 `openclaw/` prefix:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
./package_openclaw_plugin.sh
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The script runs `npm pack`, uploads the generated `.tgz` to the configured R2
|
|
189
|
+
bucket, and prints the public URL. R2 credentials are read from `.env.r2`, which
|
|
190
|
+
is ignored by git. Use `--no-upload` to build the tarball without uploading it.
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
AWS_ACCESS_KEY_ID=...
|
|
194
|
+
AWS_SECRET_ACCESS_KEY=...
|
|
195
|
+
AWS_DEFAULT_REGION=auto
|
|
196
|
+
R2_ENDPOINT=https://...
|
|
197
|
+
R2_BUCKET=...
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Install the R2-hosted tarball on a device or container with OpenClaw available:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
./install_openclaw.sh
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
To install a specific uploaded tarball, pass its URL explicitly:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
./install_openclaw.sh https://dddddddddddddtest.clawling.chat/openclaw/newbase-clawchat-openclaw-clawchat-2026.5.4-2.tgz
|
|
210
|
+
```
|
|
211
|
+
|
|
160
212
|
## License
|
|
161
213
|
|
|
162
214
|
See the repository root.
|
package/dist/index.js
CHANGED
|
@@ -1,27 +1,18 @@
|
|
|
1
|
+
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/channel-core";
|
|
1
2
|
import { openclawClawlingPlugin } from "./src/channel.js";
|
|
2
3
|
import { registerOpenclawClawlingCommands } from "./src/commands.js";
|
|
4
|
+
import { openclawClawlingConfigSchema } from "./src/config.js";
|
|
3
5
|
import { setOpenclawClawlingRuntime } from "./src/runtime.js";
|
|
4
6
|
import { registerOpenclawClawlingTools } from "./src/tools.js";
|
|
5
|
-
|
|
6
|
-
export default {
|
|
7
|
+
export default defineChannelPluginEntry({
|
|
7
8
|
id: "openclaw-clawchat",
|
|
8
9
|
name: "Clawling Chat",
|
|
9
10
|
description: "Clawling Chat Protocol v2 channel plugin (chat-sdk)",
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
plugin: openclawClawlingPlugin,
|
|
12
|
+
configSchema: { schema: openclawClawlingConfigSchema },
|
|
13
|
+
setRuntime: setOpenclawClawlingRuntime,
|
|
14
|
+
registerFull(api) {
|
|
14
15
|
registerOpenclawClawlingCommands(api);
|
|
15
16
|
registerOpenclawClawlingTools(api);
|
|
16
|
-
}
|
|
17
|
-
};
|
|
18
|
-
// export default defineChannelPluginEntry({
|
|
19
|
-
// id: "openclaw-clawchat",
|
|
20
|
-
// name: "Clawling Chat",
|
|
21
|
-
// description: "Clawling Chat Protocol v2 channel plugin (chat-sdk)",
|
|
22
|
-
// plugin: openclawClawlingPlugin,
|
|
23
|
-
// setRuntime: setOpenclawClawlingRuntime,
|
|
24
|
-
// registerFull(api) {
|
|
25
|
-
// registerOpenclawClawlingTools(api);
|
|
26
|
-
// },
|
|
27
|
-
// });
|
|
17
|
+
},
|
|
18
|
+
});
|
package/dist/src/channel.js
CHANGED
|
@@ -1,145 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createChatChannelPlugin, } from "openclaw/plugin-sdk/core";
|
|
1
|
+
import { createChatChannelPlugin } from "openclaw/plugin-sdk/core";
|
|
3
2
|
import { createEmptyChannelDirectoryAdapter } from "openclaw/plugin-sdk/directory-runtime";
|
|
4
|
-
import {
|
|
5
|
-
import { createComputedAccountStatusAdapter, createDefaultChannelRuntimeState, } from "openclaw/plugin-sdk/status-helpers";
|
|
6
|
-
import { CHANNEL_ID, listOpenclawClawlingAccountIds, mergeOpenclawClawchatToolAllow, openclawClawlingConfigSchema, resolveOpenclawClawlingAccount, } from "./config.js";
|
|
3
|
+
import { CHANNEL_ID, resolveOpenclawClawlingAccount, } from "./config.js";
|
|
7
4
|
import { openclawClawlingOutbound } from "./outbound.js";
|
|
8
5
|
import { getOpenclawClawlingRuntime, startOpenclawClawlingGateway } from "./runtime.js";
|
|
9
|
-
|
|
10
|
-
sectionKey: CHANNEL_ID,
|
|
11
|
-
resolveAccount: (cfg) => resolveOpenclawClawlingAccount(cfg),
|
|
12
|
-
listAccountIds: () => listOpenclawClawlingAccountIds(),
|
|
13
|
-
defaultAccountId: () => DEFAULT_ACCOUNT_ID,
|
|
14
|
-
deleteMode: "clear-fields",
|
|
15
|
-
clearBaseFields: [
|
|
16
|
-
"websocketUrl",
|
|
17
|
-
"baseUrl",
|
|
18
|
-
"token",
|
|
19
|
-
"userId",
|
|
20
|
-
"replyMode",
|
|
21
|
-
"forwardThinking",
|
|
22
|
-
"forwardToolCalls",
|
|
23
|
-
"richInteractions",
|
|
24
|
-
"enabled",
|
|
25
|
-
],
|
|
26
|
-
resolveAllowFrom: (account) => account.allowFrom,
|
|
27
|
-
formatAllowFrom: () => [],
|
|
28
|
-
});
|
|
29
|
-
/**
|
|
30
|
-
* Invite-code setup adapter used by OpenClaw setup surfaces that already have
|
|
31
|
-
* a concrete plugin instance. This plugin does not advertise catalog-driven
|
|
32
|
-
* one-shot setup metadata because current hosts do not discover channels from
|
|
33
|
-
* `plugins.load.paths`.
|
|
34
|
-
*
|
|
35
|
-
* Setup takes exactly ONE input: `code` (an invite code). URL + token +
|
|
36
|
-
* userId come from the login flow which is triggered automatically in
|
|
37
|
-
* `afterAccountConfigWritten`.
|
|
38
|
-
*
|
|
39
|
-
* `applyAccountConfig` itself only marks the section `enabled: true`;
|
|
40
|
-
* credentials are written by `runOpenclawClawlingLogin` via the runtime config
|
|
41
|
-
* mutator after the `/v1/agents/connect` response lands.
|
|
42
|
-
*/
|
|
43
|
-
const setupAdapter = {
|
|
44
|
-
resolveAccountId: () => DEFAULT_ACCOUNT_ID,
|
|
45
|
-
validateInput: ({ input }) => {
|
|
46
|
-
if (!input.code?.trim()) {
|
|
47
|
-
return "ClawChat invite code is required.";
|
|
48
|
-
}
|
|
49
|
-
return null;
|
|
50
|
-
},
|
|
51
|
-
applyAccountConfig: ({ cfg, }) => {
|
|
52
|
-
// Base config: just enable the channel. Credentials arrive via
|
|
53
|
-
// `afterAccountConfigWritten` → `runOpenclawClawlingLogin`.
|
|
54
|
-
const channels = (cfg.channels ?? {});
|
|
55
|
-
const current = (channels[CHANNEL_ID] ?? {});
|
|
56
|
-
return mergeOpenclawClawchatToolAllow({
|
|
57
|
-
...cfg,
|
|
58
|
-
channels: {
|
|
59
|
-
...channels,
|
|
60
|
-
[CHANNEL_ID]: { ...current, enabled: true },
|
|
61
|
-
},
|
|
62
|
-
});
|
|
63
|
-
},
|
|
64
|
-
afterAccountConfigWritten: async ({ cfg, input, runtime, }) => {
|
|
65
|
-
const code = input.code?.trim();
|
|
66
|
-
if (!code)
|
|
67
|
-
return;
|
|
68
|
-
// Lazy-import the login runtime to keep @clack/prompts / readline /
|
|
69
|
-
// config-runtime off the plugin's cold-start path. `readInviteCode`
|
|
70
|
-
// feeds the fixed code so the stdin prompt is skipped entirely.
|
|
71
|
-
const { runOpenclawClawlingLogin } = await import("./login.runtime.js");
|
|
72
|
-
await runOpenclawClawlingLogin({
|
|
73
|
-
cfg,
|
|
74
|
-
accountId: null,
|
|
75
|
-
runtime: { log: (message) => runtime.log(message) },
|
|
76
|
-
readInviteCode: async () => code,
|
|
77
|
-
mutateConfigFile: getOpenclawClawlingRuntime().config.mutateConfigFile,
|
|
78
|
-
});
|
|
79
|
-
},
|
|
80
|
-
};
|
|
6
|
+
import { openclawClawlingSetupPlugin } from "./channel.setup.js";
|
|
81
7
|
export const openclawClawlingPlugin = createChatChannelPlugin({
|
|
82
8
|
base: {
|
|
83
|
-
|
|
84
|
-
meta: {
|
|
85
|
-
id: CHANNEL_ID,
|
|
86
|
-
label: "Clawling Chat",
|
|
87
|
-
selectionLabel: "Clawling Chat",
|
|
88
|
-
docsPath: "/channels/openclaw-clawchat",
|
|
89
|
-
docsLabel: "openclaw-clawchat",
|
|
90
|
-
blurb: "Clawling Protocol v2 over WebSocket (chat-sdk).",
|
|
91
|
-
order: 110,
|
|
92
|
-
},
|
|
93
|
-
capabilities: {
|
|
94
|
-
chatTypes: ["direct", "group"],
|
|
95
|
-
media: true,
|
|
96
|
-
reactions: false,
|
|
97
|
-
threads: false,
|
|
98
|
-
polls: false,
|
|
99
|
-
blockStreaming: true,
|
|
100
|
-
},
|
|
101
|
-
reload: {
|
|
102
|
-
configPrefixes: [`channels.${CHANNEL_ID}`],
|
|
103
|
-
},
|
|
104
|
-
configSchema: {
|
|
105
|
-
schema: openclawClawlingConfigSchema,
|
|
106
|
-
},
|
|
107
|
-
config: {
|
|
108
|
-
...configAdapter,
|
|
109
|
-
isConfigured: (account) => account.configured,
|
|
110
|
-
describeAccount: (account) => ({
|
|
111
|
-
accountId: account.accountId,
|
|
112
|
-
name: account.name,
|
|
113
|
-
enabled: account.enabled,
|
|
114
|
-
configured: account.configured,
|
|
115
|
-
}),
|
|
116
|
-
},
|
|
9
|
+
...openclawClawlingSetupPlugin,
|
|
117
10
|
directory: createEmptyChannelDirectoryAdapter(),
|
|
118
|
-
setup: setupAdapter,
|
|
119
|
-
status: createComputedAccountStatusAdapter({
|
|
120
|
-
defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID, {
|
|
121
|
-
connected: false,
|
|
122
|
-
lastInboundAt: null,
|
|
123
|
-
lastOutboundAt: null,
|
|
124
|
-
}),
|
|
125
|
-
resolveAccountSnapshot: ({ account }) => ({
|
|
126
|
-
accountId: account.accountId,
|
|
127
|
-
name: account.name,
|
|
128
|
-
enabled: account.enabled,
|
|
129
|
-
configured: account.configured,
|
|
130
|
-
extra: {
|
|
131
|
-
websocketUrl: account.websocketUrl || null,
|
|
132
|
-
baseUrl: account.baseUrl || null,
|
|
133
|
-
userId: account.userId || null,
|
|
134
|
-
},
|
|
135
|
-
}),
|
|
136
|
-
}),
|
|
137
11
|
auth: {
|
|
138
12
|
login: async ({ cfg, accountId, runtime }) => {
|
|
139
|
-
// Lazy-load login.runtime: it pulls in @clack/prompts and other
|
|
140
|
-
// heavy modules that have no business loading on every plugin
|
|
141
|
-
// boot. Only the rare `openclaw channels login --channel
|
|
142
|
-
// openclaw-clawchat` invocation pays the import cost.
|
|
143
13
|
const { runOpenclawClawlingLogin } = await import("./login.runtime.js");
|
|
144
14
|
await runOpenclawClawlingLogin({
|
|
145
15
|
cfg,
|
|
@@ -152,17 +22,24 @@ export const openclawClawlingPlugin = createChatChannelPlugin({
|
|
|
152
22
|
gateway: {
|
|
153
23
|
startAccount: async (ctx) => {
|
|
154
24
|
const account = ctx.account ?? resolveOpenclawClawlingAccount(ctx.cfg);
|
|
25
|
+
ctx.log?.info?.(`[${account.accountId}] openclaw-clawchat lifecycle startAccount invoked configured=${account.configured} enabled=${account.enabled} hasToken=${Boolean(account.token)} hasUserId=${Boolean(account.userId)} websocketUrl=${account.websocketUrl || "(empty)"}`);
|
|
155
26
|
if (!account.configured) {
|
|
27
|
+
ctx.log?.error?.(`[${account.accountId}] openclaw-clawchat lifecycle startAccount refused: websocketUrl/token/userId are required`);
|
|
156
28
|
throw new Error("Clawling Chat websocketUrl/token/userId are required");
|
|
157
29
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
30
|
+
try {
|
|
31
|
+
await startOpenclawClawlingGateway({
|
|
32
|
+
cfg: ctx.cfg,
|
|
33
|
+
account,
|
|
34
|
+
abortSignal: ctx.abortSignal,
|
|
35
|
+
setStatus: ctx.setStatus,
|
|
36
|
+
getStatus: ctx.getStatus,
|
|
37
|
+
log: ctx.log,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
ctx.log?.info?.(`[${account.accountId}] openclaw-clawchat lifecycle startAccount completed/stopped`);
|
|
42
|
+
}
|
|
166
43
|
},
|
|
167
44
|
},
|
|
168
45
|
agentPrompt: {
|
|
@@ -176,6 +53,7 @@ export const openclawClawlingPlugin = createChatChannelPlugin({
|
|
|
176
53
|
],
|
|
177
54
|
},
|
|
178
55
|
messaging: {
|
|
56
|
+
targetPrefixes: ["cc", "clawchat", CHANNEL_ID],
|
|
179
57
|
normalizeTarget: (target) => target
|
|
180
58
|
.trim()
|
|
181
59
|
.replace(/^openclaw-clawchat:/i, "")
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { createTopLevelChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers";
|
|
2
|
+
import { mutateConfigFile } from "openclaw/plugin-sdk/config-mutation";
|
|
3
|
+
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
|
|
4
|
+
import { createComputedAccountStatusAdapter, createDefaultChannelRuntimeState, } from "openclaw/plugin-sdk/status-helpers";
|
|
5
|
+
import { CHANNEL_ID, listOpenclawClawlingAccountIds, mergeOpenclawClawchatToolAllow, openclawClawlingConfigSchema, resolveOpenclawClawlingAccount, } from "./config.js";
|
|
6
|
+
const configAdapter = createTopLevelChannelConfigAdapter({
|
|
7
|
+
sectionKey: CHANNEL_ID,
|
|
8
|
+
resolveAccount: (cfg) => resolveOpenclawClawlingAccount(cfg),
|
|
9
|
+
listAccountIds: () => listOpenclawClawlingAccountIds(),
|
|
10
|
+
defaultAccountId: () => DEFAULT_ACCOUNT_ID,
|
|
11
|
+
deleteMode: "clear-fields",
|
|
12
|
+
clearBaseFields: [
|
|
13
|
+
"websocketUrl",
|
|
14
|
+
"baseUrl",
|
|
15
|
+
"token",
|
|
16
|
+
"userId",
|
|
17
|
+
"replyMode",
|
|
18
|
+
"forwardThinking",
|
|
19
|
+
"forwardToolCalls",
|
|
20
|
+
"richInteractions",
|
|
21
|
+
"enabled",
|
|
22
|
+
],
|
|
23
|
+
resolveAllowFrom: (account) => account.allowFrom,
|
|
24
|
+
formatAllowFrom: () => [],
|
|
25
|
+
});
|
|
26
|
+
/**
|
|
27
|
+
* Invite-code setup adapter used by OpenClaw setup surfaces.
|
|
28
|
+
*
|
|
29
|
+
* `channels add --token` passes the invite code as setup input. The first
|
|
30
|
+
* config write only enables the channel; `afterAccountConfigWritten` exchanges
|
|
31
|
+
* the invite code and persists token/userId through the host runtime mutator.
|
|
32
|
+
*/
|
|
33
|
+
const setupAdapter = {
|
|
34
|
+
resolveAccountId: () => DEFAULT_ACCOUNT_ID,
|
|
35
|
+
validateInput: ({ input }) => {
|
|
36
|
+
const inviteCode = typeof input.code === "string" && input.code.trim()
|
|
37
|
+
? input.code.trim()
|
|
38
|
+
: typeof input.token === "string"
|
|
39
|
+
? input.token.trim()
|
|
40
|
+
: "";
|
|
41
|
+
if (!inviteCode) {
|
|
42
|
+
return "ClawChat invite code is required.";
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
},
|
|
46
|
+
applyAccountConfig: ({ cfg }) => {
|
|
47
|
+
const channels = (cfg.channels ?? {});
|
|
48
|
+
const current = (channels[CHANNEL_ID] ?? {});
|
|
49
|
+
return mergeOpenclawClawchatToolAllow({
|
|
50
|
+
...cfg,
|
|
51
|
+
channels: {
|
|
52
|
+
...channels,
|
|
53
|
+
[CHANNEL_ID]: { ...current, enabled: true },
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
afterAccountConfigWritten: async ({ cfg, input, runtime }) => {
|
|
58
|
+
runtime.log("[default] openclaw-clawchat setup afterAccountConfigWritten invoked");
|
|
59
|
+
const code = typeof input.code === "string" && input.code.trim()
|
|
60
|
+
? input.code.trim()
|
|
61
|
+
: typeof input.token === "string"
|
|
62
|
+
? input.token.trim()
|
|
63
|
+
: "";
|
|
64
|
+
if (!code) {
|
|
65
|
+
runtime.log("[default] openclaw-clawchat setup afterAccountConfigWritten skipped: empty invite code");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const { runOpenclawClawlingLogin } = await import("./login.runtime.js");
|
|
69
|
+
await runOpenclawClawlingLogin({
|
|
70
|
+
cfg,
|
|
71
|
+
accountId: null,
|
|
72
|
+
runtime: { log: (message) => runtime.log(message) },
|
|
73
|
+
readInviteCode: async () => code,
|
|
74
|
+
mutateConfigFile: mutateConfigFile,
|
|
75
|
+
});
|
|
76
|
+
runtime.log("[default] openclaw-clawchat setup afterAccountConfigWritten completed");
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
export const openclawClawlingSetupPlugin = {
|
|
80
|
+
id: CHANNEL_ID,
|
|
81
|
+
meta: {
|
|
82
|
+
id: CHANNEL_ID,
|
|
83
|
+
label: "Clawling Chat",
|
|
84
|
+
selectionLabel: "Clawling Chat",
|
|
85
|
+
docsPath: "/channels/openclaw-clawchat",
|
|
86
|
+
docsLabel: "openclaw-clawchat",
|
|
87
|
+
blurb: "Clawling Protocol v2 over WebSocket (chat-sdk).",
|
|
88
|
+
order: 110,
|
|
89
|
+
},
|
|
90
|
+
capabilities: {
|
|
91
|
+
chatTypes: ["direct", "group"],
|
|
92
|
+
media: true,
|
|
93
|
+
reactions: false,
|
|
94
|
+
threads: false,
|
|
95
|
+
polls: false,
|
|
96
|
+
blockStreaming: true,
|
|
97
|
+
},
|
|
98
|
+
reload: {
|
|
99
|
+
configPrefixes: [`channels.${CHANNEL_ID}`],
|
|
100
|
+
},
|
|
101
|
+
configSchema: {
|
|
102
|
+
schema: openclawClawlingConfigSchema,
|
|
103
|
+
},
|
|
104
|
+
config: {
|
|
105
|
+
...configAdapter,
|
|
106
|
+
isConfigured: (account) => account.configured,
|
|
107
|
+
describeAccount: (account) => ({
|
|
108
|
+
accountId: account.accountId,
|
|
109
|
+
name: account.name,
|
|
110
|
+
enabled: account.enabled,
|
|
111
|
+
configured: account.configured,
|
|
112
|
+
}),
|
|
113
|
+
},
|
|
114
|
+
setup: setupAdapter,
|
|
115
|
+
status: createComputedAccountStatusAdapter({
|
|
116
|
+
defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID, {
|
|
117
|
+
connected: false,
|
|
118
|
+
lastInboundAt: null,
|
|
119
|
+
lastOutboundAt: null,
|
|
120
|
+
}),
|
|
121
|
+
resolveAccountSnapshot: ({ account }) => ({
|
|
122
|
+
accountId: account.accountId,
|
|
123
|
+
name: account.name,
|
|
124
|
+
enabled: account.enabled,
|
|
125
|
+
configured: account.configured,
|
|
126
|
+
extra: {
|
|
127
|
+
websocketUrl: account.websocketUrl || null,
|
|
128
|
+
baseUrl: account.baseUrl || null,
|
|
129
|
+
userId: account.userId || null,
|
|
130
|
+
},
|
|
131
|
+
}),
|
|
132
|
+
}),
|
|
133
|
+
};
|
package/dist/src/client.js
CHANGED
|
@@ -44,12 +44,19 @@ function normalizeRouting(params) {
|
|
|
44
44
|
* Emit a raw v2 envelope directly over the transport so we can carry top-level
|
|
45
45
|
* `chat_id` routing without SDK-injected `to` metadata.
|
|
46
46
|
*/
|
|
47
|
-
function emitEnvelope(client, event, payload, routing) {
|
|
47
|
+
function emitEnvelope(client, event, payload, routing, options = {}) {
|
|
48
48
|
const inner = client;
|
|
49
|
-
if (!inner.
|
|
50
|
-
inner.emitRaw
|
|
49
|
+
if (!options.forceRawTransport && inner.emitRaw) {
|
|
50
|
+
inner.emitRaw(event, payload, { chat_id: routing.chatId });
|
|
51
51
|
return;
|
|
52
52
|
}
|
|
53
|
+
if (!inner.opts?.transport) {
|
|
54
|
+
if (!options.forceRawTransport && inner.emitRaw) {
|
|
55
|
+
inner.emitRaw(event, payload, { chat_id: routing.chatId });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
throw new Error("openclaw-clawchat streaming emit requires SDK raw transport");
|
|
59
|
+
}
|
|
53
60
|
const env = {
|
|
54
61
|
version: "2",
|
|
55
62
|
event,
|
|
@@ -150,7 +157,7 @@ export function emitFinalStreamReply(client, params) {
|
|
|
150
157
|
},
|
|
151
158
|
},
|
|
152
159
|
},
|
|
153
|
-
}, routing);
|
|
160
|
+
}, routing, { forceRawTransport: true });
|
|
154
161
|
}
|
|
155
162
|
export function emitStreamFailed(client, params) {
|
|
156
163
|
const now = Date.now();
|
package/dist/src/config.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
|
|
2
2
|
export const CHANNEL_ID = "openclaw-clawchat";
|
|
3
|
+
export const CLAWCHAT_TOKEN_ENV = "CLAWCHAT_TOKEN";
|
|
4
|
+
export const CLAWCHAT_USER_ID_ENV = "CLAWCHAT_USER_ID";
|
|
5
|
+
export const CLAWCHAT_REFRESH_TOKEN_ENV = "CLAWCHAT_REFRESH_TOKEN";
|
|
6
|
+
export const CLAWCHAT_BASE_URL_ENV = "CLAWCHAT_BASE_URL";
|
|
7
|
+
export const CLAWCHAT_WEBSOCKET_URL_ENV = "CLAWCHAT_WEBSOCKET_URL";
|
|
3
8
|
/**
|
|
4
9
|
* Built-in defaults for the Clawling Chat endpoints so `openclaw channel
|
|
5
10
|
* login` works out of the box without requiring a prior `openclaw channel
|
|
@@ -134,6 +139,9 @@ function readChannelSection(cfg) {
|
|
|
134
139
|
function readOptionalString(value) {
|
|
135
140
|
return typeof value === "string" ? value.trim() : "";
|
|
136
141
|
}
|
|
142
|
+
function readEnvString(env, key) {
|
|
143
|
+
return readOptionalString(env[key]);
|
|
144
|
+
}
|
|
137
145
|
function readReplyMode(value) {
|
|
138
146
|
return value === "stream" ? "stream" : "static";
|
|
139
147
|
}
|
|
@@ -175,13 +183,17 @@ function readAck(raw) {
|
|
|
175
183
|
: DEFAULT_ACK.autoResendOnTimeout,
|
|
176
184
|
};
|
|
177
185
|
}
|
|
178
|
-
export function resolveOpenclawClawlingAccount(cfg) {
|
|
186
|
+
export function resolveOpenclawClawlingAccount(cfg, env = process.env) {
|
|
179
187
|
const channel = readChannelSection(cfg);
|
|
180
188
|
// Apply built-in defaults so login/gateway work without prior setup.
|
|
181
|
-
const websocketUrl = readOptionalString(channel.websocketUrl) ||
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const
|
|
189
|
+
const websocketUrl = readOptionalString(channel.websocketUrl) ||
|
|
190
|
+
readEnvString(env, CLAWCHAT_WEBSOCKET_URL_ENV) ||
|
|
191
|
+
DEFAULT_WEBSOCKET_URL;
|
|
192
|
+
const baseUrl = readOptionalString(channel.baseUrl) ||
|
|
193
|
+
readEnvString(env, CLAWCHAT_BASE_URL_ENV) ||
|
|
194
|
+
DEFAULT_BASE_URL;
|
|
195
|
+
const token = readOptionalString(channel.token) || readEnvString(env, CLAWCHAT_TOKEN_ENV);
|
|
196
|
+
const userId = readOptionalString(channel.userId) || readEnvString(env, CLAWCHAT_USER_ID_ENV);
|
|
185
197
|
const enabled = typeof channel.enabled === "boolean" ? channel.enabled : true;
|
|
186
198
|
const replyMode = readReplyMode(channel.replyMode);
|
|
187
199
|
const groupMode = readGroupMode(channel.groupMode);
|
|
@@ -54,22 +54,31 @@ function buildLoginConfig(cfg, result) {
|
|
|
54
54
|
}
|
|
55
55
|
async function persistLoginConfig(params, result) {
|
|
56
56
|
if (params.mutateConfigFile) {
|
|
57
|
+
params.runtime.log(`Persisting ClawChat credentials for userId=${result.agent.user_id} with Gateway restart intent.`);
|
|
57
58
|
await params.mutateConfigFile({
|
|
58
|
-
afterWrite: {
|
|
59
|
+
afterWrite: {
|
|
60
|
+
mode: "restart",
|
|
61
|
+
reason: "openclaw-clawchat credentials changed",
|
|
62
|
+
},
|
|
59
63
|
mutate(draft) {
|
|
60
64
|
Object.assign(draft, buildLoginConfig(draft, result));
|
|
61
65
|
},
|
|
62
66
|
});
|
|
67
|
+
params.runtime.log(`ClawChat credentials persisted for userId=${result.agent.user_id}.`);
|
|
63
68
|
return;
|
|
64
69
|
}
|
|
65
70
|
if (params.persistConfig) {
|
|
71
|
+
params.runtime.log(`Persisting ClawChat credentials for userId=${result.agent.user_id}.`);
|
|
66
72
|
await params.persistConfig(buildLoginConfig(params.cfg, result));
|
|
73
|
+
params.runtime.log(`ClawChat credentials persisted for userId=${result.agent.user_id}.`);
|
|
67
74
|
return;
|
|
68
75
|
}
|
|
69
76
|
throw new Error("openclaw-clawchat: mutateConfigFile is required to persist login credentials");
|
|
70
77
|
}
|
|
71
78
|
/**
|
|
72
|
-
* Run the
|
|
79
|
+
* Run the invite-code credential exchange used by `clawchat_activate`,
|
|
80
|
+
* `openclaw channels add --channel openclaw-clawchat --token <invite-code>`,
|
|
81
|
+
* and `openclaw channels login --channel openclaw-clawchat`:
|
|
73
82
|
* 1. Read the existing channel section; require `baseUrl` to be set so we
|
|
74
83
|
* know which server to hit.
|
|
75
84
|
* 2. Prompt the user for an invite code on stdin.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildOutboundMediaLoadOptions, } from "openclaw/plugin-sdk/media-runtime";
|
|
1
2
|
export function inferMediaKindFromMime(mime) {
|
|
2
3
|
if (!mime)
|
|
3
4
|
return "file";
|
|
@@ -56,12 +57,12 @@ export async function uploadOutboundMedia(urls, ctx) {
|
|
|
56
57
|
const out = [];
|
|
57
58
|
for (const url of urls) {
|
|
58
59
|
try {
|
|
59
|
-
const loaded = await ctx.runtime.media.loadWebMedia(url, {
|
|
60
|
+
const loaded = await ctx.runtime.media.loadWebMedia(url, buildOutboundMediaLoadOptions({
|
|
60
61
|
maxBytes,
|
|
61
|
-
...(ctx.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
});
|
|
62
|
+
...(ctx.mediaAccess ? { mediaAccess: ctx.mediaAccess } : {}),
|
|
63
|
+
...(ctx.mediaLocalRoots ? { mediaLocalRoots: ctx.mediaLocalRoots } : {}),
|
|
64
|
+
...(ctx.mediaReadFile ? { mediaReadFile: ctx.mediaReadFile } : {}),
|
|
65
|
+
}));
|
|
65
66
|
const uploaded = await ctx.apiClient.uploadMedia({
|
|
66
67
|
buffer: loaded.buffer,
|
|
67
68
|
filename: loaded.fileName ?? "upload.bin",
|
package/dist/src/outbound.js
CHANGED
|
@@ -19,6 +19,8 @@ import { getOpenclawClawlingClient, getOpenclawClawlingRuntime, waitForOpenclawC
|
|
|
19
19
|
* - `clawchat:group:{chat_id}` → group
|
|
20
20
|
* - `openclaw-clawchat:direct:{chat_id}` → direct
|
|
21
21
|
* - `openclaw-clawchat:group:{chat_id}` → group
|
|
22
|
+
* - `direct:{chat_id}` → direct (host-normalized)
|
|
23
|
+
* - `group:{chat_id}` → group (host-normalized)
|
|
22
24
|
* - bare `{chat_id}` → direct (backward compat)
|
|
23
25
|
*/
|
|
24
26
|
export function parseOpenclawRecipient(to) {
|
|
@@ -30,6 +32,12 @@ export function parseOpenclawRecipient(to) {
|
|
|
30
32
|
return { chatId: raw, chatType: "direct" };
|
|
31
33
|
const scheme = raw.slice(0, firstColon).toLowerCase();
|
|
32
34
|
const rest = raw.slice(firstColon + 1);
|
|
35
|
+
if (scheme === "direct" || scheme === "group") {
|
|
36
|
+
const chatId = rest.trim();
|
|
37
|
+
if (!chatId)
|
|
38
|
+
throw new Error(`openclaw-clawchat: missing chat_id in "${to}"`);
|
|
39
|
+
return { chatId, chatType: scheme };
|
|
40
|
+
}
|
|
33
41
|
if (scheme !== "cc" && scheme !== "clawchat" && scheme !== CHANNEL_ID) {
|
|
34
42
|
return { chatId: raw, chatType: "direct" };
|
|
35
43
|
}
|
|
@@ -74,7 +82,7 @@ export async function sendOpenclawClawlingText(params) {
|
|
|
74
82
|
mode: "normal",
|
|
75
83
|
replyTo: {
|
|
76
84
|
msgId: params.replyCtx.replyToMessageId,
|
|
77
|
-
senderId: params.replyCtx.
|
|
85
|
+
senderId: params.replyCtx.replyPreviewSenderId,
|
|
78
86
|
nickName: params.replyCtx.replyPreviewNickName,
|
|
79
87
|
fragments: [{ kind: "text", text: params.replyCtx.replyPreviewText }],
|
|
80
88
|
},
|
|
@@ -144,7 +152,7 @@ export const openclawClawlingOutbound = {
|
|
|
144
152
|
messageId: result?.messageId ?? `${account.userId}-${Date.now()}`,
|
|
145
153
|
};
|
|
146
154
|
},
|
|
147
|
-
sendMedia: async ({ cfg, to, text, mediaUrl, mediaLocalRoots }) => {
|
|
155
|
+
sendMedia: async ({ cfg, to, text, mediaUrl, mediaAccess, mediaLocalRoots, mediaReadFile }) => {
|
|
148
156
|
const account = resolveOpenclawClawlingAccount(cfg);
|
|
149
157
|
const client = getOpenclawClawlingClient(account.accountId) ??
|
|
150
158
|
(await waitForOpenclawClawlingClient(account.accountId));
|
|
@@ -160,7 +168,9 @@ export const openclawClawlingOutbound = {
|
|
|
160
168
|
const mediaFragments = await uploadOutboundMedia([mediaUrl.trim()], {
|
|
161
169
|
apiClient,
|
|
162
170
|
runtime,
|
|
171
|
+
...(mediaAccess ? { mediaAccess } : {}),
|
|
163
172
|
...(mediaLocalRoots ? { mediaLocalRoots } : {}),
|
|
173
|
+
...(mediaReadFile ? { mediaReadFile } : {}),
|
|
164
174
|
});
|
|
165
175
|
if (mediaFragments.length === 0) {
|
|
166
176
|
throw new Error(`openclaw-clawchat failed to upload media: ${mediaUrl}`);
|