mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 12:41:37 +00:00
Matrix: improve migration startup warnings
This commit is contained in:
@@ -59,8 +59,7 @@ OpenClaw cannot automatically recover:
|
|||||||
|
|
||||||
Current warning scope:
|
Current warning scope:
|
||||||
|
|
||||||
- stale custom Matrix plugin path installs are surfaced by `openclaw doctor` today
|
- stale custom Matrix plugin path installs are surfaced by both gateway startup and `openclaw doctor`
|
||||||
- gateway startup does not currently emit a separate Matrix-specific custom-path warning
|
|
||||||
|
|
||||||
If your old installation had local-only encrypted history that was never backed up, some older encrypted messages may remain unreadable after the upgrade.
|
If your old installation had local-only encrypted history that was never backed up, some older encrypted messages may remain unreadable after the upgrade.
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ import {
|
|||||||
isTrustedSafeBinPath,
|
isTrustedSafeBinPath,
|
||||||
normalizeTrustedSafeBinDirs,
|
normalizeTrustedSafeBinDirs,
|
||||||
} from "../infra/exec-safe-bin-trust.js";
|
} from "../infra/exec-safe-bin-trust.js";
|
||||||
|
import {
|
||||||
|
detectMatrixInstallPathIssue,
|
||||||
|
formatMatrixInstallPathIssue,
|
||||||
|
} from "../infra/matrix-install-path-warnings.js";
|
||||||
import {
|
import {
|
||||||
autoPrepareLegacyMatrixCrypto,
|
autoPrepareLegacyMatrixCrypto,
|
||||||
detectLegacyMatrixCrypto,
|
detectLegacyMatrixCrypto,
|
||||||
@@ -300,6 +304,7 @@ function formatMatrixLegacyStatePreview(
|
|||||||
"- Matrix plugin upgraded in place.",
|
"- Matrix plugin upgraded in place.",
|
||||||
`- Legacy sync store: ${detection.legacyStoragePath} -> ${detection.targetStoragePath}`,
|
`- Legacy sync store: ${detection.legacyStoragePath} -> ${detection.targetStoragePath}`,
|
||||||
`- Legacy crypto store: ${detection.legacyCryptoPath} -> ${detection.targetCryptoPath}`,
|
`- Legacy crypto store: ${detection.legacyCryptoPath} -> ${detection.targetCryptoPath}`,
|
||||||
|
...(detection.selectionNote ? [`- ${detection.selectionNote}`] : []),
|
||||||
'- Run "openclaw doctor --fix" to migrate this Matrix state now.',
|
'- Run "openclaw doctor --fix" to migrate this Matrix state now.',
|
||||||
].join("\n");
|
].join("\n");
|
||||||
}
|
}
|
||||||
@@ -326,33 +331,14 @@ function formatMatrixLegacyCryptoPreview(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function collectMatrixInstallPathWarnings(cfg: OpenClawConfig): Promise<string[]> {
|
async function collectMatrixInstallPathWarnings(cfg: OpenClawConfig): Promise<string[]> {
|
||||||
const install = cfg.plugins?.installs?.matrix;
|
const issue = await detectMatrixInstallPathIssue(cfg);
|
||||||
if (!install || install.source !== "path") {
|
if (!issue) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
return formatMatrixInstallPathIssue({
|
||||||
const candidatePaths = [install.sourcePath, install.installPath]
|
issue,
|
||||||
.map((value) => (typeof value === "string" ? value.trim() : ""))
|
formatCommand: formatCliCommand,
|
||||||
.filter(Boolean);
|
}).map((entry) => `- ${entry}`);
|
||||||
if (candidatePaths.length === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const candidatePath of candidatePaths) {
|
|
||||||
try {
|
|
||||||
await fs.access(path.resolve(candidatePath));
|
|
||||||
return [];
|
|
||||||
} catch {
|
|
||||||
// keep checking remaining candidates
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const missingPath = candidatePaths[0] ?? "(unknown)";
|
|
||||||
return [
|
|
||||||
`- Matrix is installed from a custom path that no longer exists: ${missingPath}`,
|
|
||||||
`- Reinstall with "${formatCliCommand("openclaw plugins install @openclaw/matrix")}".`,
|
|
||||||
`- If you are running from a repo checkout, you can also use "${formatCliCommand("openclaw plugins install ./extensions/matrix")}".`,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function scanTelegramAllowFromUsernameEntries(cfg: OpenClawConfig): TelegramAllowFromUsernameHit[] {
|
function scanTelegramAllowFromUsernameEntries(cfg: OpenClawConfig): TelegramAllowFromUsernameHit[] {
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ import { createExecApprovalForwarder } from "../infra/exec-approval-forwarder.js
|
|||||||
import { onHeartbeatEvent } from "../infra/heartbeat-events.js";
|
import { onHeartbeatEvent } from "../infra/heartbeat-events.js";
|
||||||
import { startHeartbeatRunner, type HeartbeatRunner } from "../infra/heartbeat-runner.js";
|
import { startHeartbeatRunner, type HeartbeatRunner } from "../infra/heartbeat-runner.js";
|
||||||
import { getMachineDisplayName } from "../infra/machine-name.js";
|
import { getMachineDisplayName } from "../infra/machine-name.js";
|
||||||
|
import {
|
||||||
|
detectMatrixInstallPathIssue,
|
||||||
|
formatMatrixInstallPathIssue,
|
||||||
|
} from "../infra/matrix-install-path-warnings.js";
|
||||||
import { autoPrepareLegacyMatrixCrypto } from "../infra/matrix-legacy-crypto.js";
|
import { autoPrepareLegacyMatrixCrypto } from "../infra/matrix-legacy-crypto.js";
|
||||||
import { autoMigrateLegacyMatrixState } from "../infra/matrix-legacy-state.js";
|
import { autoMigrateLegacyMatrixState } from "../infra/matrix-legacy-state.js";
|
||||||
import { ensureOpenClawCliOnPath } from "../infra/path-env.js";
|
import { ensureOpenClawCliOnPath } from "../infra/path-env.js";
|
||||||
@@ -343,6 +347,18 @@ export async function startGatewayServer(
|
|||||||
env: process.env,
|
env: process.env,
|
||||||
log,
|
log,
|
||||||
});
|
});
|
||||||
|
const matrixInstallPathIssue = await detectMatrixInstallPathIssue(
|
||||||
|
autoEnable.changes.length > 0 ? autoEnable.config : configSnapshot.config,
|
||||||
|
);
|
||||||
|
if (matrixInstallPathIssue) {
|
||||||
|
const lines = formatMatrixInstallPathIssue({
|
||||||
|
issue: matrixInstallPathIssue,
|
||||||
|
formatCommand: formatCliCommand,
|
||||||
|
});
|
||||||
|
log.warn(
|
||||||
|
`gateway: matrix install path warning:\n${lines.map((entry) => `- ${entry}`).join("\n")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
const emitSecretsStateEvent = (
|
const emitSecretsStateEvent = (
|
||||||
code: "SECRETS_RELOADER_DEGRADED" | "SECRETS_RELOADER_RECOVERED",
|
code: "SECRETS_RELOADER_DEGRADED" | "SECRETS_RELOADER_RECOVERED",
|
||||||
message: string,
|
message: string,
|
||||||
|
|||||||
58
src/infra/matrix-install-path-warnings.test.ts
Normal file
58
src/infra/matrix-install-path-warnings.test.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import fs from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { withTempHome } from "../../test/helpers/temp-home.js";
|
||||||
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
|
import {
|
||||||
|
detectMatrixInstallPathIssue,
|
||||||
|
formatMatrixInstallPathIssue,
|
||||||
|
} from "./matrix-install-path-warnings.js";
|
||||||
|
|
||||||
|
describe("matrix install path warnings", () => {
|
||||||
|
it("detects stale custom Matrix plugin paths", async () => {
|
||||||
|
const cfg: OpenClawConfig = {
|
||||||
|
plugins: {
|
||||||
|
installs: {
|
||||||
|
matrix: {
|
||||||
|
source: "path",
|
||||||
|
sourcePath: "/tmp/openclaw-matrix-missing",
|
||||||
|
installPath: "/tmp/openclaw-matrix-missing",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const issue = await detectMatrixInstallPathIssue(cfg);
|
||||||
|
expect(issue).toEqual({ missingPath: "/tmp/openclaw-matrix-missing" });
|
||||||
|
expect(
|
||||||
|
formatMatrixInstallPathIssue({
|
||||||
|
issue: issue!,
|
||||||
|
}),
|
||||||
|
).toEqual([
|
||||||
|
"Matrix is installed from a custom path that no longer exists: /tmp/openclaw-matrix-missing",
|
||||||
|
'Reinstall with "openclaw plugins install @openclaw/matrix".',
|
||||||
|
'If you are running from a repo checkout, you can also use "openclaw plugins install ./extensions/matrix".',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips warnings when the configured custom path exists", async () => {
|
||||||
|
await withTempHome(async (home) => {
|
||||||
|
const pluginPath = path.join(home, "matrix-plugin");
|
||||||
|
await fs.mkdir(pluginPath, { recursive: true });
|
||||||
|
|
||||||
|
const cfg: OpenClawConfig = {
|
||||||
|
plugins: {
|
||||||
|
installs: {
|
||||||
|
matrix: {
|
||||||
|
source: "path",
|
||||||
|
sourcePath: pluginPath,
|
||||||
|
installPath: pluginPath,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(detectMatrixInstallPathIssue(cfg)).resolves.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
52
src/infra/matrix-install-path-warnings.ts
Normal file
52
src/infra/matrix-install-path-warnings.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import fs from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
|
|
||||||
|
export type MatrixInstallPathIssue = {
|
||||||
|
missingPath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function resolveMatrixInstallCandidatePaths(cfg: OpenClawConfig): string[] {
|
||||||
|
const install = cfg.plugins?.installs?.matrix;
|
||||||
|
if (!install || install.source !== "path") {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [install.sourcePath, install.installPath]
|
||||||
|
.map((value) => (typeof value === "string" ? value.trim() : ""))
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function detectMatrixInstallPathIssue(
|
||||||
|
cfg: OpenClawConfig,
|
||||||
|
): Promise<MatrixInstallPathIssue | null> {
|
||||||
|
const candidatePaths = resolveMatrixInstallCandidatePaths(cfg);
|
||||||
|
if (candidatePaths.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const candidatePath of candidatePaths) {
|
||||||
|
try {
|
||||||
|
await fs.access(path.resolve(candidatePath));
|
||||||
|
return null;
|
||||||
|
} catch {
|
||||||
|
// keep checking remaining candidates
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
missingPath: candidatePaths[0] ?? "(unknown)",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatMatrixInstallPathIssue(params: {
|
||||||
|
issue: MatrixInstallPathIssue;
|
||||||
|
formatCommand?: (command: string) => string;
|
||||||
|
}): string[] {
|
||||||
|
const formatCommand = params.formatCommand ?? ((command: string) => command);
|
||||||
|
return [
|
||||||
|
`Matrix is installed from a custom path that no longer exists: ${params.issue.missingPath}`,
|
||||||
|
`Reinstall with "${formatCommand("openclaw plugins install @openclaw/matrix")}".`,
|
||||||
|
`If you are running from a repo checkout, you can also use "${formatCommand("openclaw plugins install ./extensions/matrix")}".`,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -83,4 +83,40 @@ describe("matrix legacy state migration", () => {
|
|||||||
expect(fs.existsSync(detection.targetStoragePath)).toBe(true);
|
expect(fs.existsSync(detection.targetStoragePath)).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("records which account receives a flat legacy store when multiple Matrix accounts exist", async () => {
|
||||||
|
await withTempHome(async (home) => {
|
||||||
|
const stateDir = path.join(home, ".openclaw");
|
||||||
|
writeFile(path.join(stateDir, "matrix", "bot-storage.json"), '{"next_batch":"s1"}');
|
||||||
|
|
||||||
|
const cfg: OpenClawConfig = {
|
||||||
|
channels: {
|
||||||
|
matrix: {
|
||||||
|
defaultAccount: "work",
|
||||||
|
accounts: {
|
||||||
|
work: {
|
||||||
|
homeserver: "https://matrix.example.org",
|
||||||
|
userId: "@work-bot:example.org",
|
||||||
|
accessToken: "tok-work",
|
||||||
|
},
|
||||||
|
alerts: {
|
||||||
|
homeserver: "https://matrix.example.org",
|
||||||
|
userId: "@alerts-bot:example.org",
|
||||||
|
accessToken: "tok-alerts",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const detection = detectLegacyMatrixState({ cfg, env: process.env });
|
||||||
|
expect(detection && "warning" in detection).toBe(false);
|
||||||
|
if (!detection || "warning" in detection) {
|
||||||
|
throw new Error("expected a migratable Matrix legacy state plan");
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(detection.accountId).toBe("work");
|
||||||
|
expect(detection.selectionNote).toContain('account "work"');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ type MatrixLegacyStatePlan = {
|
|||||||
targetRootDir: string;
|
targetRootDir: string;
|
||||||
targetStoragePath: string;
|
targetStoragePath: string;
|
||||||
targetCryptoPath: string;
|
targetCryptoPath: string;
|
||||||
|
selectionNote?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
@@ -128,6 +129,32 @@ function resolveMatrixTargetAccountId(cfg: OpenClawConfig): string {
|
|||||||
return DEFAULT_ACCOUNT_ID;
|
return DEFAULT_ACCOUNT_ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveMatrixFlatStoreSelectionNote(params: {
|
||||||
|
channel: Record<string, unknown>;
|
||||||
|
accountId: string;
|
||||||
|
}): string | undefined {
|
||||||
|
const accounts = isRecord(params.channel.accounts) ? params.channel.accounts : null;
|
||||||
|
if (!accounts) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const configuredAccounts = Array.from(
|
||||||
|
new Set(
|
||||||
|
Object.keys(accounts)
|
||||||
|
.map((accountId) => normalizeAccountId(accountId))
|
||||||
|
.filter(Boolean),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (configuredAccounts.length <= 1) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
`Legacy Matrix flat store uses one shared on-disk state, so it will be migrated into ` +
|
||||||
|
`account "${params.accountId}".`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function resolveMatrixMigrationPlan(params: {
|
function resolveMatrixMigrationPlan(params: {
|
||||||
cfg: OpenClawConfig;
|
cfg: OpenClawConfig;
|
||||||
env: NodeJS.ProcessEnv;
|
env: NodeJS.ProcessEnv;
|
||||||
@@ -149,6 +176,7 @@ function resolveMatrixMigrationPlan(params: {
|
|||||||
const accountId = resolveMatrixTargetAccountId(params.cfg);
|
const accountId = resolveMatrixTargetAccountId(params.cfg);
|
||||||
const account = resolveMatrixAccountConfig(params.cfg, accountId);
|
const account = resolveMatrixAccountConfig(params.cfg, accountId);
|
||||||
const stored = loadStoredMatrixCredentials(params.env, accountId);
|
const stored = loadStoredMatrixCredentials(params.env, accountId);
|
||||||
|
const selectionNote = resolveMatrixFlatStoreSelectionNote({ channel, accountId });
|
||||||
|
|
||||||
const homeserver = typeof account.homeserver === "string" ? account.homeserver.trim() : "";
|
const homeserver = typeof account.homeserver === "string" ? account.homeserver.trim() : "";
|
||||||
const configUserId = typeof account.userId === "string" ? account.userId.trim() : "";
|
const configUserId = typeof account.userId === "string" ? account.userId.trim() : "";
|
||||||
@@ -191,6 +219,7 @@ function resolveMatrixMigrationPlan(params: {
|
|||||||
targetRootDir: rootDir,
|
targetRootDir: rootDir,
|
||||||
targetStoragePath: path.join(rootDir, "bot-storage.json"),
|
targetStoragePath: path.join(rootDir, "bot-storage.json"),
|
||||||
targetCryptoPath: path.join(rootDir, "crypto"),
|
targetCryptoPath: path.join(rootDir, "crypto"),
|
||||||
|
selectionNote,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,10 +295,13 @@ export async function autoMigrateLegacyMatrixState(params: {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (changes.length > 0) {
|
if (changes.length > 0) {
|
||||||
|
const details = [
|
||||||
|
...changes.map((entry) => `- ${entry}`),
|
||||||
|
...(detection.selectionNote ? [`- ${detection.selectionNote}`] : []),
|
||||||
|
"- No user action required.",
|
||||||
|
];
|
||||||
params.log?.info?.(
|
params.log?.info?.(
|
||||||
`matrix: plugin upgraded in place for account "${detection.accountId}".\n${changes
|
`matrix: plugin upgraded in place for account "${detection.accountId}".\n${details.join("\n")}`,
|
||||||
.map((entry) => `- ${entry}`)
|
|
||||||
.join("\n")}\n- No user action required.`,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (warnings.length > 0) {
|
if (warnings.length > 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user