feat(plugins): prefer clawhub for channel setup installs

This commit is contained in:
Vincent Koc
2026-05-02 07:34:36 -07:00
parent 27318663ef
commit 9281eee702
4 changed files with 141 additions and 8 deletions

View File

@@ -7,6 +7,7 @@ Docs: https://docs.openclaw.ai
### Changes
- Gateway/startup: skip plugin-backed auth-profile overlays during startup secrets preflight, reducing gateway readiness latency while keeping reload and OAuth recovery paths overlay-capable. (#68327) Thanks @JIRBOY.
- Plugins/onboarding: carry ClawHub install metadata through channel setup catalogs so missing channel plugins can install from ClawHub before npm/local fallback. Thanks @vincentkoc.
### Fixes

View File

@@ -32,14 +32,15 @@ export type ChannelUiCatalog = {
byId: Record<string, ChannelUiMetaEntry>;
};
export type ChannelPluginCatalogInstall = PluginPackageInstall &
({ clawhubSpec: string } | { npmSpec: string });
export type ChannelPluginCatalogEntry = {
id: string;
pluginId?: string;
origin?: PluginOrigin;
meta: ChannelMeta;
install: PluginPackageInstall & {
npmSpec: string;
};
install: ChannelPluginCatalogInstall;
installSource?: PluginInstallSourceInfo;
};
@@ -210,19 +211,34 @@ function resolveInstallInfo(params: {
packageDir?: string;
workspaceDir?: string;
}): ChannelPluginCatalogEntry["install"] | null {
const npmSpec = params.install?.npmSpec?.trim() ?? params.packageName?.trim();
if (!npmSpec) {
const clawhubSpec = normalizeOptionalString(params.install?.clawhubSpec);
const npmSpec =
normalizeOptionalString(params.install?.npmSpec) ?? normalizeOptionalString(params.packageName);
if (!clawhubSpec && !npmSpec) {
return null;
}
let localPath = normalizeOptionalString(params.install?.localPath);
if (!localPath && params.workspaceDir && params.packageDir) {
localPath = path.relative(params.workspaceDir, params.packageDir) || undefined;
}
const defaultChoice = params.install?.defaultChoice ?? (localPath ? "local" : "npm");
const requestedDefaultChoice = params.install?.defaultChoice;
const defaultChoice =
requestedDefaultChoice === "clawhub" && clawhubSpec
? "clawhub"
: requestedDefaultChoice === "npm" && npmSpec
? "npm"
: requestedDefaultChoice === "local" && localPath
? "local"
: clawhubSpec
? "clawhub"
: localPath
? "local"
: "npm";
return {
npmSpec,
...(clawhubSpec ? { clawhubSpec } : {}),
...(npmSpec ? { npmSpec } : {}),
...(localPath ? { localPath } : {}),
...(defaultChoice ? { defaultChoice } : {}),
defaultChoice,
...(params.install?.minHostVersion ? { minHostVersion: params.install.minHostVersion } : {}),
...(params.install?.expectedIntegrity
? { expectedIntegrity: params.install.expectedIntegrity }

View File

@@ -287,6 +287,78 @@ export function describeChannelPluginCatalogEntriesContract() {
};
},
},
{
name: "accepts external manifest entries with ClawHub-only install metadata",
setup: () => {
const dir = fs.mkdtempSync(
path.join(resolvePreferredOpenClawTmpDir(), "openclaw-catalog-clawhub-"),
);
const catalogPath = path.join(dir, "catalog.json");
fs.writeFileSync(
catalogPath,
JSON.stringify({
$schema: "./manifest.schema.json",
schemaVersion: 1,
description:
"Extension manifest. Declares plugin packages that OpenClaw can discover during onboarding and install on demand via `openclaw plugins install`.",
entries: [
{
source: "external",
kind: "channel",
openclaw: {
channel: {
id: "clawhub-chat",
label: "ClawHub Chat",
selectionLabel: "ClawHub Chat",
detailLabel: "ClawHub",
docsPath: "/channels/clawhub-chat",
docsLabel: "clawhub chat",
blurb: "ClawHub-backed chat channel.",
aliases: ["chchat"],
order: 47,
},
install: {
clawhubSpec: "clawhub:openclaw/clawhub-chat@2026.5.2",
defaultChoice: "clawhub",
minHostVersion: ">=2026.5.1",
},
},
},
],
}),
);
return {
channelId: "clawhub-chat",
catalogPaths: [catalogPath],
expected: {
id: "clawhub-chat",
meta: {
label: "ClawHub Chat",
selectionLabel: "ClawHub Chat",
detailLabel: "ClawHub",
docsPath: "/channels/clawhub-chat",
docsLabel: "clawhub chat",
blurb: "ClawHub-backed chat channel.",
},
install: {
clawhubSpec: "clawhub:openclaw/clawhub-chat@2026.5.2",
defaultChoice: "clawhub",
minHostVersion: ">=2026.5.1",
},
installSource: {
defaultChoice: "clawhub",
clawhub: {
spec: "clawhub:openclaw/clawhub-chat@2026.5.2",
packageName: "openclaw/clawhub-chat",
version: "2026.5.2",
exactVersion: true,
},
warnings: [],
},
},
};
},
},
{
name: "accepts rich external manifest entries for yuanbao with pinned npm metadata",
setup: () => {

View File

@@ -528,6 +528,50 @@ describe("ensureChannelSetupPluginInstalled", () => {
);
});
it("offers ClawHub as the first-class install source for channel catalog entries", async () => {
const runtime = makeRuntime();
const { prompter, select } = makeSkipInstallPrompter();
const cfg: OpenClawConfig = { update: { channel: "beta" } };
vi.mocked(fs.existsSync).mockReturnValue(false);
resolveBundledPluginSources.mockReturnValue(new Map());
await ensureChannelSetupPluginInstalled({
cfg,
entry: {
id: "clawhub-chat",
pluginId: "clawhub-chat",
meta: {
id: "clawhub-chat",
label: "ClawHub Chat",
selectionLabel: "ClawHub Chat",
docsPath: "/channels/clawhub-chat",
blurb: "Test",
},
install: {
clawhubSpec: "clawhub:openclaw/clawhub-chat@2026.5.2",
defaultChoice: "clawhub",
},
},
prompter,
runtime,
});
expect(select).toHaveBeenCalledWith(
expect.objectContaining({
initialValue: "clawhub",
options: [
expect.objectContaining({
value: "clawhub",
label: "Download from ClawHub (clawhub:openclaw/clawhub-chat@2026.5.2)",
}),
expect.objectContaining({
value: "skip",
}),
],
}),
);
});
it("falls back to local path after npm install failure", async () => {
const runtime = makeRuntime();
const note = vi.fn(async () => {});