@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/manifest.test.ts
CHANGED
|
@@ -1,12 +1,29 @@
|
|
|
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[];
|
|
9
|
+
scripts: Record<string, string>;
|
|
10
|
+
devDependencies: Record<string, string>;
|
|
11
|
+
peerDependencies: Record<string, string>;
|
|
7
12
|
openclaw: {
|
|
8
|
-
|
|
9
|
-
|
|
13
|
+
extensions: string[];
|
|
14
|
+
runtimeExtensions?: string[];
|
|
15
|
+
setupEntry?: string;
|
|
16
|
+
channel?: {
|
|
17
|
+
id: string;
|
|
18
|
+
label: string;
|
|
19
|
+
selectionLabel?: string;
|
|
20
|
+
docsPath?: string;
|
|
21
|
+
docsLabel?: string;
|
|
22
|
+
blurb: string;
|
|
23
|
+
order?: number;
|
|
24
|
+
aliases?: string[];
|
|
25
|
+
};
|
|
26
|
+
install: { npmSpec: string; minHostVersion: string };
|
|
10
27
|
};
|
|
11
28
|
}
|
|
12
29
|
|
|
@@ -14,9 +31,170 @@ describe("openclaw-clawchat manifest", () => {
|
|
|
14
31
|
it("keeps plugin id / channel id / package name aligned", () => {
|
|
15
32
|
expect(pluginManifest.id).toBe("openclaw-clawchat");
|
|
16
33
|
expect(pluginManifest.channels).toContain("openclaw-clawchat");
|
|
17
|
-
expect(
|
|
34
|
+
expect(pluginManifest.skills).toContain("./skills");
|
|
35
|
+
expect(pluginManifest.channelConfigs?.["openclaw-clawchat"]?.label).toBe(
|
|
36
|
+
"Clawling Chat",
|
|
37
|
+
);
|
|
38
|
+
expect(pluginManifest.channelConfigs?.["openclaw-clawchat"]?.schema?.properties).toHaveProperty(
|
|
39
|
+
"token",
|
|
40
|
+
);
|
|
41
|
+
expect(packageJson.name).toBe("@newbase-clawchat/openclaw-clawchat");
|
|
18
42
|
const pkg = packageJson as PackageJsonWithOpenclaw;
|
|
19
|
-
expect(pkg.openclaw.
|
|
20
|
-
expect(pkg.openclaw.install.npmSpec).toBe("openclaw-clawchat");
|
|
43
|
+
expect(pkg.openclaw.extensions).toContain("./index.ts");
|
|
44
|
+
expect(pkg.openclaw.install.npmSpec).toBe("@newbase-clawchat/openclaw-clawchat");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("requires an OpenClaw host with runtime config mutation support", () => {
|
|
48
|
+
const pkg = packageJson as PackageJsonWithOpenclaw;
|
|
49
|
+
expect(pkg.peerDependencies.openclaw).toBe("^2026.4.29");
|
|
50
|
+
expect(pkg.devDependencies.openclaw).toBe("2026.4.29");
|
|
51
|
+
expect(pkg.openclaw.install.minHostVersion).toBe(">=2026.4.29");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("publishes compiled runtime entrypoints for npm plugin installs", () => {
|
|
55
|
+
const pkg = packageJson as PackageJsonWithOpenclaw;
|
|
56
|
+
expect(pkg.openclaw.extensions).toEqual(["./index.ts"]);
|
|
57
|
+
expect(pkg.openclaw.runtimeExtensions).toEqual(["./dist/index.js"]);
|
|
58
|
+
expect(pkg.files).toContain("dist");
|
|
59
|
+
expect(pkg.scripts.build).toBe("tsc -p tsconfig.build.json");
|
|
60
|
+
expect(pkg.scripts.prepack).toBe("npm run build");
|
|
61
|
+
expect(fs.existsSync(new URL("../tsconfig.build.json", import.meta.url))).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("publishes channel catalog metadata for OpenClaw CLI discovery", () => {
|
|
65
|
+
const pkg = packageJson as PackageJsonWithOpenclaw;
|
|
66
|
+
expect(pkg.openclaw.channel).toEqual({
|
|
67
|
+
id: "openclaw-clawchat",
|
|
68
|
+
label: "Clawling Chat",
|
|
69
|
+
selectionLabel: "Clawling Chat",
|
|
70
|
+
docsPath: "/channels/openclaw-clawchat",
|
|
71
|
+
docsLabel: "openclaw-clawchat",
|
|
72
|
+
blurb: "Clawling Protocol v2 over WebSocket (chat-sdk).",
|
|
73
|
+
order: 110,
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("declares supported channel/command activation hints for plugin loading", () => {
|
|
78
|
+
expect(pluginManifest.activation).toEqual({
|
|
79
|
+
onStartup: true,
|
|
80
|
+
onChannels: ["openclaw-clawchat"],
|
|
81
|
+
onCommands: ["clawchat-login"],
|
|
82
|
+
});
|
|
83
|
+
expect(pluginManifest.commandAliases).toEqual([
|
|
84
|
+
{ name: "clawchat-login", kind: "runtime-slash" },
|
|
85
|
+
]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("does not publish setup migration or setup-runtime entry metadata", () => {
|
|
89
|
+
const pkg = packageJson as PackageJsonWithOpenclaw;
|
|
90
|
+
expect(pkg.files).not.toContain("setup-api.ts");
|
|
91
|
+
expect(pkg.files).not.toContain("setup-entry.ts");
|
|
92
|
+
expect(pkg.openclaw.setupEntry).toBeUndefined();
|
|
93
|
+
expect(fs.existsSync(new URL("../setup-api.ts", import.meta.url))).toBe(false);
|
|
94
|
+
expect(fs.existsSync(new URL("../setup-entry.ts", import.meta.url))).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("does not document channels add as an activation path", () => {
|
|
98
|
+
const readme = fs.readFileSync(new URL("../README.md", import.meta.url), "utf8");
|
|
99
|
+
const docs = fs.readFileSync(new URL("../docs/openclaw-clawchat.md", import.meta.url), "utf8");
|
|
100
|
+
expect(readme).not.toMatch(/channels add --channel openclaw-clawchat/i);
|
|
101
|
+
expect(docs).not.toMatch(/channels add --channel openclaw-clawchat/i);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("publishes a ClawChat account tools skill for non-activation workflows", () => {
|
|
105
|
+
const skill = fs.readFileSync(new URL("../skills/clawchat-account-tools/SKILL.md", import.meta.url), "utf8");
|
|
106
|
+
expect(skill).toMatch(/^---\nname: clawchat-account-tools\n/m);
|
|
107
|
+
expect(skill).toMatch(/description: .*Use when/i);
|
|
108
|
+
expect(skill).toMatch(/clawchat_get_account_profile/);
|
|
109
|
+
expect(skill).toMatch(/clawchat_get_user_profile/);
|
|
110
|
+
expect(skill).toMatch(/clawchat_list_account_friends/);
|
|
111
|
+
expect(skill).toMatch(/clawchat_update_account_profile/);
|
|
112
|
+
expect(skill).toMatch(/clawchat_upload_avatar_image/);
|
|
113
|
+
expect(skill).toMatch(/clawchat_upload_media_file/);
|
|
114
|
+
expect(skill).toMatch(/configured ClawChat account/i);
|
|
115
|
+
expect(skill).not.toMatch(/clawchat_activate/);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("declares ownership of registered ClawChat agent tools", () => {
|
|
119
|
+
expect(pluginManifest.contracts?.tools).toEqual([
|
|
120
|
+
"clawchat_activate",
|
|
121
|
+
"clawchat_get_account_profile",
|
|
122
|
+
"clawchat_get_user_profile",
|
|
123
|
+
"clawchat_list_account_friends",
|
|
124
|
+
"clawchat_update_account_profile",
|
|
125
|
+
"clawchat_upload_avatar_image",
|
|
126
|
+
"clawchat_upload_media_file",
|
|
127
|
+
]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("keeps the optional OpenClaw source checkout local-only", () => {
|
|
131
|
+
expect(fs.existsSync(new URL("../.gitmodules", import.meta.url))).toBe(false);
|
|
132
|
+
|
|
133
|
+
const gitignore = fs.readFileSync(new URL("../.gitignore", import.meta.url), "utf8");
|
|
134
|
+
expect(gitignore).toMatch(/^tmp\/openclaw\/$/m);
|
|
135
|
+
|
|
136
|
+
const pkg = packageJson as PackageJsonWithOpenclaw;
|
|
137
|
+
expect(pkg.scripts["dev:openclaw-source"]).toBe(
|
|
138
|
+
"test -d tmp/openclaw || git clone --depth=1 https://github.com/openclaw/openclaw.git tmp/openclaw",
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const readme = fs.readFileSync(new URL("../README.md", import.meta.url), "utf8");
|
|
142
|
+
expect(readme).toMatch(/npm run dev:openclaw-source/);
|
|
143
|
+
expect(readme).toMatch(
|
|
144
|
+
/git clone --depth=1 https:\/\/github\.com\/openclaw\/openclaw\.git tmp\/openclaw/,
|
|
145
|
+
);
|
|
146
|
+
expect(readme).toMatch(/local-only/i);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("keeps default Vitest discovery scoped to plugin sources", () => {
|
|
150
|
+
const configUrl = new URL("../vitest.config.ts", import.meta.url);
|
|
151
|
+
expect(fs.existsSync(configUrl)).toBe(true);
|
|
152
|
+
const config = fs.readFileSync(configUrl, "utf8");
|
|
153
|
+
expect(config).toMatch(/include:\s*\["src\/\*\*\/\*\.test\.ts"\]/);
|
|
154
|
+
expect(config).toMatch(/"tmp\/\*\*"/);
|
|
155
|
+
expect(config).toMatch(/"\.e2e\/\*\*"/);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("keeps the activation skill on clawchat_activate with channel-login fallback", () => {
|
|
159
|
+
const skill = fs.readFileSync(new URL("../skills/clawchat-activate/SKILL.md", import.meta.url), "utf8");
|
|
160
|
+
expect(skill).toMatch(/name:\s*clawchat-activate/);
|
|
161
|
+
expect(skill).toMatch(/clawchat_activate/);
|
|
162
|
+
expect(skill).not.toMatch(/`clawchat\s+A1B2C3`/i);
|
|
163
|
+
expect(skill).not.toMatch(/`clawchat\s*<code>`/i);
|
|
164
|
+
expect(skill).not.toMatch(/\/clawchat_activate A1B2C3/);
|
|
165
|
+
expect(skill).not.toMatch(/\/clawchat-activate A1B2C3/);
|
|
166
|
+
expect(skill).not.toMatch(/\/clawchat-login A1B2C3/);
|
|
167
|
+
expect(skill).toMatch(/openclaw channels login --channel openclaw-clawchat/);
|
|
168
|
+
expect(skill).toMatch(/do not append/i);
|
|
169
|
+
expect(skill).toMatch(/prompt[^\n]+invite code[^\n]+provide/i);
|
|
170
|
+
expect(skill).toMatch(/channel login/i);
|
|
171
|
+
expect(skill).toMatch(/openclaw channels status --probe/);
|
|
172
|
+
expect(skill).toMatch(/openclaw gateway restart/);
|
|
173
|
+
expect(skill).not.toMatch(/ask the user to send/i);
|
|
174
|
+
expect(skill).not.toMatch(/give the exact/i);
|
|
175
|
+
expect(skill).toMatch(/restart[^\n]+only when/i);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("documents clawchat_activate as the natural-language activation path with CLI fallback", () => {
|
|
179
|
+
const readme = fs.readFileSync(new URL("../README.md", import.meta.url), "utf8");
|
|
180
|
+
const docs = fs.readFileSync(new URL("../docs/openclaw-clawchat.md", import.meta.url), "utf8");
|
|
181
|
+
expect(readme).toMatch(/clawchat_activate/i);
|
|
182
|
+
expect(docs).toMatch(/clawchat_activate/i);
|
|
183
|
+
expect(readme).toMatch(/openclaw channels login --channel openclaw-clawchat/i);
|
|
184
|
+
expect(docs).toMatch(/openclaw channels login --channel openclaw-clawchat/i);
|
|
185
|
+
expect(readme).toMatch(/openclaw channels status --probe/i);
|
|
186
|
+
expect(docs).toMatch(/openclaw channels status --probe/i);
|
|
187
|
+
expect(readme).toMatch(/openclaw gateway restart/i);
|
|
188
|
+
expect(docs).toMatch(/openclaw gateway restart/i);
|
|
189
|
+
expect(readme).not.toMatch(/activation skill[^.]+\/clawchat-login/i);
|
|
190
|
+
expect(docs).not.toMatch(/natural-language activation requests[^.]+\/clawchat-login/i);
|
|
191
|
+
expect(readme).not.toMatch(/\/clawchat-activate\s+A1B2C3/i);
|
|
192
|
+
expect(docs).not.toMatch(/\/clawchat-activate\s+A1B2C3/i);
|
|
193
|
+
expect(readme).not.toMatch(/\/clawchat_activate\s+A1B2C3/i);
|
|
194
|
+
expect(docs).not.toMatch(/\/clawchat_activate\s+A1B2C3/i);
|
|
195
|
+
expect(readme).not.toMatch(/direct users to/i);
|
|
196
|
+
expect(docs).not.toMatch(/direct the\s+user/i);
|
|
197
|
+
expect(readme).toMatch(/activation skill calls/i);
|
|
198
|
+
expect(docs).toMatch(/Natural-language activation requests should call `clawchat_activate`/i);
|
|
21
199
|
});
|
|
22
200
|
});
|
package/src/outbound.test.ts
CHANGED
|
@@ -64,9 +64,10 @@ 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
68
|
body: { fragments: [{ kind: "text", text: "hello" }] },
|
|
69
69
|
});
|
|
70
|
+
expect((client.sendMessage as ReturnType<typeof vi.fn>).mock.calls[0][0]).not.toHaveProperty("chat_type");
|
|
70
71
|
expect(client.replyMessage).not.toHaveBeenCalled();
|
|
71
72
|
expect(result?.messageId).toBe("server-m1");
|
|
72
73
|
expect(result?.acceptedAt).toBe(1234);
|
|
@@ -77,25 +78,27 @@ describe("openclaw-clawchat outbound", () => {
|
|
|
77
78
|
await sendOpenclawClawlingText({
|
|
78
79
|
client,
|
|
79
80
|
account: baseAccount(),
|
|
80
|
-
to: { chatId: "
|
|
81
|
+
to: { chatId: "chat-1", chatType: "direct" },
|
|
81
82
|
text: "reply",
|
|
82
83
|
replyCtx: {
|
|
83
84
|
replyToMessageId: "m-orig",
|
|
85
|
+
replyPreviewChatId: "chat-1",
|
|
84
86
|
replyPreviewSenderId: "user-2",
|
|
85
|
-
|
|
87
|
+
replyPreviewNickName: "Sender",
|
|
86
88
|
replyPreviewText: "original",
|
|
87
89
|
},
|
|
88
90
|
});
|
|
89
91
|
expect((client.replyMessage as ReturnType<typeof vi.fn>).mock.calls[0][0]).toMatchObject({
|
|
90
|
-
|
|
92
|
+
chat_id: "chat-1",
|
|
91
93
|
replyTo: {
|
|
92
94
|
msgId: "m-orig",
|
|
93
|
-
senderId: "
|
|
94
|
-
|
|
95
|
+
senderId: "chat-1",
|
|
96
|
+
nickName: "Sender",
|
|
95
97
|
fragments: [{ kind: "text", text: "original" }],
|
|
96
98
|
},
|
|
97
99
|
body: { fragments: [{ kind: "text", text: "reply" }] },
|
|
98
100
|
});
|
|
101
|
+
expect((client.replyMessage as ReturnType<typeof vi.fn>).mock.calls[0][0]).not.toHaveProperty("chat_type");
|
|
99
102
|
expect(client.sendMessage).not.toHaveBeenCalled();
|
|
100
103
|
});
|
|
101
104
|
|
|
@@ -168,7 +171,7 @@ describe("openclaw-clawchat outbound", () => {
|
|
|
168
171
|
replyCtx: {
|
|
169
172
|
replyToMessageId: "m-orig",
|
|
170
173
|
replyPreviewSenderId: "user-2",
|
|
171
|
-
|
|
174
|
+
replyPreviewNickName: "Sender",
|
|
172
175
|
replyPreviewText: "original",
|
|
173
176
|
},
|
|
174
177
|
mediaFragments: [{ kind: "image", url: "https://cdn/x.png" }],
|
package/src/outbound.ts
CHANGED
|
@@ -22,6 +22,7 @@ export interface OutboundTarget {
|
|
|
22
22
|
|
|
23
23
|
export interface OutboundReplyCtx {
|
|
24
24
|
replyToMessageId: string;
|
|
25
|
+
replyPreviewChatId?: string;
|
|
25
26
|
replyPreviewSenderId: string;
|
|
26
27
|
replyPreviewNickName: string;
|
|
27
28
|
replyPreviewText: string;
|
|
@@ -38,6 +39,7 @@ export interface SendParams {
|
|
|
38
39
|
to: OutboundTarget;
|
|
39
40
|
text: string;
|
|
40
41
|
replyCtx?: OutboundReplyCtx;
|
|
42
|
+
richFragments?: Fragment[];
|
|
41
43
|
mediaFragments?: ClawlingMediaFragment[];
|
|
42
44
|
mentions?: string[];
|
|
43
45
|
log?: LogSink;
|
|
@@ -92,8 +94,9 @@ export function parseOpenclawRecipient(to: string): { chatId: string; chatType:
|
|
|
92
94
|
|
|
93
95
|
export async function sendOpenclawClawlingText(params: SendParams): Promise<SendResult | null> {
|
|
94
96
|
const text = (params.text ?? "").trim();
|
|
97
|
+
const richFragments = params.richFragments ?? [];
|
|
95
98
|
const mediaFragments = params.mediaFragments ?? [];
|
|
96
|
-
if (!text && mediaFragments.length === 0) {
|
|
99
|
+
if (!text && richFragments.length === 0 && mediaFragments.length === 0) {
|
|
97
100
|
params.log?.info?.(
|
|
98
101
|
`[${params.account.accountId}] openclaw-clawchat outbound suppressed: empty text and no media`,
|
|
99
102
|
);
|
|
@@ -106,7 +109,7 @@ export async function sendOpenclawClawlingText(params: SendParams): Promise<Send
|
|
|
106
109
|
// with one of the SDK's narrow Fragment members (ImageFragment / FileFragment /
|
|
107
110
|
// AudioFragment / VideoFragment) based on its runtime `kind`. The wide local
|
|
108
111
|
// shape lets us build a single uniform array without a per-kind switch.
|
|
109
|
-
const fragments = [...textFragments, ...mediaFragments] as Fragment[];
|
|
112
|
+
const fragments = [...textFragments, ...richFragments, ...mediaFragments] as Fragment[];
|
|
110
113
|
|
|
111
114
|
const useReply = params.replyCtx && mediaFragments.length === 0;
|
|
112
115
|
if (params.replyCtx && mediaFragments.length > 0) {
|
|
@@ -121,26 +124,24 @@ export async function sendOpenclawClawlingText(params: SendParams): Promise<Send
|
|
|
121
124
|
mode = "reply";
|
|
122
125
|
ack = await params.client.replyMessage({
|
|
123
126
|
chat_id: params.to.chatId,
|
|
124
|
-
chat_type: params.to.chatType,
|
|
125
127
|
mode: "normal",
|
|
126
128
|
replyTo: {
|
|
127
129
|
msgId: params.replyCtx.replyToMessageId,
|
|
128
|
-
senderId: params.replyCtx.replyPreviewSenderId,
|
|
130
|
+
senderId: params.replyCtx.replyPreviewChatId ?? params.replyCtx.replyPreviewSenderId,
|
|
129
131
|
nickName: params.replyCtx.replyPreviewNickName,
|
|
130
132
|
fragments: [{ kind: "text", text: params.replyCtx.replyPreviewText }],
|
|
131
133
|
},
|
|
132
134
|
body: { fragments },
|
|
133
135
|
context: { mentions },
|
|
134
|
-
});
|
|
136
|
+
} as Parameters<ClawlingChatClient["replyMessage"]>[0]);
|
|
135
137
|
} else {
|
|
136
138
|
mode = "send";
|
|
137
139
|
ack = await params.client.sendMessage({
|
|
138
140
|
chat_id: params.to.chatId,
|
|
139
|
-
chat_type: params.to.chatType,
|
|
140
141
|
mode: "normal",
|
|
141
142
|
body: { fragments },
|
|
142
143
|
context: { mentions, reply: null },
|
|
143
|
-
});
|
|
144
|
+
} as Parameters<ClawlingChatClient["sendMessage"]>[0]);
|
|
144
145
|
}
|
|
145
146
|
params.log?.info?.(
|
|
146
147
|
`[${params.account.accountId}] openclaw-clawchat outbound mode=${mode} msg=${ack.payload.message_id} text_len=${text.length} media=${mediaFragments.length} trace=${ack.trace_id}`,
|
|
@@ -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
|
+
}
|