fix(plugins): prefer newest official prerelease install

This commit is contained in:
Vincent Koc
2026-05-03 16:24:48 -07:00
parent b520e40cf6
commit cde9591168
3 changed files with 48 additions and 21 deletions

View File

@@ -34,7 +34,7 @@ Docs: https://docs.openclaw.ai
- Plugins/catalog: merge official external catalog descriptors into partial package channel config metadata, so lagging WeCom/Yuanbao manifests keep their own schema while still exposing host-supplied labels and setup text. Thanks @vincentkoc.
- Plugins/catalog: supplement lagging official external WeCom and Yuanbao npm manifests with channel config descriptors and declared tool contracts from the OpenClaw catalog, so trusted package sweeps no longer fail because external package metadata trails the host contract. Thanks @vincentkoc.
- Plugins/install: let trusted official `@openclaw/*` catalog installs recover when npm `latest` points at a prerelease by falling back to the newest stable version, or by allowing prerelease-only launch packages with a warning instead of making beta/development plugin sweeps fail at install time. Thanks @vincentkoc.
- Plugins/install: let trusted official `@openclaw/*` catalog installs recover when npm `latest` points at a prerelease by falling back to the newest stable version, or by selecting the newest exact prerelease for prerelease-only launch packages with a warning instead of making beta/development plugin sweeps fail at install time. Thanks @vincentkoc.
- Google Meet: grant Chrome media permissions against the actual Meet tab, start the local realtime audio bridge only after Meet joins, expose realtime transcripts in status/logs, and force explicit audio responses with current OpenAI realtime output-audio events so BlackHole capture does not keep the OpenClaw participant muted or silent.
- Google Meet: use the local call-control microphone button instead of disabled remote participant mute buttons, and block realtime speech when the OpenClaw Meet microphone remains muted.
- Google Meet: refresh realtime browser state during status and retry delayed speech after Meet finishes joining, so a just-opened in-call tab no longer leaves speech stuck behind stale `not-in-call` health.

View File

@@ -938,15 +938,24 @@ describe("installPluginFromNpmSpec", () => {
runCommandWithTimeoutMock.mockReset();
const prereleaseOnlyNpmRoot = path.join(suiteTempRootTracker.makeTempDir(), "npm");
const prereleaseOnlyWarnings: string[] = [];
mockNpmViewAndInstall({
spec: "@openclaw/voice-call",
packageName: "@openclaw/voice-call",
version: "0.0.2-beta.1",
pluginId: "voice-call",
npmRoot: prereleaseOnlyNpmRoot,
versions: ["0.0.2-beta.1"],
expectedDependencySpec: "0.0.2-beta.1",
});
mockNpmViewAndInstallMany([
{
spec: "@openclaw/voice-call",
packageName: "@openclaw/voice-call",
version: "0.0.1-beta.1",
pluginId: "voice-call",
npmRoot: prereleaseOnlyNpmRoot,
versions: ["0.0.1-beta.1", "0.0.2-beta.1"],
},
{
spec: "@openclaw/voice-call@0.0.2-beta.1",
packageName: "@openclaw/voice-call",
version: "0.0.2-beta.1",
pluginId: "voice-call",
npmRoot: prereleaseOnlyNpmRoot,
expectedDependencySpec: "0.0.2-beta.1",
},
]);
const prereleaseOnly = await installPluginFromNpmSpec({
spec: "@openclaw/voice-call",
@@ -963,7 +972,11 @@ describe("installPluginFromNpmSpec", () => {
return;
}
expect(prereleaseOnly.npmResolution?.version).toBe("0.0.2-beta.1");
expect(prereleaseOnly.npmResolution?.resolvedSpec).toBe("@openclaw/voice-call@0.0.2-beta.1");
expect(prereleaseOnlyWarnings.join("\n")).toContain("has no stable npm versions yet");
expect(prereleaseOnlyWarnings.join("\n")).toContain(
"using newest prerelease @openclaw/voice-call@0.0.2-beta.1",
);
runCommandWithTimeoutMock.mockReset();
const npmRoot = path.join(suiteTempRootTracker.makeTempDir(), "npm");

View File

@@ -26,6 +26,7 @@ import {
createSafeNpmInstallArgs,
createSafeNpmInstallEnv,
} from "../infra/safe-package-install.js";
import { compareComparableSemver, parseComparableSemver } from "../infra/semver-compare.js";
import { runCommandWithTimeout } from "../process/exec.js";
import { createLazyImportLoader } from "../shared/lazy-promise.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
@@ -160,18 +161,13 @@ function isNpmPackageNotFoundMessage(error: string): boolean {
return /E404|404 not found|not in this registry/i.test(normalized);
}
function compareStableSemver(a: string, b: string): number {
const parse = (value: string): [number, number, number] => {
const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(value.trim());
return [Number(match?.[1] ?? 0), Number(match?.[2] ?? 0), Number(match?.[3] ?? 0)];
};
const left = parse(a);
const right = parse(b);
return left[0] - right[0] || left[1] - right[1] || left[2] - right[2];
function compareNpmSemver(a: string, b: string): number {
return compareComparableSemver(parseComparableSemver(a), parseComparableSemver(b)) ?? 0;
}
type TrustedOfficialPrereleaseResolution =
| { kind: "stable"; resolution: NpmSpecResolution }
| { kind: "prerelease-only"; resolution: NpmSpecResolution }
| { kind: "allow-prerelease-only" };
async function resolveTrustedOfficialPrereleaseResolution(params: {
@@ -208,10 +204,28 @@ async function resolveTrustedOfficialPrereleaseResolution(params: {
);
const stableVersion = semverVersions
.filter((value) => !isPrereleaseSemverVersion(value))
.toSorted(compareStableSemver)
.toSorted(compareNpmSemver)
.at(-1);
if (!stableVersion) {
if (semverVersions.length > 0 && semverVersions.every(isPrereleaseSemverVersion)) {
const prereleaseVersion = semverVersions
.filter(isPrereleaseSemverVersion)
.toSorted(compareNpmSemver)
.at(-1);
if (prereleaseVersion && semverVersions.every(isPrereleaseSemverVersion)) {
if (prereleaseVersion !== params.resolvedPrereleaseVersion) {
const prereleaseSpec = `${params.spec.name}@${prereleaseVersion}`;
const metadataResult = await resolveNpmSpecMetadata({
spec: prereleaseSpec,
timeoutMs: params.timeoutMs,
});
if (!metadataResult.ok) {
return null;
}
params.logger.warn?.(
`Resolved ${params.spec.raw} to prerelease version ${params.resolvedPrereleaseVersion}; using newest prerelease ${prereleaseSpec} because this trusted official OpenClaw package has no stable npm versions yet.`,
);
return { kind: "prerelease-only", resolution: metadataResult.metadata };
}
params.logger.warn?.(
`Resolved ${params.spec.raw} to prerelease version ${params.resolvedPrereleaseVersion}; allowing it because this trusted official OpenClaw package has no stable npm versions yet.`,
);
@@ -1265,7 +1279,7 @@ export async function installPluginFromNpmSpec(
logger,
})
: null;
if (trustedResolution?.kind === "stable") {
if (trustedResolution?.kind === "stable" || trustedResolution?.kind === "prerelease-only") {
Object.assign(npmResolution, trustedResolution.resolution, {
resolvedAt: npmResolution.resolvedAt,
});