mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-10 16:51:13 +00:00
327 lines
9.5 KiB
TypeScript
327 lines
9.5 KiB
TypeScript
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
|
import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
|
|
import {
|
|
findMatrixAccountEntry,
|
|
requiresExplicitMatrixDefaultAccount,
|
|
resolveConfiguredMatrixAccountIds,
|
|
resolveMatrixChannelConfig,
|
|
resolveMatrixDefaultOrOnlyAccountId,
|
|
} from "./account-selection.js";
|
|
import { getMatrixScopedEnvVarNames } from "./env-vars.js";
|
|
import { resolveMatrixAccountStorageRoot, resolveMatrixCredentialsPath } from "./storage-paths.js";
|
|
|
|
export type MatrixStoredCredentials = {
|
|
homeserver: string;
|
|
userId: string;
|
|
accessToken: string;
|
|
deviceId?: string;
|
|
};
|
|
|
|
export type MatrixMigrationAccountTarget = {
|
|
accountId: string;
|
|
homeserver: string;
|
|
userId: string;
|
|
accessToken: string;
|
|
rootDir: string;
|
|
storedDeviceId: string | null;
|
|
};
|
|
|
|
export type MatrixLegacyFlatStoreTarget = MatrixMigrationAccountTarget & {
|
|
selectionNote?: string;
|
|
};
|
|
|
|
type MatrixLegacyFlatStoreKind = "state" | "encrypted state";
|
|
|
|
type MatrixResolvedStringField =
|
|
| "homeserver"
|
|
| "userId"
|
|
| "accessToken"
|
|
| "password"
|
|
| "deviceId"
|
|
| "deviceName";
|
|
|
|
type MatrixResolvedStringValues = Record<MatrixResolvedStringField, string>;
|
|
|
|
type MatrixStringSourceMap = Partial<Record<MatrixResolvedStringField, string>>;
|
|
|
|
const MATRIX_DEFAULT_ACCOUNT_AUTH_ONLY_FIELDS = new Set<MatrixResolvedStringField>([
|
|
"userId",
|
|
"accessToken",
|
|
"password",
|
|
"deviceId",
|
|
]);
|
|
|
|
function clean(value: unknown): string {
|
|
return typeof value === "string" ? value.trim() : "";
|
|
}
|
|
|
|
function resolveMatrixStringSourceValue(value: string | undefined): string {
|
|
return typeof value === "string" ? value : "";
|
|
}
|
|
|
|
function shouldAllowBaseAuthFallback(accountId: string, field: MatrixResolvedStringField): boolean {
|
|
return (
|
|
normalizeAccountId(accountId) === DEFAULT_ACCOUNT_ID ||
|
|
!MATRIX_DEFAULT_ACCOUNT_AUTH_ONLY_FIELDS.has(field)
|
|
);
|
|
}
|
|
|
|
function resolveMatrixAccountStringValues(params: {
|
|
accountId: string;
|
|
account?: MatrixStringSourceMap;
|
|
scopedEnv?: MatrixStringSourceMap;
|
|
channel?: MatrixStringSourceMap;
|
|
globalEnv?: MatrixStringSourceMap;
|
|
}): MatrixResolvedStringValues {
|
|
const fields: MatrixResolvedStringField[] = [
|
|
"homeserver",
|
|
"userId",
|
|
"accessToken",
|
|
"password",
|
|
"deviceId",
|
|
"deviceName",
|
|
];
|
|
const resolved = {} as MatrixResolvedStringValues;
|
|
|
|
for (const field of fields) {
|
|
resolved[field] =
|
|
resolveMatrixStringSourceValue(params.account?.[field]) ||
|
|
resolveMatrixStringSourceValue(params.scopedEnv?.[field]) ||
|
|
(shouldAllowBaseAuthFallback(params.accountId, field)
|
|
? resolveMatrixStringSourceValue(params.channel?.[field]) ||
|
|
resolveMatrixStringSourceValue(params.globalEnv?.[field])
|
|
: "");
|
|
}
|
|
|
|
return resolved;
|
|
}
|
|
|
|
function resolveScopedMatrixEnvConfig(
|
|
accountId: string,
|
|
env: NodeJS.ProcessEnv,
|
|
): {
|
|
homeserver: string;
|
|
userId: string;
|
|
accessToken: string;
|
|
} {
|
|
const keys = getMatrixScopedEnvVarNames(accountId);
|
|
return {
|
|
homeserver: clean(env[keys.homeserver]),
|
|
userId: clean(env[keys.userId]),
|
|
accessToken: clean(env[keys.accessToken]),
|
|
};
|
|
}
|
|
|
|
function resolveGlobalMatrixEnvConfig(env: NodeJS.ProcessEnv): {
|
|
homeserver: string;
|
|
userId: string;
|
|
accessToken: string;
|
|
} {
|
|
return {
|
|
homeserver: clean(env.MATRIX_HOMESERVER),
|
|
userId: clean(env.MATRIX_USER_ID),
|
|
accessToken: clean(env.MATRIX_ACCESS_TOKEN),
|
|
};
|
|
}
|
|
|
|
function resolveMatrixAccountConfigEntry(
|
|
cfg: OpenClawConfig,
|
|
accountId: string,
|
|
): Record<string, unknown> | null {
|
|
return findMatrixAccountEntry(cfg, accountId);
|
|
}
|
|
|
|
function resolveMatrixFlatStoreSelectionNote(
|
|
cfg: OpenClawConfig,
|
|
accountId: string,
|
|
): string | undefined {
|
|
if (resolveConfiguredMatrixAccountIds(cfg).length <= 1) {
|
|
return undefined;
|
|
}
|
|
return (
|
|
`Legacy Matrix flat store uses one shared on-disk state, so it will be migrated into ` +
|
|
`account "${accountId}".`
|
|
);
|
|
}
|
|
|
|
export function resolveMatrixMigrationConfigFields(params: {
|
|
cfg: OpenClawConfig;
|
|
env: NodeJS.ProcessEnv;
|
|
accountId: string;
|
|
}): {
|
|
homeserver: string;
|
|
userId: string;
|
|
accessToken: string;
|
|
} {
|
|
const channel = resolveMatrixChannelConfig(params.cfg);
|
|
const account = resolveMatrixAccountConfigEntry(params.cfg, params.accountId);
|
|
const scopedEnv = resolveScopedMatrixEnvConfig(params.accountId, params.env);
|
|
const globalEnv = resolveGlobalMatrixEnvConfig(params.env);
|
|
const normalizedAccountId = normalizeAccountId(params.accountId);
|
|
const resolvedStrings = resolveMatrixAccountStringValues({
|
|
accountId: normalizedAccountId,
|
|
account: {
|
|
homeserver: clean(account?.homeserver),
|
|
userId: clean(account?.userId),
|
|
accessToken: clean(account?.accessToken),
|
|
},
|
|
scopedEnv,
|
|
channel: {
|
|
homeserver: clean(channel?.homeserver),
|
|
userId: clean(channel?.userId),
|
|
accessToken: clean(channel?.accessToken),
|
|
},
|
|
globalEnv,
|
|
});
|
|
|
|
return {
|
|
homeserver: resolvedStrings.homeserver,
|
|
userId: resolvedStrings.userId,
|
|
accessToken: resolvedStrings.accessToken,
|
|
};
|
|
}
|
|
|
|
export function loadStoredMatrixCredentials(
|
|
env: NodeJS.ProcessEnv,
|
|
accountId: string,
|
|
): MatrixStoredCredentials | null {
|
|
const stateDir = resolveStateDir(env, os.homedir);
|
|
const credentialsPath = resolveMatrixCredentialsPath({
|
|
stateDir,
|
|
accountId: normalizeAccountId(accountId),
|
|
});
|
|
try {
|
|
if (!fs.existsSync(credentialsPath)) {
|
|
return null;
|
|
}
|
|
const parsed = JSON.parse(
|
|
fs.readFileSync(credentialsPath, "utf8"),
|
|
) as Partial<MatrixStoredCredentials>;
|
|
if (
|
|
typeof parsed.homeserver !== "string" ||
|
|
typeof parsed.userId !== "string" ||
|
|
typeof parsed.accessToken !== "string"
|
|
) {
|
|
return null;
|
|
}
|
|
return {
|
|
homeserver: parsed.homeserver,
|
|
userId: parsed.userId,
|
|
accessToken: parsed.accessToken,
|
|
deviceId: typeof parsed.deviceId === "string" ? parsed.deviceId : undefined,
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function credentialsMatchResolvedIdentity(
|
|
stored: MatrixStoredCredentials | null,
|
|
identity: {
|
|
homeserver: string;
|
|
userId: string;
|
|
accessToken: string;
|
|
},
|
|
): stored is MatrixStoredCredentials {
|
|
if (!stored || !identity.homeserver) {
|
|
return false;
|
|
}
|
|
if (!identity.userId) {
|
|
if (!identity.accessToken) {
|
|
return false;
|
|
}
|
|
return stored.homeserver === identity.homeserver && stored.accessToken === identity.accessToken;
|
|
}
|
|
return stored.homeserver === identity.homeserver && stored.userId === identity.userId;
|
|
}
|
|
|
|
export function resolveMatrixMigrationAccountTarget(params: {
|
|
cfg: OpenClawConfig;
|
|
env: NodeJS.ProcessEnv;
|
|
accountId: string;
|
|
}): MatrixMigrationAccountTarget | null {
|
|
const stored = loadStoredMatrixCredentials(params.env, params.accountId);
|
|
const resolved = resolveMatrixMigrationConfigFields(params);
|
|
const matchingStored = credentialsMatchResolvedIdentity(stored, {
|
|
homeserver: resolved.homeserver,
|
|
userId: resolved.userId,
|
|
accessToken: resolved.accessToken,
|
|
})
|
|
? stored
|
|
: null;
|
|
const homeserver = resolved.homeserver;
|
|
const userId = resolved.userId || matchingStored?.userId || "";
|
|
const accessToken = resolved.accessToken || matchingStored?.accessToken || "";
|
|
if (!homeserver || !userId || !accessToken) {
|
|
return null;
|
|
}
|
|
|
|
const stateDir = resolveStateDir(params.env, os.homedir);
|
|
const { rootDir } = resolveMatrixAccountStorageRoot({
|
|
stateDir,
|
|
homeserver,
|
|
userId,
|
|
accessToken,
|
|
accountId: params.accountId,
|
|
});
|
|
|
|
return {
|
|
accountId: params.accountId,
|
|
homeserver,
|
|
userId,
|
|
accessToken,
|
|
rootDir,
|
|
storedDeviceId: matchingStored?.deviceId ?? null,
|
|
};
|
|
}
|
|
|
|
export function resolveLegacyMatrixFlatStoreTarget(params: {
|
|
cfg: OpenClawConfig;
|
|
env: NodeJS.ProcessEnv;
|
|
detectedPath: string;
|
|
detectedKind: MatrixLegacyFlatStoreKind;
|
|
}): MatrixLegacyFlatStoreTarget | { warning: string } {
|
|
const channel = resolveMatrixChannelConfig(params.cfg);
|
|
if (!channel) {
|
|
return {
|
|
warning:
|
|
`Legacy Matrix ${params.detectedKind} detected at ${params.detectedPath}, but channels.matrix is not configured yet. ` +
|
|
'Configure Matrix, then rerun "openclaw doctor --fix" or restart the gateway.',
|
|
};
|
|
}
|
|
if (requiresExplicitMatrixDefaultAccount(params.cfg)) {
|
|
return {
|
|
warning:
|
|
`Legacy Matrix ${params.detectedKind} detected at ${params.detectedPath}, but multiple Matrix accounts are configured and channels.matrix.defaultAccount is not set. ` +
|
|
'Set "channels.matrix.defaultAccount" to the intended target account before rerunning "openclaw doctor --fix" or restarting the gateway.',
|
|
};
|
|
}
|
|
|
|
const accountId = resolveMatrixDefaultOrOnlyAccountId(params.cfg);
|
|
const target = resolveMatrixMigrationAccountTarget({
|
|
cfg: params.cfg,
|
|
env: params.env,
|
|
accountId,
|
|
});
|
|
if (!target) {
|
|
const targetDescription =
|
|
params.detectedKind === "state"
|
|
? "the new account-scoped target"
|
|
: "the account-scoped target";
|
|
return {
|
|
warning:
|
|
`Legacy Matrix ${params.detectedKind} detected at ${params.detectedPath}, but ${targetDescription} could not be resolved yet ` +
|
|
`(need homeserver, userId, and access token for channels.matrix${accountId === DEFAULT_ACCOUNT_ID ? "" : `.accounts.${accountId}`}). ` +
|
|
'Start the gateway once with a working Matrix login, or rerun "openclaw doctor --fix" after cached credentials are available.',
|
|
};
|
|
}
|
|
|
|
return {
|
|
...target,
|
|
selectionNote: resolveMatrixFlatStoreSelectionNote(params.cfg, accountId),
|
|
};
|
|
}
|