fix(plugins): repair incomplete runtime-deps mirrors

This commit is contained in:
Peter Steinberger
2026-04-30 03:46:04 +01:00
parent c403ea9063
commit df4faac71f
3 changed files with 83 additions and 8 deletions

View File

@@ -31,6 +31,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Plugins/runtime-deps: verify staged package entry files before reusing mirrored runtime roots, so browser-control repairs incomplete `ajv`/MCP SDK installs after update instead of failing after restart on a missing `ajv/dist/ajv.js`. Refs #74630. Thanks @spickeringlr.
- Channels/Feishu: retry file-typed iOS video resource downloads as `media` after a Feishu/Lark HTTP 502 and preserve the original 502 when the fallback also fails. Fixes #49855; carries forward #50164 and #73986. Thanks @alex-xuweilong.
- Providers/Amazon Bedrock: expose the full Claude Opus 4.7 thinking profile (`xhigh`, `adaptive`, and `max`) for Bedrock model refs, while keeping Opus/Sonnet 4.6 on adaptive-by-default, so `/think` menus and validation match the Anthropic transport behavior. Fixes #74701. Thanks @prasad-yashdeep, @sparkleHazard, @Sanjays2402, and @hclsys.
- Plugins/tokenjuice: compile the bundled plugin against tokenjuice 0.7.0's published OpenClaw host types instead of a local compatibility shim, so package contract drift fails in OpenClaw validation before release. Thanks @vincentkoc.

View File

@@ -52,27 +52,49 @@ function sameRuntimeDepSpecs(left: readonly string[], right: readonly string[]):
);
}
function readInstalledRuntimeDepVersion(rootDir: string, depName: string): string | null {
function readInstalledRuntimeDepPackage(
rootDir: string,
depName: string,
): { packageDir: string; packageJson: JsonObject } | null {
try {
const parsed = JSON.parse(
fs.readFileSync(resolveDependencySentinelAbsolutePath(rootDir, depName), "utf8"),
) as unknown;
const packageJsonPath = resolveDependencySentinelAbsolutePath(rootDir, depName);
const parsed = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as unknown;
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
return null;
}
const version = (parsed as JsonObject).version;
return typeof version === "string" && version.trim() ? version.trim() : null;
return { packageDir: path.dirname(packageJsonPath), packageJson: parsed as JsonObject };
} catch {
return null;
}
}
function hasInstalledRuntimeDepEntryFiles(packageDir: string, packageJson: JsonObject): boolean {
const main = packageJson.main;
if (typeof main !== "string" || main.trim() === "") {
return true;
}
const mainPath = path.resolve(packageDir, main);
if (mainPath !== packageDir && !mainPath.startsWith(`${packageDir}${path.sep}`)) {
return false;
}
return fs.existsSync(mainPath);
}
export function isRuntimeDepSatisfied(
rootDir: string,
dep: { name: string; version: string },
): boolean {
const installedVersion = readInstalledRuntimeDepVersion(rootDir, dep.name);
return Boolean(installedVersion && satisfies(installedVersion, dep.version));
const installed = readInstalledRuntimeDepPackage(rootDir, dep.name);
if (!installed) {
return false;
}
const version = installed.packageJson.version;
return Boolean(
typeof version === "string" &&
version.trim() &&
satisfies(version.trim(), dep.version) &&
hasInstalledRuntimeDepEntryFiles(installed.packageDir, installed.packageJson),
);
}
export function isRuntimeDepSatisfiedInAnyRoot(

View File

@@ -3198,6 +3198,58 @@ describe("ensureBundledPluginRuntimeDeps", () => {
expect(installRoot).not.toBe(pluginRoot);
});
it("repairs package-level mirrors when an installed package entry file is missing", () => {
const packageRoot = makeTempDir();
const stageDir = makeTempDir();
fs.writeFileSync(
path.join(packageRoot, "package.json"),
JSON.stringify({
name: "openclaw",
version: "2026.4.27",
dependencies: { ajv: "8.20.0" },
openclaw: {
bundle: {
mirroredRootRuntimeDependencies: ["ajv"],
},
},
}),
);
const pluginRoot = writeBundledPluginPackage({
packageRoot,
pluginId: "browser",
deps: {},
enabledByDefault: true,
});
const env = { OPENCLAW_PLUGIN_STAGE_DIR: stageDir };
const installRoot = resolveBundledRuntimeDependencyInstallRoot(pluginRoot, { env });
writeGeneratedRuntimeDepsManifest(installRoot, ["ajv@8.20.0"]);
const ajvRoot = path.join(installRoot, "node_modules", "ajv");
fs.mkdirSync(ajvRoot, { recursive: true });
fs.writeFileSync(
path.join(ajvRoot, "package.json"),
JSON.stringify({ name: "ajv", version: "8.20.0", main: "dist/ajv.js" }),
);
const calls: BundledRuntimeDepsInstallParams[] = [];
const result = ensureBundledPluginRuntimeDeps({
env,
pluginId: "browser",
pluginRoot,
installDeps: (params) => {
calls.push(params);
},
});
expect(result.installedSpecs).toEqual(["ajv@8.20.0"]);
expect(calls).toEqual([
{
installRoot,
missingSpecs: ["ajv@8.20.0"],
installSpecs: ["ajv@8.20.0"],
},
]);
});
it("mirrors sqlite-vec into the packaged default memory runtime deps", () => {
const packageRoot = makeTempDir();
fs.writeFileSync(