mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:30:43 +00:00
fix(doctor): repair effective plugin installs
This commit is contained in:
@@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- 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.
|
||||
- Plugins/externalization: keep ACPX, Google Chat, and LINE publishable plugin dist trees out of the core npm package file list.
|
||||
- Plugins/ClawHub: fall back to version metadata when the artifact resolver route is missing and keep the Docker ClawHub fixture aligned with npm-pack artifact resolution, avoiding false version-not-found failures during plugin install validation. Thanks @vincentkoc.
|
||||
- Status/channels: show configured channels in `openclaw status` and config-only `openclaw channels status` output even when the Gateway is unreachable, avoiding empty Channels tables on WSL and other no-Gateway paths. Thanks @vincentkoc.
|
||||
|
||||
@@ -115,7 +115,7 @@ describe("repairMissingConfiguredPluginInstalls", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("installs a missing configured OpenClaw channel plugin from ClawHub", async () => {
|
||||
it("installs a missing configured OpenClaw channel plugin from npm by default", async () => {
|
||||
mocks.listChannelPluginCatalogEntries.mockReturnValue([
|
||||
{
|
||||
id: "matrix",
|
||||
@@ -133,33 +133,33 @@ describe("repairMissingConfiguredPluginInstalls", () => {
|
||||
const result = await repairMissingConfiguredPluginInstalls({
|
||||
cfg: {
|
||||
channels: {
|
||||
matrix: { enabled: true },
|
||||
matrix: { enabled: true, homeserver: "https://matrix.example.org" },
|
||||
},
|
||||
},
|
||||
env: {},
|
||||
});
|
||||
|
||||
expect(mocks.installPluginFromClawHub).toHaveBeenCalledWith(
|
||||
expect(mocks.installPluginFromClawHub).not.toHaveBeenCalled();
|
||||
expect(mocks.installPluginFromNpmSpec).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
spec: "clawhub:@openclaw/plugin-matrix@1.2.3",
|
||||
spec: "@openclaw/plugin-matrix@1.2.3",
|
||||
extensionsDir: "/tmp/openclaw-plugins",
|
||||
expectedPluginId: "matrix",
|
||||
expectedIntegrity: "sha512-test",
|
||||
}),
|
||||
);
|
||||
expect(mocks.installPluginFromNpmSpec).not.toHaveBeenCalled();
|
||||
expect(mocks.writePersistedInstalledPluginIndexInstallRecords).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
matrix: expect.objectContaining({
|
||||
source: "clawhub",
|
||||
spec: "clawhub:@openclaw/plugin-matrix@1.2.3",
|
||||
clawhubPackage: "@openclaw/plugin-matrix",
|
||||
source: "npm",
|
||||
spec: "@openclaw/plugin-matrix@1.2.3",
|
||||
installPath: "/tmp/openclaw-plugins/matrix",
|
||||
}),
|
||||
}),
|
||||
{ env: {} },
|
||||
);
|
||||
expect(result.changes).toEqual([
|
||||
'Installed missing configured plugin "matrix" from clawhub:@openclaw/plugin-matrix@1.2.3.',
|
||||
'Installed missing configured plugin "matrix" from @openclaw/plugin-matrix@1.2.3.',
|
||||
]);
|
||||
expect(result.warnings).toEqual([]);
|
||||
});
|
||||
@@ -183,7 +183,7 @@ describe("repairMissingConfiguredPluginInstalls", () => {
|
||||
const result = await repairMissingConfiguredPluginInstalls({
|
||||
cfg: {
|
||||
channels: {
|
||||
matrix: { enabled: true },
|
||||
matrix: { enabled: true, homeserver: "https://matrix.example.org" },
|
||||
},
|
||||
},
|
||||
env: {},
|
||||
@@ -202,6 +202,62 @@ describe("repairMissingConfiguredPluginInstalls", () => {
|
||||
expect(result.warnings).toEqual([]);
|
||||
});
|
||||
|
||||
it("installs a missing channel plugin selected by environment config from npm", async () => {
|
||||
mocks.installPluginFromNpmSpec.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
pluginId: "matrix",
|
||||
targetDir: "/tmp/openclaw-plugins/matrix",
|
||||
version: "1.2.3",
|
||||
npmResolution: {
|
||||
name: "@openclaw/plugin-matrix",
|
||||
version: "1.2.3",
|
||||
resolvedSpec: "@openclaw/plugin-matrix@1.2.3",
|
||||
integrity: "sha512-matrix",
|
||||
resolvedAt: "2026-05-01T00:00:00.000Z",
|
||||
},
|
||||
});
|
||||
mocks.listChannelPluginCatalogEntries.mockReturnValue([
|
||||
{
|
||||
id: "matrix",
|
||||
pluginId: "matrix",
|
||||
meta: { label: "Matrix" },
|
||||
install: {
|
||||
npmSpec: "@openclaw/plugin-matrix@1.2.3",
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const { repairMissingConfiguredPluginInstalls } =
|
||||
await import("./missing-configured-plugin-install.js");
|
||||
const result = await repairMissingConfiguredPluginInstalls({
|
||||
cfg: {},
|
||||
env: { MATRIX_HOMESERVER: "https://matrix.example.org" },
|
||||
});
|
||||
|
||||
expect(mocks.installPluginFromClawHub).not.toHaveBeenCalled();
|
||||
expect(mocks.installPluginFromNpmSpec).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
spec: "@openclaw/plugin-matrix@1.2.3",
|
||||
extensionsDir: "/tmp/openclaw-plugins",
|
||||
expectedPluginId: "matrix",
|
||||
}),
|
||||
);
|
||||
expect(mocks.writePersistedInstalledPluginIndexInstallRecords).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
matrix: expect.objectContaining({
|
||||
source: "npm",
|
||||
spec: "@openclaw/plugin-matrix@1.2.3",
|
||||
installPath: "/tmp/openclaw-plugins/matrix",
|
||||
}),
|
||||
}),
|
||||
{ env: { MATRIX_HOMESERVER: "https://matrix.example.org" } },
|
||||
);
|
||||
expect(result.changes).toEqual([
|
||||
'Installed missing configured plugin "matrix" from @openclaw/plugin-matrix@1.2.3.',
|
||||
]);
|
||||
expect(result.warnings).toEqual([]);
|
||||
});
|
||||
|
||||
it("falls back to npm when an OpenClaw channel plugin is not on ClawHub", async () => {
|
||||
mocks.installPluginFromClawHub.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
@@ -214,6 +270,7 @@ describe("repairMissingConfiguredPluginInstalls", () => {
|
||||
pluginId: "matrix",
|
||||
meta: { label: "Matrix" },
|
||||
install: {
|
||||
clawhubSpec: "clawhub:@openclaw/plugin-matrix@stable",
|
||||
npmSpec: "@openclaw/plugin-matrix@1.2.3",
|
||||
},
|
||||
},
|
||||
@@ -235,7 +292,7 @@ describe("repairMissingConfiguredPluginInstalls", () => {
|
||||
}),
|
||||
);
|
||||
expect(result.changes).toEqual([
|
||||
'ClawHub clawhub:@openclaw/plugin-matrix@1.2.3 unavailable for "matrix"; falling back to npm @openclaw/plugin-matrix@1.2.3.',
|
||||
'ClawHub clawhub:@openclaw/plugin-matrix@stable unavailable for "matrix"; falling back to npm @openclaw/plugin-matrix@1.2.3.',
|
||||
'Installed missing configured plugin "matrix" from @openclaw/plugin-matrix@1.2.3.',
|
||||
]);
|
||||
expect(result.warnings).toEqual([]);
|
||||
@@ -339,6 +396,126 @@ describe("repairMissingConfiguredPluginInstalls", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not install disabled configured plugin entries", async () => {
|
||||
mocks.listOfficialExternalPluginCatalogEntries.mockReturnValue([
|
||||
{
|
||||
id: "diagnostics-otel",
|
||||
label: "Diagnostics OpenTelemetry",
|
||||
install: {
|
||||
npmSpec: "@openclaw/diagnostics-otel",
|
||||
defaultChoice: "npm",
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const { repairMissingConfiguredPluginInstalls } =
|
||||
await import("./missing-configured-plugin-install.js");
|
||||
const result = await repairMissingConfiguredPluginInstalls({
|
||||
cfg: {
|
||||
plugins: {
|
||||
entries: {
|
||||
"diagnostics-otel": { enabled: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
env: {},
|
||||
});
|
||||
|
||||
expect(mocks.installPluginFromClawHub).not.toHaveBeenCalled();
|
||||
expect(mocks.installPluginFromNpmSpec).not.toHaveBeenCalled();
|
||||
expect(mocks.writePersistedInstalledPluginIndexInstallRecords).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({ changes: [], warnings: [] });
|
||||
});
|
||||
|
||||
it.each([
|
||||
["enabled-only disabled stub", { channels: { matrix: { enabled: false } } }],
|
||||
[
|
||||
"disabled configured channel",
|
||||
{ channels: { matrix: { enabled: false, homeserver: "https://matrix.example.org" } } },
|
||||
],
|
||||
])("does not install channel plugins for a %s", async (_label, cfg) => {
|
||||
mocks.listChannelPluginCatalogEntries.mockReturnValue([
|
||||
{
|
||||
id: "matrix",
|
||||
pluginId: "matrix",
|
||||
meta: { label: "Matrix" },
|
||||
install: {
|
||||
npmSpec: "@openclaw/plugin-matrix@1.2.3",
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const { repairMissingConfiguredPluginInstalls } =
|
||||
await import("./missing-configured-plugin-install.js");
|
||||
const result = await repairMissingConfiguredPluginInstalls({
|
||||
cfg,
|
||||
env: {},
|
||||
});
|
||||
|
||||
expect(mocks.installPluginFromClawHub).not.toHaveBeenCalled();
|
||||
expect(mocks.installPluginFromNpmSpec).not.toHaveBeenCalled();
|
||||
expect(mocks.writePersistedInstalledPluginIndexInstallRecords).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({ changes: [], warnings: [] });
|
||||
});
|
||||
|
||||
it("does not install configured plugins when plugins are globally disabled", async () => {
|
||||
mocks.listChannelPluginCatalogEntries.mockReturnValue([
|
||||
{
|
||||
id: "matrix",
|
||||
pluginId: "matrix",
|
||||
meta: { label: "Matrix" },
|
||||
install: {
|
||||
npmSpec: "@openclaw/plugin-matrix@1.2.3",
|
||||
},
|
||||
},
|
||||
]);
|
||||
mocks.listOfficialExternalPluginCatalogEntries.mockReturnValue([
|
||||
{
|
||||
id: "codex",
|
||||
label: "Codex",
|
||||
install: {
|
||||
npmSpec: "@openclaw/codex",
|
||||
defaultChoice: "npm",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "diagnostics-otel",
|
||||
label: "Diagnostics OpenTelemetry",
|
||||
install: {
|
||||
npmSpec: "@openclaw/diagnostics-otel",
|
||||
defaultChoice: "npm",
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const { repairMissingConfiguredPluginInstalls } =
|
||||
await import("./missing-configured-plugin-install.js");
|
||||
const result = await repairMissingConfiguredPluginInstalls({
|
||||
cfg: {
|
||||
plugins: {
|
||||
enabled: false,
|
||||
entries: {
|
||||
"diagnostics-otel": { enabled: true },
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
matrix: { homeserver: "https://matrix.example.org" },
|
||||
},
|
||||
agents: {
|
||||
defaults: {
|
||||
agentRuntime: { id: "codex" },
|
||||
},
|
||||
},
|
||||
},
|
||||
env: {},
|
||||
});
|
||||
|
||||
expect(mocks.installPluginFromClawHub).not.toHaveBeenCalled();
|
||||
expect(mocks.installPluginFromNpmSpec).not.toHaveBeenCalled();
|
||||
expect(mocks.writePersistedInstalledPluginIndexInstallRecords).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({ changes: [], warnings: [] });
|
||||
});
|
||||
|
||||
it("installs a missing third-party downloadable plugin from npm only", async () => {
|
||||
mocks.installPluginFromNpmSpec.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
@@ -385,20 +562,30 @@ describe("repairMissingConfiguredPluginInstalls", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("installs the missing configured Codex runtime plugin from the beta npm tag", async () => {
|
||||
it("installs a missing default Codex runtime plugin from the official external catalog", async () => {
|
||||
mocks.installPluginFromNpmSpec.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
pluginId: "codex",
|
||||
targetDir: "/tmp/openclaw-plugins/codex",
|
||||
version: "2026.5.2-beta.1",
|
||||
version: "2026.5.2",
|
||||
npmResolution: {
|
||||
name: "@openclaw/codex",
|
||||
version: "2026.5.2-beta.1",
|
||||
resolvedSpec: "@openclaw/codex@2026.5.2-beta.1",
|
||||
integrity: "sha512-codex-beta",
|
||||
version: "2026.5.2",
|
||||
resolvedSpec: "@openclaw/codex@2026.5.2",
|
||||
integrity: "sha512-codex",
|
||||
resolvedAt: "2026-05-01T00:00:00.000Z",
|
||||
},
|
||||
});
|
||||
mocks.listOfficialExternalPluginCatalogEntries.mockReturnValue([
|
||||
{
|
||||
id: "codex",
|
||||
label: "Codex",
|
||||
install: {
|
||||
npmSpec: "@openclaw/codex",
|
||||
defaultChoice: "npm",
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const { repairMissingPluginInstallsForIds } =
|
||||
await import("./missing-configured-plugin-install.js");
|
||||
@@ -418,7 +605,7 @@ describe("repairMissingConfiguredPluginInstalls", () => {
|
||||
expect(mocks.resolveProviderInstallCatalogEntries).toHaveBeenCalled();
|
||||
expect(mocks.installPluginFromNpmSpec).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
spec: "@openclaw/codex@beta",
|
||||
spec: "@openclaw/codex",
|
||||
expectedPluginId: "codex",
|
||||
}),
|
||||
);
|
||||
@@ -426,19 +613,96 @@ describe("repairMissingConfiguredPluginInstalls", () => {
|
||||
expect.objectContaining({
|
||||
codex: expect.objectContaining({
|
||||
source: "npm",
|
||||
spec: "@openclaw/codex@beta",
|
||||
spec: "@openclaw/codex",
|
||||
installPath: "/tmp/openclaw-plugins/codex",
|
||||
version: "2026.5.2-beta.1",
|
||||
version: "2026.5.2",
|
||||
}),
|
||||
}),
|
||||
{ env: {} },
|
||||
);
|
||||
expect(result.changes).toEqual([
|
||||
'Installed missing configured plugin "codex" from @openclaw/codex@beta.',
|
||||
'Installed missing configured plugin "codex" from @openclaw/codex.',
|
||||
]);
|
||||
expect(result.warnings).toEqual([]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
"default agent runtime",
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
agentRuntime: { id: "codex" },
|
||||
},
|
||||
},
|
||||
},
|
||||
{},
|
||||
],
|
||||
[
|
||||
"agent runtime override",
|
||||
{
|
||||
agents: {
|
||||
list: [{ id: "main", agentRuntime: { id: "codex" } }],
|
||||
},
|
||||
},
|
||||
{},
|
||||
],
|
||||
["environment runtime override", {}, { OPENCLAW_AGENT_RUNTIME: "codex" }],
|
||||
])("repairs a missing Codex plugin selected by %s", async (_label, cfg, env) => {
|
||||
mocks.installPluginFromNpmSpec.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
pluginId: "codex",
|
||||
targetDir: "/tmp/openclaw-plugins/codex",
|
||||
version: "2026.5.2",
|
||||
npmResolution: {
|
||||
name: "@openclaw/codex",
|
||||
version: "2026.5.2",
|
||||
resolvedSpec: "@openclaw/codex@2026.5.2",
|
||||
integrity: "sha512-codex",
|
||||
resolvedAt: "2026-05-01T00:00:00.000Z",
|
||||
},
|
||||
});
|
||||
mocks.listOfficialExternalPluginCatalogEntries.mockReturnValue([
|
||||
{
|
||||
id: "codex",
|
||||
label: "Codex",
|
||||
install: {
|
||||
npmSpec: "@openclaw/codex",
|
||||
defaultChoice: "npm",
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const { repairMissingConfiguredPluginInstalls } =
|
||||
await import("./missing-configured-plugin-install.js");
|
||||
const result = await repairMissingConfiguredPluginInstalls({
|
||||
cfg,
|
||||
env,
|
||||
});
|
||||
|
||||
expect(mocks.installPluginFromNpmSpec).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
spec: "@openclaw/codex",
|
||||
expectedPluginId: "codex",
|
||||
}),
|
||||
);
|
||||
expect(mocks.writePersistedInstalledPluginIndexInstallRecords).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
codex: expect.objectContaining({
|
||||
source: "npm",
|
||||
spec: "@openclaw/codex",
|
||||
installPath: "/tmp/openclaw-plugins/codex",
|
||||
version: "2026.5.2",
|
||||
}),
|
||||
}),
|
||||
{ env },
|
||||
);
|
||||
expect(result).toEqual({
|
||||
changes: ['Installed missing configured plugin "codex" from @openclaw/codex.'],
|
||||
warnings: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("does not install a blocked downloadable plugin from explicit channel ids", async () => {
|
||||
mocks.listChannelPluginCatalogEntries.mockReturnValue([
|
||||
{
|
||||
@@ -691,4 +955,68 @@ describe("repairMissingConfiguredPluginInstalls", () => {
|
||||
'Installed missing configured plugin "brave" from @openclaw/brave-plugin.',
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not install a configured external web search plugin when search is disabled", async () => {
|
||||
mocks.listOfficialExternalPluginCatalogEntries.mockReturnValue([
|
||||
{
|
||||
id: "brave",
|
||||
label: "Brave",
|
||||
install: {
|
||||
npmSpec: "@openclaw/brave-plugin",
|
||||
defaultChoice: "npm",
|
||||
},
|
||||
openclaw: {
|
||||
plugin: { id: "brave", label: "Brave" },
|
||||
webSearchProviders: [
|
||||
{
|
||||
id: "brave",
|
||||
label: "Brave Search",
|
||||
hint: "Brave Search",
|
||||
envVars: ["BRAVE_API_KEY"],
|
||||
placeholder: "BSA...",
|
||||
signupUrl: "https://example.test/brave",
|
||||
credentialPath: "plugins.entries.brave.config.webSearch.apiKey",
|
||||
},
|
||||
],
|
||||
install: {
|
||||
npmSpec: "@openclaw/brave-plugin",
|
||||
defaultChoice: "npm",
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
mocks.resolveOfficialExternalPluginId.mockImplementation(
|
||||
(entry: { id?: string; openclaw?: { plugin?: { id?: string } } }) =>
|
||||
entry.openclaw?.plugin?.id ?? entry.id,
|
||||
);
|
||||
mocks.resolveOfficialExternalPluginInstall.mockImplementation(
|
||||
(entry: { install?: unknown; openclaw?: { install?: unknown } }) =>
|
||||
entry.openclaw?.install ?? entry.install ?? null,
|
||||
);
|
||||
mocks.resolveOfficialExternalPluginLabel.mockImplementation(
|
||||
(entry: { label?: string; openclaw?: { plugin?: { label?: string } } }) =>
|
||||
entry.openclaw?.plugin?.label ?? entry.label ?? "plugin",
|
||||
);
|
||||
|
||||
const { repairMissingConfiguredPluginInstalls } =
|
||||
await import("./missing-configured-plugin-install.js");
|
||||
const result = await repairMissingConfiguredPluginInstalls({
|
||||
cfg: {
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
enabled: false,
|
||||
provider: "brave",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
env: {},
|
||||
});
|
||||
|
||||
expect(mocks.installPluginFromClawHub).not.toHaveBeenCalled();
|
||||
expect(mocks.installPluginFromNpmSpec).not.toHaveBeenCalled();
|
||||
expect(mocks.writePersistedInstalledPluginIndexInstallRecords).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({ changes: [], warnings: [] });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import {
|
||||
listExplicitlyDisabledChannelIdsForConfig,
|
||||
listPotentialConfiguredChannelIds,
|
||||
} from "../../../channels/config-presence.js";
|
||||
import { listChannelPluginCatalogEntries } from "../../../channels/plugins/catalog.js";
|
||||
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
|
||||
import type { PluginInstallRecord } from "../../../config/types.plugins.js";
|
||||
import { parseRegistryNpmSpec } from "../../../infra/npm-registry-spec.js";
|
||||
import { buildClawHubPluginInstallRecordFields } from "../../../plugins/clawhub-install-records.js";
|
||||
import { CLAWHUB_INSTALL_ERROR_CODE, installPluginFromClawHub } from "../../../plugins/clawhub.js";
|
||||
import { resolveDefaultPluginExtensionsDir } from "../../../plugins/install-paths.js";
|
||||
@@ -41,18 +44,10 @@ const RUNTIME_PLUGIN_INSTALL_CANDIDATES: readonly DownloadableInstallCandidate[]
|
||||
{
|
||||
pluginId: "codex",
|
||||
label: "Codex",
|
||||
npmSpec: "@openclaw/codex@beta",
|
||||
npmSpec: "@openclaw/codex",
|
||||
},
|
||||
];
|
||||
|
||||
function buildOpenClawClawHubSpec(npmSpec: string): string | undefined {
|
||||
const parsed = parseRegistryNpmSpec(npmSpec);
|
||||
if (!parsed?.name.startsWith("@openclaw/")) {
|
||||
return undefined;
|
||||
}
|
||||
return `clawhub:${parsed.name}${parsed.selector ? `@${parsed.selector}` : ""}`;
|
||||
}
|
||||
|
||||
function shouldFallbackClawHubToNpm(result: { ok: false; code?: string }): boolean {
|
||||
return (
|
||||
result.code === CLAWHUB_INSTALL_ERROR_CODE.PACKAGE_NOT_FOUND ||
|
||||
@@ -60,41 +55,58 @@ function shouldFallbackClawHubToNpm(result: { ok: false; code?: string }): boole
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeInstallDefaultChoice(
|
||||
value: PluginPackageInstall["defaultChoice"] | undefined,
|
||||
): PluginPackageInstall["defaultChoice"] | undefined {
|
||||
return value === "clawhub" || value === "npm" || value === "local" ? value : undefined;
|
||||
}
|
||||
|
||||
function resolveCandidateClawHubSpec(install: PluginPackageInstall): string | undefined {
|
||||
const explicit = install.clawhubSpec?.trim();
|
||||
if (explicit) {
|
||||
return explicit;
|
||||
}
|
||||
const npmSpec = install.npmSpec?.trim();
|
||||
if (!npmSpec || normalizeInstallDefaultChoice(install.defaultChoice) === "npm") {
|
||||
return undefined;
|
||||
}
|
||||
return buildOpenClawClawHubSpec(npmSpec);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function collectConfiguredPluginIds(cfg: OpenClawConfig): Set<string> {
|
||||
function addConfiguredPluginId(ids: Set<string>, value: unknown): void {
|
||||
if (typeof value !== "string") {
|
||||
return;
|
||||
}
|
||||
const pluginId = value.trim();
|
||||
if (pluginId) {
|
||||
ids.add(pluginId);
|
||||
}
|
||||
}
|
||||
|
||||
function addConfiguredAgentRuntimePluginIds(
|
||||
ids: Set<string>,
|
||||
cfg: OpenClawConfig,
|
||||
env?: NodeJS.ProcessEnv,
|
||||
): void {
|
||||
addConfiguredPluginId(ids, env?.OPENCLAW_AGENT_RUNTIME);
|
||||
const agents = asObjectRecord(cfg.agents);
|
||||
const defaults = asObjectRecord(agents?.defaults);
|
||||
addConfiguredPluginId(ids, asObjectRecord(defaults?.agentRuntime)?.id);
|
||||
const list = Array.isArray(agents?.list) ? agents.list : [];
|
||||
for (const entry of list) {
|
||||
addConfiguredPluginId(ids, asObjectRecord(asObjectRecord(entry)?.agentRuntime)?.id);
|
||||
}
|
||||
}
|
||||
|
||||
function collectConfiguredPluginIds(cfg: OpenClawConfig, env?: NodeJS.ProcessEnv): Set<string> {
|
||||
const ids = new Set<string>();
|
||||
const plugins = asObjectRecord(cfg.plugins);
|
||||
if (plugins?.enabled === false) {
|
||||
return ids;
|
||||
}
|
||||
const allow = Array.isArray(plugins?.allow) ? plugins.allow : [];
|
||||
for (const value of allow) {
|
||||
if (typeof value === "string" && value.trim()) {
|
||||
ids.add(value.trim());
|
||||
}
|
||||
addConfiguredPluginId(ids, value);
|
||||
}
|
||||
const entries = asObjectRecord(plugins?.entries);
|
||||
for (const pluginId of Object.keys(entries ?? {})) {
|
||||
if (pluginId.trim()) {
|
||||
ids.add(pluginId.trim());
|
||||
for (const [pluginId, entry] of Object.entries(entries ?? {})) {
|
||||
if (asObjectRecord(entry)?.enabled === false) {
|
||||
continue;
|
||||
}
|
||||
addConfiguredPluginId(ids, pluginId);
|
||||
}
|
||||
const searchProvider = cfg.tools?.web?.search?.provider;
|
||||
if (typeof searchProvider === "string") {
|
||||
if (cfg.tools?.web?.search?.enabled !== false && typeof searchProvider === "string") {
|
||||
const installEntry = resolveWebSearchInstallCatalogEntry({ providerId: searchProvider });
|
||||
if (installEntry?.pluginId) {
|
||||
ids.add(installEntry.pluginId);
|
||||
@@ -110,15 +122,27 @@ function collectConfiguredPluginIds(cfg: OpenClawConfig): Set<string> {
|
||||
) {
|
||||
ids.add("acpx");
|
||||
}
|
||||
addConfiguredAgentRuntimePluginIds(ids, cfg, env);
|
||||
return ids;
|
||||
}
|
||||
|
||||
function collectConfiguredChannelIds(cfg: OpenClawConfig): Set<string> {
|
||||
function collectConfiguredChannelIds(cfg: OpenClawConfig, env?: NodeJS.ProcessEnv): Set<string> {
|
||||
const ids = new Set<string>();
|
||||
const channels = asObjectRecord(cfg.channels);
|
||||
for (const channelId of Object.keys(channels ?? {})) {
|
||||
if (channelId !== "defaults" && channelId.trim()) {
|
||||
ids.add(channelId.trim());
|
||||
if (asObjectRecord(cfg.plugins)?.enabled === false) {
|
||||
return ids;
|
||||
}
|
||||
const disabled = new Set(listExplicitlyDisabledChannelIdsForConfig(cfg));
|
||||
const candidateChannelIds = listChannelPluginCatalogEntries({
|
||||
env,
|
||||
excludeWorkspace: true,
|
||||
}).map((entry) => entry.id);
|
||||
for (const channelId of listPotentialConfiguredChannelIds(cfg, env, {
|
||||
channelIds: candidateChannelIds,
|
||||
includePersistedAuthState: false,
|
||||
})) {
|
||||
const normalized = channelId.trim();
|
||||
if (normalized && !disabled.has(normalized.toLowerCase())) {
|
||||
ids.add(normalized);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
@@ -132,9 +156,10 @@ function collectDownloadableInstallCandidates(params: {
|
||||
configuredChannelIds?: ReadonlySet<string>;
|
||||
blockedPluginIds?: ReadonlySet<string>;
|
||||
}): DownloadableInstallCandidate[] {
|
||||
const configuredPluginIds = params.configuredPluginIds ?? collectConfiguredPluginIds(params.cfg);
|
||||
const configuredPluginIds =
|
||||
params.configuredPluginIds ?? collectConfiguredPluginIds(params.cfg, params.env);
|
||||
const configuredChannelIds =
|
||||
params.configuredChannelIds ?? collectConfiguredChannelIds(params.cfg);
|
||||
params.configuredChannelIds ?? collectConfiguredChannelIds(params.cfg, params.env);
|
||||
const candidates = new Map<string, DownloadableInstallCandidate>();
|
||||
|
||||
for (const entry of listChannelPluginCatalogEntries({
|
||||
@@ -341,8 +366,8 @@ export async function repairMissingConfiguredPluginInstalls(params: {
|
||||
return repairMissingPluginInstalls({
|
||||
cfg: params.cfg,
|
||||
env: params.env,
|
||||
pluginIds: collectConfiguredPluginIds(params.cfg),
|
||||
channelIds: collectConfiguredChannelIds(params.cfg),
|
||||
pluginIds: collectConfiguredPluginIds(params.cfg, params.env),
|
||||
channelIds: collectConfiguredChannelIds(params.cfg, params.env),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -451,5 +476,4 @@ export const __testing = {
|
||||
collectConfiguredChannelIds,
|
||||
collectConfiguredPluginIds,
|
||||
collectDownloadableInstallCandidates,
|
||||
buildOpenClawClawHubSpec,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user