Compare commits

...

1 Commits

Author SHA1 Message Date
Peter Steinberger
ea908ac594 fix(config): improve missing channel plugin diagnostics 2026-05-04 23:10:31 +01:00
3 changed files with 69 additions and 3 deletions

View File

@@ -55,6 +55,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Agents/OpenAI: default direct OpenAI Responses models to the SSE transport instead of WebSocket auto-selection, preventing pi runtime chat turns from hanging on servers where the WebSocket path stalls while the OpenAI HTTP stream works. Thanks @vincentkoc.
- Doctor/config: report missing catalog-backed channel plugin entries as repairable installs, so upgrades with Discord or WhatsApp config point to `openclaw doctor --fix` instead of telling users to remove valid plugin config. Fixes #77483.
- CLI/update: disable and skip plugins that fail package-update plugin sync, so a broken npm/ClawHub/git/marketplace plugin cannot turn a successful OpenClaw package update into a failed update result. Thanks @vincentkoc.
- CLI/update: use an absolute POSIX npm script shell during package-manager updates, so restricted PATH environments can still run dependency lifecycle scripts while updating from `--tag main`. Fixes #77530. Thanks @PeterTremonti.
- Diagnostics: grant the internal diagnostics event bus to official installed diagnostics exporter plugins, so npm-installed `@openclaw/diagnostics-prometheus` can emit metrics without broadening the capability to arbitrary global plugins. Fixes #76628. Thanks @RayWoo.

View File

@@ -497,6 +497,45 @@ describe("config plugin validation", () => {
});
});
it("points missing installable plugin entries at doctor repair", () => {
const res = validateConfigObjectWithPlugins(
{
agents: { list: [{ id: "pi" }] },
channels: {
discord: { token: "xxx" },
whatsapp: {},
},
plugins: {
entries: {
discord: { enabled: true },
whatsapp: { enabled: true },
},
},
},
{
env: suiteEnv(),
pluginMetadataSnapshot: {
manifestRegistry: {
plugins: [],
diagnostics: [],
},
},
},
);
expect(res.ok).toBe(true);
expect(res.warnings ?? []).toContainEqual({
path: "plugins.entries.discord",
message:
"plugin not installed: discord (Discord; run openclaw doctor --fix or openclaw plugins install @openclaw/discord; config preserved)",
});
expect(res.warnings ?? []).toContainEqual({
path: "plugins.entries.whatsapp",
message:
"plugin not installed: whatsapp (WhatsApp; run openclaw doctor --fix or openclaw plugins install @openclaw/whatsapp; config preserved)",
});
});
it("uses persisted installed-plugin records as stale channel evidence", async () => {
const installedPluginIndexPath = path.join(suiteHome, ".openclaw", "plugins", "installs.json");
await mkdirSafe(path.dirname(installedPluginIndexPath));

View File

@@ -12,6 +12,11 @@ import {
import { loadInstalledPluginIndexInstallRecordsSync } from "../plugins/installed-plugin-index-record-reader.js";
import { resolveManifestCommandAliasOwnerInRegistry } from "../plugins/manifest-command-aliases.js";
import type { PluginManifestRegistry } from "../plugins/manifest-registry.js";
import {
getOfficialExternalPluginCatalogEntry,
resolveOfficialExternalPluginInstall,
resolveOfficialExternalPluginLabel,
} from "../plugins/official-external-plugin-catalog.js";
import {
loadPluginMetadataSnapshot,
type PluginMetadataSnapshot,
@@ -1473,10 +1478,23 @@ function validateConfigObjectWithPluginsBase(
blockedDiagnosticSourceMatchesPluginId(diagnostic, pluginId),
);
};
const formatInstallablePluginWarning = (pluginId: string): string | null => {
const catalogEntry = getOfficialExternalPluginCatalogEntry(pluginId);
if (!catalogEntry) {
return null;
}
const install = resolveOfficialExternalPluginInstall(catalogEntry);
const installSpec = install?.npmSpec ?? install?.clawhubSpec;
if (!installSpec) {
return null;
}
const label = resolveOfficialExternalPluginLabel(catalogEntry);
return `plugin not installed: ${pluginId} (${label}; run openclaw doctor --fix or openclaw plugins install ${installSpec}; config preserved)`;
};
const pushMissingPluginIssue = (
path: string,
pluginId: string,
opts?: { warnOnly?: boolean },
opts?: { warnOnly?: boolean; doctorRepairable?: boolean },
) => {
if (LEGACY_REMOVED_PLUGIN_IDS.has(pluginId)) {
warnings.push({
@@ -1497,9 +1515,14 @@ function validateConfigObjectWithPluginsBase(
return;
}
if (opts?.warnOnly) {
const installablePluginWarning = opts.doctorRepairable
? formatInstallablePluginWarning(pluginId)
: null;
warnings.push({
path,
message: `plugin not found: ${pluginId} (stale config entry ignored; remove it from plugins config)`,
message:
installablePluginWarning ??
`plugin not found: ${pluginId} (stale config entry ignored; remove it from plugins config)`,
});
return;
}
@@ -1516,7 +1539,10 @@ function validateConfigObjectWithPluginsBase(
for (const pluginId of Object.keys(entries)) {
if (!knownIds.has(pluginId)) {
// Keep gateway startup resilient when plugins are removed/renamed across upgrades.
pushMissingPluginIssue(`plugins.entries.${pluginId}`, pluginId, { warnOnly: true });
pushMissingPluginIssue(`plugins.entries.${pluginId}`, pluginId, {
warnOnly: true,
doctorRepairable: true,
});
}
}
}