mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:00:42 +00:00
fix(plugins): emit actionable install hint for externalized channel plugins (#77502)
Fixes #77483.\n\n- Suggest catalog-backed install commands for missing official external plugins in config validation.\n- Preserve stale/remove wording for non-catalog missing plugins.\n- Add regression coverage for plugins.entries and plugins.allow warnings.\n\nVerification:\n- pnpm exec oxfmt --check --threads=1 CHANGELOG.md src/config/validation.ts src/config/config.plugin-validation.test.ts\n- pnpm test src/config/config.plugin-validation.test.ts src/commands/doctor/shared/missing-configured-plugin-install.test.ts\n- pnpm crabbox:run -- --provider blacksmith-testbox ... pnpm check:changed\n- GitHub CI green on d1b1b10444
This commit is contained in:
@@ -253,6 +253,44 @@ describe("config plugin validation", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("reports catalog install hints for missing configured official external plugins", async () => {
|
||||
const res = validateConfigObjectWithPlugins(
|
||||
{
|
||||
agents: { list: [{ id: "pi" }] },
|
||||
plugins: {
|
||||
entries: { brave: { enabled: true } },
|
||||
allow: ["brave"],
|
||||
},
|
||||
},
|
||||
{
|
||||
env: suiteEnv(),
|
||||
pluginMetadataSnapshot: {
|
||||
manifestRegistry: {
|
||||
plugins: [],
|
||||
diagnostics: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(res.ok).toBe(true);
|
||||
const message =
|
||||
"plugin not installed: brave — install the official external plugin with: openclaw plugins install @openclaw/brave-plugin";
|
||||
expect(res.warnings ?? []).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ path: "plugins.entries.brave", message },
|
||||
{ path: "plugins.allow", message },
|
||||
]),
|
||||
);
|
||||
expect(
|
||||
(res.warnings ?? []).some(
|
||||
(warning) =>
|
||||
(warning.path === "plugins.entries.brave" || warning.path === "plugins.allow") &&
|
||||
warning.message.includes("remove it from plugins config"),
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it.runIf(process.platform !== "win32")(
|
||||
"reports configured blocked plugins without stale not-found wording",
|
||||
async () => {
|
||||
@@ -493,7 +531,7 @@ describe("config plugin validation", () => {
|
||||
expect(res.warnings ?? []).toContainEqual({
|
||||
path: "plugins.allow",
|
||||
message:
|
||||
"plugin not found: discord (stale config entry ignored; remove it from plugins config)",
|
||||
"plugin not installed: discord — install the official external plugin with: openclaw plugins install @openclaw/discord",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -12,6 +12,10 @@ 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,
|
||||
} from "../plugins/official-external-plugin-catalog.js";
|
||||
import {
|
||||
loadPluginMetadataSnapshot,
|
||||
type PluginMetadataSnapshot,
|
||||
@@ -93,6 +97,22 @@ function formatConfigPath(segments: readonly ConfigPathSegment[]): string {
|
||||
return segments.join(".");
|
||||
}
|
||||
|
||||
function formatMissingOfficialExternalPluginWarning(pluginId: string): string | null {
|
||||
const catalogEntry = getOfficialExternalPluginCatalogEntry(pluginId);
|
||||
if (!catalogEntry) {
|
||||
return null;
|
||||
}
|
||||
const install = resolveOfficialExternalPluginInstall(catalogEntry);
|
||||
const npmSpec = install?.npmSpec?.trim();
|
||||
const clawhubSpec = install?.clawhubSpec?.trim();
|
||||
const installSpec =
|
||||
install?.defaultChoice === "clawhub" ? (clawhubSpec ?? npmSpec) : (npmSpec ?? clawhubSpec);
|
||||
if (!installSpec) {
|
||||
return null;
|
||||
}
|
||||
return `plugin not installed: ${pluginId} — install the official external plugin with: openclaw plugins install ${installSpec}`;
|
||||
}
|
||||
|
||||
function asJsonSchemaLike(value: unknown): JsonSchemaLike | null {
|
||||
return value && typeof value === "object" ? (value as JsonSchemaLike) : null;
|
||||
}
|
||||
@@ -1441,6 +1461,16 @@ function validateConfigObjectWithPluginsBase(
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (opts?.warnOnly) {
|
||||
const externalInstallWarning = formatMissingOfficialExternalPluginWarning(pluginId);
|
||||
if (externalInstallWarning) {
|
||||
warnings.push({
|
||||
path,
|
||||
message: externalInstallWarning,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (opts?.warnOnly) {
|
||||
warnings.push({
|
||||
path,
|
||||
|
||||
Reference in New Issue
Block a user