@openclaw/matrix 2026.2.12 → 2026.2.14
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 +1 -1
- package/src/channel.directory.test.ts +81 -1
- package/src/channel.ts +59 -18
- package/src/directory-live.test.ts +54 -0
- package/src/directory-live.ts +4 -2
- package/src/group-mentions.ts +5 -2
- package/src/matrix/accounts.ts +80 -8
- package/src/matrix/actions/client.ts +7 -1
- package/src/matrix/actions/types.ts +1 -0
- package/src/matrix/active-client.ts +26 -5
- package/src/matrix/client/config.ts +76 -17
- package/src/matrix/client/shared.ts +67 -38
- package/src/matrix/client.ts +11 -2
- package/src/matrix/credentials.ts +31 -11
- package/src/matrix/monitor/handler.ts +3 -0
- package/src/matrix/monitor/index.ts +21 -15
- package/src/matrix/send/client.ts +52 -4
- package/src/matrix/send/formatting.ts +10 -6
- package/src/matrix/send/media.ts +2 -1
- package/src/matrix/send.test.ts +77 -12
- package/src/matrix/send.ts +3 -1
- package/src/outbound.ts +6 -3
- package/src/types.ts +5 -0
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
|
2
|
-
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
import type { CoreConfig } from "./types.js";
|
|
4
4
|
import { matrixPlugin } from "./channel.js";
|
|
5
5
|
import { setMatrixRuntime } from "./runtime.js";
|
|
6
6
|
|
|
7
|
+
vi.mock("@vector-im/matrix-bot-sdk", () => ({
|
|
8
|
+
ConsoleLogger: class {
|
|
9
|
+
trace = vi.fn();
|
|
10
|
+
debug = vi.fn();
|
|
11
|
+
info = vi.fn();
|
|
12
|
+
warn = vi.fn();
|
|
13
|
+
error = vi.fn();
|
|
14
|
+
},
|
|
15
|
+
MatrixClient: class {},
|
|
16
|
+
LogService: {
|
|
17
|
+
setLogger: vi.fn(),
|
|
18
|
+
warn: vi.fn(),
|
|
19
|
+
info: vi.fn(),
|
|
20
|
+
debug: vi.fn(),
|
|
21
|
+
},
|
|
22
|
+
SimpleFsStorageProvider: class {},
|
|
23
|
+
RustSdkCryptoStorageProvider: class {},
|
|
24
|
+
}));
|
|
25
|
+
|
|
7
26
|
describe("matrix directory", () => {
|
|
8
27
|
beforeEach(() => {
|
|
9
28
|
setMatrixRuntime({
|
|
@@ -61,4 +80,65 @@ describe("matrix directory", () => {
|
|
|
61
80
|
]),
|
|
62
81
|
);
|
|
63
82
|
});
|
|
83
|
+
|
|
84
|
+
it("resolves replyToMode from account config", () => {
|
|
85
|
+
const cfg = {
|
|
86
|
+
channels: {
|
|
87
|
+
matrix: {
|
|
88
|
+
replyToMode: "off",
|
|
89
|
+
accounts: {
|
|
90
|
+
Assistant: {
|
|
91
|
+
replyToMode: "all",
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
} as unknown as CoreConfig;
|
|
97
|
+
|
|
98
|
+
expect(matrixPlugin.threading?.resolveReplyToMode).toBeTruthy();
|
|
99
|
+
expect(
|
|
100
|
+
matrixPlugin.threading?.resolveReplyToMode?.({
|
|
101
|
+
cfg,
|
|
102
|
+
accountId: "assistant",
|
|
103
|
+
chatType: "direct",
|
|
104
|
+
}),
|
|
105
|
+
).toBe("all");
|
|
106
|
+
expect(
|
|
107
|
+
matrixPlugin.threading?.resolveReplyToMode?.({
|
|
108
|
+
cfg,
|
|
109
|
+
accountId: "default",
|
|
110
|
+
chatType: "direct",
|
|
111
|
+
}),
|
|
112
|
+
).toBe("off");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("resolves group mention policy from account config", () => {
|
|
116
|
+
const cfg = {
|
|
117
|
+
channels: {
|
|
118
|
+
matrix: {
|
|
119
|
+
groups: {
|
|
120
|
+
"!room:example.org": { requireMention: true },
|
|
121
|
+
},
|
|
122
|
+
accounts: {
|
|
123
|
+
Assistant: {
|
|
124
|
+
groups: {
|
|
125
|
+
"!room:example.org": { requireMention: false },
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
} as unknown as CoreConfig;
|
|
132
|
+
|
|
133
|
+
expect(matrixPlugin.groups.resolveRequireMention({ cfg, groupId: "!room:example.org" })).toBe(
|
|
134
|
+
true,
|
|
135
|
+
);
|
|
136
|
+
expect(
|
|
137
|
+
matrixPlugin.groups.resolveRequireMention({
|
|
138
|
+
cfg,
|
|
139
|
+
accountId: "assistant",
|
|
140
|
+
groupId: "!room:example.org",
|
|
141
|
+
}),
|
|
142
|
+
).toBe(false);
|
|
143
|
+
});
|
|
64
144
|
});
|
package/src/channel.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from "./group-mentions.js";
|
|
20
20
|
import {
|
|
21
21
|
listMatrixAccountIds,
|
|
22
|
+
resolveMatrixAccountConfig,
|
|
22
23
|
resolveDefaultMatrixAccountId,
|
|
23
24
|
resolveMatrixAccount,
|
|
24
25
|
type ResolvedMatrixAccount,
|
|
@@ -31,6 +32,9 @@ import { matrixOnboardingAdapter } from "./onboarding.js";
|
|
|
31
32
|
import { matrixOutbound } from "./outbound.js";
|
|
32
33
|
import { resolveMatrixTargets } from "./resolve-targets.js";
|
|
33
34
|
|
|
35
|
+
// Mutex for serializing account startup (workaround for concurrent dynamic import race condition)
|
|
36
|
+
let matrixStartupLock: Promise<void> = Promise.resolve();
|
|
37
|
+
|
|
34
38
|
const meta = {
|
|
35
39
|
id: "matrix",
|
|
36
40
|
label: "Matrix",
|
|
@@ -142,19 +146,28 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
|
|
|
142
146
|
configured: account.configured,
|
|
143
147
|
baseUrl: account.homeserver,
|
|
144
148
|
}),
|
|
145
|
-
resolveAllowFrom: ({ cfg }) =>
|
|
146
|
-
(
|
|
149
|
+
resolveAllowFrom: ({ cfg, accountId }) => {
|
|
150
|
+
const matrixConfig = resolveMatrixAccountConfig({ cfg: cfg as CoreConfig, accountId });
|
|
151
|
+
return (matrixConfig.dm?.allowFrom ?? []).map((entry: string | number) => String(entry));
|
|
152
|
+
},
|
|
147
153
|
formatAllowFrom: ({ allowFrom }) => normalizeMatrixAllowList(allowFrom),
|
|
148
154
|
},
|
|
149
155
|
security: {
|
|
150
|
-
resolveDmPolicy: ({ account }) =>
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
156
|
+
resolveDmPolicy: ({ account }) => {
|
|
157
|
+
const accountId = account.accountId;
|
|
158
|
+
const prefix =
|
|
159
|
+
accountId && accountId !== "default"
|
|
160
|
+
? `channels.matrix.accounts.${accountId}.dm`
|
|
161
|
+
: "channels.matrix.dm";
|
|
162
|
+
return {
|
|
163
|
+
policy: account.config.dm?.policy ?? "pairing",
|
|
164
|
+
allowFrom: account.config.dm?.allowFrom ?? [],
|
|
165
|
+
policyPath: `${prefix}.policy`,
|
|
166
|
+
allowFromPath: `${prefix}.allowFrom`,
|
|
167
|
+
approveHint: formatPairingApproveHint("matrix"),
|
|
168
|
+
normalizeEntry: (raw) => normalizeMatrixUserId(raw),
|
|
169
|
+
};
|
|
170
|
+
},
|
|
158
171
|
collectWarnings: ({ account, cfg }) => {
|
|
159
172
|
const defaultGroupPolicy = (cfg as CoreConfig).channels?.defaults?.groupPolicy;
|
|
160
173
|
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
|
|
@@ -171,7 +184,8 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
|
|
|
171
184
|
resolveToolPolicy: resolveMatrixGroupToolPolicy,
|
|
172
185
|
},
|
|
173
186
|
threading: {
|
|
174
|
-
resolveReplyToMode: ({ cfg }) =>
|
|
187
|
+
resolveReplyToMode: ({ cfg, accountId }) =>
|
|
188
|
+
resolveMatrixAccountConfig({ cfg: cfg as CoreConfig, accountId }).replyToMode ?? "off",
|
|
175
189
|
buildToolContext: ({ context, hasRepliedRef }) => {
|
|
176
190
|
const currentTarget = context.To;
|
|
177
191
|
return {
|
|
@@ -278,10 +292,10 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
|
|
|
278
292
|
.map((id) => ({ kind: "group", id }) as const);
|
|
279
293
|
return ids;
|
|
280
294
|
},
|
|
281
|
-
listPeersLive: async ({ cfg, query, limit }) =>
|
|
282
|
-
listMatrixDirectoryPeersLive({ cfg, query, limit }),
|
|
283
|
-
listGroupsLive: async ({ cfg, query, limit }) =>
|
|
284
|
-
listMatrixDirectoryGroupsLive({ cfg, query, limit }),
|
|
295
|
+
listPeersLive: async ({ cfg, accountId, query, limit }) =>
|
|
296
|
+
listMatrixDirectoryPeersLive({ cfg, accountId, query, limit }),
|
|
297
|
+
listGroupsLive: async ({ cfg, accountId, query, limit }) =>
|
|
298
|
+
listMatrixDirectoryGroupsLive({ cfg, accountId, query, limit }),
|
|
285
299
|
},
|
|
286
300
|
resolver: {
|
|
287
301
|
resolveTargets: async ({ cfg, inputs, kind, runtime }) =>
|
|
@@ -383,9 +397,12 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
|
|
|
383
397
|
probe: snapshot.probe,
|
|
384
398
|
lastProbeAt: snapshot.lastProbeAt ?? null,
|
|
385
399
|
}),
|
|
386
|
-
probeAccount: async ({ timeoutMs, cfg }) => {
|
|
400
|
+
probeAccount: async ({ account, timeoutMs, cfg }) => {
|
|
387
401
|
try {
|
|
388
|
-
const auth = await resolveMatrixAuth({
|
|
402
|
+
const auth = await resolveMatrixAuth({
|
|
403
|
+
cfg: cfg as CoreConfig,
|
|
404
|
+
accountId: account.accountId,
|
|
405
|
+
});
|
|
389
406
|
return await probeMatrix({
|
|
390
407
|
homeserver: auth.homeserver,
|
|
391
408
|
accessToken: auth.accessToken,
|
|
@@ -424,8 +441,32 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
|
|
|
424
441
|
baseUrl: account.homeserver,
|
|
425
442
|
});
|
|
426
443
|
ctx.log?.info(`[${account.accountId}] starting provider (${account.homeserver ?? "matrix"})`);
|
|
444
|
+
|
|
445
|
+
// Serialize startup: wait for any previous startup to complete import phase.
|
|
446
|
+
// This works around a race condition with concurrent dynamic imports.
|
|
447
|
+
//
|
|
448
|
+
// INVARIANT: The import() below cannot hang because:
|
|
449
|
+
// 1. It only loads local ESM modules with no circular awaits
|
|
450
|
+
// 2. Module initialization is synchronous (no top-level await in ./matrix/index.js)
|
|
451
|
+
// 3. The lock only serializes the import phase, not the provider startup
|
|
452
|
+
const previousLock = matrixStartupLock;
|
|
453
|
+
let releaseLock: () => void = () => {};
|
|
454
|
+
matrixStartupLock = new Promise<void>((resolve) => {
|
|
455
|
+
releaseLock = resolve;
|
|
456
|
+
});
|
|
457
|
+
await previousLock;
|
|
458
|
+
|
|
427
459
|
// Lazy import: the monitor pulls the reply pipeline; avoid ESM init cycles.
|
|
428
|
-
|
|
460
|
+
// Wrap in try/finally to ensure lock is released even if import fails.
|
|
461
|
+
let monitorMatrixProvider: typeof import("./matrix/index.js").monitorMatrixProvider;
|
|
462
|
+
try {
|
|
463
|
+
const module = await import("./matrix/index.js");
|
|
464
|
+
monitorMatrixProvider = module.monitorMatrixProvider;
|
|
465
|
+
} finally {
|
|
466
|
+
// Release lock after import completes or fails
|
|
467
|
+
releaseLock();
|
|
468
|
+
}
|
|
469
|
+
|
|
429
470
|
return monitorMatrixProvider({
|
|
430
471
|
runtime: ctx.runtime,
|
|
431
472
|
abortSignal: ctx.abortSignal,
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { listMatrixDirectoryGroupsLive, listMatrixDirectoryPeersLive } from "./directory-live.js";
|
|
3
|
+
import { resolveMatrixAuth } from "./matrix/client.js";
|
|
4
|
+
|
|
5
|
+
vi.mock("./matrix/client.js", () => ({
|
|
6
|
+
resolveMatrixAuth: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
describe("matrix directory live", () => {
|
|
10
|
+
const cfg = { channels: { matrix: {} } };
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
vi.mocked(resolveMatrixAuth).mockReset();
|
|
14
|
+
vi.mocked(resolveMatrixAuth).mockResolvedValue({
|
|
15
|
+
homeserver: "https://matrix.example.org",
|
|
16
|
+
userId: "@bot:example.org",
|
|
17
|
+
accessToken: "test-token",
|
|
18
|
+
});
|
|
19
|
+
vi.stubGlobal(
|
|
20
|
+
"fetch",
|
|
21
|
+
vi.fn().mockResolvedValue({
|
|
22
|
+
ok: true,
|
|
23
|
+
json: async () => ({ results: [] }),
|
|
24
|
+
text: async () => "",
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
vi.unstubAllGlobals();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("passes accountId to peer directory auth resolution", async () => {
|
|
34
|
+
await listMatrixDirectoryPeersLive({
|
|
35
|
+
cfg,
|
|
36
|
+
accountId: "assistant",
|
|
37
|
+
query: "alice",
|
|
38
|
+
limit: 10,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
expect(resolveMatrixAuth).toHaveBeenCalledWith({ cfg, accountId: "assistant" });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("passes accountId to group directory auth resolution", async () => {
|
|
45
|
+
await listMatrixDirectoryGroupsLive({
|
|
46
|
+
cfg,
|
|
47
|
+
accountId: "assistant",
|
|
48
|
+
query: "!room:example.org",
|
|
49
|
+
limit: 10,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(resolveMatrixAuth).toHaveBeenCalledWith({ cfg, accountId: "assistant" });
|
|
53
|
+
});
|
|
54
|
+
});
|
package/src/directory-live.ts
CHANGED
|
@@ -50,6 +50,7 @@ function normalizeQuery(value?: string | null): string {
|
|
|
50
50
|
|
|
51
51
|
export async function listMatrixDirectoryPeersLive(params: {
|
|
52
52
|
cfg: unknown;
|
|
53
|
+
accountId?: string | null;
|
|
53
54
|
query?: string | null;
|
|
54
55
|
limit?: number | null;
|
|
55
56
|
}): Promise<ChannelDirectoryEntry[]> {
|
|
@@ -57,7 +58,7 @@ export async function listMatrixDirectoryPeersLive(params: {
|
|
|
57
58
|
if (!query) {
|
|
58
59
|
return [];
|
|
59
60
|
}
|
|
60
|
-
const auth = await resolveMatrixAuth({ cfg: params.cfg as never });
|
|
61
|
+
const auth = await resolveMatrixAuth({ cfg: params.cfg as never, accountId: params.accountId });
|
|
61
62
|
const res = await fetchMatrixJson<MatrixUserDirectoryResponse>({
|
|
62
63
|
homeserver: auth.homeserver,
|
|
63
64
|
accessToken: auth.accessToken,
|
|
@@ -122,6 +123,7 @@ async function fetchMatrixRoomName(
|
|
|
122
123
|
|
|
123
124
|
export async function listMatrixDirectoryGroupsLive(params: {
|
|
124
125
|
cfg: unknown;
|
|
126
|
+
accountId?: string | null;
|
|
125
127
|
query?: string | null;
|
|
126
128
|
limit?: number | null;
|
|
127
129
|
}): Promise<ChannelDirectoryEntry[]> {
|
|
@@ -129,7 +131,7 @@ export async function listMatrixDirectoryGroupsLive(params: {
|
|
|
129
131
|
if (!query) {
|
|
130
132
|
return [];
|
|
131
133
|
}
|
|
132
|
-
const auth = await resolveMatrixAuth({ cfg: params.cfg as never });
|
|
134
|
+
const auth = await resolveMatrixAuth({ cfg: params.cfg as never, accountId: params.accountId });
|
|
133
135
|
const limit = typeof params.limit === "number" && params.limit > 0 ? params.limit : 20;
|
|
134
136
|
|
|
135
137
|
if (query.startsWith("#")) {
|
package/src/group-mentions.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ChannelGroupContext, GroupToolPolicyConfig } from "openclaw/plugin-sdk";
|
|
2
2
|
import type { CoreConfig } from "./types.js";
|
|
3
|
+
import { resolveMatrixAccountConfig } from "./matrix/accounts.js";
|
|
3
4
|
import { resolveMatrixRoomConfig } from "./matrix/monitor/rooms.js";
|
|
4
5
|
|
|
5
6
|
export function resolveMatrixGroupRequireMention(params: ChannelGroupContext): boolean {
|
|
@@ -18,8 +19,9 @@ export function resolveMatrixGroupRequireMention(params: ChannelGroupContext): b
|
|
|
18
19
|
const groupChannel = params.groupChannel?.trim() ?? "";
|
|
19
20
|
const aliases = groupChannel ? [groupChannel] : [];
|
|
20
21
|
const cfg = params.cfg as CoreConfig;
|
|
22
|
+
const matrixConfig = resolveMatrixAccountConfig({ cfg, accountId: params.accountId });
|
|
21
23
|
const resolved = resolveMatrixRoomConfig({
|
|
22
|
-
rooms:
|
|
24
|
+
rooms: matrixConfig.groups ?? matrixConfig.rooms,
|
|
23
25
|
roomId,
|
|
24
26
|
aliases,
|
|
25
27
|
name: groupChannel || undefined,
|
|
@@ -56,8 +58,9 @@ export function resolveMatrixGroupToolPolicy(
|
|
|
56
58
|
const groupChannel = params.groupChannel?.trim() ?? "";
|
|
57
59
|
const aliases = groupChannel ? [groupChannel] : [];
|
|
58
60
|
const cfg = params.cfg as CoreConfig;
|
|
61
|
+
const matrixConfig = resolveMatrixAccountConfig({ cfg, accountId: params.accountId });
|
|
59
62
|
const resolved = resolveMatrixRoomConfig({
|
|
60
|
-
rooms:
|
|
63
|
+
rooms: matrixConfig.groups ?? matrixConfig.rooms,
|
|
61
64
|
roomId,
|
|
62
65
|
aliases,
|
|
63
66
|
name: groupChannel || undefined,
|
package/src/matrix/accounts.ts
CHANGED
|
@@ -1,8 +1,24 @@
|
|
|
1
|
-
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk";
|
|
1
|
+
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
|
2
2
|
import type { CoreConfig, MatrixConfig } from "../types.js";
|
|
3
|
-
import {
|
|
3
|
+
import { resolveMatrixConfigForAccount } from "./client.js";
|
|
4
4
|
import { credentialsMatchConfig, loadMatrixCredentials } from "./credentials.js";
|
|
5
5
|
|
|
6
|
+
/** Merge account config with top-level defaults, preserving nested objects. */
|
|
7
|
+
function mergeAccountConfig(base: MatrixConfig, account: MatrixConfig): MatrixConfig {
|
|
8
|
+
const merged = { ...base, ...account };
|
|
9
|
+
// Deep-merge known nested objects so partial overrides inherit base fields
|
|
10
|
+
for (const key of ["dm", "actions"] as const) {
|
|
11
|
+
const b = base[key];
|
|
12
|
+
const o = account[key];
|
|
13
|
+
if (typeof b === "object" && b != null && typeof o === "object" && o != null) {
|
|
14
|
+
(merged as Record<string, unknown>)[key] = { ...b, ...o };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
// Don't propagate the accounts map into the merged per-account config
|
|
18
|
+
delete (merged as Record<string, unknown>).accounts;
|
|
19
|
+
return merged;
|
|
20
|
+
}
|
|
21
|
+
|
|
6
22
|
export type ResolvedMatrixAccount = {
|
|
7
23
|
accountId: string;
|
|
8
24
|
enabled: boolean;
|
|
@@ -13,8 +29,28 @@ export type ResolvedMatrixAccount = {
|
|
|
13
29
|
config: MatrixConfig;
|
|
14
30
|
};
|
|
15
31
|
|
|
16
|
-
|
|
17
|
-
|
|
32
|
+
function listConfiguredAccountIds(cfg: CoreConfig): string[] {
|
|
33
|
+
const accounts = cfg.channels?.matrix?.accounts;
|
|
34
|
+
if (!accounts || typeof accounts !== "object") {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
// Normalize and de-duplicate keys so listing and resolution use the same semantics
|
|
38
|
+
return [
|
|
39
|
+
...new Set(
|
|
40
|
+
Object.keys(accounts)
|
|
41
|
+
.filter(Boolean)
|
|
42
|
+
.map((id) => normalizeAccountId(id)),
|
|
43
|
+
),
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function listMatrixAccountIds(cfg: CoreConfig): string[] {
|
|
48
|
+
const ids = listConfiguredAccountIds(cfg);
|
|
49
|
+
if (ids.length === 0) {
|
|
50
|
+
// Fall back to default if no accounts configured (legacy top-level config)
|
|
51
|
+
return [DEFAULT_ACCOUNT_ID];
|
|
52
|
+
}
|
|
53
|
+
return ids.toSorted((a, b) => a.localeCompare(b));
|
|
18
54
|
}
|
|
19
55
|
|
|
20
56
|
export function resolveDefaultMatrixAccountId(cfg: CoreConfig): string {
|
|
@@ -25,20 +61,41 @@ export function resolveDefaultMatrixAccountId(cfg: CoreConfig): string {
|
|
|
25
61
|
return ids[0] ?? DEFAULT_ACCOUNT_ID;
|
|
26
62
|
}
|
|
27
63
|
|
|
64
|
+
function resolveAccountConfig(cfg: CoreConfig, accountId: string): MatrixConfig | undefined {
|
|
65
|
+
const accounts = cfg.channels?.matrix?.accounts;
|
|
66
|
+
if (!accounts || typeof accounts !== "object") {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
// Direct lookup first (fast path for already-normalized keys)
|
|
70
|
+
if (accounts[accountId]) {
|
|
71
|
+
return accounts[accountId] as MatrixConfig;
|
|
72
|
+
}
|
|
73
|
+
// Fall back to case-insensitive match (user may have mixed-case keys in config)
|
|
74
|
+
const normalized = normalizeAccountId(accountId);
|
|
75
|
+
for (const key of Object.keys(accounts)) {
|
|
76
|
+
if (normalizeAccountId(key) === normalized) {
|
|
77
|
+
return accounts[key] as MatrixConfig;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
28
83
|
export function resolveMatrixAccount(params: {
|
|
29
84
|
cfg: CoreConfig;
|
|
30
85
|
accountId?: string | null;
|
|
31
86
|
}): ResolvedMatrixAccount {
|
|
32
87
|
const accountId = normalizeAccountId(params.accountId);
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
const
|
|
88
|
+
const matrixBase = params.cfg.channels?.matrix ?? {};
|
|
89
|
+
const base = resolveMatrixAccountConfig({ cfg: params.cfg, accountId });
|
|
90
|
+
const enabled = base.enabled !== false && matrixBase.enabled !== false;
|
|
91
|
+
|
|
92
|
+
const resolved = resolveMatrixConfigForAccount(params.cfg, accountId, process.env);
|
|
36
93
|
const hasHomeserver = Boolean(resolved.homeserver);
|
|
37
94
|
const hasUserId = Boolean(resolved.userId);
|
|
38
95
|
const hasAccessToken = Boolean(resolved.accessToken);
|
|
39
96
|
const hasPassword = Boolean(resolved.password);
|
|
40
97
|
const hasPasswordAuth = hasUserId && hasPassword;
|
|
41
|
-
const stored = loadMatrixCredentials(process.env);
|
|
98
|
+
const stored = loadMatrixCredentials(process.env, accountId);
|
|
42
99
|
const hasStored =
|
|
43
100
|
stored && resolved.homeserver
|
|
44
101
|
? credentialsMatchConfig(stored, {
|
|
@@ -58,6 +115,21 @@ export function resolveMatrixAccount(params: {
|
|
|
58
115
|
};
|
|
59
116
|
}
|
|
60
117
|
|
|
118
|
+
export function resolveMatrixAccountConfig(params: {
|
|
119
|
+
cfg: CoreConfig;
|
|
120
|
+
accountId?: string | null;
|
|
121
|
+
}): MatrixConfig {
|
|
122
|
+
const accountId = normalizeAccountId(params.accountId);
|
|
123
|
+
const matrixBase = params.cfg.channels?.matrix ?? {};
|
|
124
|
+
const accountConfig = resolveAccountConfig(params.cfg, accountId);
|
|
125
|
+
if (!accountConfig) {
|
|
126
|
+
return matrixBase;
|
|
127
|
+
}
|
|
128
|
+
// Merge account-specific config with top-level defaults so settings like
|
|
129
|
+
// groupPolicy and blockStreaming inherit when not overridden.
|
|
130
|
+
return mergeAccountConfig(matrixBase, accountConfig);
|
|
131
|
+
}
|
|
132
|
+
|
|
61
133
|
export function listEnabledMatrixAccounts(cfg: CoreConfig): ResolvedMatrixAccount[] {
|
|
62
134
|
return listMatrixAccountIds(cfg)
|
|
63
135
|
.map((accountId) => resolveMatrixAccount({ cfg, accountId }))
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
|
1
2
|
import type { CoreConfig } from "../../types.js";
|
|
2
3
|
import type { MatrixActionClient, MatrixActionClientOpts } from "./types.js";
|
|
3
4
|
import { getMatrixRuntime } from "../../runtime.js";
|
|
@@ -22,7 +23,9 @@ export async function resolveActionClient(
|
|
|
22
23
|
if (opts.client) {
|
|
23
24
|
return { client: opts.client, stopOnDone: false };
|
|
24
25
|
}
|
|
25
|
-
|
|
26
|
+
// Normalize accountId early to ensure consistent keying across all lookups
|
|
27
|
+
const accountId = normalizeAccountId(opts.accountId);
|
|
28
|
+
const active = getActiveMatrixClient(accountId);
|
|
26
29
|
if (active) {
|
|
27
30
|
return { client: active, stopOnDone: false };
|
|
28
31
|
}
|
|
@@ -31,11 +34,13 @@ export async function resolveActionClient(
|
|
|
31
34
|
const client = await resolveSharedMatrixClient({
|
|
32
35
|
cfg: getMatrixRuntime().config.loadConfig() as CoreConfig,
|
|
33
36
|
timeoutMs: opts.timeoutMs,
|
|
37
|
+
accountId,
|
|
34
38
|
});
|
|
35
39
|
return { client, stopOnDone: false };
|
|
36
40
|
}
|
|
37
41
|
const auth = await resolveMatrixAuth({
|
|
38
42
|
cfg: getMatrixRuntime().config.loadConfig() as CoreConfig,
|
|
43
|
+
accountId,
|
|
39
44
|
});
|
|
40
45
|
const client = await createMatrixClient({
|
|
41
46
|
homeserver: auth.homeserver,
|
|
@@ -43,6 +48,7 @@ export async function resolveActionClient(
|
|
|
43
48
|
accessToken: auth.accessToken,
|
|
44
49
|
encryption: auth.encryption,
|
|
45
50
|
localTimeoutMs: opts.timeoutMs,
|
|
51
|
+
accountId,
|
|
46
52
|
});
|
|
47
53
|
if (auth.encryption && client.crypto) {
|
|
48
54
|
try {
|
|
@@ -1,11 +1,32 @@
|
|
|
1
1
|
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
|
2
|
+
import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
// Support multiple active clients for multi-account
|
|
5
|
+
const activeClients = new Map<string, MatrixClient>();
|
|
4
6
|
|
|
5
|
-
export function setActiveMatrixClient(
|
|
6
|
-
|
|
7
|
+
export function setActiveMatrixClient(
|
|
8
|
+
client: MatrixClient | null,
|
|
9
|
+
accountId?: string | null,
|
|
10
|
+
): void {
|
|
11
|
+
const key = normalizeAccountId(accountId);
|
|
12
|
+
if (client) {
|
|
13
|
+
activeClients.set(key, client);
|
|
14
|
+
} else {
|
|
15
|
+
activeClients.delete(key);
|
|
16
|
+
}
|
|
7
17
|
}
|
|
8
18
|
|
|
9
|
-
export function getActiveMatrixClient(): MatrixClient | null {
|
|
10
|
-
|
|
19
|
+
export function getActiveMatrixClient(accountId?: string | null): MatrixClient | null {
|
|
20
|
+
const key = normalizeAccountId(accountId);
|
|
21
|
+
return activeClients.get(key) ?? null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getAnyActiveMatrixClient(): MatrixClient | null {
|
|
25
|
+
// Return any available client (for backward compatibility)
|
|
26
|
+
const first = activeClients.values().next();
|
|
27
|
+
return first.done ? null : first.value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function clearAllActiveMatrixClients(): void {
|
|
31
|
+
activeClients.clear();
|
|
11
32
|
}
|