@openclaw/matrix 2026.2.15 → 2026.2.17
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/CHANGELOG.md +12 -0
- package/package.json +2 -2
- package/src/actions.ts +1 -1
- package/src/channel.directory.test.ts +17 -7
- package/src/channel.ts +1 -1
- package/src/group-mentions.ts +1 -1
- package/src/matrix/actions/client.ts +7 -25
- package/src/matrix/client/config.ts +2 -2
- package/src/matrix/client/create-client.ts +1 -1
- package/src/matrix/client/shared.ts +1 -1
- package/src/matrix/client/storage.ts +1 -1
- package/src/matrix/client-bootstrap.ts +39 -0
- package/src/matrix/deps.ts +1 -1
- package/src/matrix/monitor/auto-join.ts +2 -2
- package/src/matrix/monitor/handler.ts +1 -1
- package/src/matrix/monitor/index.ts +1 -1
- package/src/matrix/monitor/media.test.ts +7 -23
- package/src/matrix/monitor/replies.test.ts +132 -0
- package/src/matrix/monitor/replies.ts +11 -8
- package/src/matrix/send/client.ts +6 -25
- package/src/matrix/send.test.ts +7 -5
- package/src/onboarding.ts +3 -7
- package/src/tool-actions.ts +1 -1
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/matrix",
|
|
3
|
-
"version": "2026.2.
|
|
3
|
+
"version": "2026.2.17",
|
|
4
4
|
"description": "OpenClaw Matrix channel plugin",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@matrix-org/matrix-sdk-crypto-nodejs": "^0.4.0",
|
|
8
8
|
"@vector-im/matrix-bot-sdk": "0.8.0-element.3",
|
|
9
9
|
"markdown-it": "14.1.1",
|
|
10
|
-
"music-metadata": "^11.12.
|
|
10
|
+
"music-metadata": "^11.12.1",
|
|
11
11
|
"zod": "^4.3.6"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
package/src/actions.ts
CHANGED
|
@@ -7,9 +7,9 @@ import {
|
|
|
7
7
|
type ChannelMessageActionName,
|
|
8
8
|
type ChannelToolSend,
|
|
9
9
|
} from "openclaw/plugin-sdk";
|
|
10
|
-
import type { CoreConfig } from "./types.js";
|
|
11
10
|
import { resolveMatrixAccount } from "./matrix/accounts.js";
|
|
12
11
|
import { handleMatrixAction } from "./tool-actions.js";
|
|
12
|
+
import type { CoreConfig } from "./types.js";
|
|
13
13
|
|
|
14
14
|
export const matrixMessageActions: ChannelMessageActionAdapter = {
|
|
15
15
|
listActions: ({ cfg }) => {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
|
1
|
+
import type { PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk";
|
|
2
2
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
-
import type { CoreConfig } from "./types.js";
|
|
4
3
|
import { matrixPlugin } from "./channel.js";
|
|
5
4
|
import { setMatrixRuntime } from "./runtime.js";
|
|
5
|
+
import type { CoreConfig } from "./types.js";
|
|
6
6
|
|
|
7
7
|
vi.mock("@vector-im/matrix-bot-sdk", () => ({
|
|
8
8
|
ConsoleLogger: class {
|
|
@@ -24,10 +24,18 @@ vi.mock("@vector-im/matrix-bot-sdk", () => ({
|
|
|
24
24
|
}));
|
|
25
25
|
|
|
26
26
|
describe("matrix directory", () => {
|
|
27
|
+
const runtimeEnv: RuntimeEnv = {
|
|
28
|
+
log: vi.fn(),
|
|
29
|
+
error: vi.fn(),
|
|
30
|
+
exit: vi.fn((code: number): never => {
|
|
31
|
+
throw new Error(`exit ${code}`);
|
|
32
|
+
}),
|
|
33
|
+
};
|
|
34
|
+
|
|
27
35
|
beforeEach(() => {
|
|
28
36
|
setMatrixRuntime({
|
|
29
37
|
state: {
|
|
30
|
-
resolveStateDir: (_env, homeDir) => homeDir(),
|
|
38
|
+
resolveStateDir: (_env, homeDir) => (homeDir ?? (() => "/tmp"))(),
|
|
31
39
|
},
|
|
32
40
|
} as PluginRuntime);
|
|
33
41
|
});
|
|
@@ -51,11 +59,12 @@ describe("matrix directory", () => {
|
|
|
51
59
|
expect(matrixPlugin.directory?.listGroups).toBeTruthy();
|
|
52
60
|
|
|
53
61
|
await expect(
|
|
54
|
-
matrixPlugin.directory!.listPeers({
|
|
62
|
+
matrixPlugin.directory!.listPeers!({
|
|
55
63
|
cfg,
|
|
56
64
|
accountId: undefined,
|
|
57
65
|
query: undefined,
|
|
58
66
|
limit: undefined,
|
|
67
|
+
runtime: runtimeEnv,
|
|
59
68
|
}),
|
|
60
69
|
).resolves.toEqual(
|
|
61
70
|
expect.arrayContaining([
|
|
@@ -67,11 +76,12 @@ describe("matrix directory", () => {
|
|
|
67
76
|
);
|
|
68
77
|
|
|
69
78
|
await expect(
|
|
70
|
-
matrixPlugin.directory!.listGroups({
|
|
79
|
+
matrixPlugin.directory!.listGroups!({
|
|
71
80
|
cfg,
|
|
72
81
|
accountId: undefined,
|
|
73
82
|
query: undefined,
|
|
74
83
|
limit: undefined,
|
|
84
|
+
runtime: runtimeEnv,
|
|
75
85
|
}),
|
|
76
86
|
).resolves.toEqual(
|
|
77
87
|
expect.arrayContaining([
|
|
@@ -130,11 +140,11 @@ describe("matrix directory", () => {
|
|
|
130
140
|
},
|
|
131
141
|
} as unknown as CoreConfig;
|
|
132
142
|
|
|
133
|
-
expect(matrixPlugin.groups
|
|
143
|
+
expect(matrixPlugin.groups!.resolveRequireMention!({ cfg, groupId: "!room:example.org" })).toBe(
|
|
134
144
|
true,
|
|
135
145
|
);
|
|
136
146
|
expect(
|
|
137
|
-
matrixPlugin.groups
|
|
147
|
+
matrixPlugin.groups!.resolveRequireMention!({
|
|
138
148
|
cfg,
|
|
139
149
|
accountId: "assistant",
|
|
140
150
|
groupId: "!room:example.org",
|
package/src/channel.ts
CHANGED
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
setAccountEnabledInConfigSection,
|
|
10
10
|
type ChannelPlugin,
|
|
11
11
|
} from "openclaw/plugin-sdk";
|
|
12
|
-
import type { CoreConfig } from "./types.js";
|
|
13
12
|
import { matrixMessageActions } from "./actions.js";
|
|
14
13
|
import { MatrixConfigSchema } from "./config-schema.js";
|
|
15
14
|
import { listMatrixDirectoryGroupsLive, listMatrixDirectoryPeersLive } from "./directory-live.js";
|
|
@@ -31,6 +30,7 @@ import { sendMessageMatrix } from "./matrix/send.js";
|
|
|
31
30
|
import { matrixOnboardingAdapter } from "./onboarding.js";
|
|
32
31
|
import { matrixOutbound } from "./outbound.js";
|
|
33
32
|
import { resolveMatrixTargets } from "./resolve-targets.js";
|
|
33
|
+
import type { CoreConfig } from "./types.js";
|
|
34
34
|
|
|
35
35
|
// Mutex for serializing account startup (workaround for concurrent dynamic import race condition)
|
|
36
36
|
let matrixStartupLock: Promise<void> = Promise.resolve();
|
package/src/group-mentions.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ChannelGroupContext, GroupToolPolicyConfig } from "openclaw/plugin-sdk";
|
|
2
|
-
import type { CoreConfig } from "./types.js";
|
|
3
2
|
import { resolveMatrixAccountConfig } from "./matrix/accounts.js";
|
|
4
3
|
import { resolveMatrixRoomConfig } from "./matrix/monitor/rooms.js";
|
|
4
|
+
import type { CoreConfig } from "./types.js";
|
|
5
5
|
|
|
6
6
|
function stripLeadingPrefixCaseInsensitive(value: string, prefix: string): string {
|
|
7
7
|
return value.toLowerCase().startsWith(prefix.toLowerCase())
|
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
|
2
|
-
import type { CoreConfig } from "../../types.js";
|
|
3
|
-
import type { MatrixActionClient, MatrixActionClientOpts } from "./types.js";
|
|
4
2
|
import { getMatrixRuntime } from "../../runtime.js";
|
|
3
|
+
import type { CoreConfig } from "../../types.js";
|
|
5
4
|
import { getActiveMatrixClient } from "../active-client.js";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
resolveMatrixAuth,
|
|
10
|
-
resolveSharedMatrixClient,
|
|
11
|
-
} from "../client.js";
|
|
5
|
+
import { createPreparedMatrixClient } from "../client-bootstrap.js";
|
|
6
|
+
import { isBunRuntime, resolveMatrixAuth, resolveSharedMatrixClient } from "../client.js";
|
|
7
|
+
import type { MatrixActionClient, MatrixActionClientOpts } from "./types.js";
|
|
12
8
|
|
|
13
9
|
export function ensureNodeRuntime() {
|
|
14
10
|
if (isBunRuntime()) {
|
|
@@ -42,24 +38,10 @@ export async function resolveActionClient(
|
|
|
42
38
|
cfg: getMatrixRuntime().config.loadConfig() as CoreConfig,
|
|
43
39
|
accountId,
|
|
44
40
|
});
|
|
45
|
-
const client = await
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
accessToken: auth.accessToken,
|
|
49
|
-
encryption: auth.encryption,
|
|
50
|
-
localTimeoutMs: opts.timeoutMs,
|
|
41
|
+
const client = await createPreparedMatrixClient({
|
|
42
|
+
auth,
|
|
43
|
+
timeoutMs: opts.timeoutMs,
|
|
51
44
|
accountId,
|
|
52
45
|
});
|
|
53
|
-
if (auth.encryption && client.crypto) {
|
|
54
|
-
try {
|
|
55
|
-
const joinedRooms = await client.getJoinedRooms();
|
|
56
|
-
await (client.crypto as { prepare: (rooms?: string[]) => Promise<void> }).prepare(
|
|
57
|
-
joinedRooms,
|
|
58
|
-
);
|
|
59
|
-
} catch {
|
|
60
|
-
// Ignore crypto prep failures for one-off actions.
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
await client.start();
|
|
64
46
|
return { client, stopOnDone: true };
|
|
65
47
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
|
2
2
|
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
|
3
|
-
import type { CoreConfig } from "../../types.js";
|
|
4
|
-
import type { MatrixAuth, MatrixResolvedConfig } from "./types.js";
|
|
5
3
|
import { getMatrixRuntime } from "../../runtime.js";
|
|
4
|
+
import type { CoreConfig } from "../../types.js";
|
|
6
5
|
import { ensureMatrixSdkLoggingConfigured } from "./logging.js";
|
|
6
|
+
import type { MatrixAuth, MatrixResolvedConfig } from "./types.js";
|
|
7
7
|
|
|
8
8
|
function clean(value?: string): string {
|
|
9
9
|
return value?.trim() ?? "";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
1
2
|
import type { IStorageProvider, ICryptoStorageProvider } from "@vector-im/matrix-bot-sdk";
|
|
2
3
|
import {
|
|
3
4
|
LogService,
|
|
@@ -5,7 +6,6 @@ import {
|
|
|
5
6
|
SimpleFsStorageProvider,
|
|
6
7
|
RustSdkCryptoStorageProvider,
|
|
7
8
|
} from "@vector-im/matrix-bot-sdk";
|
|
8
|
-
import fs from "node:fs";
|
|
9
9
|
import { ensureMatrixSdkLoggingConfigured } from "./logging.js";
|
|
10
10
|
import {
|
|
11
11
|
maybeMigrateLegacyStorage,
|
|
@@ -2,10 +2,10 @@ import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
|
|
2
2
|
import { LogService } from "@vector-im/matrix-bot-sdk";
|
|
3
3
|
import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
|
4
4
|
import type { CoreConfig } from "../../types.js";
|
|
5
|
-
import type { MatrixAuth } from "./types.js";
|
|
6
5
|
import { resolveMatrixAuth } from "./config.js";
|
|
7
6
|
import { createMatrixClient } from "./create-client.js";
|
|
8
7
|
import { DEFAULT_ACCOUNT_KEY } from "./storage.js";
|
|
8
|
+
import type { MatrixAuth } from "./types.js";
|
|
9
9
|
|
|
10
10
|
type SharedMatrixClientState = {
|
|
11
11
|
client: MatrixClient;
|
|
@@ -2,8 +2,8 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import type { MatrixStoragePaths } from "./types.js";
|
|
6
5
|
import { getMatrixRuntime } from "../../runtime.js";
|
|
6
|
+
import type { MatrixStoragePaths } from "./types.js";
|
|
7
7
|
|
|
8
8
|
export const DEFAULT_ACCOUNT_KEY = "default";
|
|
9
9
|
const STORAGE_META_FILENAME = "storage-meta.json";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createMatrixClient } from "./client.js";
|
|
2
|
+
|
|
3
|
+
type MatrixClientBootstrapAuth = {
|
|
4
|
+
homeserver: string;
|
|
5
|
+
userId: string;
|
|
6
|
+
accessToken: string;
|
|
7
|
+
encryption?: boolean;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type MatrixCryptoPrepare = {
|
|
11
|
+
prepare: (rooms?: string[]) => Promise<void>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type MatrixBootstrapClient = Awaited<ReturnType<typeof createMatrixClient>>;
|
|
15
|
+
|
|
16
|
+
export async function createPreparedMatrixClient(opts: {
|
|
17
|
+
auth: MatrixClientBootstrapAuth;
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
accountId?: string;
|
|
20
|
+
}): Promise<MatrixBootstrapClient> {
|
|
21
|
+
const client = await createMatrixClient({
|
|
22
|
+
homeserver: opts.auth.homeserver,
|
|
23
|
+
userId: opts.auth.userId,
|
|
24
|
+
accessToken: opts.auth.accessToken,
|
|
25
|
+
encryption: opts.auth.encryption,
|
|
26
|
+
localTimeoutMs: opts.timeoutMs,
|
|
27
|
+
accountId: opts.accountId,
|
|
28
|
+
});
|
|
29
|
+
if (opts.auth.encryption && client.crypto) {
|
|
30
|
+
try {
|
|
31
|
+
const joinedRooms = await client.getJoinedRooms();
|
|
32
|
+
await (client.crypto as MatrixCryptoPrepare).prepare(joinedRooms);
|
|
33
|
+
} catch {
|
|
34
|
+
// Ignore crypto prep failures for one-off requests.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
await client.start();
|
|
38
|
+
return client;
|
|
39
|
+
}
|
package/src/matrix/deps.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { RuntimeEnv } from "openclaw/plugin-sdk";
|
|
2
1
|
import fs from "node:fs";
|
|
3
2
|
import { createRequire } from "node:module";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import type { RuntimeEnv } from "openclaw/plugin-sdk";
|
|
6
6
|
import { getMatrixRuntime } from "../runtime.js";
|
|
7
7
|
|
|
8
8
|
const MATRIX_SDK_PACKAGE = "@vector-im/matrix-bot-sdk";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
|
2
|
-
import type { RuntimeEnv } from "openclaw/plugin-sdk";
|
|
3
2
|
import { AutojoinRoomsMixin } from "@vector-im/matrix-bot-sdk";
|
|
4
|
-
import type {
|
|
3
|
+
import type { RuntimeEnv } from "openclaw/plugin-sdk";
|
|
5
4
|
import { getMatrixRuntime } from "../../runtime.js";
|
|
5
|
+
import type { CoreConfig } from "../../types.js";
|
|
6
6
|
|
|
7
7
|
export function registerMatrixAutoJoin(params: {
|
|
8
8
|
client: MatrixClient;
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
type RuntimeLogger,
|
|
12
12
|
} from "openclaw/plugin-sdk";
|
|
13
13
|
import type { CoreConfig, MatrixRoomConfig, ReplyToMode } from "../../types.js";
|
|
14
|
-
import type { MatrixRawEvent, RoomMessageEventContent } from "./types.js";
|
|
15
14
|
import { fetchEventSummary } from "../actions/summary.js";
|
|
16
15
|
import {
|
|
17
16
|
formatPollAsText,
|
|
@@ -36,6 +35,7 @@ import { resolveMentions } from "./mentions.js";
|
|
|
36
35
|
import { deliverMatrixReplies } from "./replies.js";
|
|
37
36
|
import { resolveMatrixRoomConfig } from "./rooms.js";
|
|
38
37
|
import { resolveMatrixThreadRootId, resolveMatrixThreadTarget } from "./threads.js";
|
|
38
|
+
import type { MatrixRawEvent, RoomMessageEventContent } from "./types.js";
|
|
39
39
|
import { EventType, RelationType } from "./types.js";
|
|
40
40
|
|
|
41
41
|
export type MatrixMonitorHandlerParams = {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { format } from "node:util";
|
|
2
2
|
import { mergeAllowlist, summarizeMapping, type RuntimeEnv } from "openclaw/plugin-sdk";
|
|
3
|
-
import type { CoreConfig, ReplyToMode } from "../../types.js";
|
|
4
3
|
import { resolveMatrixTargets } from "../../resolve-targets.js";
|
|
5
4
|
import { getMatrixRuntime } from "../../runtime.js";
|
|
5
|
+
import type { CoreConfig, ReplyToMode } from "../../types.js";
|
|
6
6
|
import { resolveMatrixAccount } from "../accounts.js";
|
|
7
7
|
import { setActiveMatrixClient } from "../active-client.js";
|
|
8
8
|
import {
|
|
@@ -22,14 +22,12 @@ describe("downloadMatrixMedia", () => {
|
|
|
22
22
|
setMatrixRuntime(runtimeStub);
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
function makeEncryptedMediaFixture() {
|
|
26
26
|
const decryptMedia = vi.fn().mockResolvedValue(Buffer.from("decrypted"));
|
|
27
|
-
|
|
28
27
|
const client = {
|
|
29
28
|
crypto: { decryptMedia },
|
|
30
29
|
mxcToHttp: vi.fn().mockReturnValue("https://example/mxc"),
|
|
31
30
|
} as unknown as import("@vector-im/matrix-bot-sdk").MatrixClient;
|
|
32
|
-
|
|
33
31
|
const file = {
|
|
34
32
|
url: "mxc://example/file",
|
|
35
33
|
key: {
|
|
@@ -43,6 +41,11 @@ describe("downloadMatrixMedia", () => {
|
|
|
43
41
|
hashes: { sha256: "hash" },
|
|
44
42
|
v: "v2",
|
|
45
43
|
};
|
|
44
|
+
return { decryptMedia, client, file };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
it("decrypts encrypted media when file payloads are present", async () => {
|
|
48
|
+
const { decryptMedia, client, file } = makeEncryptedMediaFixture();
|
|
46
49
|
|
|
47
50
|
const result = await downloadMatrixMedia({
|
|
48
51
|
client,
|
|
@@ -64,26 +67,7 @@ describe("downloadMatrixMedia", () => {
|
|
|
64
67
|
});
|
|
65
68
|
|
|
66
69
|
it("rejects encrypted media that exceeds maxBytes before decrypting", async () => {
|
|
67
|
-
const decryptMedia =
|
|
68
|
-
|
|
69
|
-
const client = {
|
|
70
|
-
crypto: { decryptMedia },
|
|
71
|
-
mxcToHttp: vi.fn().mockReturnValue("https://example/mxc"),
|
|
72
|
-
} as unknown as import("@vector-im/matrix-bot-sdk").MatrixClient;
|
|
73
|
-
|
|
74
|
-
const file = {
|
|
75
|
-
url: "mxc://example/file",
|
|
76
|
-
key: {
|
|
77
|
-
kty: "oct",
|
|
78
|
-
key_ops: ["encrypt", "decrypt"],
|
|
79
|
-
alg: "A256CTR",
|
|
80
|
-
k: "secret",
|
|
81
|
-
ext: true,
|
|
82
|
-
},
|
|
83
|
-
iv: "iv",
|
|
84
|
-
hashes: { sha256: "hash" },
|
|
85
|
-
v: "v2",
|
|
86
|
-
};
|
|
70
|
+
const { decryptMedia, client, file } = makeEncryptedMediaFixture();
|
|
87
71
|
|
|
88
72
|
await expect(
|
|
89
73
|
downloadMatrixMedia({
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
|
2
|
+
import type { PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk";
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
const sendMessageMatrixMock = vi.hoisted(() => vi.fn().mockResolvedValue({ messageId: "mx-1" }));
|
|
6
|
+
|
|
7
|
+
vi.mock("../send.js", () => ({
|
|
8
|
+
sendMessageMatrix: (to: string, message: string, opts?: unknown) =>
|
|
9
|
+
sendMessageMatrixMock(to, message, opts),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
import { setMatrixRuntime } from "../../runtime.js";
|
|
13
|
+
import { deliverMatrixReplies } from "./replies.js";
|
|
14
|
+
|
|
15
|
+
describe("deliverMatrixReplies", () => {
|
|
16
|
+
const loadConfigMock = vi.fn(() => ({}));
|
|
17
|
+
const resolveMarkdownTableModeMock = vi.fn(() => "code");
|
|
18
|
+
const convertMarkdownTablesMock = vi.fn((text: string) => text);
|
|
19
|
+
const resolveChunkModeMock = vi.fn(() => "length");
|
|
20
|
+
const chunkMarkdownTextWithModeMock = vi.fn((text: string) => [text]);
|
|
21
|
+
|
|
22
|
+
const runtimeStub = {
|
|
23
|
+
config: {
|
|
24
|
+
loadConfig: () => loadConfigMock(),
|
|
25
|
+
},
|
|
26
|
+
channel: {
|
|
27
|
+
text: {
|
|
28
|
+
resolveMarkdownTableMode: () => resolveMarkdownTableModeMock(),
|
|
29
|
+
convertMarkdownTables: (text: string) => convertMarkdownTablesMock(text),
|
|
30
|
+
resolveChunkMode: () => resolveChunkModeMock(),
|
|
31
|
+
chunkMarkdownTextWithMode: (text: string) => chunkMarkdownTextWithModeMock(text),
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
logging: {
|
|
35
|
+
shouldLogVerbose: () => false,
|
|
36
|
+
},
|
|
37
|
+
} as unknown as PluginRuntime;
|
|
38
|
+
|
|
39
|
+
const runtimeEnv: RuntimeEnv = {
|
|
40
|
+
log: vi.fn(),
|
|
41
|
+
error: vi.fn(),
|
|
42
|
+
} as unknown as RuntimeEnv;
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
vi.clearAllMocks();
|
|
46
|
+
setMatrixRuntime(runtimeStub);
|
|
47
|
+
chunkMarkdownTextWithModeMock.mockImplementation((text: string) => [text]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("keeps replyToId on first reply only when replyToMode=first", async () => {
|
|
51
|
+
chunkMarkdownTextWithModeMock.mockImplementation((text: string) => text.split("|"));
|
|
52
|
+
|
|
53
|
+
await deliverMatrixReplies({
|
|
54
|
+
replies: [
|
|
55
|
+
{ text: "first-a|first-b", replyToId: "reply-1" },
|
|
56
|
+
{ text: "second", replyToId: "reply-2" },
|
|
57
|
+
],
|
|
58
|
+
roomId: "room:1",
|
|
59
|
+
client: {} as MatrixClient,
|
|
60
|
+
runtime: runtimeEnv,
|
|
61
|
+
textLimit: 4000,
|
|
62
|
+
replyToMode: "first",
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(sendMessageMatrixMock).toHaveBeenCalledTimes(3);
|
|
66
|
+
expect(sendMessageMatrixMock.mock.calls[0]?.[2]).toEqual(
|
|
67
|
+
expect.objectContaining({ replyToId: "reply-1", threadId: undefined }),
|
|
68
|
+
);
|
|
69
|
+
expect(sendMessageMatrixMock.mock.calls[1]?.[2]).toEqual(
|
|
70
|
+
expect.objectContaining({ replyToId: "reply-1", threadId: undefined }),
|
|
71
|
+
);
|
|
72
|
+
expect(sendMessageMatrixMock.mock.calls[2]?.[2]).toEqual(
|
|
73
|
+
expect.objectContaining({ replyToId: undefined, threadId: undefined }),
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("keeps replyToId on every reply when replyToMode=all", async () => {
|
|
78
|
+
await deliverMatrixReplies({
|
|
79
|
+
replies: [
|
|
80
|
+
{
|
|
81
|
+
text: "caption",
|
|
82
|
+
mediaUrls: ["https://example.com/a.jpg", "https://example.com/b.jpg"],
|
|
83
|
+
replyToId: "reply-media",
|
|
84
|
+
audioAsVoice: true,
|
|
85
|
+
},
|
|
86
|
+
{ text: "plain", replyToId: "reply-text" },
|
|
87
|
+
],
|
|
88
|
+
roomId: "room:2",
|
|
89
|
+
client: {} as MatrixClient,
|
|
90
|
+
runtime: runtimeEnv,
|
|
91
|
+
textLimit: 4000,
|
|
92
|
+
replyToMode: "all",
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
expect(sendMessageMatrixMock).toHaveBeenCalledTimes(3);
|
|
96
|
+
expect(sendMessageMatrixMock.mock.calls[0]).toEqual([
|
|
97
|
+
"room:2",
|
|
98
|
+
"caption",
|
|
99
|
+
expect.objectContaining({ mediaUrl: "https://example.com/a.jpg", replyToId: "reply-media" }),
|
|
100
|
+
]);
|
|
101
|
+
expect(sendMessageMatrixMock.mock.calls[1]).toEqual([
|
|
102
|
+
"room:2",
|
|
103
|
+
"",
|
|
104
|
+
expect.objectContaining({ mediaUrl: "https://example.com/b.jpg", replyToId: "reply-media" }),
|
|
105
|
+
]);
|
|
106
|
+
expect(sendMessageMatrixMock.mock.calls[2]?.[2]).toEqual(
|
|
107
|
+
expect.objectContaining({ replyToId: "reply-text" }),
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("suppresses replyToId when threadId is set", async () => {
|
|
112
|
+
chunkMarkdownTextWithModeMock.mockImplementation((text: string) => text.split("|"));
|
|
113
|
+
|
|
114
|
+
await deliverMatrixReplies({
|
|
115
|
+
replies: [{ text: "hello|thread", replyToId: "reply-thread" }],
|
|
116
|
+
roomId: "room:3",
|
|
117
|
+
client: {} as MatrixClient,
|
|
118
|
+
runtime: runtimeEnv,
|
|
119
|
+
textLimit: 4000,
|
|
120
|
+
replyToMode: "all",
|
|
121
|
+
threadId: "thread-77",
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(sendMessageMatrixMock).toHaveBeenCalledTimes(2);
|
|
125
|
+
expect(sendMessageMatrixMock.mock.calls[0]?.[2]).toEqual(
|
|
126
|
+
expect.objectContaining({ replyToId: undefined, threadId: "thread-77" }),
|
|
127
|
+
);
|
|
128
|
+
expect(sendMessageMatrixMock.mock.calls[1]?.[2]).toEqual(
|
|
129
|
+
expect.objectContaining({ replyToId: undefined, threadId: "thread-77" }),
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -53,8 +53,10 @@ export async function deliverMatrixReplies(params: {
|
|
|
53
53
|
|
|
54
54
|
const shouldIncludeReply = (id?: string) =>
|
|
55
55
|
Boolean(id) && (params.replyToMode === "all" || !hasReplied);
|
|
56
|
+
const replyToIdForReply = shouldIncludeReply(replyToId) ? replyToId : undefined;
|
|
56
57
|
|
|
57
58
|
if (mediaList.length === 0) {
|
|
59
|
+
let sentTextChunk = false;
|
|
58
60
|
for (const chunk of core.channel.text.chunkMarkdownTextWithMode(
|
|
59
61
|
text,
|
|
60
62
|
chunkLimit,
|
|
@@ -66,13 +68,14 @@ export async function deliverMatrixReplies(params: {
|
|
|
66
68
|
}
|
|
67
69
|
await sendMessageMatrix(params.roomId, trimmed, {
|
|
68
70
|
client: params.client,
|
|
69
|
-
replyToId:
|
|
71
|
+
replyToId: replyToIdForReply,
|
|
70
72
|
threadId: params.threadId,
|
|
71
73
|
accountId: params.accountId,
|
|
72
74
|
});
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
sentTextChunk = true;
|
|
76
|
+
}
|
|
77
|
+
if (replyToIdForReply && !hasReplied && sentTextChunk) {
|
|
78
|
+
hasReplied = true;
|
|
76
79
|
}
|
|
77
80
|
continue;
|
|
78
81
|
}
|
|
@@ -83,15 +86,15 @@ export async function deliverMatrixReplies(params: {
|
|
|
83
86
|
await sendMessageMatrix(params.roomId, caption, {
|
|
84
87
|
client: params.client,
|
|
85
88
|
mediaUrl,
|
|
86
|
-
replyToId:
|
|
89
|
+
replyToId: replyToIdForReply,
|
|
87
90
|
threadId: params.threadId,
|
|
88
91
|
audioAsVoice: reply.audioAsVoice,
|
|
89
92
|
accountId: params.accountId,
|
|
90
93
|
});
|
|
91
|
-
if (shouldIncludeReply(replyToId)) {
|
|
92
|
-
hasReplied = true;
|
|
93
|
-
}
|
|
94
94
|
first = false;
|
|
95
95
|
}
|
|
96
|
+
if (replyToIdForReply && !hasReplied) {
|
|
97
|
+
hasReplied = true;
|
|
98
|
+
}
|
|
96
99
|
}
|
|
97
100
|
}
|
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
|
2
2
|
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
|
3
|
-
import type { CoreConfig } from "../../types.js";
|
|
4
3
|
import { getMatrixRuntime } from "../../runtime.js";
|
|
4
|
+
import type { CoreConfig } from "../../types.js";
|
|
5
5
|
import { getActiveMatrixClient, getAnyActiveMatrixClient } from "../active-client.js";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
isBunRuntime,
|
|
9
|
-
resolveMatrixAuth,
|
|
10
|
-
resolveSharedMatrixClient,
|
|
11
|
-
} from "../client.js";
|
|
6
|
+
import { createPreparedMatrixClient } from "../client-bootstrap.js";
|
|
7
|
+
import { isBunRuntime, resolveMatrixAuth, resolveSharedMatrixClient } from "../client.js";
|
|
12
8
|
|
|
13
9
|
const getCore = () => getMatrixRuntime();
|
|
14
10
|
|
|
@@ -92,25 +88,10 @@ export async function resolveMatrixClient(opts: {
|
|
|
92
88
|
return { client, stopOnDone: false };
|
|
93
89
|
}
|
|
94
90
|
const auth = await resolveMatrixAuth({ accountId });
|
|
95
|
-
const client = await
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
accessToken: auth.accessToken,
|
|
99
|
-
encryption: auth.encryption,
|
|
100
|
-
localTimeoutMs: opts.timeoutMs,
|
|
91
|
+
const client = await createPreparedMatrixClient({
|
|
92
|
+
auth,
|
|
93
|
+
timeoutMs: opts.timeoutMs,
|
|
101
94
|
accountId,
|
|
102
95
|
});
|
|
103
|
-
if (auth.encryption && client.crypto) {
|
|
104
|
-
try {
|
|
105
|
-
const joinedRooms = await client.getJoinedRooms();
|
|
106
|
-
await (client.crypto as { prepare: (rooms?: string[]) => Promise<void> }).prepare(
|
|
107
|
-
joinedRooms,
|
|
108
|
-
);
|
|
109
|
-
} catch {
|
|
110
|
-
// Ignore crypto prep failures for one-off sends; normal sync will retry.
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
// @vector-im/matrix-bot-sdk uses start() instead of startClient()
|
|
114
|
-
await client.start();
|
|
115
96
|
return { client, stopOnDone: true };
|
|
116
97
|
}
|
package/src/matrix/send.test.ts
CHANGED
|
@@ -40,11 +40,13 @@ const runtimeStub = {
|
|
|
40
40
|
loadConfig: () => ({}),
|
|
41
41
|
},
|
|
42
42
|
media: {
|
|
43
|
-
loadWebMedia:
|
|
44
|
-
mediaKindFromMime:
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
loadWebMedia: loadWebMediaMock as unknown as PluginRuntime["media"]["loadWebMedia"],
|
|
44
|
+
mediaKindFromMime:
|
|
45
|
+
mediaKindFromMimeMock as unknown as PluginRuntime["media"]["mediaKindFromMime"],
|
|
46
|
+
isVoiceCompatibleAudio:
|
|
47
|
+
isVoiceCompatibleAudioMock as unknown as PluginRuntime["media"]["isVoiceCompatibleAudio"],
|
|
48
|
+
getImageMetadata: getImageMetadataMock as unknown as PluginRuntime["media"]["getImageMetadata"],
|
|
49
|
+
resizeToJpeg: resizeToJpegMock as unknown as PluginRuntime["media"]["resizeToJpeg"],
|
|
48
50
|
},
|
|
49
51
|
channel: {
|
|
50
52
|
text: {
|
package/src/onboarding.ts
CHANGED
|
@@ -2,16 +2,17 @@ import type { DmPolicy } from "openclaw/plugin-sdk";
|
|
|
2
2
|
import {
|
|
3
3
|
addWildcardAllowFrom,
|
|
4
4
|
formatDocsLink,
|
|
5
|
+
mergeAllowFromEntries,
|
|
5
6
|
promptChannelAccessConfig,
|
|
6
7
|
type ChannelOnboardingAdapter,
|
|
7
8
|
type ChannelOnboardingDmPolicy,
|
|
8
9
|
type WizardPrompter,
|
|
9
10
|
} from "openclaw/plugin-sdk";
|
|
10
|
-
import type { CoreConfig } from "./types.js";
|
|
11
11
|
import { listMatrixDirectoryGroupsLive } from "./directory-live.js";
|
|
12
12
|
import { resolveMatrixAccount } from "./matrix/accounts.js";
|
|
13
13
|
import { ensureMatrixSdkInstalled, isMatrixSdkAvailable } from "./matrix/deps.js";
|
|
14
14
|
import { resolveMatrixTargets } from "./resolve-targets.js";
|
|
15
|
+
import type { CoreConfig } from "./types.js";
|
|
15
16
|
|
|
16
17
|
const channel = "matrix" as const;
|
|
17
18
|
|
|
@@ -118,12 +119,7 @@ async function promptMatrixAllowFrom(params: {
|
|
|
118
119
|
continue;
|
|
119
120
|
}
|
|
120
121
|
|
|
121
|
-
const unique =
|
|
122
|
-
...new Set([
|
|
123
|
-
...existingAllowFrom.map((item) => String(item).trim()).filter(Boolean),
|
|
124
|
-
...resolvedIds,
|
|
125
|
-
]),
|
|
126
|
-
];
|
|
122
|
+
const unique = mergeAllowFromEntries(existingAllowFrom, resolvedIds);
|
|
127
123
|
return {
|
|
128
124
|
...cfg,
|
|
129
125
|
channels: {
|
package/src/tool-actions.ts
CHANGED
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
readReactionParams,
|
|
7
7
|
readStringParam,
|
|
8
8
|
} from "openclaw/plugin-sdk";
|
|
9
|
-
import type { CoreConfig } from "./types.js";
|
|
10
9
|
import {
|
|
11
10
|
deleteMatrixMessage,
|
|
12
11
|
editMatrixMessage,
|
|
@@ -21,6 +20,7 @@ import {
|
|
|
21
20
|
unpinMatrixMessage,
|
|
22
21
|
} from "./matrix/actions.js";
|
|
23
22
|
import { reactMatrixMessage } from "./matrix/send.js";
|
|
23
|
+
import type { CoreConfig } from "./types.js";
|
|
24
24
|
|
|
25
25
|
const messageActions = new Set(["sendMessage", "editMessage", "deleteMessage", "readMessages"]);
|
|
26
26
|
const reactionActions = new Set(["react", "reactions"]);
|