mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-30 02:22:25 +00:00
Doctor: fix matrix default-account migration
This commit is contained in:
@@ -5,6 +5,7 @@ import type { ChannelSetupInput } from "./types.core.js";
|
||||
|
||||
type ChannelSectionBase = {
|
||||
name?: string;
|
||||
defaultAccount?: string;
|
||||
accounts?: Record<string, Record<string, unknown>>;
|
||||
};
|
||||
|
||||
@@ -363,6 +364,45 @@ const SINGLE_ACCOUNT_KEYS_TO_MOVE_BY_CHANNEL: Record<string, ReadonlySet<string>
|
||||
telegram: new Set(["streaming"]),
|
||||
};
|
||||
|
||||
const MATRIX_NAMED_ACCOUNT_PROMOTION_KEYS = new Set([
|
||||
"name",
|
||||
"homeserver",
|
||||
"userId",
|
||||
"accessToken",
|
||||
"password",
|
||||
"deviceId",
|
||||
"deviceName",
|
||||
"avatarUrl",
|
||||
"initialSyncLimit",
|
||||
"encryption",
|
||||
]);
|
||||
|
||||
export const MATRIX_SHARED_MULTI_ACCOUNT_DEFAULT_KEYS = new Set([
|
||||
"dmPolicy",
|
||||
"allowFrom",
|
||||
"groupPolicy",
|
||||
"groupAllowFrom",
|
||||
"allowlistOnly",
|
||||
"replyToMode",
|
||||
"threadReplies",
|
||||
"textChunkLimit",
|
||||
"chunkMode",
|
||||
"responsePrefix",
|
||||
"ackReaction",
|
||||
"ackReactionScope",
|
||||
"reactionNotifications",
|
||||
"threadBindings",
|
||||
"startupVerification",
|
||||
"startupVerificationCooldownHours",
|
||||
"mediaMaxMb",
|
||||
"autoJoin",
|
||||
"autoJoinAllowlist",
|
||||
"dm",
|
||||
"groups",
|
||||
"rooms",
|
||||
"actions",
|
||||
]);
|
||||
|
||||
export function shouldMoveSingleAccountChannelKey(params: {
|
||||
channelKey: string;
|
||||
key: string;
|
||||
@@ -373,6 +413,54 @@ export function shouldMoveSingleAccountChannelKey(params: {
|
||||
return SINGLE_ACCOUNT_KEYS_TO_MOVE_BY_CHANNEL[params.channelKey]?.has(params.key) ?? false;
|
||||
}
|
||||
|
||||
export function resolveSingleAccountKeysToMove(params: {
|
||||
channelKey: string;
|
||||
channel: Record<string, unknown>;
|
||||
}): string[] {
|
||||
const hasNamedAccounts =
|
||||
Object.keys((params.channel.accounts as Record<string, unknown>) ?? {}).filter(Boolean).length >
|
||||
0;
|
||||
return Object.entries(params.channel)
|
||||
.filter(([key, value]) => {
|
||||
if (key === "accounts" || key === "enabled" || value === undefined) {
|
||||
return false;
|
||||
}
|
||||
if (!shouldMoveSingleAccountChannelKey({ channelKey: params.channelKey, key })) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
params.channelKey === "matrix" &&
|
||||
hasNamedAccounts &&
|
||||
!MATRIX_NAMED_ACCOUNT_PROMOTION_KEYS.has(key)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map(([key]) => key);
|
||||
}
|
||||
|
||||
export function resolveSingleAccountPromotionTarget(params: {
|
||||
channelKey: string;
|
||||
channel: ChannelSectionBase;
|
||||
}): string {
|
||||
if (params.channelKey !== "matrix") {
|
||||
return DEFAULT_ACCOUNT_ID;
|
||||
}
|
||||
const normalizedDefaultAccount = normalizeAccountId(params.channel.defaultAccount);
|
||||
if (normalizedDefaultAccount === DEFAULT_ACCOUNT_ID) {
|
||||
return DEFAULT_ACCOUNT_ID;
|
||||
}
|
||||
const accounts = params.channel.accounts ?? {};
|
||||
if (
|
||||
accounts[normalizedDefaultAccount] &&
|
||||
typeof accounts[normalizedDefaultAccount] === "object"
|
||||
) {
|
||||
return normalizedDefaultAccount;
|
||||
}
|
||||
return DEFAULT_ACCOUNT_ID;
|
||||
}
|
||||
|
||||
function cloneIfObject<T>(value: T): T {
|
||||
if (value && typeof value === "object") {
|
||||
return structuredClone(value);
|
||||
@@ -397,18 +485,50 @@ export function moveSingleAccountChannelSectionToDefaultAccount(params: {
|
||||
|
||||
const accounts = base.accounts ?? {};
|
||||
if (Object.keys(accounts).length > 0) {
|
||||
return params.cfg;
|
||||
}
|
||||
if (params.channelKey !== "matrix") {
|
||||
return params.cfg;
|
||||
}
|
||||
const keysToMove = resolveSingleAccountKeysToMove({
|
||||
channelKey: params.channelKey,
|
||||
channel: base,
|
||||
});
|
||||
if (keysToMove.length === 0) {
|
||||
return params.cfg;
|
||||
}
|
||||
|
||||
const keysToMove = Object.entries(base)
|
||||
.filter(
|
||||
([key, value]) =>
|
||||
key !== "accounts" &&
|
||||
key !== "enabled" &&
|
||||
value !== undefined &&
|
||||
shouldMoveSingleAccountChannelKey({ channelKey: params.channelKey, key }),
|
||||
)
|
||||
.map(([key]) => key);
|
||||
const targetAccountId = resolveSingleAccountPromotionTarget({
|
||||
channelKey: params.channelKey,
|
||||
channel: base,
|
||||
});
|
||||
const defaultAccount: Record<string, unknown> = {
|
||||
...accounts[targetAccountId],
|
||||
};
|
||||
for (const key of keysToMove) {
|
||||
const value = base[key];
|
||||
defaultAccount[key] = cloneIfObject(value);
|
||||
}
|
||||
const nextChannel: ChannelSectionRecord = { ...base };
|
||||
for (const key of keysToMove) {
|
||||
delete nextChannel[key];
|
||||
}
|
||||
return {
|
||||
...params.cfg,
|
||||
channels: {
|
||||
...params.cfg.channels,
|
||||
[params.channelKey]: {
|
||||
...nextChannel,
|
||||
accounts: {
|
||||
...accounts,
|
||||
[targetAccountId]: defaultAccount,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
}
|
||||
const keysToMove = resolveSingleAccountKeysToMove({
|
||||
channelKey: params.channelKey,
|
||||
channel: base,
|
||||
});
|
||||
const defaultAccount: Record<string, unknown> = {};
|
||||
for (const key of keysToMove) {
|
||||
const value = base[key];
|
||||
|
||||
@@ -263,6 +263,64 @@ describe("normalizeCompatibilityConfigValues", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("does not create matrix.accounts.default for shared top-level defaults", () => {
|
||||
const res = normalizeCompatibilityConfigValues({
|
||||
channels: {
|
||||
matrix: {
|
||||
defaultAccount: "main",
|
||||
dm: { policy: "pairing" },
|
||||
accounts: {
|
||||
main: {
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@main:example.org",
|
||||
password: "main-password",
|
||||
},
|
||||
poe: {
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@poe:example.org",
|
||||
password: "poe-password",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.config.channels?.matrix?.dm).toEqual({ policy: "pairing" });
|
||||
expect(res.config.channels?.matrix?.accounts?.default).toBeUndefined();
|
||||
expect(res.changes).toEqual([]);
|
||||
});
|
||||
|
||||
it("repairs synthetic matrix.accounts.default shared defaults back to top-level", () => {
|
||||
const res = normalizeCompatibilityConfigValues({
|
||||
channels: {
|
||||
matrix: {
|
||||
defaultAccount: "main",
|
||||
accounts: {
|
||||
main: {
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@main:example.org",
|
||||
password: "main-password",
|
||||
},
|
||||
poe: {
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@poe:example.org",
|
||||
password: "poe-password",
|
||||
},
|
||||
default: {
|
||||
dm: { policy: "pairing" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.config.channels?.matrix?.dm).toEqual({ policy: "pairing" });
|
||||
expect(res.config.channels?.matrix?.accounts?.default).toBeUndefined();
|
||||
expect(res.changes).toContain(
|
||||
"Moved shared Matrix defaults from channels.matrix.accounts.default back to channels.matrix.*.",
|
||||
);
|
||||
});
|
||||
|
||||
it("migrates browser ssrfPolicy allowPrivateNetwork to dangerouslyAllowPrivateNetwork", () => {
|
||||
const res = normalizeCompatibilityConfigValues({
|
||||
browser: {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { shouldMoveSingleAccountChannelKey } from "../channels/plugins/setup-helpers.js";
|
||||
import {
|
||||
MATRIX_SHARED_MULTI_ACCOUNT_DEFAULT_KEYS,
|
||||
resolveSingleAccountKeysToMove,
|
||||
resolveSingleAccountPromotionTarget,
|
||||
} from "../channels/plugins/setup-helpers.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import {
|
||||
formatSlackStreamingBooleanMigrationMessage,
|
||||
@@ -377,20 +381,23 @@ export function normalizeCompatibilityConfigValues(cfg: OpenClawConfig): {
|
||||
continue;
|
||||
}
|
||||
|
||||
const keysToMove = Object.entries(rawChannel)
|
||||
.filter(
|
||||
([key, value]) =>
|
||||
key !== "accounts" &&
|
||||
key !== "enabled" &&
|
||||
value !== undefined &&
|
||||
shouldMoveSingleAccountChannelKey({ channelKey: channelId, key }),
|
||||
)
|
||||
.map(([key]) => key);
|
||||
const keysToMove = resolveSingleAccountKeysToMove({
|
||||
channelKey: channelId,
|
||||
channel: rawChannel,
|
||||
});
|
||||
if (keysToMove.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const defaultAccount: Record<string, unknown> = {};
|
||||
const targetAccountId = resolveSingleAccountPromotionTarget({
|
||||
channelKey: channelId,
|
||||
channel: rawChannel,
|
||||
});
|
||||
const defaultAccount: Record<string, unknown> = {
|
||||
...(typeof rawAccounts[targetAccountId] === "object" && rawAccounts[targetAccountId]
|
||||
? structuredClone(rawAccounts[targetAccountId] as Record<string, unknown>)
|
||||
: {}),
|
||||
};
|
||||
for (const key of keysToMove) {
|
||||
const value = rawChannel[key];
|
||||
defaultAccount[key] = value && typeof value === "object" ? structuredClone(value) : value;
|
||||
@@ -403,13 +410,13 @@ export function normalizeCompatibilityConfigValues(cfg: OpenClawConfig): {
|
||||
}
|
||||
nextChannel.accounts = {
|
||||
...rawAccounts,
|
||||
[DEFAULT_ACCOUNT_ID]: defaultAccount,
|
||||
[targetAccountId]: defaultAccount,
|
||||
};
|
||||
|
||||
nextChannels[channelId] = nextChannel;
|
||||
channelsChanged = true;
|
||||
changes.push(
|
||||
`Moved channels.${channelId} single-account top-level values into channels.${channelId}.accounts.default.`,
|
||||
`Moved channels.${channelId} single-account top-level values into channels.${channelId}.accounts.${targetAccountId}.`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -428,6 +435,57 @@ export function normalizeCompatibilityConfigValues(cfg: OpenClawConfig): {
|
||||
seedMissingDefaultAccountsFromSingleAccountBase();
|
||||
normalizeLegacyBrowserProfiles();
|
||||
|
||||
const repairSyntheticMatrixDefaultAccount = () => {
|
||||
const rawMatrix = next.channels?.matrix;
|
||||
if (!isRecord(rawMatrix)) {
|
||||
return;
|
||||
}
|
||||
const rawAccounts = rawMatrix.accounts;
|
||||
if (!isRecord(rawAccounts)) {
|
||||
return;
|
||||
}
|
||||
const syntheticDefault = rawAccounts[DEFAULT_ACCOUNT_ID];
|
||||
if (!isRecord(syntheticDefault)) {
|
||||
return;
|
||||
}
|
||||
const namedDefaultAccount =
|
||||
typeof rawMatrix.defaultAccount === "string" ? rawMatrix.defaultAccount.trim() : "";
|
||||
if (!namedDefaultAccount || namedDefaultAccount.toLowerCase() === DEFAULT_ACCOUNT_ID) {
|
||||
return;
|
||||
}
|
||||
const defaultKeys = Object.keys(syntheticDefault);
|
||||
if (
|
||||
defaultKeys.length === 0 ||
|
||||
defaultKeys.some((key) => !MATRIX_SHARED_MULTI_ACCOUNT_DEFAULT_KEYS.has(key))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextMatrix: Record<string, unknown> = { ...rawMatrix };
|
||||
for (const key of defaultKeys) {
|
||||
if (nextMatrix[key] === undefined) {
|
||||
const value = syntheticDefault[key];
|
||||
nextMatrix[key] = value && typeof value === "object" ? structuredClone(value) : value;
|
||||
}
|
||||
}
|
||||
const nextAccounts = { ...rawAccounts };
|
||||
delete nextAccounts[DEFAULT_ACCOUNT_ID];
|
||||
nextMatrix.accounts = nextAccounts;
|
||||
|
||||
next = {
|
||||
...next,
|
||||
channels: {
|
||||
...next.channels,
|
||||
matrix: nextMatrix as OpenClawConfig["channels"]["matrix"],
|
||||
},
|
||||
};
|
||||
changes.push(
|
||||
"Moved shared Matrix defaults from channels.matrix.accounts.default back to channels.matrix.*.",
|
||||
);
|
||||
};
|
||||
|
||||
repairSyntheticMatrixDefaultAccount();
|
||||
|
||||
const normalizeBrowserSsrFPolicyAlias = () => {
|
||||
const rawBrowser = next.browser;
|
||||
if (!isRecord(rawBrowser)) {
|
||||
|
||||
Reference in New Issue
Block a user