@cored-im/openclaw-plugin 0.1.7 → 0.1.11
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 +1 -5
- package/README.zh.md +1 -5
- package/dist/index.cjs +124 -143
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -9
- package/dist/index.d.ts +11 -9
- package/dist/index.js +124 -143
- package/dist/index.js.map +1 -1
- package/dist/setup-entry.cjs +114 -117
- package/dist/setup-entry.cjs.map +1 -1
- package/dist/setup-entry.d.cts +4 -9
- package/dist/setup-entry.d.ts +4 -9
- package/dist/setup-entry.js +114 -117
- package/dist/setup-entry.js.map +1 -1
- package/dist/types-BEkT8vuK.d.cts +11 -0
- package/dist/types-BEkT8vuK.d.ts +11 -0
- package/openclaw.plugin.json +6 -53
- package/package.json +3 -2
- package/src/channel.ts +129 -134
- package/src/config.test.ts +4 -10
- package/src/config.ts +1 -11
- package/src/index.ts +15 -21
- package/src/messaging/inbound.test.ts +12 -160
- package/src/messaging/inbound.ts +12 -41
- package/src/setup-entry.ts +2 -2
- package/src/types.ts +3 -16
- package/src/typings/openclaw-plugin-sdk.d.ts +0 -162
package/src/config.test.ts
CHANGED
|
@@ -61,15 +61,12 @@ describe("resolveAccountConfig", () => {
|
|
|
61
61
|
const account = resolveAccountConfig(cfg);
|
|
62
62
|
expect(account).toEqual({
|
|
63
63
|
accountId: "default",
|
|
64
|
-
enabled: true,
|
|
65
64
|
appId: "app_1",
|
|
66
65
|
appSecret: "secret_1",
|
|
67
66
|
backendUrl: "https://your-backend-url.com",
|
|
67
|
+
enabled: true,
|
|
68
68
|
enableEncryption: true,
|
|
69
69
|
requestTimeout: 30_000,
|
|
70
|
-
requireMention: true,
|
|
71
|
-
botUserId: undefined,
|
|
72
|
-
inboundWhitelist: [],
|
|
73
70
|
});
|
|
74
71
|
});
|
|
75
72
|
|
|
@@ -82,7 +79,7 @@ describe("resolveAccountConfig", () => {
|
|
|
82
79
|
appId: "app_bot1",
|
|
83
80
|
appSecret: "secret_bot1",
|
|
84
81
|
backendUrl: "http://localhost:11000",
|
|
85
|
-
|
|
82
|
+
enableEncryption: false,
|
|
86
83
|
},
|
|
87
84
|
},
|
|
88
85
|
},
|
|
@@ -91,8 +88,7 @@ describe("resolveAccountConfig", () => {
|
|
|
91
88
|
const account = resolveAccountConfig(cfg, "bot1");
|
|
92
89
|
expect(account.accountId).toBe("bot1");
|
|
93
90
|
expect(account.appId).toBe("app_bot1");
|
|
94
|
-
expect(account.
|
|
95
|
-
expect(account.enableEncryption).toBe(true); // default
|
|
91
|
+
expect(account.enableEncryption).toBe(false);
|
|
96
92
|
});
|
|
97
93
|
|
|
98
94
|
it("falls back to env vars when config values are missing", () => {
|
|
@@ -136,14 +132,12 @@ describe("resolveAccountConfig", () => {
|
|
|
136
132
|
describe("validateAccountConfig", () => {
|
|
137
133
|
const validConfig = {
|
|
138
134
|
accountId: "default",
|
|
139
|
-
enabled: true,
|
|
140
135
|
appId: "app_1",
|
|
141
136
|
appSecret: "secret_1",
|
|
142
137
|
backendUrl: "https://api.cored.im",
|
|
138
|
+
enabled: true,
|
|
143
139
|
enableEncryption: true,
|
|
144
140
|
requestTimeout: 30_000,
|
|
145
|
-
requireMention: true,
|
|
146
|
-
inboundWhitelist: [],
|
|
147
141
|
};
|
|
148
142
|
|
|
149
143
|
it("returns no errors for valid config", () => {
|
package/src/config.ts
CHANGED
|
@@ -6,8 +6,6 @@ import type { CoredChannelConfig, CoredAccountConfig } from "./types.js";
|
|
|
6
6
|
const DEFAULTS = {
|
|
7
7
|
enableEncryption: true,
|
|
8
8
|
requestTimeout: 30_000,
|
|
9
|
-
requireMention: true,
|
|
10
|
-
inboundWhitelist: [] as string[],
|
|
11
9
|
} as const;
|
|
12
10
|
|
|
13
11
|
const ENV_PREFIX = "CORED_";
|
|
@@ -35,10 +33,6 @@ function readEnvConfig(): Partial<CoredAccountConfig> {
|
|
|
35
33
|
env[`${ENV_PREFIX}ENABLE_ENCRYPTION`] !== "false";
|
|
36
34
|
if (env[`${ENV_PREFIX}REQUEST_TIMEOUT`])
|
|
37
35
|
result.requestTimeout = Number(env[`${ENV_PREFIX}REQUEST_TIMEOUT`]);
|
|
38
|
-
if (env[`${ENV_PREFIX}REQUIRE_MENTION`] !== undefined)
|
|
39
|
-
result.requireMention = env[`${ENV_PREFIX}REQUIRE_MENTION`] !== "false";
|
|
40
|
-
if (env[`${ENV_PREFIX}BOT_USER_ID`])
|
|
41
|
-
result.botUserId = env[`${ENV_PREFIX}BOT_USER_ID`];
|
|
42
36
|
|
|
43
37
|
return result;
|
|
44
38
|
}
|
|
@@ -69,18 +63,14 @@ export function resolveAccountConfig(
|
|
|
69
63
|
|
|
70
64
|
return {
|
|
71
65
|
accountId: id,
|
|
72
|
-
enabled: raw?.enabled ?? true,
|
|
73
66
|
appId: raw?.appId ?? envConfig.appId ?? "",
|
|
74
67
|
appSecret: raw?.appSecret ?? envConfig.appSecret ?? "",
|
|
75
68
|
backendUrl: raw?.backendUrl ?? envConfig.backendUrl ?? "",
|
|
69
|
+
enabled: raw?.enabled ?? true,
|
|
76
70
|
enableEncryption:
|
|
77
71
|
raw?.enableEncryption ?? envConfig.enableEncryption ?? DEFAULTS.enableEncryption,
|
|
78
72
|
requestTimeout:
|
|
79
73
|
raw?.requestTimeout ?? envConfig.requestTimeout ?? DEFAULTS.requestTimeout,
|
|
80
|
-
requireMention:
|
|
81
|
-
raw?.requireMention ?? envConfig.requireMention ?? DEFAULTS.requireMention,
|
|
82
|
-
botUserId: raw?.botUserId ?? envConfig.botUserId,
|
|
83
|
-
inboundWhitelist: raw?.inboundWhitelist ?? [...DEFAULTS.inboundWhitelist],
|
|
84
74
|
};
|
|
85
75
|
}
|
|
86
76
|
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Copyright (c) 2026 Cored Limited
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
import { defineChannelPluginEntry
|
|
4
|
+
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
|
|
5
5
|
import { coredPlugin } from "./channel.js";
|
|
6
6
|
import { listEnabledAccountConfigs, validateAccountConfig } from "./config.js";
|
|
7
7
|
import {
|
|
@@ -9,64 +9,58 @@ import {
|
|
|
9
9
|
destroyAllClients,
|
|
10
10
|
clientCount,
|
|
11
11
|
} from "./core/cored-client.js";
|
|
12
|
-
import {
|
|
12
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
|
13
|
+
import { processInboundMessage } from "./messaging/inbound.js";
|
|
13
14
|
import { makeDeliver, setTyping, clearTyping, readMessage } from "./messaging/outbound.js";
|
|
14
15
|
import type { CoredAccountConfig, CoredMessageEvent } from "./types.js";
|
|
15
16
|
|
|
16
|
-
// Extended PluginApi with config type for service registration
|
|
17
|
-
interface ServicePluginApi extends ExtendedPluginApi {
|
|
18
|
-
config: { channels?: Record<string, unknown> };
|
|
19
|
-
}
|
|
20
|
-
|
|
21
17
|
export default defineChannelPluginEntry({
|
|
22
18
|
id: "cored",
|
|
23
19
|
name: "Cored",
|
|
24
20
|
description: "Connect OpenClaw with Cored",
|
|
25
21
|
plugin: coredPlugin,
|
|
26
22
|
registerFull(api) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
typedApi.registerService({
|
|
23
|
+
api.registerService({
|
|
30
24
|
id: "cored-sdk",
|
|
31
25
|
start: async () => {
|
|
32
26
|
if (clientCount() > 0) return;
|
|
33
27
|
|
|
34
|
-
const accounts = listEnabledAccountConfigs(
|
|
28
|
+
const accounts = listEnabledAccountConfigs(api.config);
|
|
35
29
|
if (accounts.length === 0) {
|
|
36
|
-
|
|
30
|
+
api.logger?.warn?.("[cored] no enabled account config found — service idle");
|
|
37
31
|
return;
|
|
38
32
|
}
|
|
39
33
|
|
|
40
34
|
for (const account of accounts) {
|
|
41
35
|
const errors = validateAccountConfig(account);
|
|
42
36
|
if (errors.length > 0) {
|
|
43
|
-
|
|
37
|
+
api.logger?.warn?.(
|
|
44
38
|
`[cored] skipping account=${account.accountId}: ${errors.map((e) => e.message).join("; ")}`,
|
|
45
39
|
);
|
|
46
40
|
continue;
|
|
47
41
|
}
|
|
48
42
|
|
|
49
43
|
try {
|
|
50
|
-
await startAccount(
|
|
51
|
-
|
|
44
|
+
await startAccount(api, account);
|
|
45
|
+
api.logger?.info?.(
|
|
52
46
|
`[cored] account=${account.accountId} connected (appId=${account.appId})`,
|
|
53
47
|
);
|
|
54
48
|
} catch (err) {
|
|
55
|
-
|
|
49
|
+
api.logger?.error?.(
|
|
56
50
|
`[cored] account=${account.accountId} failed to start: ${err instanceof Error ? err.message : String(err)}`,
|
|
57
51
|
);
|
|
58
52
|
}
|
|
59
53
|
}
|
|
60
54
|
|
|
61
|
-
|
|
55
|
+
api.logger?.info?.(`[cored] service started with ${clientCount()} account(s)`);
|
|
62
56
|
},
|
|
63
57
|
stop: async () => {
|
|
64
58
|
await destroyAllClients();
|
|
65
|
-
|
|
59
|
+
api.logger?.info?.("[cored] service stopped — all clients disconnected");
|
|
66
60
|
},
|
|
67
61
|
});
|
|
68
62
|
|
|
69
|
-
|
|
63
|
+
api.logger?.info?.("[cored] plugin registered");
|
|
70
64
|
},
|
|
71
65
|
});
|
|
72
66
|
|
|
@@ -74,7 +68,7 @@ export default defineChannelPluginEntry({
|
|
|
74
68
|
* Start a single account — create client, subscribe to inbound events.
|
|
75
69
|
*/
|
|
76
70
|
async function startAccount(
|
|
77
|
-
api:
|
|
71
|
+
api: OpenClawPluginApi,
|
|
78
72
|
account: CoredAccountConfig,
|
|
79
73
|
): Promise<void> {
|
|
80
74
|
const deliver = makeDeliver(account.accountId, (msg) => api.logger?.warn?.(msg));
|
|
@@ -97,7 +91,7 @@ async function startAccount(
|
|
|
97
91
|
* Handle a single inbound message with typing indicator lifecycle.
|
|
98
92
|
*/
|
|
99
93
|
async function handleInbound(
|
|
100
|
-
api:
|
|
94
|
+
api: OpenClawPluginApi,
|
|
101
95
|
account: CoredAccountConfig,
|
|
102
96
|
event: CoredMessageEvent,
|
|
103
97
|
deliver: (chatId: string, text: string) => Promise<void>,
|
|
@@ -5,7 +5,6 @@ import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
|
5
5
|
import {
|
|
6
6
|
parseMessageEvent,
|
|
7
7
|
checkMessageGate,
|
|
8
|
-
isBotMentioned,
|
|
9
8
|
isDuplicate,
|
|
10
9
|
_resetDedup,
|
|
11
10
|
buildContext,
|
|
@@ -14,8 +13,8 @@ import {
|
|
|
14
13
|
import type {
|
|
15
14
|
CoredAccountConfig,
|
|
16
15
|
CoredMessageEvent,
|
|
17
|
-
PluginApi,
|
|
18
16
|
} from "../types.js";
|
|
17
|
+
import type { InboundPluginApi } from "./inbound.js";
|
|
19
18
|
|
|
20
19
|
// ---------------------------------------------------------------------------
|
|
21
20
|
// Helpers
|
|
@@ -26,15 +25,12 @@ function makeAccount(
|
|
|
26
25
|
): CoredAccountConfig {
|
|
27
26
|
return {
|
|
28
27
|
accountId: "test-account",
|
|
29
|
-
enabled: true,
|
|
30
28
|
appId: "app_test",
|
|
31
29
|
appSecret: "secret",
|
|
32
30
|
backendUrl: "https://your-backend-url.com",
|
|
31
|
+
enabled: true,
|
|
33
32
|
enableEncryption: true,
|
|
34
33
|
requestTimeout: 30_000,
|
|
35
|
-
requireMention: true,
|
|
36
|
-
botUserId: "bot_user_001",
|
|
37
|
-
inboundWhitelist: [],
|
|
38
34
|
...overrides,
|
|
39
35
|
};
|
|
40
36
|
}
|
|
@@ -56,7 +52,7 @@ function makeEvent(
|
|
|
56
52
|
};
|
|
57
53
|
}
|
|
58
54
|
|
|
59
|
-
function makeMockApi():
|
|
55
|
+
function makeMockApi(): InboundPluginApi {
|
|
60
56
|
return {
|
|
61
57
|
registerChannel: vi.fn(),
|
|
62
58
|
registerService: vi.fn(),
|
|
@@ -197,112 +193,11 @@ describe("parseMessageEvent", () => {
|
|
|
197
193
|
describe("checkMessageGate", () => {
|
|
198
194
|
it("passes a normal direct message", () => {
|
|
199
195
|
const msg = parseMessageEvent(makeEvent())!;
|
|
200
|
-
const account = makeAccount({ requireMention: false });
|
|
201
|
-
const result = checkMessageGate(msg, account);
|
|
202
|
-
expect(result.pass).toBe(true);
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it("blocks self-messages", () => {
|
|
206
|
-
const event = makeEvent({
|
|
207
|
-
sender: { userId: "bot_user_001" },
|
|
208
|
-
});
|
|
209
|
-
const msg = parseMessageEvent(event)!;
|
|
210
196
|
const account = makeAccount();
|
|
211
197
|
const result = checkMessageGate(msg, account);
|
|
212
|
-
expect(result.pass).toBe(false);
|
|
213
|
-
expect(result.reason).toBe("self-message");
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
it("blocks sender not in whitelist when whitelist is set", () => {
|
|
217
|
-
const msg = parseMessageEvent(makeEvent())!;
|
|
218
|
-
const account = makeAccount({
|
|
219
|
-
inboundWhitelist: ["allowed_user"],
|
|
220
|
-
});
|
|
221
|
-
const result = checkMessageGate(msg, account);
|
|
222
|
-
expect(result.pass).toBe(false);
|
|
223
|
-
expect(result.reason).toBe("sender-not-in-whitelist");
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it("passes sender in whitelist", () => {
|
|
227
|
-
const msg = parseMessageEvent(makeEvent())!;
|
|
228
|
-
const account = makeAccount({
|
|
229
|
-
inboundWhitelist: ["user_sender_001"],
|
|
230
|
-
requireMention: false,
|
|
231
|
-
});
|
|
232
|
-
const result = checkMessageGate(msg, account);
|
|
233
198
|
expect(result.pass).toBe(true);
|
|
234
199
|
});
|
|
235
200
|
|
|
236
|
-
it("blocks group message without mention when requireMention is true", () => {
|
|
237
|
-
const event = makeEvent({ chatType: "group" });
|
|
238
|
-
const msg = parseMessageEvent(event)!;
|
|
239
|
-
const account = makeAccount({ requireMention: true });
|
|
240
|
-
const result = checkMessageGate(msg, account);
|
|
241
|
-
expect(result.pass).toBe(false);
|
|
242
|
-
expect(result.reason).toBe("group-no-mention");
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
it("passes group message with mention", () => {
|
|
246
|
-
const event = makeEvent({
|
|
247
|
-
chatType: "group",
|
|
248
|
-
mentionUserList: [{ userId: "bot_user_001" }],
|
|
249
|
-
});
|
|
250
|
-
const msg = parseMessageEvent(event)!;
|
|
251
|
-
const account = makeAccount({ requireMention: true });
|
|
252
|
-
const result = checkMessageGate(msg, account);
|
|
253
|
-
expect(result.pass).toBe(true);
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
it("passes group message when requireMention is false", () => {
|
|
257
|
-
const event = makeEvent({ chatType: "group" });
|
|
258
|
-
const msg = parseMessageEvent(event)!;
|
|
259
|
-
const account = makeAccount({ requireMention: false });
|
|
260
|
-
const result = checkMessageGate(msg, account);
|
|
261
|
-
expect(result.pass).toBe(true);
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
it("skips self-message check when botUserId is not set", () => {
|
|
265
|
-
const event = makeEvent({
|
|
266
|
-
sender: { userId: "any_user" },
|
|
267
|
-
});
|
|
268
|
-
const msg = parseMessageEvent(event)!;
|
|
269
|
-
const account = makeAccount({ botUserId: undefined });
|
|
270
|
-
const result = checkMessageGate(msg, account);
|
|
271
|
-
expect(result.pass).toBe(true);
|
|
272
|
-
});
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
// ---------------------------------------------------------------------------
|
|
276
|
-
// isBotMentioned
|
|
277
|
-
// ---------------------------------------------------------------------------
|
|
278
|
-
|
|
279
|
-
describe("isBotMentioned", () => {
|
|
280
|
-
it("returns true when bot is in mentionUserIds", () => {
|
|
281
|
-
const event = makeEvent({
|
|
282
|
-
mentionUserList: [{ userId: "bot_user_001" }],
|
|
283
|
-
});
|
|
284
|
-
const msg = parseMessageEvent(event)!;
|
|
285
|
-
const account = makeAccount();
|
|
286
|
-
expect(isBotMentioned(msg, account)).toBe(true);
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
it("returns false when bot is not mentioned", () => {
|
|
290
|
-
const event = makeEvent({
|
|
291
|
-
mentionUserList: [{ userId: "other_user" }],
|
|
292
|
-
});
|
|
293
|
-
const msg = parseMessageEvent(event)!;
|
|
294
|
-
const account = makeAccount();
|
|
295
|
-
expect(isBotMentioned(msg, account)).toBe(false);
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
it("returns false when botUserId is not set", () => {
|
|
299
|
-
const event = makeEvent({
|
|
300
|
-
mentionUserList: [{ userId: "bot_user_001" }],
|
|
301
|
-
});
|
|
302
|
-
const msg = parseMessageEvent(event)!;
|
|
303
|
-
const account = makeAccount({ botUserId: undefined });
|
|
304
|
-
expect(isBotMentioned(msg, account)).toBe(false);
|
|
305
|
-
});
|
|
306
201
|
});
|
|
307
202
|
|
|
308
203
|
// ---------------------------------------------------------------------------
|
|
@@ -348,7 +243,7 @@ describe("buildContext", () => {
|
|
|
348
243
|
expect(ctx.AccountId).toBe("test-account");
|
|
349
244
|
expect(ctx.Body).toBe("hello world");
|
|
350
245
|
expect(ctx.From).toBe("cored:user:user_sender_001");
|
|
351
|
-
expect(ctx.To).toBe("cored:bot:
|
|
246
|
+
expect(ctx.To).toBe("cored:bot:app_test");
|
|
352
247
|
expect(ctx.SessionKey).toBe("cored:user:user_sender_001");
|
|
353
248
|
expect(ctx.CommandAuthorized).toBe(true);
|
|
354
249
|
expect(ctx._cored.isGroup).toBe(false);
|
|
@@ -367,12 +262,6 @@ describe("buildContext", () => {
|
|
|
367
262
|
expect(ctx._cored.chatId).toBe("chat_group_1");
|
|
368
263
|
});
|
|
369
264
|
|
|
370
|
-
it("falls back to appId when botUserId is not set", () => {
|
|
371
|
-
const msg = parseMessageEvent(makeEvent())!;
|
|
372
|
-
const account = makeAccount({ botUserId: undefined });
|
|
373
|
-
const ctx = buildContext(msg, account);
|
|
374
|
-
expect(ctx.To).toBe("cored:bot:app_test");
|
|
375
|
-
});
|
|
376
265
|
});
|
|
377
266
|
|
|
378
267
|
// ---------------------------------------------------------------------------
|
|
@@ -386,7 +275,7 @@ describe("processInboundMessage", () => {
|
|
|
386
275
|
|
|
387
276
|
it("dispatches a valid direct message", async () => {
|
|
388
277
|
const api = makeMockApi();
|
|
389
|
-
const account = makeAccount(
|
|
278
|
+
const account = makeAccount();
|
|
390
279
|
const event = makeEvent();
|
|
391
280
|
const deliver = vi.fn().mockResolvedValue(undefined);
|
|
392
281
|
|
|
@@ -397,21 +286,9 @@ describe("processInboundMessage", () => {
|
|
|
397
286
|
expect(api.runtime.channel.session.recordInboundSession).toHaveBeenCalledOnce();
|
|
398
287
|
});
|
|
399
288
|
|
|
400
|
-
it("filters self-messages", async () => {
|
|
401
|
-
const api = makeMockApi();
|
|
402
|
-
const account = makeAccount();
|
|
403
|
-
const event = makeEvent({ sender: { userId: "bot_user_001" } });
|
|
404
|
-
const deliver = vi.fn();
|
|
405
|
-
|
|
406
|
-
const result = await processInboundMessage(api, account, event, { deliver });
|
|
407
|
-
|
|
408
|
-
expect(result).toBe(false);
|
|
409
|
-
expect(api.runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
|
|
410
|
-
});
|
|
411
|
-
|
|
412
289
|
it("filters duplicate messages", async () => {
|
|
413
290
|
const api = makeMockApi();
|
|
414
|
-
const account = makeAccount(
|
|
291
|
+
const account = makeAccount();
|
|
415
292
|
const msgId = "msg_dedup_test";
|
|
416
293
|
const event = makeEvent({ messageId: msgId });
|
|
417
294
|
const deliver = vi.fn();
|
|
@@ -423,31 +300,6 @@ describe("processInboundMessage", () => {
|
|
|
423
300
|
expect(second).toBe(false);
|
|
424
301
|
});
|
|
425
302
|
|
|
426
|
-
it("filters group messages without mention when required", async () => {
|
|
427
|
-
const api = makeMockApi();
|
|
428
|
-
const account = makeAccount({ requireMention: true });
|
|
429
|
-
const event = makeEvent({ chatType: "group" });
|
|
430
|
-
const deliver = vi.fn();
|
|
431
|
-
|
|
432
|
-
const result = await processInboundMessage(api, account, event, { deliver });
|
|
433
|
-
|
|
434
|
-
expect(result).toBe(false);
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
it("dispatches group message with mention", async () => {
|
|
438
|
-
const api = makeMockApi();
|
|
439
|
-
const account = makeAccount({ requireMention: true });
|
|
440
|
-
const event = makeEvent({
|
|
441
|
-
chatType: "group",
|
|
442
|
-
mentionUserList: [{ userId: "bot_user_001" }],
|
|
443
|
-
});
|
|
444
|
-
const deliver = vi.fn().mockResolvedValue(undefined);
|
|
445
|
-
|
|
446
|
-
const result = await processInboundMessage(api, account, event, { deliver });
|
|
447
|
-
|
|
448
|
-
expect(result).toBe(true);
|
|
449
|
-
});
|
|
450
|
-
|
|
451
303
|
it("returns false for unparseable event", async () => {
|
|
452
304
|
const api = makeMockApi();
|
|
453
305
|
const account = makeAccount();
|
|
@@ -463,7 +315,7 @@ describe("processInboundMessage", () => {
|
|
|
463
315
|
const api = makeMockApi();
|
|
464
316
|
// Remove dispatch function
|
|
465
317
|
(api.runtime.channel.reply as Record<string, unknown>).dispatchReplyWithBufferedBlockDispatcher = undefined;
|
|
466
|
-
const account = makeAccount(
|
|
318
|
+
const account = makeAccount();
|
|
467
319
|
const event = makeEvent();
|
|
468
320
|
const deliver = vi.fn();
|
|
469
321
|
|
|
@@ -482,7 +334,7 @@ describe("processInboundMessage", () => {
|
|
|
482
334
|
});
|
|
483
335
|
api.runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher = mockDispatch;
|
|
484
336
|
|
|
485
|
-
const account = makeAccount(
|
|
337
|
+
const account = makeAccount();
|
|
486
338
|
const event = makeEvent({ chatId: "chat_deliver_test" });
|
|
487
339
|
const deliver = vi.fn().mockResolvedValue(undefined);
|
|
488
340
|
|
|
@@ -500,7 +352,7 @@ describe("processInboundMessage", () => {
|
|
|
500
352
|
});
|
|
501
353
|
api.runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher = mockDispatch;
|
|
502
354
|
|
|
503
|
-
const account = makeAccount(
|
|
355
|
+
const account = makeAccount();
|
|
504
356
|
const event = makeEvent();
|
|
505
357
|
const deliver = vi.fn();
|
|
506
358
|
|
|
@@ -511,7 +363,7 @@ describe("processInboundMessage", () => {
|
|
|
511
363
|
|
|
512
364
|
it("records session with updateLastRoute for DM", async () => {
|
|
513
365
|
const api = makeMockApi();
|
|
514
|
-
const account = makeAccount(
|
|
366
|
+
const account = makeAccount();
|
|
515
367
|
const event = makeEvent({ chatId: "chat_dm_001" });
|
|
516
368
|
const deliver = vi.fn().mockResolvedValue(undefined);
|
|
517
369
|
|
|
@@ -528,7 +380,7 @@ describe("processInboundMessage", () => {
|
|
|
528
380
|
|
|
529
381
|
it("does not set updateLastRoute for group messages", async () => {
|
|
530
382
|
const api = makeMockApi();
|
|
531
|
-
const account = makeAccount(
|
|
383
|
+
const account = makeAccount();
|
|
532
384
|
const event = makeEvent({ chatType: "group" });
|
|
533
385
|
const deliver = vi.fn().mockResolvedValue(undefined);
|
|
534
386
|
|
|
@@ -547,7 +399,7 @@ describe("processInboundMessage", () => {
|
|
|
547
399
|
});
|
|
548
400
|
api.runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher = mockDispatch;
|
|
549
401
|
|
|
550
|
-
const account = makeAccount(
|
|
402
|
+
const account = makeAccount();
|
|
551
403
|
const event = makeEvent();
|
|
552
404
|
const deliver = vi.fn();
|
|
553
405
|
|
package/src/messaging/inbound.ts
CHANGED
|
@@ -14,22 +14,25 @@ import type {
|
|
|
14
14
|
CoredAccountConfig,
|
|
15
15
|
CoredMessage,
|
|
16
16
|
CoredMessageEvent,
|
|
17
|
-
PluginApi,
|
|
18
17
|
} from "../types.js";
|
|
19
18
|
|
|
20
|
-
//
|
|
21
|
-
|
|
19
|
+
// Plugin API surface used by this module.
|
|
20
|
+
// At runtime the full OpenClawPluginApi is provided by the gateway;
|
|
21
|
+
// we only declare the subset we access so the module stays decoupled
|
|
22
|
+
// and testable without importing the full SDK.
|
|
23
|
+
export interface InboundPluginApi {
|
|
24
|
+
config: Record<string, unknown>;
|
|
22
25
|
runtime?: {
|
|
23
26
|
channel?: {
|
|
24
27
|
reply?: {
|
|
25
|
-
dispatchReplyWithBufferedBlockDispatcher?: (
|
|
28
|
+
dispatchReplyWithBufferedBlockDispatcher?: (...args: any[]) => any;
|
|
26
29
|
};
|
|
27
30
|
session?: {
|
|
28
|
-
recordInboundSession?: (
|
|
29
|
-
resolveStorePath?: (
|
|
31
|
+
recordInboundSession?: (...args: any[]) => any;
|
|
32
|
+
resolveStorePath?: (...args: any[]) => string;
|
|
30
33
|
};
|
|
31
34
|
routing?: {
|
|
32
|
-
resolveAgentRoute?: (
|
|
35
|
+
resolveAgentRoute?: (...args: any[]) => unknown;
|
|
33
36
|
};
|
|
34
37
|
};
|
|
35
38
|
};
|
|
@@ -148,41 +151,9 @@ export function checkMessageGate(
|
|
|
148
151
|
msg: ParsedInboundMessage,
|
|
149
152
|
account: CoredAccountConfig,
|
|
150
153
|
): GateResult {
|
|
151
|
-
// 1. Self-message filter: ignore messages from the bot itself
|
|
152
|
-
if (account.botUserId && msg.senderId === account.botUserId) {
|
|
153
|
-
return { pass: false, reason: "self-message" };
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// 2. Whitelist filter: if whitelist is non-empty, only allow listed senders
|
|
157
|
-
if (account.inboundWhitelist.length > 0) {
|
|
158
|
-
if (!account.inboundWhitelist.includes(msg.senderId)) {
|
|
159
|
-
return { pass: false, reason: "sender-not-in-whitelist" };
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// 3. Group mention filter: in group chats, require @mention if configured
|
|
164
|
-
if (
|
|
165
|
-
msg.chatType === "group" &&
|
|
166
|
-
account.requireMention &&
|
|
167
|
-
!isBotMentioned(msg, account)
|
|
168
|
-
) {
|
|
169
|
-
return { pass: false, reason: "group-no-mention" };
|
|
170
|
-
}
|
|
171
|
-
|
|
172
154
|
return { pass: true };
|
|
173
155
|
}
|
|
174
156
|
|
|
175
|
-
/**
|
|
176
|
-
* Check if the bot was @mentioned in a message.
|
|
177
|
-
*/
|
|
178
|
-
export function isBotMentioned(
|
|
179
|
-
msg: ParsedInboundMessage,
|
|
180
|
-
account: CoredAccountConfig,
|
|
181
|
-
): boolean {
|
|
182
|
-
if (!account.botUserId) return false;
|
|
183
|
-
return msg.mentionUserIds.includes(account.botUserId);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
157
|
// ---------------------------------------------------------------------------
|
|
187
158
|
// Dedup — skip already-processed message IDs
|
|
188
159
|
// ---------------------------------------------------------------------------
|
|
@@ -276,7 +247,7 @@ export function buildContext(
|
|
|
276
247
|
return {
|
|
277
248
|
Body: msg.body,
|
|
278
249
|
From: isGroup ? `cored:chat:${msg.chatId}` : `cored:user:${msg.senderId}`,
|
|
279
|
-
To: `cored:bot:${account.
|
|
250
|
+
To: `cored:bot:${account.appId}`,
|
|
280
251
|
SessionKey: sessionKey,
|
|
281
252
|
AccountId: account.accountId,
|
|
282
253
|
ChatType: isGroup ? "group" : "direct",
|
|
@@ -314,7 +285,7 @@ export interface InboundDispatchOptions {
|
|
|
314
285
|
* Returns `true` if the message was dispatched, `false` if filtered.
|
|
315
286
|
*/
|
|
316
287
|
export async function processInboundMessage(
|
|
317
|
-
api:
|
|
288
|
+
api: InboundPluginApi,
|
|
318
289
|
account: CoredAccountConfig,
|
|
319
290
|
event: CoredMessageEvent,
|
|
320
291
|
opts: InboundDispatchOptions,
|
package/src/setup-entry.ts
CHANGED
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
4
|
import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core";
|
|
5
|
-
import {
|
|
5
|
+
import { base } from "./channel.js";
|
|
6
6
|
|
|
7
|
-
export default defineSetupPluginEntry(
|
|
7
|
+
export default defineSetupPluginEntry(base);
|
package/src/types.ts
CHANGED
|
@@ -5,39 +5,30 @@
|
|
|
5
5
|
|
|
6
6
|
export interface CoredAccountConfig {
|
|
7
7
|
accountId: string;
|
|
8
|
-
enabled: boolean;
|
|
9
8
|
appId: string;
|
|
10
9
|
appSecret: string;
|
|
11
10
|
backendUrl: string;
|
|
11
|
+
enabled: boolean;
|
|
12
12
|
enableEncryption: boolean;
|
|
13
13
|
requestTimeout: number;
|
|
14
|
-
requireMention: boolean;
|
|
15
|
-
botUserId?: string;
|
|
16
|
-
inboundWhitelist: string[];
|
|
17
14
|
}
|
|
18
15
|
|
|
19
16
|
export interface CoredRawAccountConfig {
|
|
20
17
|
appId?: string;
|
|
21
18
|
appSecret?: string;
|
|
22
19
|
backendUrl?: string;
|
|
20
|
+
enabled?: boolean;
|
|
23
21
|
enableEncryption?: boolean;
|
|
24
22
|
requestTimeout?: number;
|
|
25
|
-
requireMention?: boolean;
|
|
26
|
-
enabled?: boolean;
|
|
27
|
-
botUserId?: string;
|
|
28
|
-
inboundWhitelist?: string[];
|
|
29
23
|
}
|
|
30
24
|
|
|
31
25
|
export interface CoredChannelConfig {
|
|
32
26
|
appId?: string;
|
|
33
27
|
appSecret?: string;
|
|
34
28
|
backendUrl?: string;
|
|
29
|
+
enabled?: boolean;
|
|
35
30
|
enableEncryption?: boolean;
|
|
36
31
|
requestTimeout?: number;
|
|
37
|
-
requireMention?: boolean;
|
|
38
|
-
enabled?: boolean;
|
|
39
|
-
botUserId?: string;
|
|
40
|
-
inboundWhitelist?: string[];
|
|
41
32
|
accounts?: Record<string, CoredRawAccountConfig>;
|
|
42
33
|
}
|
|
43
34
|
|
|
@@ -79,7 +70,3 @@ export type ConnectionState =
|
|
|
79
70
|
| "connected"
|
|
80
71
|
| "disconnecting";
|
|
81
72
|
|
|
82
|
-
// --- OpenClaw Plugin API (re-export from SDK) ---
|
|
83
|
-
|
|
84
|
-
// Re-export PluginApi from SDK for convenience
|
|
85
|
-
export type { PluginApi } from "openclaw/plugin-sdk/core";
|