mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:40:44 +00:00
fix: repair stale configured channel plugins
This commit is contained in:
@@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai
|
||||
### Fixes
|
||||
|
||||
- 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`.
|
||||
- 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.
|
||||
- Upgrade/config: validate configured web-search providers and statically suppressed model/provider pairs against the active plugin set at config load, so stale plugin state fails loud before runtime fallback.
|
||||
- Status/update: resolve beta update-channel checks from the installed version when config still says `stable`, and let `status --deep` reuse live gateway channel credential state instead of warning on command-path-only token misses.
|
||||
|
||||
@@ -791,6 +791,101 @@ describe("repairMissingConfiguredPluginInstalls", () => {
|
||||
expect(result.changes).toEqual(['Repaired missing configured plugin "demo".']);
|
||||
});
|
||||
|
||||
it("updates a configured plugin when its installed manifest lacks channel config descriptors", async () => {
|
||||
const records = {
|
||||
discord: {
|
||||
source: "npm",
|
||||
spec: "@openclaw/discord",
|
||||
installPath: "/tmp/openclaw-plugins/discord",
|
||||
},
|
||||
};
|
||||
mocks.loadInstalledPluginIndexInstallRecords.mockResolvedValue(records);
|
||||
mocks.listChannelPluginCatalogEntries.mockReturnValue([
|
||||
{
|
||||
id: "discord",
|
||||
pluginId: "discord",
|
||||
meta: { label: "Discord" },
|
||||
install: {
|
||||
npmSpec: "@openclaw/discord",
|
||||
},
|
||||
},
|
||||
]);
|
||||
mocks.loadPluginMetadataSnapshot.mockReturnValue({
|
||||
plugins: [
|
||||
{
|
||||
id: "discord",
|
||||
channels: ["discord"],
|
||||
},
|
||||
],
|
||||
diagnostics: [
|
||||
{
|
||||
level: "warn",
|
||||
pluginId: "discord",
|
||||
message:
|
||||
"channel plugin manifest declares discord without channelConfigs metadata; add openclaw.plugin.json#channelConfigs so config schema and setup surfaces work before runtime loads",
|
||||
},
|
||||
],
|
||||
});
|
||||
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: {
|
||||
update: { channel: "beta" },
|
||||
plugins: {
|
||||
entries: {
|
||||
discord: { enabled: true },
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
discord: { enabled: true },
|
||||
},
|
||||
},
|
||||
env: {},
|
||||
});
|
||||
|
||||
expect(mocks.updateNpmInstalledPlugins).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
pluginIds: ["discord"],
|
||||
updateChannel: "beta",
|
||||
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).toEqual({
|
||||
changes: ['Repaired missing configured plugin "discord".'],
|
||||
warnings: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("reinstalls a recorded external web search plugin from provider-only config", async () => {
|
||||
const records = {
|
||||
brave: {
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
resolveOfficialExternalPluginInstall,
|
||||
resolveOfficialExternalPluginLabel,
|
||||
} from "../../../plugins/official-external-plugin-catalog.js";
|
||||
import type { PluginMetadataSnapshot } from "../../../plugins/plugin-metadata-snapshot.types.js";
|
||||
import { resolveProviderInstallCatalogEntries } from "../../../plugins/provider-install-catalog.js";
|
||||
import { updateNpmInstalledPlugins } from "../../../plugins/update.js";
|
||||
import { resolveWebSearchInstallCatalogEntry } from "../../../plugins/web-search-install-catalog.js";
|
||||
@@ -48,6 +49,8 @@ const RUNTIME_PLUGIN_INSTALL_CANDIDATES: readonly DownloadableInstallCandidate[]
|
||||
},
|
||||
];
|
||||
|
||||
const MISSING_CHANNEL_CONFIG_DESCRIPTOR_DIAGNOSTIC = "without channelConfigs metadata";
|
||||
|
||||
function shouldFallbackClawHubToNpm(result: { ok: false; code?: string }): boolean {
|
||||
return (
|
||||
result.code === CLAWHUB_INSTALL_ERROR_CODE.PACKAGE_NOT_FOUND ||
|
||||
@@ -264,6 +267,29 @@ function collectDownloadableInstallCandidates(params: {
|
||||
);
|
||||
}
|
||||
|
||||
function collectConfiguredPluginIdsWithMissingChannelConfigDescriptors(params: {
|
||||
snapshot: PluginMetadataSnapshot;
|
||||
configuredPluginIds: ReadonlySet<string>;
|
||||
configuredChannelIds: ReadonlySet<string>;
|
||||
}): Set<string> {
|
||||
const stalePluginIds = new Set<string>();
|
||||
const pluginsById = new Map(params.snapshot.plugins.map((plugin) => [plugin.id, plugin]));
|
||||
for (const diagnostic of params.snapshot.diagnostics) {
|
||||
const pluginId = diagnostic.pluginId?.trim();
|
||||
if (!pluginId || !diagnostic.message.includes(MISSING_CHANNEL_CONFIG_DESCRIPTOR_DIAGNOSTIC)) {
|
||||
continue;
|
||||
}
|
||||
const plugin = pluginsById.get(pluginId);
|
||||
const ownsConfiguredChannel = plugin?.channels.some((channelId) =>
|
||||
params.configuredChannelIds.has(channelId),
|
||||
);
|
||||
if (params.configuredPluginIds.has(pluginId) || ownsConfiguredChannel) {
|
||||
stalePluginIds.add(pluginId);
|
||||
}
|
||||
}
|
||||
return stalePluginIds;
|
||||
}
|
||||
|
||||
async function installCandidate(params: {
|
||||
candidate: DownloadableInstallCandidate;
|
||||
records: Record<string, PluginInstallRecord>;
|
||||
@@ -405,15 +431,22 @@ async function repairMissingPluginInstalls(params: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): Promise<{ changes: string[]; warnings: string[] }> {
|
||||
const env = params.env ?? process.env;
|
||||
const knownIds = new Set(
|
||||
loadManifestMetadataSnapshot({
|
||||
config: params.cfg,
|
||||
env,
|
||||
}).plugins.map((plugin) => plugin.id),
|
||||
);
|
||||
const snapshot = loadManifestMetadataSnapshot({
|
||||
config: params.cfg,
|
||||
env,
|
||||
});
|
||||
const knownIds = new Set(snapshot.plugins.map((plugin) => plugin.id));
|
||||
const configuredPluginIdsWithStaleDescriptors =
|
||||
collectConfiguredPluginIdsWithMissingChannelConfigDescriptors({
|
||||
snapshot,
|
||||
configuredPluginIds: params.pluginIds,
|
||||
configuredChannelIds: params.channelIds,
|
||||
});
|
||||
const records = await loadInstalledPluginIndexInstallRecords({ env });
|
||||
const missingRecordedPluginIds = Object.keys(records).filter(
|
||||
(pluginId) => params.pluginIds.has(pluginId) && !knownIds.has(pluginId),
|
||||
(pluginId) =>
|
||||
(params.pluginIds.has(pluginId) && !knownIds.has(pluginId)) ||
|
||||
configuredPluginIdsWithStaleDescriptors.has(pluginId),
|
||||
);
|
||||
const changes: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
@@ -429,6 +462,7 @@ async function repairMissingPluginInstalls(params: {
|
||||
},
|
||||
},
|
||||
pluginIds: missingRecordedPluginIds,
|
||||
updateChannel: params.cfg.update?.channel,
|
||||
logger: {
|
||||
warn: (message) => warnings.push(message),
|
||||
error: (message) => warnings.push(message),
|
||||
|
||||
Reference in New Issue
Block a user