@newbase-clawchat/openclaw-clawchat 2026.4.24 → 2026.4.29
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 +40 -13
- package/index.ts +2 -1
- package/openclaw.plugin.json +69 -1
- package/package.json +3 -11
- package/skills/clawchat-account-tools/SKILL.md +26 -0
- package/skills/clawchat-activate/SKILL.md +38 -0
- package/src/api-client.test.ts +6 -5
- package/src/api-client.ts +8 -3
- package/src/buffered-stream.test.ts +4 -4
- package/src/buffered-stream.ts +16 -8
- package/src/channel.outbound.test.ts +49 -35
- package/src/channel.test.ts +45 -10
- package/src/channel.ts +15 -14
- package/src/client.test.ts +2 -1
- package/src/client.ts +37 -11
- package/src/commands.test.ts +33 -0
- package/src/commands.ts +37 -0
- package/src/config.test.ts +37 -3
- package/src/config.ts +53 -4
- package/src/inbound.test.ts +5 -5
- package/src/inbound.ts +43 -9
- package/src/login.runtime.test.ts +106 -3
- package/src/login.runtime.ts +8 -3
- package/src/manifest.test.ts +106 -4
- package/src/outbound.test.ts +7 -5
- package/src/plugin-entry.test.ts +27 -0
- package/src/protocol.ts +5 -0
- package/src/reply-dispatcher.test.ts +4 -2
- package/src/runtime.test.ts +23 -7
- package/src/runtime.ts +12 -1
- package/src/streaming.test.ts +3 -3
- package/src/streaming.ts +19 -9
- package/src/tools-schema.ts +28 -19
- package/src/tools.test.ts +115 -37
- package/src/tools.ts +137 -116
|
@@ -95,7 +95,7 @@ describe("runOpenclawClawlingLogin", () => {
|
|
|
95
95
|
});
|
|
96
96
|
|
|
97
97
|
expect(agentsConnect).toHaveBeenCalledWith({
|
|
98
|
-
|
|
98
|
+
code: "INV-ABC",
|
|
99
99
|
platform: "openclaw",
|
|
100
100
|
type: "clawbot",
|
|
101
101
|
});
|
|
@@ -112,6 +112,109 @@ describe("runOpenclawClawlingLogin", () => {
|
|
|
112
112
|
expect(log).toHaveBeenCalledWith(expect.stringContaining("login succeeded"));
|
|
113
113
|
});
|
|
114
114
|
|
|
115
|
+
it("preserves other configured channels when persisting ClawChat credentials", async () => {
|
|
116
|
+
const cfg = {
|
|
117
|
+
channels: {
|
|
118
|
+
telegram: {
|
|
119
|
+
enabled: true,
|
|
120
|
+
token: "telegram-token",
|
|
121
|
+
},
|
|
122
|
+
[CHANNEL_ID]: {
|
|
123
|
+
baseUrl: "https://api.example.com",
|
|
124
|
+
websocketUrl: "wss://ws.example.com/v2/client",
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
} as unknown as OpenClawConfig;
|
|
128
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
129
|
+
agent: { user_id: "agent-123" },
|
|
130
|
+
access_token: "access-tok",
|
|
131
|
+
refresh_token: "refresh-tok",
|
|
132
|
+
});
|
|
133
|
+
const persistConfig = vi.fn();
|
|
134
|
+
|
|
135
|
+
await runOpenclawClawlingLogin({
|
|
136
|
+
cfg,
|
|
137
|
+
runtime: { log: vi.fn() },
|
|
138
|
+
readInviteCode: async () => "INV-ABC",
|
|
139
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
140
|
+
persistConfig,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const savedCfg = persistConfig.mock.calls[0]![0] as OpenClawConfig;
|
|
144
|
+
expect(savedCfg.channels?.telegram).toEqual({
|
|
145
|
+
enabled: true,
|
|
146
|
+
token: "telegram-token",
|
|
147
|
+
});
|
|
148
|
+
expect(savedCfg.channels?.[CHANNEL_ID]).toMatchObject({
|
|
149
|
+
baseUrl: "https://api.example.com",
|
|
150
|
+
websocketUrl: "wss://ws.example.com/v2/client",
|
|
151
|
+
token: "access-tok",
|
|
152
|
+
userId: "agent-123",
|
|
153
|
+
refreshToken: "refresh-tok",
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("enables the channel when login fills a pre-activation disabled skeleton", async () => {
|
|
158
|
+
const cfg = buildCfg({
|
|
159
|
+
enabled: false,
|
|
160
|
+
baseUrl: "https://api.example.com",
|
|
161
|
+
});
|
|
162
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
163
|
+
agent: { user_id: "agent-123" },
|
|
164
|
+
access_token: "access-tok",
|
|
165
|
+
refresh_token: "refresh-tok",
|
|
166
|
+
});
|
|
167
|
+
const persistConfig = vi.fn();
|
|
168
|
+
|
|
169
|
+
await runOpenclawClawlingLogin({
|
|
170
|
+
cfg,
|
|
171
|
+
runtime: { log: vi.fn() },
|
|
172
|
+
readInviteCode: async () => "INV-ABC",
|
|
173
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
174
|
+
persistConfig,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const savedCfg = persistConfig.mock.calls[0]![0] as OpenClawConfig;
|
|
178
|
+
const section = (savedCfg.channels as Record<string, Record<string, unknown>>)[CHANNEL_ID]!;
|
|
179
|
+
expect(section.enabled).toBe(true);
|
|
180
|
+
expect(section.token).toBe("access-tok");
|
|
181
|
+
expect(section.userId).toBe("agent-123");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("allows openclaw-clawchat plugin tools after successful login without replacing policy", async () => {
|
|
185
|
+
const cfg = {
|
|
186
|
+
...buildCfg({ baseUrl: "https://api.example.com" }),
|
|
187
|
+
tools: {
|
|
188
|
+
profile: "coding",
|
|
189
|
+
allow: [],
|
|
190
|
+
deny: ["exec"],
|
|
191
|
+
alsoAllow: ["browser"],
|
|
192
|
+
},
|
|
193
|
+
} as unknown as OpenClawConfig;
|
|
194
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
195
|
+
agent: { user_id: "agent-123" },
|
|
196
|
+
access_token: "access-tok",
|
|
197
|
+
refresh_token: "refresh-tok",
|
|
198
|
+
});
|
|
199
|
+
const persistConfig = vi.fn();
|
|
200
|
+
|
|
201
|
+
await runOpenclawClawlingLogin({
|
|
202
|
+
cfg,
|
|
203
|
+
runtime: { log: vi.fn() },
|
|
204
|
+
readInviteCode: async () => "INV-ABC",
|
|
205
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
206
|
+
persistConfig,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const savedCfg = persistConfig.mock.calls[0]![0] as OpenClawConfig;
|
|
210
|
+
expect(savedCfg.tools).toMatchObject({
|
|
211
|
+
profile: "coding",
|
|
212
|
+
allow: [],
|
|
213
|
+
deny: ["exec"],
|
|
214
|
+
alsoAllow: ["browser", "openclaw-clawchat"],
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
115
218
|
it("surfaces agents/connect API errors with the kind and message", async () => {
|
|
116
219
|
const cfg = buildCfg({ baseUrl: "https://api.example.com" });
|
|
117
220
|
const agentsConnect = vi.fn().mockRejectedValue(
|
|
@@ -219,7 +322,7 @@ describe("runOpenclawClawlingLogin", () => {
|
|
|
219
322
|
persistConfig: vi.fn(),
|
|
220
323
|
});
|
|
221
324
|
expect(agentsConnect).toHaveBeenCalledWith({
|
|
222
|
-
|
|
325
|
+
code: "INV-TRIM",
|
|
223
326
|
platform: "openclaw",
|
|
224
327
|
type: "clawbot",
|
|
225
328
|
});
|
|
@@ -243,7 +346,7 @@ describe("runOpenclawClawlingLogin (non-interactive via readInviteCode)", () =>
|
|
|
243
346
|
persistConfig,
|
|
244
347
|
});
|
|
245
348
|
expect(agentsConnect).toHaveBeenCalledWith({
|
|
246
|
-
|
|
349
|
+
code: "INV-PROGRAMMATIC",
|
|
247
350
|
platform: "openclaw",
|
|
248
351
|
type: "clawbot",
|
|
249
352
|
});
|
package/src/login.runtime.ts
CHANGED
|
@@ -3,7 +3,11 @@ import { writeConfigFile } from "openclaw/plugin-sdk/config-runtime";
|
|
|
3
3
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
4
4
|
import { createOpenclawClawlingApiClient } from "./api-client.ts";
|
|
5
5
|
import { ClawlingApiError } from "./api-types.ts";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
CHANNEL_ID,
|
|
8
|
+
mergeOpenclawClawchatToolAllow,
|
|
9
|
+
resolveOpenclawClawlingAccount,
|
|
10
|
+
} from "./config.ts";
|
|
7
11
|
|
|
8
12
|
/**
|
|
9
13
|
* Platform tag sent to `/v1/agents/connect`. Identifies the host of this
|
|
@@ -120,16 +124,17 @@ export async function runOpenclawClawlingLogin(params: LoginParams): Promise<voi
|
|
|
120
124
|
const existing = (channels[CHANNEL_ID] ?? {}) as Record<string, unknown>;
|
|
121
125
|
const nextSection: Record<string, unknown> = {
|
|
122
126
|
...existing,
|
|
127
|
+
enabled: true,
|
|
123
128
|
token: result.access_token,
|
|
124
129
|
userId: result.agent.user_id,
|
|
125
130
|
};
|
|
126
131
|
if (result.refresh_token) {
|
|
127
132
|
nextSection.refreshToken = result.refresh_token;
|
|
128
133
|
}
|
|
129
|
-
const nextCfg: OpenClawConfig = {
|
|
134
|
+
const nextCfg: OpenClawConfig = mergeOpenclawClawchatToolAllow({
|
|
130
135
|
...cfg,
|
|
131
136
|
channels: { ...channels, [CHANNEL_ID]: nextSection },
|
|
132
|
-
};
|
|
137
|
+
});
|
|
133
138
|
|
|
134
139
|
const tokenPreview = redactToken(result.access_token);
|
|
135
140
|
runtime.log(
|
package/src/manifest.test.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
1
2
|
import { describe, expect, it } from "vitest";
|
|
2
3
|
import pluginManifest from "../openclaw.plugin.json" with { type: "json" };
|
|
3
4
|
import packageJson from "../package.json" with { type: "json" };
|
|
4
5
|
|
|
5
6
|
interface PackageJsonWithOpenclaw {
|
|
6
7
|
name: string;
|
|
8
|
+
files: string[];
|
|
7
9
|
openclaw: {
|
|
8
|
-
|
|
10
|
+
extensions: string[];
|
|
11
|
+
setupEntry?: string;
|
|
12
|
+
channel?: { id: string; aliases?: string[]; cliAddOptions?: Array<{ flags: string }> };
|
|
9
13
|
install: { npmSpec: string };
|
|
10
14
|
};
|
|
11
15
|
}
|
|
@@ -14,9 +18,107 @@ describe("openclaw-clawchat manifest", () => {
|
|
|
14
18
|
it("keeps plugin id / channel id / package name aligned", () => {
|
|
15
19
|
expect(pluginManifest.id).toBe("openclaw-clawchat");
|
|
16
20
|
expect(pluginManifest.channels).toContain("openclaw-clawchat");
|
|
17
|
-
expect(
|
|
21
|
+
expect(pluginManifest.skills).toContain("./skills");
|
|
22
|
+
expect(pluginManifest.channelConfigs?.["openclaw-clawchat"]?.label).toBe(
|
|
23
|
+
"Clawling Chat",
|
|
24
|
+
);
|
|
25
|
+
expect(pluginManifest.channelConfigs?.["openclaw-clawchat"]?.schema?.properties).toHaveProperty(
|
|
26
|
+
"token",
|
|
27
|
+
);
|
|
28
|
+
expect(packageJson.name).toBe("@newbase-clawchat/openclaw-clawchat");
|
|
18
29
|
const pkg = packageJson as PackageJsonWithOpenclaw;
|
|
19
|
-
expect(pkg.openclaw.
|
|
20
|
-
expect(pkg.openclaw.install.npmSpec).toBe("openclaw-clawchat");
|
|
30
|
+
expect(pkg.openclaw.extensions).toContain("./index.ts");
|
|
31
|
+
expect(pkg.openclaw.install.npmSpec).toBe("@newbase-clawchat/openclaw-clawchat");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("declares supported channel/command activation hints for plugin loading", () => {
|
|
35
|
+
expect(pluginManifest.activation).toEqual({
|
|
36
|
+
onChannels: ["openclaw-clawchat"],
|
|
37
|
+
onCommands: ["clawchat-login"],
|
|
38
|
+
});
|
|
39
|
+
expect(pluginManifest.activation).not.toHaveProperty("onStartup");
|
|
40
|
+
expect(pluginManifest.commandAliases).toEqual([
|
|
41
|
+
{ name: "clawchat-login", kind: "runtime-slash" },
|
|
42
|
+
]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("does not publish setup migration or setup-runtime channel metadata", () => {
|
|
46
|
+
const pkg = packageJson as PackageJsonWithOpenclaw;
|
|
47
|
+
expect(pkg.files).not.toContain("setup-api.ts");
|
|
48
|
+
expect(pkg.files).not.toContain("setup-entry.ts");
|
|
49
|
+
expect(pkg.openclaw.setupEntry).toBeUndefined();
|
|
50
|
+
expect(pkg.openclaw.channel).toBeUndefined();
|
|
51
|
+
expect(fs.existsSync(new URL("../setup-api.ts", import.meta.url))).toBe(false);
|
|
52
|
+
expect(fs.existsSync(new URL("../setup-entry.ts", import.meta.url))).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("does not document channels add as an activation path", () => {
|
|
56
|
+
const readme = fs.readFileSync(new URL("../README.md", import.meta.url), "utf8");
|
|
57
|
+
const docs = fs.readFileSync(new URL("../docs/openclaw-clawchat.md", import.meta.url), "utf8");
|
|
58
|
+
expect(readme).not.toMatch(/channels add --channel openclaw-clawchat/i);
|
|
59
|
+
expect(docs).not.toMatch(/channels add --channel openclaw-clawchat/i);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("publishes a ClawChat account tools skill for non-activation workflows", () => {
|
|
63
|
+
const skill = fs.readFileSync(new URL("../skills/clawchat-account-tools/SKILL.md", import.meta.url), "utf8");
|
|
64
|
+
expect(skill).toMatch(/^---\nname: clawchat-account-tools\n/m);
|
|
65
|
+
expect(skill).toMatch(/description: .*Use when/i);
|
|
66
|
+
expect(skill).toMatch(/clawchat_get_account_profile/);
|
|
67
|
+
expect(skill).toMatch(/clawchat_get_user_profile/);
|
|
68
|
+
expect(skill).toMatch(/clawchat_list_account_friends/);
|
|
69
|
+
expect(skill).toMatch(/clawchat_update_account_profile/);
|
|
70
|
+
expect(skill).toMatch(/clawchat_upload_avatar_image/);
|
|
71
|
+
expect(skill).toMatch(/clawchat_upload_media_file/);
|
|
72
|
+
expect(skill).toMatch(/configured ClawChat account/i);
|
|
73
|
+
expect(skill).not.toMatch(/clawchat_activate/);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("keeps the activation skill on channel login without tool or slash-command dispatch", () => {
|
|
77
|
+
const skill = fs.readFileSync(new URL("../skills/clawchat-activate/SKILL.md", import.meta.url), "utf8");
|
|
78
|
+
expect(skill).toMatch(/name:\s*clawchat-activate/);
|
|
79
|
+
expect(skill).not.toMatch(/clawchat_activate/);
|
|
80
|
+
expect(skill).not.toMatch(/command-dispatch:\s*tool/);
|
|
81
|
+
expect(skill).not.toMatch(/command-tool:/);
|
|
82
|
+
expect(skill).not.toMatch(/command-dispatch:/);
|
|
83
|
+
expect(skill).not.toMatch(/command-command:/);
|
|
84
|
+
expect(skill).not.toMatch(/command-arg-mode:/);
|
|
85
|
+
expect(skill).not.toMatch(/user-invocable:/);
|
|
86
|
+
expect(skill).not.toMatch(/`clawchat\s+A1B2C3`/i);
|
|
87
|
+
expect(skill).not.toMatch(/`clawchat\s*<code>`/i);
|
|
88
|
+
expect(skill).not.toMatch(/\/clawchat_activate A1B2C3/);
|
|
89
|
+
expect(skill).not.toMatch(/\/clawchat-activate A1B2C3/);
|
|
90
|
+
expect(skill).not.toMatch(/\/clawchat-login A1B2C3/);
|
|
91
|
+
expect(skill).toMatch(/openclaw channels login --channel openclaw-clawchat/);
|
|
92
|
+
expect(skill).toMatch(/do not append/i);
|
|
93
|
+
expect(skill).toMatch(/prompt[^\n]+invite code[^\n]+provide/i);
|
|
94
|
+
expect(skill).toMatch(/channel login/i);
|
|
95
|
+
expect(skill).toMatch(/openclaw gateway restart/);
|
|
96
|
+
expect(skill).not.toMatch(/ask the user to send/i);
|
|
97
|
+
expect(skill).not.toMatch(/give the exact/i);
|
|
98
|
+
expect(skill).toMatch(/execute[^\n]+openclaw channels login --channel openclaw-clawchat/i);
|
|
99
|
+
expect(skill).toMatch(/execute[^\n]+openclaw gateway restart/i);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("documents channel login as the natural-language activation path", () => {
|
|
103
|
+
const readme = fs.readFileSync(new URL("../README.md", import.meta.url), "utf8");
|
|
104
|
+
const docs = fs.readFileSync(new URL("../docs/openclaw-clawchat.md", import.meta.url), "utf8");
|
|
105
|
+
expect(readme).toMatch(/openclaw channels login --channel openclaw-clawchat/i);
|
|
106
|
+
expect(docs).toMatch(/openclaw channels login --channel openclaw-clawchat/i);
|
|
107
|
+
expect(readme).toMatch(/openclaw gateway restart/i);
|
|
108
|
+
expect(docs).toMatch(/openclaw gateway restart/i);
|
|
109
|
+
expect(readme).not.toMatch(/activation skill[^.]+\/clawchat-login/i);
|
|
110
|
+
expect(docs).not.toMatch(/natural-language activation requests[^.]+\/clawchat-login/i);
|
|
111
|
+
expect(readme).not.toMatch(/\/clawchat-activate\s+A1B2C3/i);
|
|
112
|
+
expect(docs).not.toMatch(/\/clawchat-activate\s+A1B2C3/i);
|
|
113
|
+
expect(readme).not.toMatch(/clawchat_activate/);
|
|
114
|
+
expect(docs).not.toMatch(/clawchat_activate/);
|
|
115
|
+
expect(readme).not.toMatch(/\/clawchat_activate\s+A1B2C3/i);
|
|
116
|
+
expect(docs).not.toMatch(/\/clawchat_activate\s+A1B2C3/i);
|
|
117
|
+
expect(readme).not.toMatch(/direct users to/i);
|
|
118
|
+
expect(docs).not.toMatch(/direct the\s+user/i);
|
|
119
|
+
expect(docs).not.toMatch(/call the tool with the extracted code/i);
|
|
120
|
+
expect(docs).not.toMatch(/AGT->>PLG: clawchat_activate/);
|
|
121
|
+
expect(readme).toMatch(/activation skill[^.]+execute/i);
|
|
122
|
+
expect(docs).toMatch(/natural-language activation requests[^.]+execute/i);
|
|
21
123
|
});
|
|
22
124
|
});
|
package/src/outbound.test.ts
CHANGED
|
@@ -64,7 +64,8 @@ describe("openclaw-clawchat outbound", () => {
|
|
|
64
64
|
text: "hello",
|
|
65
65
|
});
|
|
66
66
|
expect((client.sendMessage as ReturnType<typeof vi.fn>).mock.calls[0][0]).toMatchObject({
|
|
67
|
-
|
|
67
|
+
chat_id: "user-1",
|
|
68
|
+
chat_type: "direct",
|
|
68
69
|
body: { fragments: [{ kind: "text", text: "hello" }] },
|
|
69
70
|
});
|
|
70
71
|
expect(client.replyMessage).not.toHaveBeenCalled();
|
|
@@ -82,16 +83,17 @@ describe("openclaw-clawchat outbound", () => {
|
|
|
82
83
|
replyCtx: {
|
|
83
84
|
replyToMessageId: "m-orig",
|
|
84
85
|
replyPreviewSenderId: "user-2",
|
|
85
|
-
|
|
86
|
+
replyPreviewNickName: "Sender",
|
|
86
87
|
replyPreviewText: "original",
|
|
87
88
|
},
|
|
88
89
|
});
|
|
89
90
|
expect((client.replyMessage as ReturnType<typeof vi.fn>).mock.calls[0][0]).toMatchObject({
|
|
90
|
-
|
|
91
|
+
chat_id: "user-1",
|
|
92
|
+
chat_type: "direct",
|
|
91
93
|
replyTo: {
|
|
92
94
|
msgId: "m-orig",
|
|
93
95
|
senderId: "user-2",
|
|
94
|
-
|
|
96
|
+
nickName: "Sender",
|
|
95
97
|
fragments: [{ kind: "text", text: "original" }],
|
|
96
98
|
},
|
|
97
99
|
body: { fragments: [{ kind: "text", text: "reply" }] },
|
|
@@ -168,7 +170,7 @@ describe("openclaw-clawchat outbound", () => {
|
|
|
168
170
|
replyCtx: {
|
|
169
171
|
replyToMessageId: "m-orig",
|
|
170
172
|
replyPreviewSenderId: "user-2",
|
|
171
|
-
|
|
173
|
+
replyPreviewNickName: "Sender",
|
|
172
174
|
replyPreviewText: "original",
|
|
173
175
|
},
|
|
174
176
|
mediaFragments: [{ kind: "image", url: "https://cdn/x.png" }],
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import pluginEntry from "../index.ts";
|
|
3
|
+
|
|
4
|
+
describe("openclaw-clawchat plugin entry", () => {
|
|
5
|
+
it("registers the channel/tools and native activation command without bootstrap migration", () => {
|
|
6
|
+
const api = {
|
|
7
|
+
config: {},
|
|
8
|
+
runtime: {},
|
|
9
|
+
logger: { debug: vi.fn(), info: vi.fn(), error: vi.fn() },
|
|
10
|
+
registerChannel: vi.fn(),
|
|
11
|
+
registerCommand: vi.fn(),
|
|
12
|
+
registerConfigMigration: vi.fn(),
|
|
13
|
+
registerTool: vi.fn(),
|
|
14
|
+
} as never;
|
|
15
|
+
|
|
16
|
+
pluginEntry.register(api);
|
|
17
|
+
|
|
18
|
+
expect(api.registerChannel).toHaveBeenCalledTimes(1);
|
|
19
|
+
expect(api.registerConfigMigration).not.toHaveBeenCalled();
|
|
20
|
+
expect(api.registerCommand).toHaveBeenCalledTimes(1);
|
|
21
|
+
expect(api.registerCommand).toHaveBeenCalledWith(
|
|
22
|
+
expect.objectContaining({
|
|
23
|
+
name: "clawchat-login",
|
|
24
|
+
}),
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
});
|
package/src/protocol.ts
CHANGED
|
@@ -40,3 +40,8 @@ export function hasRenderableText(message: {
|
|
|
40
40
|
(f as { url: string }).url.trim().length > 0)),
|
|
41
41
|
);
|
|
42
42
|
}
|
|
43
|
+
|
|
44
|
+
export function isGroupSender(sender: unknown): boolean {
|
|
45
|
+
if (!sender || typeof sender !== "object") return false;
|
|
46
|
+
return (sender as { type?: unknown }).type === "group";
|
|
47
|
+
}
|
|
@@ -185,7 +185,8 @@ describe("openclaw-clawchat reply-dispatcher", () => {
|
|
|
185
185
|
|
|
186
186
|
expect(client.sendMessage).toHaveBeenCalledWith(
|
|
187
187
|
expect.objectContaining({
|
|
188
|
-
|
|
188
|
+
chat_id: "chat-1",
|
|
189
|
+
chat_type: "direct",
|
|
189
190
|
body: { fragments: [{ kind: "text", text: "Error: boom" }] },
|
|
190
191
|
}),
|
|
191
192
|
);
|
|
@@ -244,7 +245,8 @@ describe("openclaw-clawchat reply-dispatcher", () => {
|
|
|
244
245
|
|
|
245
246
|
expect(client.sendMessage).toHaveBeenCalledWith(
|
|
246
247
|
expect.objectContaining({
|
|
247
|
-
|
|
248
|
+
chat_id: "chat-1",
|
|
249
|
+
chat_type: "direct",
|
|
248
250
|
body: { fragments: [{ kind: "text", text: "Error: boom" }] },
|
|
249
251
|
}),
|
|
250
252
|
);
|
package/src/runtime.test.ts
CHANGED
|
@@ -56,15 +56,19 @@ describe("openclaw-clawchat runtime media ingest", () => {
|
|
|
56
56
|
capturedCtx = ctx;
|
|
57
57
|
return ctx;
|
|
58
58
|
});
|
|
59
|
+
const resolveAgentRoute = vi.fn(() => ({
|
|
60
|
+
agentId: "u",
|
|
61
|
+
accountId: "default",
|
|
62
|
+
sessionKey: "s",
|
|
63
|
+
}));
|
|
64
|
+
const cfg = {
|
|
65
|
+
session: { store: "/tmp/sessions.json", dmScope: "main" },
|
|
66
|
+
} as unknown as OpenClawConfig;
|
|
59
67
|
|
|
60
68
|
const runtime = {
|
|
61
69
|
channel: {
|
|
62
70
|
routing: {
|
|
63
|
-
resolveAgentRoute
|
|
64
|
-
agentId: "u",
|
|
65
|
-
accountId: "default",
|
|
66
|
-
sessionKey: "s",
|
|
67
|
-
})),
|
|
71
|
+
resolveAgentRoute,
|
|
68
72
|
},
|
|
69
73
|
session: {
|
|
70
74
|
resolveStorePath: vi.fn(() => "/tmp/sessions.json"),
|
|
@@ -108,7 +112,7 @@ describe("openclaw-clawchat runtime media ingest", () => {
|
|
|
108
112
|
const abortController = new AbortController();
|
|
109
113
|
|
|
110
114
|
const startPromise = startOpenclawClawlingGateway({
|
|
111
|
-
cfg
|
|
115
|
+
cfg,
|
|
112
116
|
account: {
|
|
113
117
|
accountId: "default",
|
|
114
118
|
name: "openclaw-clawchat",
|
|
@@ -203,6 +207,18 @@ describe("openclaw-clawchat runtime media ingest", () => {
|
|
|
203
207
|
expect(capturedCtx?.From).toBe("openclaw-clawchat:chat-1");
|
|
204
208
|
expect(capturedCtx?.ConversationLabel).toBe("chat-1");
|
|
205
209
|
expect(capturedCtx?.SenderId).toBe("user-1");
|
|
210
|
+
expect(resolveAgentRoute).toHaveBeenCalledWith(
|
|
211
|
+
expect.objectContaining({
|
|
212
|
+
cfg: expect.objectContaining({
|
|
213
|
+
session: expect.objectContaining({
|
|
214
|
+
dmScope: "per-account-channel-peer",
|
|
215
|
+
store: "/tmp/sessions.json",
|
|
216
|
+
}),
|
|
217
|
+
}),
|
|
218
|
+
peer: { kind: "direct", id: "chat-1" },
|
|
219
|
+
}),
|
|
220
|
+
);
|
|
221
|
+
expect(cfg.session?.dmScope).toBe("main");
|
|
206
222
|
});
|
|
207
223
|
|
|
208
224
|
it("uses group chat_id as the canonical conversation identity", async () => {
|
|
@@ -494,7 +510,7 @@ describe("openclaw-clawchat runtime reply dispatch lifecycle", () => {
|
|
|
494
510
|
expect(dispatchReplyFromConfig).toHaveBeenCalledTimes(1);
|
|
495
511
|
expect(markDispatchIdle).toHaveBeenCalledTimes(1);
|
|
496
512
|
expect(logError).toHaveBeenCalledWith(
|
|
497
|
-
expect.stringContaining("openclaw-clawchat
|
|
513
|
+
expect.stringContaining("openclaw-clawchat dispatch failed msg=m-fail"),
|
|
498
514
|
);
|
|
499
515
|
});
|
|
500
516
|
});
|
package/src/runtime.ts
CHANGED
|
@@ -107,6 +107,16 @@ function formatConversationSubject(peer: { kind: "direct" | "group"; id: string
|
|
|
107
107
|
return peer.kind === "group" ? `group:${peer.id}` : peer.id;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
function withClawChatSessionScope(cfg: OpenClawConfig): OpenClawConfig {
|
|
111
|
+
return {
|
|
112
|
+
...cfg,
|
|
113
|
+
session: {
|
|
114
|
+
...(cfg.session ?? {}),
|
|
115
|
+
dmScope: "per-account-channel-peer",
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
110
120
|
export interface StartGatewayParams {
|
|
111
121
|
cfg: OpenClawConfig;
|
|
112
122
|
account: ResolvedOpenclawClawlingAccount;
|
|
@@ -172,8 +182,9 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
172
182
|
ingest: async (turn) => {
|
|
173
183
|
const rt = runtime.channel;
|
|
174
184
|
const storePath = rt.session.resolveStorePath(cfg.session?.store);
|
|
185
|
+
const routeCfg = withClawChatSessionScope(cfg);
|
|
175
186
|
const route = rt.routing.resolveAgentRoute({
|
|
176
|
-
cfg,
|
|
187
|
+
cfg: routeCfg,
|
|
177
188
|
channel: CHANNEL_ID,
|
|
178
189
|
accountId,
|
|
179
190
|
peer: turn.peer,
|
package/src/streaming.test.ts
CHANGED
|
@@ -37,8 +37,8 @@ describe("openclaw-clawchat streaming", () => {
|
|
|
37
37
|
"message.done",
|
|
38
38
|
]);
|
|
39
39
|
expect((client.typing as ReturnType<typeof vi.fn>).mock.calls).toEqual([
|
|
40
|
-
[
|
|
41
|
-
[
|
|
40
|
+
["u1", true, "direct"],
|
|
41
|
+
["u1", false, "direct"],
|
|
42
42
|
]);
|
|
43
43
|
});
|
|
44
44
|
|
|
@@ -110,7 +110,7 @@ describe("openclaw-clawchat streaming", () => {
|
|
|
110
110
|
expect(sent[0]!.payload.sequence).toBe(3);
|
|
111
111
|
expect(sent[0]!.payload.reason).toBe("boom");
|
|
112
112
|
expect((client.typing as ReturnType<typeof vi.fn>).mock.calls).toEqual([
|
|
113
|
-
[
|
|
113
|
+
["u1", false, "direct"],
|
|
114
114
|
]);
|
|
115
115
|
});
|
|
116
116
|
});
|
package/src/streaming.ts
CHANGED
|
@@ -10,7 +10,8 @@ import {
|
|
|
10
10
|
|
|
11
11
|
export interface StreamingSendParams {
|
|
12
12
|
client: ClawlingChatClient;
|
|
13
|
-
routing
|
|
13
|
+
routing?: EnvelopeRouting;
|
|
14
|
+
to?: { id: string; type: "direct" | "group" };
|
|
14
15
|
sender: StreamSender;
|
|
15
16
|
/**
|
|
16
17
|
* Pre-chunked text. Use `chunkMarkdownText` from
|
|
@@ -22,6 +23,12 @@ export interface StreamingSendParams {
|
|
|
22
23
|
emitTyping?: boolean;
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
function resolveRouting(params: { routing?: EnvelopeRouting; to?: { id: string; type: "direct" | "group" } }): EnvelopeRouting {
|
|
27
|
+
if (params.routing) return params.routing;
|
|
28
|
+
if (params.to) return { chatId: params.to.id, chatType: params.to.type };
|
|
29
|
+
throw new Error("openclaw-clawchat streaming requires routing");
|
|
30
|
+
}
|
|
31
|
+
|
|
25
32
|
/**
|
|
26
33
|
* Emit one full streaming lifecycle for a pre-chunked reply.
|
|
27
34
|
*
|
|
@@ -35,13 +42,14 @@ export interface StreamingSendParams {
|
|
|
35
42
|
* With zero chunks: typing(true) -> created -> done -> typing(false).
|
|
36
43
|
*/
|
|
37
44
|
export async function sendStreamingText(params: StreamingSendParams): Promise<void> {
|
|
45
|
+
const routing = resolveRouting(params);
|
|
38
46
|
const emitTyping = params.emitTyping !== false;
|
|
39
47
|
if (emitTyping) {
|
|
40
|
-
params.client.typing(
|
|
48
|
+
params.client.typing(routing.chatId, true, routing.chatType);
|
|
41
49
|
}
|
|
42
50
|
emitStreamCreated(params.client, {
|
|
43
51
|
messageId: params.messageId,
|
|
44
|
-
routing
|
|
52
|
+
routing,
|
|
45
53
|
});
|
|
46
54
|
let sequence = 0;
|
|
47
55
|
let fullText = "";
|
|
@@ -50,7 +58,7 @@ export async function sendStreamingText(params: StreamingSendParams): Promise<vo
|
|
|
50
58
|
fullText += chunk;
|
|
51
59
|
emitStreamAdd(params.client, {
|
|
52
60
|
messageId: params.messageId,
|
|
53
|
-
routing
|
|
61
|
+
routing,
|
|
54
62
|
sequence,
|
|
55
63
|
fullText,
|
|
56
64
|
textDelta: chunk,
|
|
@@ -58,18 +66,19 @@ export async function sendStreamingText(params: StreamingSendParams): Promise<vo
|
|
|
58
66
|
}
|
|
59
67
|
emitStreamDone(params.client, {
|
|
60
68
|
messageId: params.messageId,
|
|
61
|
-
routing
|
|
69
|
+
routing,
|
|
62
70
|
finalSequence: sequence,
|
|
63
71
|
finalText: fullText,
|
|
64
72
|
});
|
|
65
73
|
if (emitTyping) {
|
|
66
|
-
params.client.typing(
|
|
74
|
+
params.client.typing(routing.chatId, false, routing.chatType);
|
|
67
75
|
}
|
|
68
76
|
}
|
|
69
77
|
|
|
70
78
|
export interface StreamingFailureParams {
|
|
71
79
|
client: ClawlingChatClient;
|
|
72
|
-
routing
|
|
80
|
+
routing?: EnvelopeRouting;
|
|
81
|
+
to?: { id: string; type: "direct" | "group" };
|
|
73
82
|
messageId: string;
|
|
74
83
|
currentSequence: number;
|
|
75
84
|
reason?: string;
|
|
@@ -77,13 +86,14 @@ export interface StreamingFailureParams {
|
|
|
77
86
|
}
|
|
78
87
|
|
|
79
88
|
export async function sendStreamingFailure(params: StreamingFailureParams): Promise<void> {
|
|
89
|
+
const routing = resolveRouting(params);
|
|
80
90
|
emitStreamFailed(params.client, {
|
|
81
91
|
messageId: params.messageId,
|
|
82
|
-
routing
|
|
92
|
+
routing,
|
|
83
93
|
sequence: params.currentSequence + 1,
|
|
84
94
|
reason: params.reason,
|
|
85
95
|
});
|
|
86
96
|
if (params.emitTyping !== false) {
|
|
87
|
-
params.client.typing(
|
|
97
|
+
params.client.typing(routing.chatId, false, routing.chatType);
|
|
88
98
|
}
|
|
89
99
|
}
|
package/src/tools-schema.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { Type, type Static } from "@sinclair/typebox";
|
|
2
2
|
|
|
3
|
-
export const
|
|
4
|
-
export type
|
|
3
|
+
export const ClawchatGetAccountProfileSchema = Type.Object({});
|
|
4
|
+
export type ClawchatGetAccountProfileParams = Static<typeof ClawchatGetAccountProfileSchema>;
|
|
5
5
|
|
|
6
|
-
export const
|
|
7
|
-
userId: Type.String({
|
|
6
|
+
export const ClawchatGetUserProfileSchema = Type.Object({
|
|
7
|
+
userId: Type.String({
|
|
8
|
+
description: "Target ClawChat user id (required). Do not infer this from a nickname.",
|
|
9
|
+
}),
|
|
8
10
|
});
|
|
9
|
-
export type
|
|
11
|
+
export type ClawchatGetUserProfileParams = Static<typeof ClawchatGetUserProfileSchema>;
|
|
10
12
|
|
|
11
|
-
export const
|
|
13
|
+
export const ClawchatListAccountFriendsSchema = Type.Object({
|
|
12
14
|
page: Type.Optional(Type.Integer({ minimum: 1, description: "1-based page number (default 1)" })),
|
|
13
15
|
pageSize: Type.Optional(
|
|
14
16
|
Type.Integer({
|
|
@@ -18,35 +20,42 @@ export const ClawchatListFriendsSchema = Type.Object({
|
|
|
18
20
|
}),
|
|
19
21
|
),
|
|
20
22
|
});
|
|
21
|
-
export type
|
|
23
|
+
export type ClawchatListAccountFriendsParams = Static<typeof ClawchatListAccountFriendsSchema>;
|
|
22
24
|
|
|
23
|
-
export const
|
|
24
|
-
nickname: Type.Optional(Type.String({ description: "New
|
|
25
|
+
export const ClawchatUpdateAccountProfileSchema = Type.Object({
|
|
26
|
+
nickname: Type.Optional(Type.String({ description: "New ClawChat account nickname" })),
|
|
25
27
|
avatar_url: Type.Optional(
|
|
26
|
-
Type.String({
|
|
28
|
+
Type.String({
|
|
29
|
+
description:
|
|
30
|
+
"Avatar URL for the ClawChat account profile (use clawchat_upload_avatar_image first to obtain one from a local image)",
|
|
31
|
+
}),
|
|
32
|
+
),
|
|
33
|
+
bio: Type.Optional(
|
|
34
|
+
Type.String({ description: "New ClawChat account self-introduction / bio text" }),
|
|
27
35
|
),
|
|
28
|
-
bio: Type.Optional(Type.String({ description: "New self-introduction / bio text" })),
|
|
29
36
|
});
|
|
30
|
-
export type
|
|
37
|
+
export type ClawchatUpdateAccountProfileParams = Static<
|
|
38
|
+
typeof ClawchatUpdateAccountProfileSchema
|
|
39
|
+
>;
|
|
31
40
|
|
|
32
|
-
export const
|
|
41
|
+
export const ClawchatUploadMediaFileSchema = Type.Object({
|
|
33
42
|
filePath: Type.String({
|
|
34
|
-
description: "Absolute local path of the file to upload (max 20MB)",
|
|
43
|
+
description: "Absolute local path of the media/file to upload to ClawChat (max 20MB)",
|
|
35
44
|
}),
|
|
36
45
|
});
|
|
37
|
-
export type
|
|
46
|
+
export type ClawchatUploadMediaFileParams = Static<typeof ClawchatUploadMediaFileSchema>;
|
|
38
47
|
|
|
39
|
-
export const
|
|
48
|
+
export const ClawchatUploadAvatarImageSchema = Type.Object({
|
|
40
49
|
filePath: Type.String({
|
|
41
|
-
description: "Absolute local path of the avatar image to upload (max 20MB)",
|
|
50
|
+
description: "Absolute local path of the avatar image to upload to ClawChat (max 20MB)",
|
|
42
51
|
}),
|
|
43
52
|
});
|
|
44
|
-
export type
|
|
53
|
+
export type ClawchatUploadAvatarImageParams = Static<typeof ClawchatUploadAvatarImageSchema>;
|
|
45
54
|
|
|
46
55
|
export const ClawchatActivateSchema = Type.Object({
|
|
47
56
|
code: Type.String({
|
|
48
57
|
description:
|
|
49
|
-
"The invite code (e.g. '
|
|
58
|
+
"The invite code (six uppercase letters/digits, e.g. 'A1B2C3') extracted from the user's message. " +
|
|
50
59
|
"Whitespace is trimmed automatically.",
|
|
51
60
|
}),
|
|
52
61
|
});
|