fix(plugins): allow prerelease-only official packages

This commit is contained in:
Vincent Koc
2026-05-03 16:10:31 -07:00
parent 0a8694c522
commit 3efa82de86
3 changed files with 55 additions and 10 deletions

View File

@@ -32,6 +32,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- 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.
- 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.
- Plugins/install: recover the install ledger from the managed npm root when `plugins/installs.json` is empty or partial, so reinstalling Discord and Codex no longer makes the other installed plugin disappear.

View File

@@ -935,6 +935,36 @@ describe("installPluginFromNpmSpec", () => {
expect(officialFallback.npmResolution?.resolvedSpec).toBe("@openclaw/voice-call@0.0.1");
expect(warnings.join("\n")).toContain("falling back to stable @openclaw/voice-call@0.0.1");
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",
});
const prereleaseOnly = await installPluginFromNpmSpec({
spec: "@openclaw/voice-call",
npmDir: prereleaseOnlyNpmRoot,
expectedPluginId: "voice-call",
trustedSourceLinkedOfficialInstall: true,
logger: {
info: () => {},
warn: (msg: string) => prereleaseOnlyWarnings.push(msg),
},
});
expect(prereleaseOnly.ok).toBe(true);
if (!prereleaseOnly.ok) {
return;
}
expect(prereleaseOnly.npmResolution?.version).toBe("0.0.2-beta.1");
expect(prereleaseOnlyWarnings.join("\n")).toContain("has no stable npm versions yet");
runCommandWithTimeoutMock.mockReset();
const npmRoot = path.join(suiteTempRootTracker.makeTempDir(), "npm");
mockNpmViewAndInstall({

View File

@@ -170,12 +170,16 @@ function compareStableSemver(a: string, b: string): number {
return left[0] - right[0] || left[1] - right[1] || left[2] - right[2];
}
async function resolveTrustedOfficialStableNpmResolution(params: {
type TrustedOfficialPrereleaseResolution =
| { kind: "stable"; resolution: NpmSpecResolution }
| { kind: "allow-prerelease-only" };
async function resolveTrustedOfficialPrereleaseResolution(params: {
spec: ParsedRegistryNpmSpec;
resolvedPrereleaseVersion: string;
timeoutMs: number;
logger: PluginInstallLogger;
}): Promise<NpmSpecResolution | null> {
}): Promise<TrustedOfficialPrereleaseResolution | null> {
if (!params.spec.name.startsWith("@openclaw/")) {
return null;
}
@@ -199,12 +203,20 @@ async function resolveTrustedOfficialStableNpmResolution(params: {
} catch {
return null;
}
const stableVersion = (Array.isArray(parsed) ? parsed : [parsed])
.filter((value): value is string => typeof value === "string")
.filter((value) => isExactSemverVersion(value) && !isPrereleaseSemverVersion(value))
const semverVersions = (Array.isArray(parsed) ? parsed : [parsed]).filter(
(value): value is string => typeof value === "string" && isExactSemverVersion(value),
);
const stableVersion = semverVersions
.filter((value) => !isPrereleaseSemverVersion(value))
.sort(compareStableSemver)
.at(-1);
if (!stableVersion) {
if (semverVersions.length > 0 && semverVersions.every(isPrereleaseSemverVersion)) {
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.`,
);
return { kind: "allow-prerelease-only" };
}
return null;
}
@@ -219,7 +231,7 @@ async function resolveTrustedOfficialStableNpmResolution(params: {
params.logger.warn?.(
`Resolved ${params.spec.raw} to prerelease version ${params.resolvedPrereleaseVersion}; falling back to stable ${stableSpec} for this trusted official OpenClaw install.`,
);
return metadataResult.metadata;
return { kind: "stable", resolution: metadataResult.metadata };
}
function buildFileInstallResult(pluginId: string, targetFile: string): InstallPluginResult {
@@ -1245,18 +1257,20 @@ export async function installPluginFromNpmSpec(
resolvedVersion: npmResolution.version,
})
) {
const stableResolution = params.trustedSourceLinkedOfficialInstall
? await resolveTrustedOfficialStableNpmResolution({
const trustedResolution = params.trustedSourceLinkedOfficialInstall
? await resolveTrustedOfficialPrereleaseResolution({
spec: parsedSpec,
resolvedPrereleaseVersion: npmResolution.version,
timeoutMs,
logger,
})
: null;
if (stableResolution) {
Object.assign(npmResolution, stableResolution, {
if (trustedResolution?.kind === "stable") {
Object.assign(npmResolution, trustedResolution.resolution, {
resolvedAt: npmResolution.resolvedAt,
});
} else if (trustedResolution?.kind === "allow-prerelease-only") {
// Keep the original prerelease resolution. The package has no stable line yet.
} else {
return {
ok: false,