mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:40:44 +00:00
fix(doctor): repair phantom configured plugin installs
This commit is contained in:
@@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Channels/setup: label installable channel picker hints as remote npm installs and hide remote install hints for bundled plugins that already ship with OpenClaw.
|
||||
- CLI/plugins: stop treating the non-plugin `auth` command root as a bundled plugin id, so restrictive `plugins.allow` configs no longer tell users to add stale `auth` plugin entries.
|
||||
- Doctor/plugins: update configured plugin installs whose stale manifests still declare channels without `channelConfigs`, so beta upgrades repair old Discord-style package payloads during `doctor --fix`.
|
||||
- Doctor/plugins: repair configured external plugin installs whose persisted install record points at a missing package directory, so upgrades reconcile phantom npm metadata before plugin runtime validation. Thanks @vincentkoc.
|
||||
- Active Memory: keep non-empty `memory_search` results from being fast-failed as empty when debug telemetry reports zero hits.
|
||||
- Plugins/externalization: repair missing configured plugin installs from npm by default, reserve ClawHub downloads for explicit `clawhubSpec` metadata, and cover agent-runtime/env-selected plugin repair. Thanks @vincentkoc.
|
||||
- Plugins/install: allow official catalog-matched npm channel plugins such as Feishu to pass the trusted install scanner path while keeping spoofed package names blocked. Thanks @vincentkoc.
|
||||
|
||||
@@ -791,6 +791,79 @@ describe("repairMissingConfiguredPluginInstalls", () => {
|
||||
expect(result.changes).toEqual(['Repaired missing configured plugin "demo".']);
|
||||
});
|
||||
|
||||
it("reinstalls a known configured plugin when its recorded install path is missing", async () => {
|
||||
const records = {
|
||||
discord: {
|
||||
source: "npm",
|
||||
spec: "@openclaw/discord",
|
||||
installPath: "/tmp/openclaw-missing-discord-install-record",
|
||||
},
|
||||
};
|
||||
mocks.loadInstalledPluginIndexInstallRecords.mockResolvedValue(records);
|
||||
mocks.loadPluginMetadataSnapshot.mockReturnValue({
|
||||
plugins: [
|
||||
{
|
||||
id: "discord",
|
||||
channels: ["discord"],
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
});
|
||||
mocks.updateNpmInstalledPlugins.mockResolvedValue({
|
||||
changed: true,
|
||||
config: {
|
||||
plugins: {
|
||||
installs: {
|
||||
discord: {
|
||||
source: "npm",
|
||||
spec: "@openclaw/discord",
|
||||
installPath: "/tmp/openclaw-plugins/discord",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: [
|
||||
{
|
||||
pluginId: "discord",
|
||||
status: "updated",
|
||||
message: "Updated discord.",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { repairMissingConfiguredPluginInstalls } =
|
||||
await import("./missing-configured-plugin-install.js");
|
||||
const result = await repairMissingConfiguredPluginInstalls({
|
||||
cfg: {
|
||||
plugins: {
|
||||
entries: {
|
||||
discord: { enabled: true },
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
discord: { enabled: true },
|
||||
},
|
||||
},
|
||||
env: {},
|
||||
});
|
||||
|
||||
expect(mocks.updateNpmInstalledPlugins).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
pluginIds: ["discord"],
|
||||
config: expect.objectContaining({
|
||||
plugins: expect.objectContaining({ installs: records }),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(mocks.writePersistedInstalledPluginIndexInstallRecords).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
discord: expect.objectContaining({ installPath: "/tmp/openclaw-plugins/discord" }),
|
||||
}),
|
||||
{ env: {} },
|
||||
);
|
||||
expect(result.changes).toEqual(['Repaired missing configured plugin "discord".']);
|
||||
});
|
||||
|
||||
it("updates a configured plugin when its installed manifest lacks channel config descriptors", async () => {
|
||||
const records = {
|
||||
discord: {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import {
|
||||
listExplicitlyDisabledChannelIdsForConfig,
|
||||
listPotentialConfiguredChannelIds,
|
||||
@@ -24,6 +26,7 @@ import type { PluginMetadataSnapshot } from "../../../plugins/plugin-metadata-sn
|
||||
import { resolveProviderInstallCatalogEntries } from "../../../plugins/provider-install-catalog.js";
|
||||
import { updateNpmInstalledPlugins } from "../../../plugins/update.js";
|
||||
import { resolveWebSearchInstallCatalogEntry } from "../../../plugins/web-search-install-catalog.js";
|
||||
import { resolveUserPath } from "../../../utils.js";
|
||||
import { asObjectRecord } from "./object.js";
|
||||
|
||||
type DownloadableInstallCandidate = {
|
||||
@@ -290,6 +293,18 @@ function collectConfiguredPluginIdsWithMissingChannelConfigDescriptors(params: {
|
||||
return stalePluginIds;
|
||||
}
|
||||
|
||||
function isInstalledRecordMissingOnDisk(
|
||||
record: PluginInstallRecord | undefined,
|
||||
env: NodeJS.ProcessEnv,
|
||||
): boolean {
|
||||
const installPath = record?.installPath?.trim();
|
||||
if (!installPath) {
|
||||
return true;
|
||||
}
|
||||
const resolved = resolveUserPath(installPath, env);
|
||||
return !existsSync(path.join(resolved, "package.json"));
|
||||
}
|
||||
|
||||
async function installCandidate(params: {
|
||||
candidate: DownloadableInstallCandidate;
|
||||
records: Record<string, PluginInstallRecord>;
|
||||
@@ -445,7 +460,8 @@ async function repairMissingPluginInstalls(params: {
|
||||
const records = await loadInstalledPluginIndexInstallRecords({ env });
|
||||
const missingRecordedPluginIds = Object.keys(records).filter(
|
||||
(pluginId) =>
|
||||
(params.pluginIds.has(pluginId) && !knownIds.has(pluginId)) ||
|
||||
(params.pluginIds.has(pluginId) &&
|
||||
(!knownIds.has(pluginId) || isInstalledRecordMissingOnDisk(records[pluginId], env))) ||
|
||||
configuredPluginIdsWithStaleDescriptors.has(pluginId),
|
||||
);
|
||||
const changes: string[] = [];
|
||||
|
||||
Reference in New Issue
Block a user