fix(config): suggest official plugin installs

This commit is contained in:
Vincent Koc
2026-05-04 14:28:38 -07:00
parent a4f2bf273a
commit 7bb69e17a2
3 changed files with 70 additions and 1 deletions

View File

@@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai
### Changes
- Plugins/migration: emit catalog-backed install hints when `plugins.entries` or `plugins.allow` references an official external plugin that is not installed, so upgraded configs point operators to `openclaw plugins install <spec>` instead of telling them to remove valid plugin config. (#77483) Thanks @hclsys.
- Plugins/active-memory: skip session-store channel entries that contain `:` when resolving the recall subagent's channel, so QQ c2c agent IDs (e.g. `c2c:10D4F7C2…`) and other scoped conversation IDs do not reach bundled-plugin `dirName` validation and crash the recall run. The same guard already applied to explicit `channelId` params (#76704); this extends it to store-derived channels. (#77396) Thanks @hclsys.
- Models/auth: add `openclaw models auth list [--provider <id>] [--json]` so users can inspect saved per-agent auth profiles without dumping secrets or hitting the old “too many arguments” path. Thanks @vincentkoc.
- Control UI/header: show the active agent name in dashboard breadcrumbs without adding the current session key, keeping non-chat views oriented without crowding the topbar.

View File

@@ -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",
});
});

View File

@@ -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,
@@ -96,6 +100,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;
}
@@ -1496,6 +1516,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,