@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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
|
2
|
+
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
|
2
3
|
import type { CoreConfig } from "../../types.js";
|
|
3
4
|
import type { MatrixAuth, MatrixResolvedConfig } from "./types.js";
|
|
4
5
|
import { getMatrixRuntime } from "../../runtime.js";
|
|
@@ -8,11 +9,49 @@ function clean(value?: string): string {
|
|
|
8
9
|
return value?.trim() ?? "";
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
/** Shallow-merge known nested config sub-objects so partial overrides inherit base values. */
|
|
13
|
+
function deepMergeConfig<T extends Record<string, unknown>>(base: T, override: Partial<T>): T {
|
|
14
|
+
const merged = { ...base, ...override } as Record<string, unknown>;
|
|
15
|
+
// Merge known nested objects (dm, actions) so partial overrides keep base fields
|
|
16
|
+
for (const key of ["dm", "actions"] as const) {
|
|
17
|
+
const b = base[key];
|
|
18
|
+
const o = override[key];
|
|
19
|
+
if (typeof b === "object" && b !== null && typeof o === "object" && o !== null) {
|
|
20
|
+
merged[key] = { ...(b as Record<string, unknown>), ...(o as Record<string, unknown>) };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return merged as T;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolve Matrix config for a specific account, with fallback to top-level config.
|
|
28
|
+
* This supports both multi-account (channels.matrix.accounts.*) and
|
|
29
|
+
* single-account (channels.matrix.*) configurations.
|
|
30
|
+
*/
|
|
31
|
+
export function resolveMatrixConfigForAccount(
|
|
12
32
|
cfg: CoreConfig = getMatrixRuntime().config.loadConfig() as CoreConfig,
|
|
33
|
+
accountId?: string | null,
|
|
13
34
|
env: NodeJS.ProcessEnv = process.env,
|
|
14
35
|
): MatrixResolvedConfig {
|
|
15
|
-
const
|
|
36
|
+
const normalizedAccountId = normalizeAccountId(accountId);
|
|
37
|
+
const matrixBase = cfg.channels?.matrix ?? {};
|
|
38
|
+
const accounts = cfg.channels?.matrix?.accounts;
|
|
39
|
+
|
|
40
|
+
// Try to get account-specific config first (direct lookup, then case-insensitive fallback)
|
|
41
|
+
let accountConfig = accounts?.[normalizedAccountId];
|
|
42
|
+
if (!accountConfig && accounts) {
|
|
43
|
+
for (const key of Object.keys(accounts)) {
|
|
44
|
+
if (normalizeAccountId(key) === normalizedAccountId) {
|
|
45
|
+
accountConfig = accounts[key];
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Deep merge: account-specific values override top-level values, preserving
|
|
52
|
+
// nested object inheritance (dm, actions, groups) so partial overrides work.
|
|
53
|
+
const matrix = accountConfig ? deepMergeConfig(matrixBase, accountConfig) : matrixBase;
|
|
54
|
+
|
|
16
55
|
const homeserver = clean(matrix.homeserver) || clean(env.MATRIX_HOMESERVER);
|
|
17
56
|
const userId = clean(matrix.userId) || clean(env.MATRIX_USER_ID);
|
|
18
57
|
const accessToken = clean(matrix.accessToken) || clean(env.MATRIX_ACCESS_TOKEN) || undefined;
|
|
@@ -34,13 +73,24 @@ export function resolveMatrixConfig(
|
|
|
34
73
|
};
|
|
35
74
|
}
|
|
36
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Single-account function for backward compatibility - resolves default account config.
|
|
78
|
+
*/
|
|
79
|
+
export function resolveMatrixConfig(
|
|
80
|
+
cfg: CoreConfig = getMatrixRuntime().config.loadConfig() as CoreConfig,
|
|
81
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
82
|
+
): MatrixResolvedConfig {
|
|
83
|
+
return resolveMatrixConfigForAccount(cfg, DEFAULT_ACCOUNT_ID, env);
|
|
84
|
+
}
|
|
85
|
+
|
|
37
86
|
export async function resolveMatrixAuth(params?: {
|
|
38
87
|
cfg?: CoreConfig;
|
|
39
88
|
env?: NodeJS.ProcessEnv;
|
|
89
|
+
accountId?: string | null;
|
|
40
90
|
}): Promise<MatrixAuth> {
|
|
41
91
|
const cfg = params?.cfg ?? (getMatrixRuntime().config.loadConfig() as CoreConfig);
|
|
42
92
|
const env = params?.env ?? process.env;
|
|
43
|
-
const resolved =
|
|
93
|
+
const resolved = resolveMatrixConfigForAccount(cfg, params?.accountId, env);
|
|
44
94
|
if (!resolved.homeserver) {
|
|
45
95
|
throw new Error("Matrix homeserver is required (matrix.homeserver)");
|
|
46
96
|
}
|
|
@@ -52,7 +102,8 @@ export async function resolveMatrixAuth(params?: {
|
|
|
52
102
|
touchMatrixCredentials,
|
|
53
103
|
} = await import("../credentials.js");
|
|
54
104
|
|
|
55
|
-
const
|
|
105
|
+
const accountId = params?.accountId;
|
|
106
|
+
const cached = loadMatrixCredentials(env, accountId);
|
|
56
107
|
const cachedCredentials =
|
|
57
108
|
cached &&
|
|
58
109
|
credentialsMatchConfig(cached, {
|
|
@@ -72,13 +123,17 @@ export async function resolveMatrixAuth(params?: {
|
|
|
72
123
|
const whoami = await tempClient.getUserId();
|
|
73
124
|
userId = whoami;
|
|
74
125
|
// Save the credentials with the fetched userId
|
|
75
|
-
saveMatrixCredentials(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
126
|
+
saveMatrixCredentials(
|
|
127
|
+
{
|
|
128
|
+
homeserver: resolved.homeserver,
|
|
129
|
+
userId,
|
|
130
|
+
accessToken: resolved.accessToken,
|
|
131
|
+
},
|
|
132
|
+
env,
|
|
133
|
+
accountId,
|
|
134
|
+
);
|
|
80
135
|
} else if (cachedCredentials && cachedCredentials.accessToken === resolved.accessToken) {
|
|
81
|
-
touchMatrixCredentials(env);
|
|
136
|
+
touchMatrixCredentials(env, accountId);
|
|
82
137
|
}
|
|
83
138
|
return {
|
|
84
139
|
homeserver: resolved.homeserver,
|
|
@@ -91,7 +146,7 @@ export async function resolveMatrixAuth(params?: {
|
|
|
91
146
|
}
|
|
92
147
|
|
|
93
148
|
if (cachedCredentials) {
|
|
94
|
-
touchMatrixCredentials(env);
|
|
149
|
+
touchMatrixCredentials(env, accountId);
|
|
95
150
|
return {
|
|
96
151
|
homeserver: cachedCredentials.homeserver,
|
|
97
152
|
userId: cachedCredentials.userId,
|
|
@@ -149,12 +204,16 @@ export async function resolveMatrixAuth(params?: {
|
|
|
149
204
|
encryption: resolved.encryption,
|
|
150
205
|
};
|
|
151
206
|
|
|
152
|
-
saveMatrixCredentials(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
207
|
+
saveMatrixCredentials(
|
|
208
|
+
{
|
|
209
|
+
homeserver: auth.homeserver,
|
|
210
|
+
userId: auth.userId,
|
|
211
|
+
accessToken: auth.accessToken,
|
|
212
|
+
deviceId: login.device_id,
|
|
213
|
+
},
|
|
214
|
+
env,
|
|
215
|
+
accountId,
|
|
216
|
+
);
|
|
158
217
|
|
|
159
218
|
return auth;
|
|
160
219
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
|
2
2
|
import { LogService } from "@vector-im/matrix-bot-sdk";
|
|
3
|
+
import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
|
3
4
|
import type { CoreConfig } from "../../types.js";
|
|
4
5
|
import type { MatrixAuth } from "./types.js";
|
|
5
6
|
import { resolveMatrixAuth } from "./config.js";
|
|
@@ -13,17 +14,19 @@ type SharedMatrixClientState = {
|
|
|
13
14
|
cryptoReady: boolean;
|
|
14
15
|
};
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
// Support multiple accounts with separate clients
|
|
18
|
+
const sharedClientStates = new Map<string, SharedMatrixClientState>();
|
|
19
|
+
const sharedClientPromises = new Map<string, Promise<SharedMatrixClientState>>();
|
|
20
|
+
const sharedClientStartPromises = new Map<string, Promise<void>>();
|
|
19
21
|
|
|
20
22
|
function buildSharedClientKey(auth: MatrixAuth, accountId?: string | null): string {
|
|
23
|
+
const normalizedAccountId = normalizeAccountId(accountId);
|
|
21
24
|
return [
|
|
22
25
|
auth.homeserver,
|
|
23
26
|
auth.userId,
|
|
24
27
|
auth.accessToken,
|
|
25
28
|
auth.encryption ? "e2ee" : "plain",
|
|
26
|
-
|
|
29
|
+
normalizedAccountId || DEFAULT_ACCOUNT_KEY,
|
|
27
30
|
].join("|");
|
|
28
31
|
}
|
|
29
32
|
|
|
@@ -57,11 +60,13 @@ async function ensureSharedClientStarted(params: {
|
|
|
57
60
|
if (params.state.started) {
|
|
58
61
|
return;
|
|
59
62
|
}
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
const key = params.state.key;
|
|
64
|
+
const existingStartPromise = sharedClientStartPromises.get(key);
|
|
65
|
+
if (existingStartPromise) {
|
|
66
|
+
await existingStartPromise;
|
|
62
67
|
return;
|
|
63
68
|
}
|
|
64
|
-
|
|
69
|
+
const startPromise = (async () => {
|
|
65
70
|
const client = params.state.client;
|
|
66
71
|
|
|
67
72
|
// Initialize crypto if enabled
|
|
@@ -82,10 +87,11 @@ async function ensureSharedClientStarted(params: {
|
|
|
82
87
|
await client.start();
|
|
83
88
|
params.state.started = true;
|
|
84
89
|
})();
|
|
90
|
+
sharedClientStartPromises.set(key, startPromise);
|
|
85
91
|
try {
|
|
86
|
-
await
|
|
92
|
+
await startPromise;
|
|
87
93
|
} finally {
|
|
88
|
-
|
|
94
|
+
sharedClientStartPromises.delete(key);
|
|
89
95
|
}
|
|
90
96
|
}
|
|
91
97
|
|
|
@@ -99,48 +105,51 @@ export async function resolveSharedMatrixClient(
|
|
|
99
105
|
accountId?: string | null;
|
|
100
106
|
} = {},
|
|
101
107
|
): Promise<MatrixClient> {
|
|
102
|
-
const
|
|
103
|
-
const
|
|
108
|
+
const accountId = normalizeAccountId(params.accountId);
|
|
109
|
+
const auth =
|
|
110
|
+
params.auth ?? (await resolveMatrixAuth({ cfg: params.cfg, env: params.env, accountId }));
|
|
111
|
+
const key = buildSharedClientKey(auth, accountId);
|
|
104
112
|
const shouldStart = params.startClient !== false;
|
|
105
113
|
|
|
106
|
-
if
|
|
114
|
+
// Check if we already have a client for this key
|
|
115
|
+
const existingState = sharedClientStates.get(key);
|
|
116
|
+
if (existingState) {
|
|
107
117
|
if (shouldStart) {
|
|
108
118
|
await ensureSharedClientStarted({
|
|
109
|
-
state:
|
|
119
|
+
state: existingState,
|
|
110
120
|
timeoutMs: params.timeoutMs,
|
|
111
121
|
initialSyncLimit: auth.initialSyncLimit,
|
|
112
122
|
encryption: auth.encryption,
|
|
113
123
|
});
|
|
114
124
|
}
|
|
115
|
-
return
|
|
125
|
+
return existingState.client;
|
|
116
126
|
}
|
|
117
127
|
|
|
118
|
-
if
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
return pending.client;
|
|
128
|
+
// Check if there's a pending creation for this key
|
|
129
|
+
const existingPromise = sharedClientPromises.get(key);
|
|
130
|
+
if (existingPromise) {
|
|
131
|
+
const pending = await existingPromise;
|
|
132
|
+
if (shouldStart) {
|
|
133
|
+
await ensureSharedClientStarted({
|
|
134
|
+
state: pending,
|
|
135
|
+
timeoutMs: params.timeoutMs,
|
|
136
|
+
initialSyncLimit: auth.initialSyncLimit,
|
|
137
|
+
encryption: auth.encryption,
|
|
138
|
+
});
|
|
130
139
|
}
|
|
131
|
-
pending.client
|
|
132
|
-
sharedClientState = null;
|
|
133
|
-
sharedClientPromise = null;
|
|
140
|
+
return pending.client;
|
|
134
141
|
}
|
|
135
142
|
|
|
136
|
-
|
|
143
|
+
// Create a new client for this account
|
|
144
|
+
const createPromise = createSharedMatrixClient({
|
|
137
145
|
auth,
|
|
138
146
|
timeoutMs: params.timeoutMs,
|
|
139
|
-
accountId
|
|
147
|
+
accountId,
|
|
140
148
|
});
|
|
149
|
+
sharedClientPromises.set(key, createPromise);
|
|
141
150
|
try {
|
|
142
|
-
const created = await
|
|
143
|
-
|
|
151
|
+
const created = await createPromise;
|
|
152
|
+
sharedClientStates.set(key, created);
|
|
144
153
|
if (shouldStart) {
|
|
145
154
|
await ensureSharedClientStarted({
|
|
146
155
|
state: created,
|
|
@@ -151,7 +160,7 @@ export async function resolveSharedMatrixClient(
|
|
|
151
160
|
}
|
|
152
161
|
return created.client;
|
|
153
162
|
} finally {
|
|
154
|
-
|
|
163
|
+
sharedClientPromises.delete(key);
|
|
155
164
|
}
|
|
156
165
|
}
|
|
157
166
|
|
|
@@ -164,9 +173,29 @@ export async function waitForMatrixSync(_params: {
|
|
|
164
173
|
// This is kept for API compatibility but is essentially a no-op now
|
|
165
174
|
}
|
|
166
175
|
|
|
167
|
-
export function stopSharedClient(): void {
|
|
168
|
-
if (
|
|
169
|
-
|
|
170
|
-
|
|
176
|
+
export function stopSharedClient(key?: string): void {
|
|
177
|
+
if (key) {
|
|
178
|
+
// Stop a specific client
|
|
179
|
+
const state = sharedClientStates.get(key);
|
|
180
|
+
if (state) {
|
|
181
|
+
state.client.stop();
|
|
182
|
+
sharedClientStates.delete(key);
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
// Stop all clients (backward compatible behavior)
|
|
186
|
+
for (const state of sharedClientStates.values()) {
|
|
187
|
+
state.client.stop();
|
|
188
|
+
}
|
|
189
|
+
sharedClientStates.clear();
|
|
171
190
|
}
|
|
172
191
|
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Stop the shared client for a specific account.
|
|
195
|
+
* Use this instead of stopSharedClient() when shutting down a single account
|
|
196
|
+
* to avoid stopping all accounts.
|
|
197
|
+
*/
|
|
198
|
+
export function stopSharedClientForAccount(auth: MatrixAuth, accountId?: string | null): void {
|
|
199
|
+
const key = buildSharedClientKey(auth, normalizeAccountId(accountId));
|
|
200
|
+
stopSharedClient(key);
|
|
201
|
+
}
|
package/src/matrix/client.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
export type { MatrixAuth, MatrixResolvedConfig } from "./client/types.js";
|
|
2
2
|
export { isBunRuntime } from "./client/runtime.js";
|
|
3
|
-
export {
|
|
3
|
+
export {
|
|
4
|
+
resolveMatrixConfig,
|
|
5
|
+
resolveMatrixConfigForAccount,
|
|
6
|
+
resolveMatrixAuth,
|
|
7
|
+
} from "./client/config.js";
|
|
4
8
|
export { createMatrixClient } from "./client/create-client.js";
|
|
5
|
-
export {
|
|
9
|
+
export {
|
|
10
|
+
resolveSharedMatrixClient,
|
|
11
|
+
waitForMatrixSync,
|
|
12
|
+
stopSharedClient,
|
|
13
|
+
stopSharedClientForAccount,
|
|
14
|
+
} from "./client/shared.js";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
|
4
5
|
import { getMatrixRuntime } from "../runtime.js";
|
|
5
6
|
|
|
6
7
|
export type MatrixStoredCredentials = {
|
|
@@ -12,7 +13,15 @@ export type MatrixStoredCredentials = {
|
|
|
12
13
|
lastUsedAt?: string;
|
|
13
14
|
};
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
function credentialsFilename(accountId?: string | null): string {
|
|
17
|
+
const normalized = normalizeAccountId(accountId);
|
|
18
|
+
if (normalized === DEFAULT_ACCOUNT_ID) {
|
|
19
|
+
return "credentials.json";
|
|
20
|
+
}
|
|
21
|
+
// normalizeAccountId produces lowercase [a-z0-9-] strings, already filesystem-safe.
|
|
22
|
+
// Different raw IDs that normalize to the same value are the same logical account.
|
|
23
|
+
return `credentials-${normalized}.json`;
|
|
24
|
+
}
|
|
16
25
|
|
|
17
26
|
export function resolveMatrixCredentialsDir(
|
|
18
27
|
env: NodeJS.ProcessEnv = process.env,
|
|
@@ -22,15 +31,19 @@ export function resolveMatrixCredentialsDir(
|
|
|
22
31
|
return path.join(resolvedStateDir, "credentials", "matrix");
|
|
23
32
|
}
|
|
24
33
|
|
|
25
|
-
export function resolveMatrixCredentialsPath(
|
|
34
|
+
export function resolveMatrixCredentialsPath(
|
|
35
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
36
|
+
accountId?: string | null,
|
|
37
|
+
): string {
|
|
26
38
|
const dir = resolveMatrixCredentialsDir(env);
|
|
27
|
-
return path.join(dir,
|
|
39
|
+
return path.join(dir, credentialsFilename(accountId));
|
|
28
40
|
}
|
|
29
41
|
|
|
30
42
|
export function loadMatrixCredentials(
|
|
31
43
|
env: NodeJS.ProcessEnv = process.env,
|
|
44
|
+
accountId?: string | null,
|
|
32
45
|
): MatrixStoredCredentials | null {
|
|
33
|
-
const credPath = resolveMatrixCredentialsPath(env);
|
|
46
|
+
const credPath = resolveMatrixCredentialsPath(env, accountId);
|
|
34
47
|
try {
|
|
35
48
|
if (!fs.existsSync(credPath)) {
|
|
36
49
|
return null;
|
|
@@ -53,13 +66,14 @@ export function loadMatrixCredentials(
|
|
|
53
66
|
export function saveMatrixCredentials(
|
|
54
67
|
credentials: Omit<MatrixStoredCredentials, "createdAt" | "lastUsedAt">,
|
|
55
68
|
env: NodeJS.ProcessEnv = process.env,
|
|
69
|
+
accountId?: string | null,
|
|
56
70
|
): void {
|
|
57
71
|
const dir = resolveMatrixCredentialsDir(env);
|
|
58
72
|
fs.mkdirSync(dir, { recursive: true });
|
|
59
73
|
|
|
60
|
-
const credPath = resolveMatrixCredentialsPath(env);
|
|
74
|
+
const credPath = resolveMatrixCredentialsPath(env, accountId);
|
|
61
75
|
|
|
62
|
-
const existing = loadMatrixCredentials(env);
|
|
76
|
+
const existing = loadMatrixCredentials(env, accountId);
|
|
63
77
|
const now = new Date().toISOString();
|
|
64
78
|
|
|
65
79
|
const toSave: MatrixStoredCredentials = {
|
|
@@ -71,19 +85,25 @@ export function saveMatrixCredentials(
|
|
|
71
85
|
fs.writeFileSync(credPath, JSON.stringify(toSave, null, 2), "utf-8");
|
|
72
86
|
}
|
|
73
87
|
|
|
74
|
-
export function touchMatrixCredentials(
|
|
75
|
-
|
|
88
|
+
export function touchMatrixCredentials(
|
|
89
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
90
|
+
accountId?: string | null,
|
|
91
|
+
): void {
|
|
92
|
+
const existing = loadMatrixCredentials(env, accountId);
|
|
76
93
|
if (!existing) {
|
|
77
94
|
return;
|
|
78
95
|
}
|
|
79
96
|
|
|
80
97
|
existing.lastUsedAt = new Date().toISOString();
|
|
81
|
-
const credPath = resolveMatrixCredentialsPath(env);
|
|
98
|
+
const credPath = resolveMatrixCredentialsPath(env, accountId);
|
|
82
99
|
fs.writeFileSync(credPath, JSON.stringify(existing, null, 2), "utf-8");
|
|
83
100
|
}
|
|
84
101
|
|
|
85
|
-
export function clearMatrixCredentials(
|
|
86
|
-
|
|
102
|
+
export function clearMatrixCredentials(
|
|
103
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
104
|
+
accountId?: string | null,
|
|
105
|
+
): void {
|
|
106
|
+
const credPath = resolveMatrixCredentialsPath(env, accountId);
|
|
87
107
|
try {
|
|
88
108
|
if (fs.existsSync(credPath)) {
|
|
89
109
|
fs.unlinkSync(credPath);
|
|
@@ -68,6 +68,7 @@ export type MatrixMonitorHandlerParams = {
|
|
|
68
68
|
roomId: string,
|
|
69
69
|
) => Promise<{ name?: string; canonicalAlias?: string; altAliases: string[] }>;
|
|
70
70
|
getMemberDisplayName: (roomId: string, userId: string) => Promise<string>;
|
|
71
|
+
accountId?: string | null;
|
|
71
72
|
};
|
|
72
73
|
|
|
73
74
|
export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParams) {
|
|
@@ -93,6 +94,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
|
|
93
94
|
directTracker,
|
|
94
95
|
getRoomInfo,
|
|
95
96
|
getMemberDisplayName,
|
|
97
|
+
accountId,
|
|
96
98
|
} = params;
|
|
97
99
|
|
|
98
100
|
return async (roomId: string, event: MatrixRawEvent) => {
|
|
@@ -435,6 +437,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
|
|
435
437
|
const baseRoute = core.channel.routing.resolveAgentRoute({
|
|
436
438
|
cfg,
|
|
437
439
|
channel: "matrix",
|
|
440
|
+
accountId,
|
|
438
441
|
peer: {
|
|
439
442
|
kind: isDirectMessage ? "direct" : "channel",
|
|
440
443
|
id: isDirectMessage ? senderId : roomId,
|
|
@@ -3,12 +3,13 @@ import { mergeAllowlist, summarizeMapping, type RuntimeEnv } from "openclaw/plug
|
|
|
3
3
|
import type { CoreConfig, ReplyToMode } from "../../types.js";
|
|
4
4
|
import { resolveMatrixTargets } from "../../resolve-targets.js";
|
|
5
5
|
import { getMatrixRuntime } from "../../runtime.js";
|
|
6
|
+
import { resolveMatrixAccount } from "../accounts.js";
|
|
6
7
|
import { setActiveMatrixClient } from "../active-client.js";
|
|
7
8
|
import {
|
|
8
9
|
isBunRuntime,
|
|
9
10
|
resolveMatrixAuth,
|
|
10
11
|
resolveSharedMatrixClient,
|
|
11
|
-
|
|
12
|
+
stopSharedClientForAccount,
|
|
12
13
|
} from "../client.js";
|
|
13
14
|
import { normalizeMatrixUserId } from "./allowlist.js";
|
|
14
15
|
import { registerMatrixAutoJoin } from "./auto-join.js";
|
|
@@ -121,10 +122,14 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
|
|
121
122
|
return allowList.map(String);
|
|
122
123
|
};
|
|
123
124
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
// Resolve account-specific config for multi-account support
|
|
126
|
+
const account = resolveMatrixAccount({ cfg, accountId: opts.accountId });
|
|
127
|
+
const accountConfig = account.config;
|
|
128
|
+
|
|
129
|
+
const allowlistOnly = accountConfig.allowlistOnly === true;
|
|
130
|
+
let allowFrom: string[] = (accountConfig.dm?.allowFrom ?? []).map(String);
|
|
131
|
+
let groupAllowFrom: string[] = (accountConfig.groupAllowFrom ?? []).map(String);
|
|
132
|
+
let roomsConfig = accountConfig.groups ?? accountConfig.rooms;
|
|
128
133
|
|
|
129
134
|
allowFrom = await resolveUserAllowlist("matrix dm allowlist", allowFrom);
|
|
130
135
|
groupAllowFrom = await resolveUserAllowlist("matrix group allowlist", groupAllowFrom);
|
|
@@ -213,13 +218,13 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
|
|
213
218
|
...cfg.channels?.matrix?.dm,
|
|
214
219
|
allowFrom,
|
|
215
220
|
},
|
|
216
|
-
|
|
221
|
+
groupAllowFrom,
|
|
217
222
|
...(roomsConfig ? { groups: roomsConfig } : {}),
|
|
218
223
|
},
|
|
219
224
|
},
|
|
220
225
|
};
|
|
221
226
|
|
|
222
|
-
const auth = await resolveMatrixAuth({ cfg });
|
|
227
|
+
const auth = await resolveMatrixAuth({ cfg, accountId: opts.accountId });
|
|
223
228
|
const resolvedInitialSyncLimit =
|
|
224
229
|
typeof opts.initialSyncLimit === "number"
|
|
225
230
|
? Math.max(0, Math.floor(opts.initialSyncLimit))
|
|
@@ -234,20 +239,20 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
|
|
234
239
|
startClient: false,
|
|
235
240
|
accountId: opts.accountId,
|
|
236
241
|
});
|
|
237
|
-
setActiveMatrixClient(client);
|
|
242
|
+
setActiveMatrixClient(client, opts.accountId);
|
|
238
243
|
|
|
239
244
|
const mentionRegexes = core.channel.mentions.buildMentionRegexes(cfg);
|
|
240
245
|
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
|
|
241
|
-
const groupPolicyRaw =
|
|
246
|
+
const groupPolicyRaw = accountConfig.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
|
|
242
247
|
const groupPolicy = allowlistOnly && groupPolicyRaw === "open" ? "allowlist" : groupPolicyRaw;
|
|
243
|
-
const replyToMode = opts.replyToMode ??
|
|
244
|
-
const threadReplies =
|
|
245
|
-
const dmConfig =
|
|
248
|
+
const replyToMode = opts.replyToMode ?? accountConfig.replyToMode ?? "off";
|
|
249
|
+
const threadReplies = accountConfig.threadReplies ?? "inbound";
|
|
250
|
+
const dmConfig = accountConfig.dm;
|
|
246
251
|
const dmEnabled = dmConfig?.enabled ?? true;
|
|
247
252
|
const dmPolicyRaw = dmConfig?.policy ?? "pairing";
|
|
248
253
|
const dmPolicy = allowlistOnly && dmPolicyRaw !== "disabled" ? "allowlist" : dmPolicyRaw;
|
|
249
254
|
const textLimit = core.channel.text.resolveTextChunkLimit(cfg, "matrix");
|
|
250
|
-
const mediaMaxMb = opts.mediaMaxMb ??
|
|
255
|
+
const mediaMaxMb = opts.mediaMaxMb ?? accountConfig.mediaMaxMb ?? DEFAULT_MEDIA_MAX_MB;
|
|
251
256
|
const mediaMaxBytes = Math.max(1, mediaMaxMb) * 1024 * 1024;
|
|
252
257
|
const startupMs = Date.now();
|
|
253
258
|
const startupGraceMs = 0;
|
|
@@ -279,6 +284,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
|
|
279
284
|
directTracker,
|
|
280
285
|
getRoomInfo,
|
|
281
286
|
getMemberDisplayName,
|
|
287
|
+
accountId: opts.accountId,
|
|
282
288
|
});
|
|
283
289
|
|
|
284
290
|
registerMatrixMonitorEvents({
|
|
@@ -324,9 +330,9 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
|
|
324
330
|
const onAbort = () => {
|
|
325
331
|
try {
|
|
326
332
|
logVerboseMessage("matrix: stopping client");
|
|
327
|
-
|
|
333
|
+
stopSharedClientForAccount(auth, opts.accountId);
|
|
328
334
|
} finally {
|
|
329
|
-
setActiveMatrixClient(null);
|
|
335
|
+
setActiveMatrixClient(null, opts.accountId);
|
|
330
336
|
resolve();
|
|
331
337
|
}
|
|
332
338
|
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
|
2
|
+
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
|
2
3
|
import type { CoreConfig } from "../../types.js";
|
|
3
4
|
import { getMatrixRuntime } from "../../runtime.js";
|
|
4
|
-
import { getActiveMatrixClient } from "../active-client.js";
|
|
5
|
+
import { getActiveMatrixClient, getAnyActiveMatrixClient } from "../active-client.js";
|
|
5
6
|
import {
|
|
6
7
|
createMatrixClient,
|
|
7
8
|
isBunRuntime,
|
|
@@ -17,8 +18,35 @@ export function ensureNodeRuntime() {
|
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
/** Look up account config with case-insensitive key fallback. */
|
|
22
|
+
function findAccountConfig(
|
|
23
|
+
accounts: Record<string, unknown> | undefined,
|
|
24
|
+
accountId: string,
|
|
25
|
+
): Record<string, unknown> | undefined {
|
|
26
|
+
if (!accounts) return undefined;
|
|
27
|
+
const normalized = normalizeAccountId(accountId);
|
|
28
|
+
// Direct lookup first
|
|
29
|
+
if (accounts[normalized]) return accounts[normalized] as Record<string, unknown>;
|
|
30
|
+
// Case-insensitive fallback
|
|
31
|
+
for (const key of Object.keys(accounts)) {
|
|
32
|
+
if (normalizeAccountId(key) === normalized) {
|
|
33
|
+
return accounts[key] as Record<string, unknown>;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function resolveMediaMaxBytes(accountId?: string): number | undefined {
|
|
21
40
|
const cfg = getCore().config.loadConfig() as CoreConfig;
|
|
41
|
+
// Check account-specific config first (case-insensitive key matching)
|
|
42
|
+
const accountConfig = findAccountConfig(
|
|
43
|
+
cfg.channels?.matrix?.accounts as Record<string, unknown> | undefined,
|
|
44
|
+
accountId ?? "",
|
|
45
|
+
);
|
|
46
|
+
if (typeof accountConfig?.mediaMaxMb === "number") {
|
|
47
|
+
return (accountConfig.mediaMaxMb as number) * 1024 * 1024;
|
|
48
|
+
}
|
|
49
|
+
// Fall back to top-level config
|
|
22
50
|
if (typeof cfg.channels?.matrix?.mediaMaxMb === "number") {
|
|
23
51
|
return cfg.channels.matrix.mediaMaxMb * 1024 * 1024;
|
|
24
52
|
}
|
|
@@ -28,29 +56,49 @@ export function resolveMediaMaxBytes(): number | undefined {
|
|
|
28
56
|
export async function resolveMatrixClient(opts: {
|
|
29
57
|
client?: MatrixClient;
|
|
30
58
|
timeoutMs?: number;
|
|
59
|
+
accountId?: string;
|
|
31
60
|
}): Promise<{ client: MatrixClient; stopOnDone: boolean }> {
|
|
32
61
|
ensureNodeRuntime();
|
|
33
62
|
if (opts.client) {
|
|
34
63
|
return { client: opts.client, stopOnDone: false };
|
|
35
64
|
}
|
|
36
|
-
const
|
|
65
|
+
const accountId =
|
|
66
|
+
typeof opts.accountId === "string" && opts.accountId.trim().length > 0
|
|
67
|
+
? normalizeAccountId(opts.accountId)
|
|
68
|
+
: undefined;
|
|
69
|
+
// Try to get the client for the specific account
|
|
70
|
+
const active = getActiveMatrixClient(accountId);
|
|
37
71
|
if (active) {
|
|
38
72
|
return { client: active, stopOnDone: false };
|
|
39
73
|
}
|
|
74
|
+
// When no account is specified, try the default account first; only fall back to
|
|
75
|
+
// any active client as a last resort (prevents sending from an arbitrary account).
|
|
76
|
+
if (!accountId) {
|
|
77
|
+
const defaultClient = getActiveMatrixClient(DEFAULT_ACCOUNT_ID);
|
|
78
|
+
if (defaultClient) {
|
|
79
|
+
return { client: defaultClient, stopOnDone: false };
|
|
80
|
+
}
|
|
81
|
+
const anyActive = getAnyActiveMatrixClient();
|
|
82
|
+
if (anyActive) {
|
|
83
|
+
return { client: anyActive, stopOnDone: false };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
40
86
|
const shouldShareClient = Boolean(process.env.OPENCLAW_GATEWAY_PORT);
|
|
41
87
|
if (shouldShareClient) {
|
|
42
88
|
const client = await resolveSharedMatrixClient({
|
|
43
89
|
timeoutMs: opts.timeoutMs,
|
|
90
|
+
accountId,
|
|
44
91
|
});
|
|
45
92
|
return { client, stopOnDone: false };
|
|
46
93
|
}
|
|
47
|
-
const auth = await resolveMatrixAuth();
|
|
94
|
+
const auth = await resolveMatrixAuth({ accountId });
|
|
48
95
|
const client = await createMatrixClient({
|
|
49
96
|
homeserver: auth.homeserver,
|
|
50
97
|
userId: auth.userId,
|
|
51
98
|
accessToken: auth.accessToken,
|
|
52
99
|
encryption: auth.encryption,
|
|
53
100
|
localTimeoutMs: opts.timeoutMs,
|
|
101
|
+
accountId,
|
|
54
102
|
});
|
|
55
103
|
if (auth.encryption && client.crypto) {
|
|
56
104
|
try {
|