diff --git a/extensions/matrix/index.ts b/extensions/matrix/index.ts index 10df32f7f79..b14ce2771e6 100644 --- a/extensions/matrix/index.ts +++ b/extensions/matrix/index.ts @@ -1,6 +1,6 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; import { emptyPluginConfigSchema } from "openclaw/plugin-sdk"; -import { matrixPlugin } from "./src/channel.js"; +import { ensureMatrixCryptoRuntime } from "./src/matrix/deps.js"; import { setMatrixRuntime } from "./src/runtime.js"; const plugin = { @@ -8,8 +8,10 @@ const plugin = { name: "Matrix", description: "Matrix channel plugin (matrix-js-sdk)", configSchema: emptyPluginConfigSchema(), - register(api: OpenClawPluginApi) { + async register(api: OpenClawPluginApi) { setMatrixRuntime(api.runtime); + await ensureMatrixCryptoRuntime(); + const { matrixPlugin } = await import("./src/channel.js"); api.registerChannel({ plugin: matrixPlugin }); }, }; diff --git a/extensions/matrix/src/matrix/deps.test.ts b/extensions/matrix/src/matrix/deps.test.ts new file mode 100644 index 00000000000..7c5d17d1a95 --- /dev/null +++ b/extensions/matrix/src/matrix/deps.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, it, vi } from "vitest"; +import { ensureMatrixCryptoRuntime } from "./deps.js"; + +const logStub = vi.fn(); + +describe("ensureMatrixCryptoRuntime", () => { + it("returns immediately when matrix SDK loads", async () => { + const runCommand = vi.fn(); + const requireFn = vi.fn(() => ({})); + + await ensureMatrixCryptoRuntime({ + log: logStub, + requireFn, + runCommand, + resolveFn: () => "/tmp/download-lib.js", + nodeExecutable: "/usr/bin/node", + }); + + expect(requireFn).toHaveBeenCalledTimes(1); + expect(runCommand).not.toHaveBeenCalled(); + }); + + it("bootstraps missing crypto runtime and retries matrix SDK load", async () => { + let bootstrapped = false; + const requireFn = vi.fn(() => { + if (!bootstrapped) { + throw new Error( + "Cannot find module '@matrix-org/matrix-sdk-crypto-nodejs-linux-x64-gnu' (required by matrix sdk)", + ); + } + return {}; + }); + const runCommand = vi.fn(async () => { + bootstrapped = true; + return { code: 0, stdout: "", stderr: "" }; + }); + + await ensureMatrixCryptoRuntime({ + log: logStub, + requireFn, + runCommand, + resolveFn: () => "/tmp/download-lib.js", + nodeExecutable: "/usr/bin/node", + }); + + expect(runCommand).toHaveBeenCalledWith({ + argv: ["/usr/bin/node", "/tmp/download-lib.js"], + cwd: "/tmp", + timeoutMs: 300_000, + env: { COREPACK_ENABLE_DOWNLOAD_PROMPT: "0" }, + }); + expect(requireFn).toHaveBeenCalledTimes(2); + }); + + it("rethrows non-crypto module errors without bootstrapping", async () => { + const runCommand = vi.fn(); + const requireFn = vi.fn(() => { + throw new Error("Cannot find module '@vector-im/matrix-bot-sdk'"); + }); + + await expect( + ensureMatrixCryptoRuntime({ + log: logStub, + requireFn, + runCommand, + resolveFn: () => "/tmp/download-lib.js", + nodeExecutable: "/usr/bin/node", + }), + ).rejects.toThrow("Cannot find module '@vector-im/matrix-bot-sdk'"); + + expect(runCommand).not.toHaveBeenCalled(); + expect(requireFn).toHaveBeenCalledTimes(1); + }); +}); diff --git a/extensions/matrix/src/matrix/deps.ts b/extensions/matrix/src/matrix/deps.ts index 6941af8af68..c1e9957fe23 100644 --- a/extensions/matrix/src/matrix/deps.ts +++ b/extensions/matrix/src/matrix/deps.ts @@ -5,6 +5,27 @@ import { fileURLToPath } from "node:url"; import { runPluginCommandWithTimeout, type RuntimeEnv } from "openclaw/plugin-sdk"; const MATRIX_SDK_PACKAGE = "@vector-im/matrix-bot-sdk"; +const MATRIX_CRYPTO_DOWNLOAD_HELPER = "@matrix-org/matrix-sdk-crypto-nodejs/download-lib.js"; + +function formatCommandError(result: { stderr: string; stdout: string }): string { + const stderr = result.stderr.trim(); + if (stderr) { + return stderr; + } + const stdout = result.stdout.trim(); + if (stdout) { + return stdout; + } + return "unknown error"; +} + +function isMissingMatrixCryptoRuntimeError(err: unknown): boolean { + const message = err instanceof Error ? err.message : String(err ?? ""); + return ( + message.includes("Cannot find module") && + message.includes("@matrix-org/matrix-sdk-crypto-nodejs-") + ); +} export function isMatrixSdkAvailable(): boolean { try { @@ -21,6 +42,51 @@ function resolvePluginRoot(): string { return path.resolve(currentDir, "..", ".."); } +export async function ensureMatrixCryptoRuntime( + params: { + log?: (message: string) => void; + requireFn?: (id: string) => unknown; + resolveFn?: (id: string) => string; + runCommand?: typeof runPluginCommandWithTimeout; + nodeExecutable?: string; + } = {}, +): Promise { + const req = createRequire(import.meta.url); + const requireFn = params.requireFn ?? ((id: string) => req(id)); + const resolveFn = params.resolveFn ?? ((id: string) => req.resolve(id)); + const runCommand = params.runCommand ?? runPluginCommandWithTimeout; + const nodeExecutable = params.nodeExecutable ?? process.execPath; + + try { + requireFn(MATRIX_SDK_PACKAGE); + return; + } catch (err) { + if (!isMissingMatrixCryptoRuntimeError(err)) { + throw err; + } + } + + const scriptPath = resolveFn(MATRIX_CRYPTO_DOWNLOAD_HELPER); + params.log?.("matrix: crypto runtime missing; downloading platform library…"); + const result = await runCommand({ + argv: [nodeExecutable, scriptPath], + cwd: path.dirname(scriptPath), + timeoutMs: 300_000, + env: { COREPACK_ENABLE_DOWNLOAD_PROMPT: "0" }, + }); + if (result.code !== 0) { + throw new Error(`Matrix crypto runtime bootstrap failed: ${formatCommandError(result)}`); + } + + try { + requireFn(MATRIX_SDK_PACKAGE); + } catch (err) { + throw new Error( + `Matrix crypto runtime remains unavailable after bootstrap: ${err instanceof Error ? err.message : String(err)}`, + ); + } +} + export async function ensureMatrixSdkInstalled(params: { runtime: RuntimeEnv; confirm?: (message: string) => Promise;