diff --git a/src/actions.ts b/src/actions.ts index 113680a3b..ce1059233 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -45,6 +45,9 @@ export const matrixMessageActions: ChannelMessageActionAdapter = { }, handleAction: async (ctx: ChannelMessageActionContext) => { const { action, params, cfg } = ctx; + // Get accountId from context for multi-account support + const accountId = (ctx as { accountId?: string }).accountId ?? undefined; + const resolveRoomId = () => readStringParam(params, "roomId") ?? readStringParam(params, "channelId") ?? @@ -67,6 +70,7 @@ export const matrixMessageActions: ChannelMessageActionAdapter = { mediaUrl: mediaUrl ?? undefined, replyToId: replyTo ?? undefined, threadId: threadId ?? undefined, + accountId, }, cfg, ); @@ -83,6 +87,7 @@ export const matrixMessageActions: ChannelMessageActionAdapter = { messageId, emoji, remove, + accountId, }, cfg, ); @@ -97,6 +102,7 @@ export const matrixMessageActions: ChannelMessageActionAdapter = { roomId: resolveRoomId(), messageId, limit, + accountId, }, cfg, ); @@ -111,6 +117,7 @@ export const matrixMessageActions: ChannelMessageActionAdapter = { limit, before: readStringParam(params, "before"), after: readStringParam(params, "after"), + accountId, }, cfg, ); @@ -125,6 +132,7 @@ export const matrixMessageActions: ChannelMessageActionAdapter = { roomId: resolveRoomId(), messageId, content, + accountId, }, cfg, ); @@ -137,6 +145,7 @@ export const matrixMessageActions: ChannelMessageActionAdapter = { action: "deleteMessage", roomId: resolveRoomId(), messageId, + accountId, }, cfg, ); @@ -153,6 +162,7 @@ export const matrixMessageActions: ChannelMessageActionAdapter = { action === "pin" ? "pinMessage" : action === "unpin" ? "unpinMessage" : "listPins", roomId: resolveRoomId(), messageId, + accountId, }, cfg, ); @@ -165,6 +175,7 @@ export const matrixMessageActions: ChannelMessageActionAdapter = { action: "memberInfo", userId, roomId: readStringParam(params, "roomId") ?? readStringParam(params, "channelId"), + accountId, }, cfg, ); @@ -175,6 +186,7 @@ export const matrixMessageActions: ChannelMessageActionAdapter = { { action: "channelInfo", roomId: resolveRoomId(), + accountId, }, cfg, ); diff --git a/src/matrix/actions/client.ts b/src/matrix/actions/client.ts index efbd6d62b..88705fd04 100644 --- a/src/matrix/actions/client.ts +++ b/src/matrix/actions/client.ts @@ -20,18 +20,23 @@ export async function resolveActionClient( ): Promise { ensureNodeRuntime(); if (opts.client) return { client: opts.client, stopOnDone: false }; - const active = getActiveMatrixClient(); + + // Try to get the active client for the specified account + const active = getActiveMatrixClient(opts.accountId); if (active) return { client: active, stopOnDone: false }; + const shouldShareClient = Boolean(process.env.OPENCLAW_GATEWAY_PORT); if (shouldShareClient) { const client = await resolveSharedMatrixClient({ cfg: getMatrixRuntime().config.loadConfig() as CoreConfig, timeoutMs: opts.timeoutMs, + accountId: opts.accountId, }); return { client, stopOnDone: false }; } const auth = await resolveMatrixAuth({ cfg: getMatrixRuntime().config.loadConfig() as CoreConfig, + accountId: opts.accountId ?? undefined, }); const client = await createMatrixClient({ homeserver: auth.homeserver, @@ -39,6 +44,7 @@ export async function resolveActionClient( accessToken: auth.accessToken, encryption: auth.encryption, localTimeoutMs: opts.timeoutMs, + accountId: opts.accountId ?? undefined, }); if (auth.encryption && client.crypto) { try { diff --git a/src/matrix/actions/messages.ts b/src/matrix/actions/messages.ts index 60f69e219..2bc32e849 100644 --- a/src/matrix/actions/messages.ts +++ b/src/matrix/actions/messages.ts @@ -26,6 +26,7 @@ export async function sendMatrixMessage( threadId: opts.threadId, client: opts.client, timeoutMs: opts.timeoutMs, + accountId: opts.accountId, }); } diff --git a/src/matrix/actions/types.ts b/src/matrix/actions/types.ts index 75fddbd9c..96694f4c7 100644 --- a/src/matrix/actions/types.ts +++ b/src/matrix/actions/types.ts @@ -57,6 +57,7 @@ export type MatrixRawEvent = { export type MatrixActionClientOpts = { client?: MatrixClient; timeoutMs?: number; + accountId?: string | null; }; export type MatrixMessageSummary = { diff --git a/src/matrix/active-client.ts b/src/matrix/active-client.ts index 5ff540926..eb2c41ca8 100644 --- a/src/matrix/active-client.ts +++ b/src/matrix/active-client.ts @@ -1,11 +1,34 @@ import type { MatrixClient } from "@vector-im/matrix-bot-sdk"; -let activeClient: MatrixClient | null = null; +const DEFAULT_ACCOUNT_KEY = "default"; -export function setActiveMatrixClient(client: MatrixClient | null): void { - activeClient = client; +// Multi-account: Map of accountId -> client +const activeClients = new Map(); + +function normalizeAccountKey(accountId?: string | null): string { + return accountId?.trim().toLowerCase() || DEFAULT_ACCOUNT_KEY; } -export function getActiveMatrixClient(): MatrixClient | null { - return activeClient; +export function setActiveMatrixClient(client: MatrixClient | null, accountId?: string | null): void { + const key = normalizeAccountKey(accountId); + if (client) { + activeClients.set(key, client); + } else { + activeClients.delete(key); + } +} + +export function getActiveMatrixClient(accountId?: string | null): MatrixClient | null { + const key = normalizeAccountKey(accountId); + const client = activeClients.get(key); + if (client) return client; + // Fallback: if specific account not found, try default + if (key !== DEFAULT_ACCOUNT_KEY) { + return activeClients.get(DEFAULT_ACCOUNT_KEY) ?? null; + } + return null; +} + +export function listActiveMatrixClients(): Array<{ accountId: string; client: MatrixClient }> { + return Array.from(activeClients.entries()).map(([accountId, client]) => ({ accountId, client })); } diff --git a/src/matrix/monitor/events.ts b/src/matrix/monitor/events.ts index d8c273e10..a8d823386 100644 --- a/src/matrix/monitor/events.ts +++ b/src/matrix/monitor/events.ts @@ -38,6 +38,8 @@ export function registerMatrixMonitorEvents(params: { const eventId = event?.event_id ?? "unknown"; const eventType = event?.type ?? "unknown"; logVerboseMessage(`matrix: decrypted event room=${roomId} type=${eventType} id=${eventId}`); + // Process decrypted messages through the normal handler + onRoomMessage(roomId, event); }); client.on( diff --git a/src/matrix/monitor/handler.ts b/src/matrix/monitor/handler.ts index 1753da233..acaff70bf 100644 --- a/src/matrix/monitor/handler.ts +++ b/src/matrix/monitor/handler.ts @@ -328,6 +328,11 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam ? content.file : undefined; const mediaUrl = contentUrl ?? contentFile?.url; + + // DEBUG: Log media detection + const msgtype = "msgtype" in content ? content.msgtype : undefined; + logVerboseMessage(`matrix: content check msgtype=${msgtype} contentUrl=${contentUrl ?? "none"} mediaUrl=${mediaUrl ?? "none"} rawBody="${rawBody.slice(0,50)}"`); + if (!rawBody && !mediaUrl) { return; } @@ -340,6 +345,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam const contentSize = typeof contentInfo?.size === "number" ? contentInfo.size : undefined; if (mediaUrl?.startsWith("mxc://")) { + logVerboseMessage(`matrix: attempting media download url=${mediaUrl} size=${contentSize ?? "unknown"} maxBytes=${mediaMaxBytes}`); try { media = await downloadMatrixMedia({ client, @@ -349,9 +355,12 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam maxBytes: mediaMaxBytes, file: contentFile, }); + logVerboseMessage(`matrix: media download success path=${media?.path ?? "none"}`); } catch (err) { logVerboseMessage(`matrix: media download failed: ${String(err)}`); } + } else if (mediaUrl) { + logVerboseMessage(`matrix: skipping non-mxc media url=${mediaUrl}`); } const bodyText = rawBody || media?.placeholder || ""; @@ -544,7 +553,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam }), ); if (shouldAckReaction() && messageId) { - reactMatrixMessage(roomId, messageId, ackReaction, client).catch((err) => { + reactMatrixMessage(roomId, messageId, ackReaction, { client }).catch((err) => { logVerboseMessage(`matrix react failed for room ${roomId}: ${String(err)}`); }); } diff --git a/src/matrix/monitor/index.ts b/src/matrix/monitor/index.ts index f02293fa2..5f0dca70f 100644 --- a/src/matrix/monitor/index.ts +++ b/src/matrix/monitor/index.ts @@ -178,7 +178,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi startClient: false, accountId: opts.accountId, }); - setActiveMatrixClient(client); + setActiveMatrixClient(client, opts.accountId); const mentionRegexes = core.channel.mentions.buildMentionRegexes(cfg); const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; @@ -267,7 +267,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi logVerboseMessage(`matrix: stopping client for account ${opts.accountId ?? "default"}`); stopSharedClient(opts.accountId); } finally { - setActiveMatrixClient(null); + setActiveMatrixClient(null, opts.accountId); resolve(); } }; diff --git a/src/matrix/send.ts b/src/matrix/send.ts index 8f853ff2a..55c2293e8 100644 --- a/src/matrix/send.ts +++ b/src/matrix/send.ts @@ -46,6 +46,7 @@ export async function sendMessageMatrix( const { client, stopOnDone } = await resolveMatrixClient({ client: opts.client, timeoutMs: opts.timeoutMs, + accountId: opts.accountId, }); try { const roomId = await resolveMatrixRoomId(client, to); @@ -229,13 +230,14 @@ export async function reactMatrixMessage( roomId: string, messageId: string, emoji: string, - client?: MatrixClient, + opts: { client?: MatrixClient; accountId?: string | null } = {}, ): Promise { if (!emoji.trim()) { throw new Error("Matrix reaction requires an emoji"); } const { client: resolved, stopOnDone } = await resolveMatrixClient({ - client, + client: opts.client, + accountId: opts.accountId, }); try { const resolvedRoom = await resolveMatrixRoomId(resolved, roomId); diff --git a/src/matrix/send/client.ts b/src/matrix/send/client.ts index 296a790fe..0cc745608 100644 --- a/src/matrix/send/client.ts +++ b/src/matrix/send/client.ts @@ -29,25 +29,31 @@ export function resolveMediaMaxBytes(): number | undefined { export async function resolveMatrixClient(opts: { client?: MatrixClient; timeoutMs?: number; + accountId?: string | null; }): Promise<{ client: MatrixClient; stopOnDone: boolean }> { ensureNodeRuntime(); if (opts.client) return { client: opts.client, stopOnDone: false }; - const active = getActiveMatrixClient(); + + // Try to get the active client for the specified account + const active = getActiveMatrixClient(opts.accountId); if (active) return { client: active, stopOnDone: false }; + const shouldShareClient = Boolean(process.env.OPENCLAW_GATEWAY_PORT); if (shouldShareClient) { const client = await resolveSharedMatrixClient({ timeoutMs: opts.timeoutMs, + accountId: opts.accountId, }); return { client, stopOnDone: false }; } - const auth = await resolveMatrixAuth(); + const auth = await resolveMatrixAuth({ accountId: opts.accountId ?? undefined }); const client = await createMatrixClient({ homeserver: auth.homeserver, userId: auth.userId, accessToken: auth.accessToken, encryption: auth.encryption, localTimeoutMs: opts.timeoutMs, + accountId: opts.accountId ?? undefined, }); if (auth.encryption && client.crypto) { try { diff --git a/src/tool-actions.ts b/src/tool-actions.ts index cdc704c42..eee9a6e45 100644 --- a/src/tool-actions.ts +++ b/src/tool-actions.ts @@ -39,6 +39,7 @@ export async function handleMatrixAction( cfg: CoreConfig, ): Promise> { const action = readStringParam(params, "action", { required: true }); + const accountId = readStringParam(params, "accountId") ?? undefined; const isActionEnabled = createActionGate(cfg.channels?.matrix?.actions); if (reactionActions.has(action)) { @@ -54,13 +55,14 @@ export async function handleMatrixAction( if (remove || isEmpty) { const result = await removeMatrixReactions(roomId, messageId, { emoji: remove ? emoji : undefined, + accountId, }); return jsonResult({ ok: true, removed: result.removed }); } - await reactMatrixMessage(roomId, messageId, emoji); + await reactMatrixMessage(roomId, messageId, emoji, { accountId }); return jsonResult({ ok: true, added: emoji }); } - const reactions = await listMatrixReactions(roomId, messageId); + const reactions = await listMatrixReactions(roomId, messageId, { accountId }); return jsonResult({ ok: true, reactions }); } @@ -82,6 +84,7 @@ export async function handleMatrixAction( mediaUrl: mediaUrl ?? undefined, replyToId: replyToId ?? undefined, threadId: threadId ?? undefined, + accountId, }); return jsonResult({ ok: true, result }); } @@ -89,14 +92,14 @@ export async function handleMatrixAction( const roomId = readRoomId(params); const messageId = readStringParam(params, "messageId", { required: true }); const content = readStringParam(params, "content", { required: true }); - const result = await editMatrixMessage(roomId, messageId, content); + const result = await editMatrixMessage(roomId, messageId, content, { accountId }); return jsonResult({ ok: true, result }); } case "deleteMessage": { const roomId = readRoomId(params); const messageId = readStringParam(params, "messageId", { required: true }); const reason = readStringParam(params, "reason"); - await deleteMatrixMessage(roomId, messageId, { reason: reason ?? undefined }); + await deleteMatrixMessage(roomId, messageId, { reason: reason ?? undefined, accountId }); return jsonResult({ ok: true, deleted: true }); } case "readMessages": { @@ -108,6 +111,7 @@ export async function handleMatrixAction( limit: limit ?? undefined, before: before ?? undefined, after: after ?? undefined, + accountId, }); return jsonResult({ ok: true, ...result }); } @@ -123,15 +127,15 @@ export async function handleMatrixAction( const roomId = readRoomId(params); if (action === "pinMessage") { const messageId = readStringParam(params, "messageId", { required: true }); - const result = await pinMatrixMessage(roomId, messageId); + const result = await pinMatrixMessage(roomId, messageId, { accountId }); return jsonResult({ ok: true, pinned: result.pinned }); } if (action === "unpinMessage") { const messageId = readStringParam(params, "messageId", { required: true }); - const result = await unpinMatrixMessage(roomId, messageId); + const result = await unpinMatrixMessage(roomId, messageId, { accountId }); return jsonResult({ ok: true, pinned: result.pinned }); } - const result = await listMatrixPins(roomId); + const result = await listMatrixPins(roomId, { accountId }); return jsonResult({ ok: true, pinned: result.pinned, events: result.events }); } @@ -143,6 +147,7 @@ export async function handleMatrixAction( const roomId = readStringParam(params, "roomId") ?? readStringParam(params, "channelId"); const result = await getMatrixMemberInfo(userId, { roomId: roomId ?? undefined, + accountId, }); return jsonResult({ ok: true, member: result }); } @@ -152,7 +157,7 @@ export async function handleMatrixAction( throw new Error("Matrix room info is disabled."); } const roomId = readRoomId(params); - const result = await getMatrixRoomInfo(roomId); + const result = await getMatrixRoomInfo(roomId, { accountId }); return jsonResult({ ok: true, room: result }); }