Files
openclaw/src/config/plugin-auto-enable.prefer-over.ts
2026-04-05 09:34:16 +01:00

144 lines
4.4 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
import { getChatChannelMeta, normalizeChatChannelId } from "../channels/registry.js";
import type { PluginManifestRegistry } from "../plugins/manifest-registry.js";
import { isRecord, resolveConfigDir, resolveUserPath } from "../utils.js";
import type { OpenClawConfig } from "./config.js";
import type { PluginAutoEnableCandidate } from "./plugin-auto-enable.shared.js";
type ExternalCatalogChannelEntry = {
id: string;
preferOver: string[];
};
const ENV_CATALOG_PATHS = ["OPENCLAW_PLUGIN_CATALOG_PATHS", "OPENCLAW_MPM_CATALOG_PATHS"];
function splitEnvPaths(value: string): string[] {
const trimmed = value.trim();
if (!trimmed) {
return [];
}
return trimmed
.split(/[;,]/g)
.flatMap((chunk) => chunk.split(path.delimiter))
.map((entry) => entry.trim())
.filter(Boolean);
}
function resolveExternalCatalogPaths(env: NodeJS.ProcessEnv): string[] {
for (const key of ENV_CATALOG_PATHS) {
const raw = env[key];
if (raw && raw.trim()) {
return splitEnvPaths(raw);
}
}
const configDir = resolveConfigDir(env);
return [
path.join(configDir, "mpm", "plugins.json"),
path.join(configDir, "mpm", "catalog.json"),
path.join(configDir, "plugins", "catalog.json"),
];
}
function parseExternalCatalogChannelEntries(raw: unknown): ExternalCatalogChannelEntry[] {
const list = (() => {
if (Array.isArray(raw)) {
return raw;
}
if (!isRecord(raw)) {
return [];
}
const entries = raw.entries ?? raw.packages ?? raw.plugins;
return Array.isArray(entries) ? entries : [];
})();
const channels: ExternalCatalogChannelEntry[] = [];
for (const entry of list) {
if (!isRecord(entry) || !isRecord(entry.openclaw) || !isRecord(entry.openclaw.channel)) {
continue;
}
const channel = entry.openclaw.channel;
const id = typeof channel.id === "string" ? channel.id.trim() : "";
if (!id) {
continue;
}
const preferOver = Array.isArray(channel.preferOver)
? channel.preferOver.filter((value): value is string => typeof value === "string")
: [];
channels.push({ id, preferOver });
}
return channels;
}
function resolveExternalCatalogPreferOver(channelId: string, env: NodeJS.ProcessEnv): string[] {
for (const rawPath of resolveExternalCatalogPaths(env)) {
const resolved = resolveUserPath(rawPath, env);
if (!fs.existsSync(resolved)) {
continue;
}
try {
const payload = JSON.parse(fs.readFileSync(resolved, "utf-8")) as unknown;
const channel = parseExternalCatalogChannelEntries(payload).find(
(entry) => entry.id === channelId,
);
if (channel) {
return channel.preferOver;
}
} catch {
// Ignore invalid catalog files.
}
}
return [];
}
function resolvePreferredOverIds(
pluginId: string,
env: NodeJS.ProcessEnv,
registry: PluginManifestRegistry,
): string[] {
const normalized = normalizeChatChannelId(pluginId);
if (normalized) {
return [...(getChatChannelMeta(normalized).preferOver ?? [])];
}
const installedPlugin = registry.plugins.find((record) => record.id === pluginId);
const manifestChannelPreferOver = installedPlugin?.channelConfigs?.[pluginId]?.preferOver;
if (manifestChannelPreferOver?.length) {
return [...manifestChannelPreferOver];
}
const installedChannelMeta = installedPlugin?.channelCatalogMeta;
if (installedChannelMeta?.preferOver?.length) {
return [...installedChannelMeta.preferOver];
}
return resolveExternalCatalogPreferOver(pluginId, env);
}
export function shouldSkipPreferredPluginAutoEnable(params: {
config: OpenClawConfig;
entry: PluginAutoEnableCandidate;
configured: readonly PluginAutoEnableCandidate[];
env: NodeJS.ProcessEnv;
registry: PluginManifestRegistry;
isPluginDenied: (config: OpenClawConfig, pluginId: string) => boolean;
isPluginExplicitlyDisabled: (config: OpenClawConfig, pluginId: string) => boolean;
}): boolean {
for (const other of params.configured) {
if (other.pluginId === params.entry.pluginId) {
continue;
}
if (
params.isPluginDenied(params.config, other.pluginId) ||
params.isPluginExplicitlyDisabled(params.config, other.pluginId)
) {
continue;
}
if (
resolvePreferredOverIds(other.pluginId, params.env, params.registry).includes(
params.entry.pluginId,
)
) {
return true;
}
}
return false;
}