mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 07:01:24 +00:00
fix(plugins): fallback bundled channel specs when npm install returns 404 (#12849)
* plugins: add bundled source resolver * plugins: add bundled source resolver tests * cli: fallback npm 404 plugin installs to bundled sources * plugins: use bundled source resolver during updates * protocol: regenerate macos gateway swift models * protocol: regenerate shared swift models * Revert "protocol: regenerate shared swift models" This reverts commit6a2b08c47d. * Revert "protocol: regenerate macos gateway swift models" This reverts commit27c03010c6.
This commit is contained in:
97
src/plugins/bundled-sources.test.ts
Normal file
97
src/plugins/bundled-sources.test.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { findBundledPluginByNpmSpec, resolveBundledPluginSources } from "./bundled-sources.js";
|
||||
|
||||
const discoverOpenClawPluginsMock = vi.fn();
|
||||
const loadPluginManifestMock = vi.fn();
|
||||
|
||||
vi.mock("./discovery.js", () => ({
|
||||
discoverOpenClawPlugins: (...args: unknown[]) => discoverOpenClawPluginsMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("./manifest.js", () => ({
|
||||
loadPluginManifest: (...args: unknown[]) => loadPluginManifestMock(...args),
|
||||
}));
|
||||
|
||||
describe("bundled plugin sources", () => {
|
||||
beforeEach(() => {
|
||||
discoverOpenClawPluginsMock.mockReset();
|
||||
loadPluginManifestMock.mockReset();
|
||||
});
|
||||
|
||||
it("resolves bundled sources keyed by plugin id", () => {
|
||||
discoverOpenClawPluginsMock.mockReturnValue({
|
||||
candidates: [
|
||||
{
|
||||
origin: "global",
|
||||
rootDir: "/global/feishu",
|
||||
packageName: "@openclaw/feishu",
|
||||
packageManifest: { install: { npmSpec: "@openclaw/feishu" } },
|
||||
},
|
||||
{
|
||||
origin: "bundled",
|
||||
rootDir: "/app/extensions/feishu",
|
||||
packageName: "@openclaw/feishu",
|
||||
packageManifest: { install: { npmSpec: "@openclaw/feishu" } },
|
||||
},
|
||||
{
|
||||
origin: "bundled",
|
||||
rootDir: "/app/extensions/feishu-dup",
|
||||
packageName: "@openclaw/feishu",
|
||||
packageManifest: { install: { npmSpec: "@openclaw/feishu" } },
|
||||
},
|
||||
{
|
||||
origin: "bundled",
|
||||
rootDir: "/app/extensions/msteams",
|
||||
packageName: "@openclaw/msteams",
|
||||
packageManifest: { install: { npmSpec: "@openclaw/msteams" } },
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
});
|
||||
|
||||
loadPluginManifestMock.mockImplementation((rootDir: string) => {
|
||||
if (rootDir === "/app/extensions/feishu") {
|
||||
return { ok: true, manifest: { id: "feishu" } };
|
||||
}
|
||||
if (rootDir === "/app/extensions/msteams") {
|
||||
return { ok: true, manifest: { id: "msteams" } };
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
error: "invalid manifest",
|
||||
manifestPath: `${rootDir}/openclaw.plugin.json`,
|
||||
};
|
||||
});
|
||||
|
||||
const map = resolveBundledPluginSources({});
|
||||
|
||||
expect(Array.from(map.keys())).toEqual(["feishu", "msteams"]);
|
||||
expect(map.get("feishu")).toEqual({
|
||||
pluginId: "feishu",
|
||||
localPath: "/app/extensions/feishu",
|
||||
npmSpec: "@openclaw/feishu",
|
||||
});
|
||||
});
|
||||
|
||||
it("finds bundled source by npm spec", () => {
|
||||
discoverOpenClawPluginsMock.mockReturnValue({
|
||||
candidates: [
|
||||
{
|
||||
origin: "bundled",
|
||||
rootDir: "/app/extensions/feishu",
|
||||
packageName: "@openclaw/feishu",
|
||||
packageManifest: { install: { npmSpec: "@openclaw/feishu" } },
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
});
|
||||
loadPluginManifestMock.mockReturnValue({ ok: true, manifest: { id: "feishu" } });
|
||||
|
||||
const resolved = findBundledPluginByNpmSpec({ spec: "@openclaw/feishu" });
|
||||
const missing = findBundledPluginByNpmSpec({ spec: "@openclaw/not-found" });
|
||||
|
||||
expect(resolved?.pluginId).toBe("feishu");
|
||||
expect(resolved?.localPath).toBe("/app/extensions/feishu");
|
||||
expect(missing).toBeUndefined();
|
||||
});
|
||||
});
|
||||
59
src/plugins/bundled-sources.ts
Normal file
59
src/plugins/bundled-sources.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { discoverOpenClawPlugins } from "./discovery.js";
|
||||
import { loadPluginManifest } from "./manifest.js";
|
||||
|
||||
export type BundledPluginSource = {
|
||||
pluginId: string;
|
||||
localPath: string;
|
||||
npmSpec?: string;
|
||||
};
|
||||
|
||||
export function resolveBundledPluginSources(params: {
|
||||
workspaceDir?: string;
|
||||
}): Map<string, BundledPluginSource> {
|
||||
const discovery = discoverOpenClawPlugins({ workspaceDir: params.workspaceDir });
|
||||
const bundled = new Map<string, BundledPluginSource>();
|
||||
|
||||
for (const candidate of discovery.candidates) {
|
||||
if (candidate.origin !== "bundled") {
|
||||
continue;
|
||||
}
|
||||
const manifest = loadPluginManifest(candidate.rootDir);
|
||||
if (!manifest.ok) {
|
||||
continue;
|
||||
}
|
||||
const pluginId = manifest.manifest.id;
|
||||
if (bundled.has(pluginId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const npmSpec =
|
||||
candidate.packageManifest?.install?.npmSpec?.trim() ||
|
||||
candidate.packageName?.trim() ||
|
||||
undefined;
|
||||
|
||||
bundled.set(pluginId, {
|
||||
pluginId,
|
||||
localPath: candidate.rootDir,
|
||||
npmSpec,
|
||||
});
|
||||
}
|
||||
|
||||
return bundled;
|
||||
}
|
||||
|
||||
export function findBundledPluginByNpmSpec(params: {
|
||||
spec: string;
|
||||
workspaceDir?: string;
|
||||
}): BundledPluginSource | undefined {
|
||||
const targetSpec = params.spec.trim();
|
||||
if (!targetSpec) {
|
||||
return undefined;
|
||||
}
|
||||
const bundled = resolveBundledPluginSources({ workspaceDir: params.workspaceDir });
|
||||
for (const source of bundled.values()) {
|
||||
if (source.npmSpec === targetSpec) {
|
||||
return source;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -4,10 +4,9 @@ import type { OpenClawConfig } from "../config/config.js";
|
||||
import { openBoundaryFileSync } from "../infra/boundary-file-read.js";
|
||||
import type { UpdateChannel } from "../infra/update-channels.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import { discoverOpenClawPlugins } from "./discovery.js";
|
||||
import { resolveBundledPluginSources } from "./bundled-sources.js";
|
||||
import { installPluginFromNpmSpec, resolvePluginInstallDir } from "./install.js";
|
||||
import { buildNpmResolutionInstallFields, recordPluginInstall } from "./installs.js";
|
||||
import { loadPluginManifest } from "./manifest.js";
|
||||
|
||||
export type PluginUpdateLogger = {
|
||||
info?: (message: string) => void;
|
||||
@@ -54,12 +53,6 @@ export type PluginChannelSyncResult = {
|
||||
summary: PluginChannelSyncSummary;
|
||||
};
|
||||
|
||||
type BundledPluginSource = {
|
||||
pluginId: string;
|
||||
localPath: string;
|
||||
npmSpec?: string;
|
||||
};
|
||||
|
||||
type InstallIntegrityDrift = {
|
||||
spec: string;
|
||||
expectedIntegrity: string;
|
||||
@@ -91,40 +84,6 @@ async function readInstalledPackageVersion(dir: string): Promise<string | undefi
|
||||
}
|
||||
}
|
||||
|
||||
function resolveBundledPluginSources(params: {
|
||||
workspaceDir?: string;
|
||||
}): Map<string, BundledPluginSource> {
|
||||
const discovery = discoverOpenClawPlugins({ workspaceDir: params.workspaceDir });
|
||||
const bundled = new Map<string, BundledPluginSource>();
|
||||
|
||||
for (const candidate of discovery.candidates) {
|
||||
if (candidate.origin !== "bundled") {
|
||||
continue;
|
||||
}
|
||||
const manifest = loadPluginManifest(candidate.rootDir);
|
||||
if (!manifest.ok) {
|
||||
continue;
|
||||
}
|
||||
const pluginId = manifest.manifest.id;
|
||||
if (bundled.has(pluginId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const npmSpec =
|
||||
candidate.packageManifest?.install?.npmSpec?.trim() ||
|
||||
candidate.packageName?.trim() ||
|
||||
undefined;
|
||||
|
||||
bundled.set(pluginId, {
|
||||
pluginId,
|
||||
localPath: candidate.rootDir,
|
||||
npmSpec,
|
||||
});
|
||||
}
|
||||
|
||||
return bundled;
|
||||
}
|
||||
|
||||
function pathsEqual(left?: string, right?: string): boolean {
|
||||
if (!left || !right) {
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user