diff --git a/extensions/matrix/src/matrix/client/config.ts b/extensions/matrix/src/matrix/client/config.ts index cb075c10a8..5265e7680f 100644 --- a/extensions/matrix/src/matrix/client/config.ts +++ b/extensions/matrix/src/matrix/client/config.ts @@ -9,6 +9,29 @@ function clean(value?: string): string { return value?.trim() ?? ""; } +/** Shallow-merge known nested config sub-objects so partial overrides inherit base values. */ +function deepMergeConfig( + base: Record, + override: Record, +): Record { + const merged = { ...base, ...override }; + // Merge known nested objects (dm, actions) so partial overrides keep base fields + for (const key of ["dm", "actions"] as const) { + if ( + typeof base[key] === "object" && + base[key] !== null && + typeof override[key] === "object" && + override[key] !== null + ) { + merged[key] = { + ...(base[key] as Record), + ...(override[key] as Record), + }; + } + } + return merged; +} + /** * Resolve Matrix config for a specific account, with fallback to top-level config. * This supports both multi-account (channels.matrix.accounts.*) and @@ -34,10 +57,10 @@ export function resolveMatrixConfigForAccount( } } - // Merge: account-specific values override top-level values - // For DEFAULT_ACCOUNT_ID with no accounts, use top-level directly + // Deep merge: account-specific values override top-level values, preserving + // nested object inheritance (dm, actions, groups) so partial overrides work. const useAccountConfig = accountConfig !== undefined; - const matrix = useAccountConfig ? { ...matrixBase, ...accountConfig } : matrixBase; + const matrix = useAccountConfig ? deepMergeConfig(matrixBase, accountConfig) : matrixBase; const homeserver = clean(matrix.homeserver) || clean(env.MATRIX_HOMESERVER); const userId = clean(matrix.userId) || clean(env.MATRIX_USER_ID); diff --git a/extensions/matrix/src/matrix/credentials.ts b/extensions/matrix/src/matrix/credentials.ts index 9fa29c5118..4e1cf84cf0 100644 --- a/extensions/matrix/src/matrix/credentials.ts +++ b/extensions/matrix/src/matrix/credentials.ts @@ -18,9 +18,9 @@ function credentialsFilename(accountId?: string | null): string { if (normalized === DEFAULT_ACCOUNT_ID) { return "credentials.json"; } - // Sanitize accountId for use in filename - const safe = normalized.replace(/[^a-zA-Z0-9_-]/g, "_"); - return `credentials-${safe}.json`; + // normalizeAccountId produces lowercase [a-z0-9-] strings, already filesystem-safe. + // Different raw IDs that normalize to the same value are the same logical account. + return `credentials-${normalized}.json`; } export function resolveMatrixCredentialsDir( diff --git a/extensions/matrix/src/matrix/send/client.ts b/extensions/matrix/src/matrix/send/client.ts index 8bbc364d22..e37f557c6d 100644 --- a/extensions/matrix/src/matrix/send/client.ts +++ b/extensions/matrix/src/matrix/send/client.ts @@ -1,5 +1,5 @@ import type { MatrixClient } from "@vector-im/matrix-bot-sdk"; -import { normalizeAccountId } from "openclaw/plugin-sdk"; +import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk"; import type { CoreConfig } from "../../types.js"; import { getMatrixRuntime } from "../../runtime.js"; import { getActiveMatrixClient, getAnyActiveMatrixClient } from "../active-client.js"; @@ -67,8 +67,13 @@ export async function resolveMatrixClient(opts: { if (active) { return { client: active, stopOnDone: false }; } - // Only fall back to any active client when no specific account is requested + // When no account is specified, try the default account first; only fall back to + // any active client as a last resort (prevents sending from an arbitrary account). if (!opts.accountId) { + const defaultClient = getActiveMatrixClient(DEFAULT_ACCOUNT_ID); + if (defaultClient) { + return { client: defaultClient, stopOnDone: false }; + } const anyActive = getAnyActiveMatrixClient(); if (anyActive) { return { client: anyActive, stopOnDone: false };