@newbase-clawchat/openclaw-clawchat 2026.4.24 → 2026.4.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -16
- 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/index.ts +2 -1
- package/openclaw.plugin.json +81 -1
- package/package.json +21 -9
- package/skills/clawchat-account-tools/SKILL.md +26 -0
- package/skills/clawchat-activate/SKILL.md +47 -0
- package/src/api-client.test.ts +6 -5
- package/src/api-client.ts +8 -3
- package/src/buffered-stream.test.ts +14 -4
- package/src/buffered-stream.ts +19 -11
- package/src/channel.outbound.test.ts +49 -35
- package/src/channel.test.ts +45 -10
- package/src/channel.ts +26 -17
- package/src/client.test.ts +9 -1
- package/src/client.ts +48 -21
- package/src/commands.test.ts +39 -0
- package/src/commands.ts +41 -0
- package/src/config.test.ts +40 -3
- package/src/config.ts +60 -4
- package/src/inbound.test.ts +9 -6
- package/src/inbound.ts +51 -16
- package/src/login.runtime.test.ts +142 -3
- package/src/login.runtime.ts +59 -26
- package/src/manifest.test.ts +183 -5
- package/src/outbound.test.ts +10 -7
- package/src/outbound.ts +8 -7
- package/src/plugin-entry.test.ts +27 -0
- package/src/protocol.ts +5 -0
- package/src/reply-dispatcher.test.ts +420 -3
- package/src/reply-dispatcher.ts +137 -12
- package/src/runtime.test.ts +23 -7
- package/src/runtime.ts +13 -1
- package/src/streaming.test.ts +12 -9
- package/src/streaming.ts +22 -12
- package/src/tools-schema.ts +28 -19
- package/src/tools.test.ts +181 -40
- package/src/tools.ts +107 -95
package/src/channel.test.ts
CHANGED
|
@@ -17,20 +17,20 @@ describe("openclaw-clawchat plugin", () => {
|
|
|
17
17
|
expect(openclawClawlingPlugin.capabilities.media).toBe(true);
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
-
it("setup.validateInput requires
|
|
20
|
+
it("setup.validateInput requires an invite code", () => {
|
|
21
21
|
const validate = openclawClawlingPlugin.setup?.validateInput as
|
|
22
22
|
| ((args: { cfg: unknown; accountId: string; input: Record<string, unknown> }) => string | null)
|
|
23
23
|
| undefined;
|
|
24
24
|
expect(validate).toBeDefined();
|
|
25
25
|
expect(validate!({ cfg: {}, accountId: "default", input: {} })).toMatch(
|
|
26
|
-
|
|
26
|
+
/invite code is required/i,
|
|
27
27
|
);
|
|
28
28
|
expect(
|
|
29
29
|
validate!({ cfg: {}, accountId: "default", input: { code: " " } }),
|
|
30
|
-
).toMatch(
|
|
30
|
+
).toMatch(/invite code is required/i);
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
-
it("setup.validateInput passes when
|
|
33
|
+
it("setup.validateInput passes when code is present", () => {
|
|
34
34
|
const validate = openclawClawlingPlugin.setup?.validateInput as (args: {
|
|
35
35
|
cfg: unknown;
|
|
36
36
|
accountId: string;
|
|
@@ -62,17 +62,52 @@ describe("openclaw-clawchat plugin", () => {
|
|
|
62
62
|
expect(section.baseUrl).toBeUndefined();
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
+
it("setup.applyAccountConfig allows openclaw-clawchat plugin tools without replacing policy", () => {
|
|
66
|
+
const apply = openclawClawlingPlugin.setup?.applyAccountConfig as (args: {
|
|
67
|
+
cfg: unknown;
|
|
68
|
+
accountId: string;
|
|
69
|
+
input: Record<string, unknown>;
|
|
70
|
+
}) => Record<string, unknown>;
|
|
71
|
+
const next = apply({
|
|
72
|
+
cfg: {
|
|
73
|
+
tools: {
|
|
74
|
+
profile: "coding",
|
|
75
|
+
allow: [],
|
|
76
|
+
deny: ["exec"],
|
|
77
|
+
alsoAllow: ["browser"],
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
accountId: "default",
|
|
81
|
+
input: { code: "INV-XXXX" },
|
|
82
|
+
}) as { tools: Record<string, unknown> };
|
|
83
|
+
|
|
84
|
+
expect(next.tools.profile).toBe("coding");
|
|
85
|
+
expect(next.tools.allow).toEqual([]);
|
|
86
|
+
expect(next.tools.deny).toEqual(["exec"]);
|
|
87
|
+
expect(next.tools.alsoAllow).toEqual(["browser", "openclaw-clawchat"]);
|
|
88
|
+
});
|
|
89
|
+
|
|
65
90
|
it("publishes clawchat-specific agentPrompt hints", () => {
|
|
66
91
|
const hints = openclawClawlingPlugin.agentPrompt?.messageToolHints?.({
|
|
67
92
|
cfg: {} as never,
|
|
68
93
|
accountId: "default",
|
|
69
94
|
});
|
|
70
|
-
expect(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
95
|
+
expect(hints).toEqual([
|
|
96
|
+
"To send an image or file to the current chat, use the message tool with action='send' and set 'media' to a local file path or a remote URL.",
|
|
97
|
+
"When the user asks you to find an image from the web, find a suitable HTTPS image URL and send it using the message tool with 'media' set to that URL — do NOT download the image first.",
|
|
98
|
+
"For configured ClawChat account profile, user profile, friends, avatar, or standalone media upload/share-link workflows, use `clawchat-account-tools` for tool-selection details.",
|
|
99
|
+
"For ClawChat account avatar changes using a local image, call `clawchat_upload_avatar_image` first, then `clawchat_update_account_profile` with `avatar_url`.",
|
|
100
|
+
"- Targeting: omit `target` to reply here; for a different chat use `target=\"cc:{chat_id}\"` for direct or `target=\"cc:group:{chat_id}\"` for group.",
|
|
101
|
+
"- ClawChat supports image / file / audio / video media alongside text.",
|
|
102
|
+
]);
|
|
103
|
+
const joined = hints!.join("\n");
|
|
104
|
+
expect(joined).not.toMatch(/clawchat_get_account_profile/);
|
|
105
|
+
expect(joined).not.toMatch(/clawchat_get_user_profile/);
|
|
106
|
+
expect(joined).not.toMatch(/clawchat_list_account_friends/);
|
|
107
|
+
expect(joined).not.toMatch(/clawchat_upload_media_file/);
|
|
108
|
+
expect(joined).not.toMatch(/stream mode/i);
|
|
109
|
+
expect(joined).not.toMatch(/clawchat:/);
|
|
110
|
+
expect(joined).not.toMatch(/specify 'to'/);
|
|
76
111
|
});
|
|
77
112
|
|
|
78
113
|
it("normalizes openclaw-clawchat targets for host resolution", () => {
|
package/src/channel.ts
CHANGED
|
@@ -14,12 +14,14 @@ import {
|
|
|
14
14
|
import {
|
|
15
15
|
CHANNEL_ID,
|
|
16
16
|
listOpenclawClawlingAccountIds,
|
|
17
|
+
mergeOpenclawClawchatToolAllow,
|
|
17
18
|
openclawClawlingConfigSchema,
|
|
18
19
|
resolveOpenclawClawlingAccount,
|
|
19
20
|
type ResolvedOpenclawClawlingAccount,
|
|
20
21
|
} from "./config.ts";
|
|
22
|
+
import type { OpenclawClawchatMutateConfigFile } from "./login.runtime.ts";
|
|
21
23
|
import { openclawClawlingOutbound } from "./outbound.ts";
|
|
22
|
-
import { startOpenclawClawlingGateway } from "./runtime.ts";
|
|
24
|
+
import { getOpenclawClawlingRuntime, startOpenclawClawlingGateway } from "./runtime.ts";
|
|
23
25
|
|
|
24
26
|
const configAdapter = createTopLevelChannelConfigAdapter<ResolvedOpenclawClawlingAccount>({
|
|
25
27
|
sectionKey: CHANNEL_ID,
|
|
@@ -35,6 +37,7 @@ const configAdapter = createTopLevelChannelConfigAdapter<ResolvedOpenclawClawlin
|
|
|
35
37
|
"replyMode",
|
|
36
38
|
"forwardThinking",
|
|
37
39
|
"forwardToolCalls",
|
|
40
|
+
"richInteractions",
|
|
38
41
|
"enabled",
|
|
39
42
|
],
|
|
40
43
|
resolveAllowFrom: (account) => account.allowFrom,
|
|
@@ -42,23 +45,24 @@ const configAdapter = createTopLevelChannelConfigAdapter<ResolvedOpenclawClawlin
|
|
|
42
45
|
});
|
|
43
46
|
|
|
44
47
|
/**
|
|
45
|
-
*
|
|
48
|
+
* Invite-code setup adapter used by OpenClaw setup surfaces that already have
|
|
49
|
+
* a concrete plugin instance. This plugin does not advertise catalog-driven
|
|
50
|
+
* one-shot setup metadata because current hosts do not discover channels from
|
|
51
|
+
* `plugins.load.paths`.
|
|
46
52
|
*
|
|
47
53
|
* Setup takes exactly ONE input: `code` (an invite code). URL + token +
|
|
48
54
|
* userId come from the login flow which is triggered automatically in
|
|
49
|
-
* `afterAccountConfigWritten
|
|
50
|
-
*
|
|
51
|
-
* openclaw channels setup --channel openclaw-clawchat --code INV-XXXX
|
|
55
|
+
* `afterAccountConfigWritten`.
|
|
52
56
|
*
|
|
53
57
|
* `applyAccountConfig` itself only marks the section `enabled: true`;
|
|
54
|
-
* credentials are written by `runOpenclawClawlingLogin`
|
|
55
|
-
*
|
|
58
|
+
* credentials are written by `runOpenclawClawlingLogin` via the runtime config
|
|
59
|
+
* mutator after the `/v1/agents/connect` response lands.
|
|
56
60
|
*/
|
|
57
61
|
const setupAdapter = {
|
|
58
62
|
resolveAccountId: () => DEFAULT_ACCOUNT_ID,
|
|
59
63
|
validateInput: ({ input }: { cfg: unknown; accountId: string; input: ChannelSetupInput }) => {
|
|
60
64
|
if (!input.code?.trim()) {
|
|
61
|
-
return "
|
|
65
|
+
return "ClawChat invite code is required.";
|
|
62
66
|
}
|
|
63
67
|
return null;
|
|
64
68
|
},
|
|
@@ -73,13 +77,13 @@ const setupAdapter = {
|
|
|
73
77
|
// `afterAccountConfigWritten` → `runOpenclawClawlingLogin`.
|
|
74
78
|
const channels = (cfg.channels ?? {}) as Record<string, unknown>;
|
|
75
79
|
const current = (channels[CHANNEL_ID] ?? {}) as Record<string, unknown>;
|
|
76
|
-
return {
|
|
80
|
+
return mergeOpenclawClawchatToolAllow({
|
|
77
81
|
...cfg,
|
|
78
82
|
channels: {
|
|
79
83
|
...channels,
|
|
80
84
|
[CHANNEL_ID]: { ...current, enabled: true },
|
|
81
85
|
},
|
|
82
|
-
};
|
|
86
|
+
});
|
|
83
87
|
},
|
|
84
88
|
afterAccountConfigWritten: async ({
|
|
85
89
|
cfg,
|
|
@@ -103,6 +107,9 @@ const setupAdapter = {
|
|
|
103
107
|
accountId: null,
|
|
104
108
|
runtime: { log: (message: string) => runtime.log(message) },
|
|
105
109
|
readInviteCode: async () => code,
|
|
110
|
+
mutateConfigFile: (getOpenclawClawlingRuntime().config as unknown as {
|
|
111
|
+
mutateConfigFile: OpenclawClawchatMutateConfigFile;
|
|
112
|
+
}).mutateConfigFile,
|
|
106
113
|
});
|
|
107
114
|
},
|
|
108
115
|
};
|
|
@@ -175,6 +182,9 @@ export const openclawClawlingPlugin: ChannelPlugin<ResolvedOpenclawClawlingAccou
|
|
|
175
182
|
cfg,
|
|
176
183
|
accountId: accountId ?? null,
|
|
177
184
|
runtime: { log: (message: string) => runtime.log(message) },
|
|
185
|
+
mutateConfigFile: (getOpenclawClawlingRuntime().config as unknown as {
|
|
186
|
+
mutateConfigFile: OpenclawClawchatMutateConfigFile;
|
|
187
|
+
}).mutateConfigFile,
|
|
178
188
|
});
|
|
179
189
|
},
|
|
180
190
|
},
|
|
@@ -196,13 +206,12 @@ export const openclawClawlingPlugin: ChannelPlugin<ResolvedOpenclawClawlingAccou
|
|
|
196
206
|
},
|
|
197
207
|
agentPrompt: {
|
|
198
208
|
messageToolHints: () => [
|
|
199
|
-
"To send an image or file to the current
|
|
200
|
-
"When the user asks you to find an image from the web,
|
|
201
|
-
"
|
|
202
|
-
"
|
|
203
|
-
"-
|
|
204
|
-
"- ClawChat supports
|
|
205
|
-
"- ClawChat stream mode emits `message.created` → progressive `message.add` deltas → `message.done`, followed by a consolidated `message.reply` with the merged text.",
|
|
209
|
+
"To send an image or file to the current chat, use the message tool with action='send' and set 'media' to a local file path or a remote URL.",
|
|
210
|
+
"When the user asks you to find an image from the web, find a suitable HTTPS image URL and send it using the message tool with 'media' set to that URL — do NOT download the image first.",
|
|
211
|
+
"For configured ClawChat account profile, user profile, friends, avatar, or standalone media upload/share-link workflows, use `clawchat-account-tools` for tool-selection details.",
|
|
212
|
+
"For ClawChat account avatar changes using a local image, call `clawchat_upload_avatar_image` first, then `clawchat_update_account_profile` with `avatar_url`.",
|
|
213
|
+
"- Targeting: omit `target` to reply here; for a different chat use `target=\"cc:{chat_id}\"` for direct or `target=\"cc:group:{chat_id}\"` for group.",
|
|
214
|
+
"- ClawChat supports image / file / audio / video media alongside text.",
|
|
206
215
|
],
|
|
207
216
|
},
|
|
208
217
|
messaging: {
|
package/src/client.test.ts
CHANGED
|
@@ -91,7 +91,9 @@ describe("openclaw-clawchat client", () => {
|
|
|
91
91
|
expect(transport.sent).toHaveLength(1);
|
|
92
92
|
const env = JSON.parse(transport.sent[0]!);
|
|
93
93
|
expect(env.event).toBe("message.created");
|
|
94
|
-
expect(env.
|
|
94
|
+
expect(env.chat_id).toBe("user-1");
|
|
95
|
+
expect(env).not.toHaveProperty("chat_type");
|
|
96
|
+
expect(env).not.toHaveProperty("sender");
|
|
95
97
|
// Payload is intentionally minimal: just message_id, no message body /
|
|
96
98
|
// context / sender / streaming metadata.
|
|
97
99
|
expect(env.payload).toEqual({ message_id: "msg-1" });
|
|
@@ -117,6 +119,9 @@ describe("openclaw-clawchat client", () => {
|
|
|
117
119
|
const env = JSON.parse(transport.sent[0]!);
|
|
118
120
|
expect(env.event).toBe("message.add");
|
|
119
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");
|
|
120
125
|
expect(env.payload.fragments).toEqual([
|
|
121
126
|
{ kind: "text", text: "Hello, wor", delta: "wor" },
|
|
122
127
|
]);
|
|
@@ -168,7 +173,10 @@ describe("openclaw-clawchat client", () => {
|
|
|
168
173
|
expect(env.event).toBe("message.failed");
|
|
169
174
|
expect(env.payload.message_id).toBe("msg-1");
|
|
170
175
|
expect(env.payload.reason).toBe("upstream_error");
|
|
176
|
+
expect(env.payload.fragments).toEqual([{ kind: "text", text: "upstream_error" }]);
|
|
171
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");
|
|
172
180
|
client.close();
|
|
173
181
|
});
|
|
174
182
|
});
|
package/src/client.ts
CHANGED
|
@@ -64,12 +64,20 @@ export interface EnvelopeRouting {
|
|
|
64
64
|
chatType: ChatType;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
function normalizeRouting(params: {
|
|
68
|
+
routing?: EnvelopeRouting;
|
|
69
|
+
to?: { id?: string; type?: ChatType };
|
|
70
|
+
}): EnvelopeRouting {
|
|
71
|
+
if (params.routing) return params.routing;
|
|
72
|
+
if (params.to?.id) {
|
|
73
|
+
return { chatId: params.to.id, chatType: params.to.type ?? "direct" };
|
|
74
|
+
}
|
|
75
|
+
throw new Error("openclaw-clawchat streaming emit requires routing");
|
|
76
|
+
}
|
|
77
|
+
|
|
67
78
|
/**
|
|
68
|
-
* Emit a raw v2 envelope directly over the transport so we can carry
|
|
69
|
-
* `chat_id`
|
|
70
|
-
* `emitRaw` can't express `chat_type` and always writes `to`; we bypass it
|
|
71
|
-
* entirely for the events we construct ourselves (streaming lifecycle +
|
|
72
|
-
* 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.
|
|
73
81
|
*/
|
|
74
82
|
function emitEnvelope(
|
|
75
83
|
client: ClawlingChatClient,
|
|
@@ -78,18 +86,22 @@ function emitEnvelope(
|
|
|
78
86
|
routing: EnvelopeRouting,
|
|
79
87
|
): void {
|
|
80
88
|
const inner = client as unknown as {
|
|
81
|
-
opts
|
|
89
|
+
opts?: {
|
|
82
90
|
transport: { send: (data: string) => void };
|
|
83
91
|
traceIdFactory: () => string;
|
|
84
92
|
};
|
|
93
|
+
emitRaw?: (event: string, payload: object, routing?: { to?: { id: string; type: ChatType } }) => void;
|
|
85
94
|
};
|
|
95
|
+
if (!inner.opts?.transport) {
|
|
96
|
+
inner.emitRaw?.(event, payload, { to: { id: routing.chatId, type: routing.chatType } });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
86
99
|
const env = {
|
|
87
100
|
version: "2" as const,
|
|
88
101
|
event,
|
|
89
102
|
trace_id: inner.opts.traceIdFactory(),
|
|
90
103
|
emitted_at: Date.now(),
|
|
91
104
|
chat_id: routing.chatId,
|
|
92
|
-
chat_type: routing.chatType,
|
|
93
105
|
payload,
|
|
94
106
|
};
|
|
95
107
|
inner.opts.transport.send(JSON.stringify(env));
|
|
@@ -107,14 +119,16 @@ export function emitStreamCreated(
|
|
|
107
119
|
client: ClawlingChatClient,
|
|
108
120
|
params: {
|
|
109
121
|
messageId: string;
|
|
110
|
-
routing
|
|
122
|
+
routing?: EnvelopeRouting;
|
|
123
|
+
to?: { id: string; type: ChatType };
|
|
111
124
|
},
|
|
112
125
|
): void {
|
|
126
|
+
const routing = normalizeRouting(params);
|
|
113
127
|
emitEnvelope(
|
|
114
128
|
client,
|
|
115
129
|
"message.created",
|
|
116
130
|
{ message_id: params.messageId },
|
|
117
|
-
|
|
131
|
+
routing,
|
|
118
132
|
);
|
|
119
133
|
}
|
|
120
134
|
|
|
@@ -130,7 +144,8 @@ export function emitStreamAdd(
|
|
|
130
144
|
client: ClawlingChatClient,
|
|
131
145
|
params: {
|
|
132
146
|
messageId: string;
|
|
133
|
-
routing
|
|
147
|
+
routing?: EnvelopeRouting;
|
|
148
|
+
to?: { id: string; type: ChatType };
|
|
134
149
|
sequence: number;
|
|
135
150
|
/** Running cumulative text after this delta is applied. */
|
|
136
151
|
fullText: string;
|
|
@@ -139,6 +154,7 @@ export function emitStreamAdd(
|
|
|
139
154
|
},
|
|
140
155
|
): void {
|
|
141
156
|
const now = Date.now();
|
|
157
|
+
const routing = normalizeRouting(params);
|
|
142
158
|
emitEnvelope(
|
|
143
159
|
client,
|
|
144
160
|
"message.add",
|
|
@@ -158,7 +174,7 @@ export function emitStreamAdd(
|
|
|
158
174
|
},
|
|
159
175
|
added_at: now,
|
|
160
176
|
},
|
|
161
|
-
|
|
177
|
+
routing,
|
|
162
178
|
);
|
|
163
179
|
}
|
|
164
180
|
|
|
@@ -171,12 +187,14 @@ export function emitStreamDone(
|
|
|
171
187
|
client: ClawlingChatClient,
|
|
172
188
|
params: {
|
|
173
189
|
messageId: string;
|
|
174
|
-
routing
|
|
190
|
+
routing?: EnvelopeRouting;
|
|
191
|
+
to?: { id: string; type: ChatType };
|
|
175
192
|
finalSequence: number;
|
|
176
193
|
finalText: string;
|
|
177
194
|
},
|
|
178
195
|
): void {
|
|
179
196
|
const now = Date.now();
|
|
197
|
+
const routing = normalizeRouting(params);
|
|
180
198
|
emitEnvelope(
|
|
181
199
|
client,
|
|
182
200
|
"message.done",
|
|
@@ -192,7 +210,7 @@ export function emitStreamDone(
|
|
|
192
210
|
},
|
|
193
211
|
completed_at: now,
|
|
194
212
|
},
|
|
195
|
-
|
|
213
|
+
routing,
|
|
196
214
|
);
|
|
197
215
|
}
|
|
198
216
|
|
|
@@ -211,11 +229,12 @@ export function emitFinalStreamReply(
|
|
|
211
229
|
params: {
|
|
212
230
|
/** The streaming message_id — must equal the id used on created/add/done. */
|
|
213
231
|
messageId: string;
|
|
214
|
-
routing
|
|
232
|
+
routing?: EnvelopeRouting;
|
|
233
|
+
to?: { id: string; type: ChatType };
|
|
215
234
|
/** The user message this stream is a reply to (usually the inbound turn). */
|
|
216
235
|
replyTo: {
|
|
217
236
|
msgId: string;
|
|
218
|
-
|
|
237
|
+
previewId: string;
|
|
219
238
|
nickName: string;
|
|
220
239
|
fragments: Fragment[];
|
|
221
240
|
};
|
|
@@ -223,6 +242,7 @@ export function emitFinalStreamReply(
|
|
|
223
242
|
mentions?: string[];
|
|
224
243
|
},
|
|
225
244
|
): void {
|
|
245
|
+
const routing = normalizeRouting(params);
|
|
226
246
|
emitEnvelope(
|
|
227
247
|
client,
|
|
228
248
|
"message.reply",
|
|
@@ -236,7 +256,7 @@ export function emitFinalStreamReply(
|
|
|
236
256
|
reply: {
|
|
237
257
|
reply_to_msg_id: params.replyTo.msgId,
|
|
238
258
|
reply_preview: {
|
|
239
|
-
id: params.replyTo.
|
|
259
|
+
id: params.replyTo.previewId,
|
|
240
260
|
nick_name: params.replyTo.nickName,
|
|
241
261
|
fragments: params.replyTo.fragments,
|
|
242
262
|
},
|
|
@@ -244,7 +264,7 @@ export function emitFinalStreamReply(
|
|
|
244
264
|
},
|
|
245
265
|
},
|
|
246
266
|
},
|
|
247
|
-
|
|
267
|
+
routing,
|
|
248
268
|
);
|
|
249
269
|
}
|
|
250
270
|
|
|
@@ -252,19 +272,26 @@ export function emitStreamFailed(
|
|
|
252
272
|
client: ClawlingChatClient,
|
|
253
273
|
params: {
|
|
254
274
|
messageId: string;
|
|
255
|
-
routing
|
|
275
|
+
routing?: EnvelopeRouting;
|
|
276
|
+
to?: { id: string; type: ChatType };
|
|
256
277
|
sequence: number;
|
|
257
278
|
reason?: string;
|
|
258
279
|
},
|
|
259
280
|
): void {
|
|
260
281
|
const now = Date.now();
|
|
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
|
+
: {};
|
|
261
287
|
emitEnvelope(
|
|
262
288
|
client,
|
|
263
289
|
"message.failed",
|
|
264
290
|
{
|
|
265
291
|
message_id: params.messageId,
|
|
266
292
|
sequence: params.sequence,
|
|
267
|
-
reason
|
|
293
|
+
reason,
|
|
294
|
+
...reasonFragment,
|
|
268
295
|
streaming: {
|
|
269
296
|
status: "failed",
|
|
270
297
|
sequence: params.sequence,
|
|
@@ -272,8 +299,8 @@ export function emitStreamFailed(
|
|
|
272
299
|
started_at: null,
|
|
273
300
|
completed_at: now,
|
|
274
301
|
},
|
|
275
|
-
|
|
302
|
+
completed_at: now,
|
|
276
303
|
},
|
|
277
|
-
|
|
304
|
+
routing,
|
|
278
305
|
);
|
|
279
306
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { registerOpenclawClawlingCommands } from "./commands.ts";
|
|
3
|
+
|
|
4
|
+
const loginRuntime = vi.hoisted(() => ({
|
|
5
|
+
runOpenclawClawlingLogin: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
vi.mock("./login.runtime.ts", () => loginRuntime);
|
|
9
|
+
|
|
10
|
+
describe("registerOpenclawClawlingCommands", () => {
|
|
11
|
+
it("registers a distinct slash login command that passes the invite code to login", async () => {
|
|
12
|
+
loginRuntime.runOpenclawClawlingLogin.mockResolvedValue(undefined);
|
|
13
|
+
const commands: Array<{ name: string; acceptsArgs?: boolean; handler: (ctx: unknown) => Promise<{ text: string }> }> = [];
|
|
14
|
+
const api = {
|
|
15
|
+
registerCommand: (command: (typeof commands)[number]) => commands.push(command),
|
|
16
|
+
runtime: {
|
|
17
|
+
config: {
|
|
18
|
+
mutateConfigFile: vi.fn(),
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
} as never;
|
|
22
|
+
|
|
23
|
+
registerOpenclawClawlingCommands(api);
|
|
24
|
+
|
|
25
|
+
expect(commands.map((command) => command.name)).toEqual(["clawchat-login"]);
|
|
26
|
+
expect(commands[0]?.acceptsArgs).toBe(true);
|
|
27
|
+
|
|
28
|
+
const result = await commands[0]!.handler({
|
|
29
|
+
args: "A1B2C3",
|
|
30
|
+
config: { channels: { "openclaw-clawchat": { websocketUrl: "wss://w" } } },
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
expect(loginRuntime.runOpenclawClawlingLogin).toHaveBeenCalledTimes(1);
|
|
34
|
+
const params = loginRuntime.runOpenclawClawlingLogin.mock.calls[0]?.[0];
|
|
35
|
+
expect(params.mutateConfigFile).toBe(api.runtime.config.mutateConfigFile);
|
|
36
|
+
await expect(params.readInviteCode()).resolves.toBe("A1B2C3");
|
|
37
|
+
expect(result.text).toMatch(/activated successfully/i);
|
|
38
|
+
});
|
|
39
|
+
});
|
package/src/commands.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import type { OpenclawClawchatMutateConfigFile } from "./login.runtime.ts";
|
|
3
|
+
|
|
4
|
+
function extractInviteCode(value: unknown): string {
|
|
5
|
+
const raw = typeof value === "string" ? value.trim() : "";
|
|
6
|
+
return raw.match(/\b[A-Z0-9]{6}\b/u)?.[0] ?? "";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function errorMessage(err: unknown): string {
|
|
10
|
+
return err instanceof Error ? err.message : String(err);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function registerOpenclawClawlingCommands(api: Pick<OpenClawPluginApi, "registerCommand" | "logger" | "runtime">): void {
|
|
14
|
+
api.registerCommand({
|
|
15
|
+
name: "clawchat-login",
|
|
16
|
+
description: "Activate ClawChat with an invite code, e.g. /clawchat-login A1B2C3.",
|
|
17
|
+
acceptsArgs: true,
|
|
18
|
+
requireAuth: true,
|
|
19
|
+
async handler(ctx) {
|
|
20
|
+
const code = extractInviteCode(ctx.args ?? ctx.commandBody);
|
|
21
|
+
if (!code) {
|
|
22
|
+
return { text: "ClawChat invite code is required. Usage: /clawchat-login A1B2C3" };
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const { runOpenclawClawlingLogin } = await import("./login.runtime.ts");
|
|
26
|
+
await runOpenclawClawlingLogin({
|
|
27
|
+
cfg: ctx.config,
|
|
28
|
+
accountId: ctx.accountId ?? null,
|
|
29
|
+
runtime: { log: (message: string) => api.logger?.info?.(message) },
|
|
30
|
+
readInviteCode: async () => code,
|
|
31
|
+
mutateConfigFile: (api.runtime.config as unknown as {
|
|
32
|
+
mutateConfigFile: OpenclawClawchatMutateConfigFile;
|
|
33
|
+
}).mutateConfigFile,
|
|
34
|
+
});
|
|
35
|
+
return { text: "✅ ClawChat activated successfully." };
|
|
36
|
+
} catch (err) {
|
|
37
|
+
return { text: `❌ ${errorMessage(err)}` };
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
package/src/config.test.ts
CHANGED
|
@@ -2,7 +2,10 @@ import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
|
|
|
2
2
|
import { describe, expect, it } from "vitest";
|
|
3
3
|
import {
|
|
4
4
|
CHANNEL_ID,
|
|
5
|
+
DEFAULT_BASE_URL,
|
|
6
|
+
DEFAULT_WEBSOCKET_URL,
|
|
5
7
|
DEFAULT_STREAM,
|
|
8
|
+
mergeOpenclawClawchatToolAllow,
|
|
6
9
|
resolveOpenclawClawlingAccount,
|
|
7
10
|
listOpenclawClawlingAccountIds,
|
|
8
11
|
} from "./config.ts";
|
|
@@ -20,6 +23,7 @@ describe("openclaw-clawchat config", () => {
|
|
|
20
23
|
expect(account.replyMode).toBe("static");
|
|
21
24
|
expect(account.forwardThinking).toBe(true);
|
|
22
25
|
expect(account.forwardToolCalls).toBe(false);
|
|
26
|
+
expect(account.richInteractions).toBe(false);
|
|
23
27
|
expect(account.stream).toEqual(DEFAULT_STREAM);
|
|
24
28
|
});
|
|
25
29
|
|
|
@@ -33,6 +37,7 @@ describe("openclaw-clawchat config", () => {
|
|
|
33
37
|
replyMode: "stream",
|
|
34
38
|
forwardThinking: false,
|
|
35
39
|
forwardToolCalls: true,
|
|
40
|
+
richInteractions: true,
|
|
36
41
|
stream: { flushIntervalMs: 500, minChunkChars: 50, maxBufferChars: 3000 },
|
|
37
42
|
},
|
|
38
43
|
},
|
|
@@ -45,6 +50,7 @@ describe("openclaw-clawchat config", () => {
|
|
|
45
50
|
expect(account.replyMode).toBe("stream");
|
|
46
51
|
expect(account.forwardThinking).toBe(false);
|
|
47
52
|
expect(account.forwardToolCalls).toBe(true);
|
|
53
|
+
expect(account.richInteractions).toBe(true);
|
|
48
54
|
expect(account.stream.flushIntervalMs).toBe(500);
|
|
49
55
|
expect(account.stream.minChunkChars).toBe(50);
|
|
50
56
|
expect(account.stream.maxBufferChars).toBe(3000);
|
|
@@ -80,19 +86,21 @@ describe("openclaw-clawchat config", () => {
|
|
|
80
86
|
});
|
|
81
87
|
|
|
82
88
|
it("falls back to the built-in DEFAULT_BASE_URL when unset", async () => {
|
|
83
|
-
const { DEFAULT_BASE_URL } = await import("./config.ts");
|
|
84
89
|
const account = resolveOpenclawClawlingAccount({});
|
|
85
90
|
expect(account.baseUrl).toBe(DEFAULT_BASE_URL);
|
|
86
91
|
});
|
|
87
92
|
|
|
88
93
|
it("falls back to the built-in DEFAULT_WEBSOCKET_URL when unset", async () => {
|
|
89
|
-
const { DEFAULT_WEBSOCKET_URL } = await import("./config.ts");
|
|
90
94
|
const account = resolveOpenclawClawlingAccount({});
|
|
91
95
|
expect(account.websocketUrl).toBe(DEFAULT_WEBSOCKET_URL);
|
|
92
96
|
});
|
|
93
97
|
|
|
98
|
+
it("uses the production ClawChat service as the built-in fallback endpoint", () => {
|
|
99
|
+
expect(DEFAULT_BASE_URL).toBe("http://company.newbaselab.com:10086");
|
|
100
|
+
expect(DEFAULT_WEBSOCKET_URL).toBe("ws://company.newbaselab.com:10086/ws");
|
|
101
|
+
});
|
|
102
|
+
|
|
94
103
|
it("does NOT include baseUrl in the configured predicate (channel still works without it)", async () => {
|
|
95
|
-
const { DEFAULT_BASE_URL } = await import("./config.ts");
|
|
96
104
|
const cfg = {
|
|
97
105
|
channels: {
|
|
98
106
|
"openclaw-clawchat": {
|
|
@@ -107,4 +115,33 @@ describe("openclaw-clawchat config", () => {
|
|
|
107
115
|
expect(account.configured).toBe(true);
|
|
108
116
|
expect(account.baseUrl).toBe(DEFAULT_BASE_URL);
|
|
109
117
|
});
|
|
118
|
+
|
|
119
|
+
it("adds the plugin to tools.allow when an explicit allowlist is already in use", () => {
|
|
120
|
+
const cfg = mergeOpenclawClawchatToolAllow({
|
|
121
|
+
tools: {
|
|
122
|
+
allow: ["bash"],
|
|
123
|
+
deny: ["exec"],
|
|
124
|
+
},
|
|
125
|
+
} as never) as { tools: Record<string, unknown> };
|
|
126
|
+
|
|
127
|
+
expect(cfg.tools.allow).toEqual(["bash", "openclaw-clawchat"]);
|
|
128
|
+
expect(cfg.tools.deny).toEqual(["exec"]);
|
|
129
|
+
expect(cfg.tools.alsoAllow).toBeUndefined();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("adds the plugin to tools.alsoAllow when no explicit allowlist is in use", () => {
|
|
133
|
+
const cfg = mergeOpenclawClawchatToolAllow({
|
|
134
|
+
tools: {
|
|
135
|
+
profile: "coding",
|
|
136
|
+
allow: [],
|
|
137
|
+
},
|
|
138
|
+
} as never) as { tools: Record<string, unknown> };
|
|
139
|
+
|
|
140
|
+
expect(cfg.tools).toEqual({
|
|
141
|
+
profile: "coding",
|
|
142
|
+
allow: [],
|
|
143
|
+
alsoAllow: ["openclaw-clawchat"],
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
110
147
|
});
|