mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
Matrix: improve migration startup warnings
This commit is contained in:
@@ -59,8 +59,7 @@ OpenClaw cannot automatically recover:
|
||||
|
||||
Current warning scope:
|
||||
|
||||
- stale custom Matrix plugin path installs are surfaced by `openclaw doctor` today
|
||||
- gateway startup does not currently emit a separate Matrix-specific custom-path warning
|
||||
- stale custom Matrix plugin path installs are surfaced by both gateway startup and `openclaw doctor`
|
||||
|
||||
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,
|
||||
normalizeTrustedSafeBinDirs,
|
||||
} from "../infra/exec-safe-bin-trust.js";
|
||||
import {
|
||||
detectMatrixInstallPathIssue,
|
||||
formatMatrixInstallPathIssue,
|
||||
} from "../infra/matrix-install-path-warnings.js";
|
||||
import {
|
||||
autoPrepareLegacyMatrixCrypto,
|
||||
detectLegacyMatrixCrypto,
|
||||
@@ -300,6 +304,7 @@ function formatMatrixLegacyStatePreview(
|
||||
"- Matrix plugin upgraded in place.",
|
||||
`- Legacy sync store: ${detection.legacyStoragePath} -> ${detection.targetStoragePath}`,
|
||||
`- Legacy crypto store: ${detection.legacyCryptoPath} -> ${detection.targetCryptoPath}`,
|
||||
...(detection.selectionNote ? [`- ${detection.selectionNote}`] : []),
|
||||
'- Run "openclaw doctor --fix" to migrate this Matrix state now.',
|
||||
].join("\n");
|
||||
}
|
||||
@@ -326,33 +331,14 @@ function formatMatrixLegacyCryptoPreview(
|
||||
}
|
||||
|
||||
async function collectMatrixInstallPathWarnings(cfg: OpenClawConfig): Promise<string[]> {
|
||||
const install = cfg.plugins?.installs?.matrix;
|
||||
if (!install || install.source !== "path") {
|
||||
const issue = await detectMatrixInstallPathIssue(cfg);
|
||||
if (!issue) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const candidatePaths = [install.sourcePath, install.installPath]
|
||||
.map((value) => (typeof value === "string" ? value.trim() : ""))
|
||||
.filter(Boolean);
|
||||
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")}".`,
|
||||
];
|
||||
return formatMatrixInstallPathIssue({
|
||||
issue,
|
||||
formatCommand: formatCliCommand,
|
||||
}).map((entry) => `- ${entry}`);
|
||||
}
|
||||
|
||||
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 { startHeartbeatRunner, type HeartbeatRunner } from "../infra/heartbeat-runner.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 { autoMigrateLegacyMatrixState } from "../infra/matrix-legacy-state.js";
|
||||
import { ensureOpenClawCliOnPath } from "../infra/path-env.js";
|
||||
@@ -343,6 +347,18 @@ export async function startGatewayServer(
|
||||
env: process.env,
|
||||
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 = (
|
||||
code: "SECRETS_RELOADER_DEGRADED" | "SECRETS_RELOADER_RECOVERED",
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
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;
|
||||
targetStoragePath: string;
|
||||
targetCryptoPath: string;
|
||||
selectionNote?: string;
|
||||
};
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
@@ -128,6 +129,32 @@ function resolveMatrixTargetAccountId(cfg: OpenClawConfig): string {
|
||||
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: {
|
||||
cfg: OpenClawConfig;
|
||||
env: NodeJS.ProcessEnv;
|
||||
@@ -149,6 +176,7 @@ function resolveMatrixMigrationPlan(params: {
|
||||
const accountId = resolveMatrixTargetAccountId(params.cfg);
|
||||
const account = resolveMatrixAccountConfig(params.cfg, accountId);
|
||||
const stored = loadStoredMatrixCredentials(params.env, accountId);
|
||||
const selectionNote = resolveMatrixFlatStoreSelectionNote({ channel, accountId });
|
||||
|
||||
const homeserver = typeof account.homeserver === "string" ? account.homeserver.trim() : "";
|
||||
const configUserId = typeof account.userId === "string" ? account.userId.trim() : "";
|
||||
@@ -191,6 +219,7 @@ function resolveMatrixMigrationPlan(params: {
|
||||
targetRootDir: rootDir,
|
||||
targetStoragePath: path.join(rootDir, "bot-storage.json"),
|
||||
targetCryptoPath: path.join(rootDir, "crypto"),
|
||||
selectionNote,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -266,10 +295,13 @@ export async function autoMigrateLegacyMatrixState(params: {
|
||||
});
|
||||
|
||||
if (changes.length > 0) {
|
||||
const details = [
|
||||
...changes.map((entry) => `- ${entry}`),
|
||||
...(detection.selectionNote ? [`- ${detection.selectionNote}`] : []),
|
||||
"- No user action required.",
|
||||
];
|
||||
params.log?.info?.(
|
||||
`matrix: plugin upgraded in place for account "${detection.accountId}".\n${changes
|
||||
.map((entry) => `- ${entry}`)
|
||||
.join("\n")}\n- No user action required.`,
|
||||
`matrix: plugin upgraded in place for account "${detection.accountId}".\n${details.join("\n")}`,
|
||||
);
|
||||
}
|
||||
if (warnings.length > 0) {
|
||||
|
||||
Reference in New Issue
Block a user