668 lines
20 KiB
JavaScript
668 lines
20 KiB
JavaScript
import { d as resolveConfigDir, m as resolveUserPath } from "./utils-PmTbZoD1.js";
|
|
import path from "node:path";
|
|
import fs from "node:fs";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
//#region src/plugins/slots.ts
|
|
const SLOT_BY_KIND = { memory: "memory" };
|
|
const DEFAULT_SLOT_BY_KEY = { memory: "memory-core" };
|
|
function slotKeyForPluginKind(kind) {
|
|
if (!kind) return null;
|
|
return SLOT_BY_KIND[kind] ?? null;
|
|
}
|
|
function defaultSlotIdForKey(slotKey) {
|
|
return DEFAULT_SLOT_BY_KEY[slotKey];
|
|
}
|
|
function applyExclusiveSlotSelection(params) {
|
|
const slotKey = slotKeyForPluginKind(params.selectedKind);
|
|
if (!slotKey) return {
|
|
config: params.config,
|
|
warnings: [],
|
|
changed: false
|
|
};
|
|
const warnings = [];
|
|
const pluginsConfig = params.config.plugins ?? {};
|
|
const prevSlot = pluginsConfig.slots?.[slotKey];
|
|
const slots = {
|
|
...pluginsConfig.slots,
|
|
[slotKey]: params.selectedId
|
|
};
|
|
const inferredPrevSlot = prevSlot ?? defaultSlotIdForKey(slotKey);
|
|
if (inferredPrevSlot && inferredPrevSlot !== params.selectedId) warnings.push(`Exclusive slot "${slotKey}" switched from "${inferredPrevSlot}" to "${params.selectedId}".`);
|
|
const entries = { ...pluginsConfig.entries };
|
|
const disabledIds = [];
|
|
if (params.registry) for (const plugin of params.registry.plugins) {
|
|
if (plugin.id === params.selectedId) continue;
|
|
if (plugin.kind !== params.selectedKind) continue;
|
|
const entry = entries[plugin.id];
|
|
if (!entry || entry.enabled !== false) {
|
|
entries[plugin.id] = {
|
|
...entry,
|
|
enabled: false
|
|
};
|
|
disabledIds.push(plugin.id);
|
|
}
|
|
}
|
|
if (disabledIds.length > 0) warnings.push(`Disabled other "${slotKey}" slot plugins: ${disabledIds.toSorted().join(", ")}.`);
|
|
if (!(prevSlot !== params.selectedId || disabledIds.length > 0)) return {
|
|
config: params.config,
|
|
warnings: [],
|
|
changed: false
|
|
};
|
|
return {
|
|
config: {
|
|
...params.config,
|
|
plugins: {
|
|
...pluginsConfig,
|
|
slots,
|
|
entries
|
|
}
|
|
},
|
|
warnings,
|
|
changed: true
|
|
};
|
|
}
|
|
|
|
//#endregion
|
|
//#region src/plugins/config-state.ts
|
|
const BUNDLED_ENABLED_BY_DEFAULT = /* @__PURE__ */ new Set();
|
|
const normalizeList = (value) => {
|
|
if (!Array.isArray(value)) return [];
|
|
return value.map((entry) => typeof entry === "string" ? entry.trim() : "").filter(Boolean);
|
|
};
|
|
const normalizeSlotValue = (value) => {
|
|
if (typeof value !== "string") return;
|
|
const trimmed = value.trim();
|
|
if (!trimmed) return;
|
|
if (trimmed.toLowerCase() === "none") return null;
|
|
return trimmed;
|
|
};
|
|
const normalizePluginEntries = (entries) => {
|
|
if (!entries || typeof entries !== "object" || Array.isArray(entries)) return {};
|
|
const normalized = {};
|
|
for (const [key, value] of Object.entries(entries)) {
|
|
if (!key.trim()) continue;
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
normalized[key] = {};
|
|
continue;
|
|
}
|
|
const entry = value;
|
|
normalized[key] = {
|
|
enabled: typeof entry.enabled === "boolean" ? entry.enabled : void 0,
|
|
config: "config" in entry ? entry.config : void 0
|
|
};
|
|
}
|
|
return normalized;
|
|
};
|
|
const normalizePluginsConfig = (config) => {
|
|
const memorySlot = normalizeSlotValue(config?.slots?.memory);
|
|
return {
|
|
enabled: config?.enabled !== false,
|
|
allow: normalizeList(config?.allow),
|
|
deny: normalizeList(config?.deny),
|
|
loadPaths: normalizeList(config?.load?.paths),
|
|
slots: { memory: memorySlot === void 0 ? defaultSlotIdForKey("memory") : memorySlot },
|
|
entries: normalizePluginEntries(config?.entries)
|
|
};
|
|
};
|
|
const hasExplicitMemorySlot = (plugins) => Boolean(plugins?.slots && Object.prototype.hasOwnProperty.call(plugins.slots, "memory"));
|
|
const hasExplicitMemoryEntry = (plugins) => Boolean(plugins?.entries && Object.prototype.hasOwnProperty.call(plugins.entries, "memory-core"));
|
|
function isTestDefaultMemorySlotDisabled(cfg, env = process.env) {
|
|
if (!env.VITEST) return false;
|
|
const plugins = cfg.plugins;
|
|
if (hasExplicitMemorySlot(plugins) || hasExplicitMemoryEntry(plugins)) return false;
|
|
return true;
|
|
}
|
|
function resolveEnableState(id, origin, config) {
|
|
if (!config.enabled) return {
|
|
enabled: false,
|
|
reason: "plugins disabled"
|
|
};
|
|
if (config.deny.includes(id)) return {
|
|
enabled: false,
|
|
reason: "blocked by denylist"
|
|
};
|
|
if (config.allow.length > 0 && !config.allow.includes(id)) return {
|
|
enabled: false,
|
|
reason: "not in allowlist"
|
|
};
|
|
if (config.slots.memory === id) return { enabled: true };
|
|
const entry = config.entries[id];
|
|
if (entry?.enabled === true) return { enabled: true };
|
|
if (entry?.enabled === false) return {
|
|
enabled: false,
|
|
reason: "disabled in config"
|
|
};
|
|
if (origin === "bundled" && BUNDLED_ENABLED_BY_DEFAULT.has(id)) return { enabled: true };
|
|
if (origin === "bundled") return {
|
|
enabled: false,
|
|
reason: "bundled (disabled by default)"
|
|
};
|
|
return { enabled: true };
|
|
}
|
|
function resolveMemorySlotDecision(params) {
|
|
if (params.kind !== "memory") return { enabled: true };
|
|
if (params.slot === null) return {
|
|
enabled: false,
|
|
reason: "memory slot disabled"
|
|
};
|
|
if (typeof params.slot === "string") {
|
|
if (params.slot === params.id) return {
|
|
enabled: true,
|
|
selected: true
|
|
};
|
|
return {
|
|
enabled: false,
|
|
reason: `memory slot set to "${params.slot}"`
|
|
};
|
|
}
|
|
if (params.selectedId && params.selectedId !== params.id) return {
|
|
enabled: false,
|
|
reason: `memory slot already filled by "${params.selectedId}"`
|
|
};
|
|
return {
|
|
enabled: true,
|
|
selected: true
|
|
};
|
|
}
|
|
|
|
//#endregion
|
|
//#region src/plugins/bundled-dir.ts
|
|
function resolveBundledPluginsDir() {
|
|
const override = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR?.trim();
|
|
if (override) return override;
|
|
try {
|
|
const execDir = path.dirname(process.execPath);
|
|
const sibling = path.join(execDir, "extensions");
|
|
if (fs.existsSync(sibling)) return sibling;
|
|
} catch {}
|
|
try {
|
|
let cursor = path.dirname(fileURLToPath(import.meta.url));
|
|
for (let i = 0; i < 6; i += 1) {
|
|
const candidate = path.join(cursor, "extensions");
|
|
if (fs.existsSync(candidate)) return candidate;
|
|
const parent = path.dirname(cursor);
|
|
if (parent === cursor) break;
|
|
cursor = parent;
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
//#endregion
|
|
//#region src/compat/legacy-names.ts
|
|
const PROJECT_NAME = "openclaw";
|
|
const LEGACY_PROJECT_NAMES = [];
|
|
const MANIFEST_KEY = PROJECT_NAME;
|
|
const LEGACY_MANIFEST_KEYS = LEGACY_PROJECT_NAMES;
|
|
|
|
//#endregion
|
|
//#region src/plugins/manifest.ts
|
|
const PLUGIN_MANIFEST_FILENAME = "openclaw.plugin.json";
|
|
const PLUGIN_MANIFEST_FILENAMES = [PLUGIN_MANIFEST_FILENAME];
|
|
function normalizeStringList(value) {
|
|
if (!Array.isArray(value)) return [];
|
|
return value.map((entry) => typeof entry === "string" ? entry.trim() : "").filter(Boolean);
|
|
}
|
|
function isRecord(value) {
|
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
}
|
|
function resolvePluginManifestPath(rootDir) {
|
|
for (const filename of PLUGIN_MANIFEST_FILENAMES) {
|
|
const candidate = path.join(rootDir, filename);
|
|
if (fs.existsSync(candidate)) return candidate;
|
|
}
|
|
return path.join(rootDir, PLUGIN_MANIFEST_FILENAME);
|
|
}
|
|
function loadPluginManifest(rootDir) {
|
|
const manifestPath = resolvePluginManifestPath(rootDir);
|
|
if (!fs.existsSync(manifestPath)) return {
|
|
ok: false,
|
|
error: `plugin manifest not found: ${manifestPath}`,
|
|
manifestPath
|
|
};
|
|
let raw;
|
|
try {
|
|
raw = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
} catch (err) {
|
|
return {
|
|
ok: false,
|
|
error: `failed to parse plugin manifest: ${String(err)}`,
|
|
manifestPath
|
|
};
|
|
}
|
|
if (!isRecord(raw)) return {
|
|
ok: false,
|
|
error: "plugin manifest must be an object",
|
|
manifestPath
|
|
};
|
|
const id = typeof raw.id === "string" ? raw.id.trim() : "";
|
|
if (!id) return {
|
|
ok: false,
|
|
error: "plugin manifest requires id",
|
|
manifestPath
|
|
};
|
|
const configSchema = isRecord(raw.configSchema) ? raw.configSchema : null;
|
|
if (!configSchema) return {
|
|
ok: false,
|
|
error: "plugin manifest requires configSchema",
|
|
manifestPath
|
|
};
|
|
const kind = typeof raw.kind === "string" ? raw.kind : void 0;
|
|
const name = typeof raw.name === "string" ? raw.name.trim() : void 0;
|
|
const description = typeof raw.description === "string" ? raw.description.trim() : void 0;
|
|
const version = typeof raw.version === "string" ? raw.version.trim() : void 0;
|
|
const channels = normalizeStringList(raw.channels);
|
|
const providers = normalizeStringList(raw.providers);
|
|
const skills = normalizeStringList(raw.skills);
|
|
let uiHints;
|
|
if (isRecord(raw.uiHints)) uiHints = raw.uiHints;
|
|
return {
|
|
ok: true,
|
|
manifest: {
|
|
id,
|
|
configSchema,
|
|
kind,
|
|
channels,
|
|
providers,
|
|
skills,
|
|
name,
|
|
description,
|
|
version,
|
|
uiHints
|
|
},
|
|
manifestPath
|
|
};
|
|
}
|
|
function getPackageManifestMetadata(manifest) {
|
|
if (!manifest) return;
|
|
return manifest[MANIFEST_KEY];
|
|
}
|
|
|
|
//#endregion
|
|
//#region src/plugins/discovery.ts
|
|
const EXTENSION_EXTS = new Set([
|
|
".ts",
|
|
".js",
|
|
".mts",
|
|
".cts",
|
|
".mjs",
|
|
".cjs"
|
|
]);
|
|
function isExtensionFile(filePath) {
|
|
const ext = path.extname(filePath);
|
|
if (!EXTENSION_EXTS.has(ext)) return false;
|
|
return !filePath.endsWith(".d.ts");
|
|
}
|
|
function readPackageManifest(dir) {
|
|
const manifestPath = path.join(dir, "package.json");
|
|
if (!fs.existsSync(manifestPath)) return null;
|
|
try {
|
|
const raw = fs.readFileSync(manifestPath, "utf-8");
|
|
return JSON.parse(raw);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
function resolvePackageExtensions(manifest) {
|
|
const raw = getPackageManifestMetadata(manifest)?.extensions;
|
|
if (!Array.isArray(raw)) return [];
|
|
return raw.map((entry) => typeof entry === "string" ? entry.trim() : "").filter(Boolean);
|
|
}
|
|
function deriveIdHint(params) {
|
|
const base = path.basename(params.filePath, path.extname(params.filePath));
|
|
const rawPackageName = params.packageName?.trim();
|
|
if (!rawPackageName) return base;
|
|
const unscoped = rawPackageName.includes("/") ? rawPackageName.split("/").pop() ?? rawPackageName : rawPackageName;
|
|
if (!params.hasMultipleExtensions) return unscoped;
|
|
return `${unscoped}/${base}`;
|
|
}
|
|
function addCandidate(params) {
|
|
const resolved = path.resolve(params.source);
|
|
if (params.seen.has(resolved)) return;
|
|
params.seen.add(resolved);
|
|
const manifest = params.manifest ?? null;
|
|
params.candidates.push({
|
|
idHint: params.idHint,
|
|
source: resolved,
|
|
rootDir: path.resolve(params.rootDir),
|
|
origin: params.origin,
|
|
workspaceDir: params.workspaceDir,
|
|
packageName: manifest?.name?.trim() || void 0,
|
|
packageVersion: manifest?.version?.trim() || void 0,
|
|
packageDescription: manifest?.description?.trim() || void 0,
|
|
packageDir: params.packageDir,
|
|
packageManifest: getPackageManifestMetadata(manifest ?? void 0)
|
|
});
|
|
}
|
|
function discoverInDirectory(params) {
|
|
if (!fs.existsSync(params.dir)) return;
|
|
let entries = [];
|
|
try {
|
|
entries = fs.readdirSync(params.dir, { withFileTypes: true });
|
|
} catch (err) {
|
|
params.diagnostics.push({
|
|
level: "warn",
|
|
message: `failed to read extensions dir: ${params.dir} (${String(err)})`,
|
|
source: params.dir
|
|
});
|
|
return;
|
|
}
|
|
for (const entry of entries) {
|
|
const fullPath = path.join(params.dir, entry.name);
|
|
if (entry.isFile()) {
|
|
if (!isExtensionFile(fullPath)) continue;
|
|
addCandidate({
|
|
candidates: params.candidates,
|
|
seen: params.seen,
|
|
idHint: path.basename(entry.name, path.extname(entry.name)),
|
|
source: fullPath,
|
|
rootDir: path.dirname(fullPath),
|
|
origin: params.origin,
|
|
workspaceDir: params.workspaceDir
|
|
});
|
|
}
|
|
if (!entry.isDirectory()) continue;
|
|
const manifest = readPackageManifest(fullPath);
|
|
const extensions = manifest ? resolvePackageExtensions(manifest) : [];
|
|
if (extensions.length > 0) {
|
|
for (const extPath of extensions) {
|
|
const resolved = path.resolve(fullPath, extPath);
|
|
addCandidate({
|
|
candidates: params.candidates,
|
|
seen: params.seen,
|
|
idHint: deriveIdHint({
|
|
filePath: resolved,
|
|
packageName: manifest?.name,
|
|
hasMultipleExtensions: extensions.length > 1
|
|
}),
|
|
source: resolved,
|
|
rootDir: fullPath,
|
|
origin: params.origin,
|
|
workspaceDir: params.workspaceDir,
|
|
manifest,
|
|
packageDir: fullPath
|
|
});
|
|
}
|
|
continue;
|
|
}
|
|
const indexFile = [
|
|
"index.ts",
|
|
"index.js",
|
|
"index.mjs",
|
|
"index.cjs"
|
|
].map((candidate) => path.join(fullPath, candidate)).find((candidate) => fs.existsSync(candidate));
|
|
if (indexFile && isExtensionFile(indexFile)) addCandidate({
|
|
candidates: params.candidates,
|
|
seen: params.seen,
|
|
idHint: entry.name,
|
|
source: indexFile,
|
|
rootDir: fullPath,
|
|
origin: params.origin,
|
|
workspaceDir: params.workspaceDir,
|
|
manifest,
|
|
packageDir: fullPath
|
|
});
|
|
}
|
|
}
|
|
function discoverFromPath(params) {
|
|
const resolved = resolveUserPath(params.rawPath);
|
|
if (!fs.existsSync(resolved)) {
|
|
params.diagnostics.push({
|
|
level: "error",
|
|
message: `plugin path not found: ${resolved}`,
|
|
source: resolved
|
|
});
|
|
return;
|
|
}
|
|
const stat = fs.statSync(resolved);
|
|
if (stat.isFile()) {
|
|
if (!isExtensionFile(resolved)) {
|
|
params.diagnostics.push({
|
|
level: "error",
|
|
message: `plugin path is not a supported file: ${resolved}`,
|
|
source: resolved
|
|
});
|
|
return;
|
|
}
|
|
addCandidate({
|
|
candidates: params.candidates,
|
|
seen: params.seen,
|
|
idHint: path.basename(resolved, path.extname(resolved)),
|
|
source: resolved,
|
|
rootDir: path.dirname(resolved),
|
|
origin: params.origin,
|
|
workspaceDir: params.workspaceDir
|
|
});
|
|
return;
|
|
}
|
|
if (stat.isDirectory()) {
|
|
const manifest = readPackageManifest(resolved);
|
|
const extensions = manifest ? resolvePackageExtensions(manifest) : [];
|
|
if (extensions.length > 0) {
|
|
for (const extPath of extensions) {
|
|
const source = path.resolve(resolved, extPath);
|
|
addCandidate({
|
|
candidates: params.candidates,
|
|
seen: params.seen,
|
|
idHint: deriveIdHint({
|
|
filePath: source,
|
|
packageName: manifest?.name,
|
|
hasMultipleExtensions: extensions.length > 1
|
|
}),
|
|
source,
|
|
rootDir: resolved,
|
|
origin: params.origin,
|
|
workspaceDir: params.workspaceDir,
|
|
manifest,
|
|
packageDir: resolved
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
const indexFile = [
|
|
"index.ts",
|
|
"index.js",
|
|
"index.mjs",
|
|
"index.cjs"
|
|
].map((candidate) => path.join(resolved, candidate)).find((candidate) => fs.existsSync(candidate));
|
|
if (indexFile && isExtensionFile(indexFile)) {
|
|
addCandidate({
|
|
candidates: params.candidates,
|
|
seen: params.seen,
|
|
idHint: path.basename(resolved),
|
|
source: indexFile,
|
|
rootDir: resolved,
|
|
origin: params.origin,
|
|
workspaceDir: params.workspaceDir,
|
|
manifest,
|
|
packageDir: resolved
|
|
});
|
|
return;
|
|
}
|
|
discoverInDirectory({
|
|
dir: resolved,
|
|
origin: params.origin,
|
|
workspaceDir: params.workspaceDir,
|
|
candidates: params.candidates,
|
|
diagnostics: params.diagnostics,
|
|
seen: params.seen
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
function discoverOpenClawPlugins(params) {
|
|
const candidates = [];
|
|
const diagnostics = [];
|
|
const seen = /* @__PURE__ */ new Set();
|
|
const workspaceDir = params.workspaceDir?.trim();
|
|
const extra = params.extraPaths ?? [];
|
|
for (const extraPath of extra) {
|
|
if (typeof extraPath !== "string") continue;
|
|
const trimmed = extraPath.trim();
|
|
if (!trimmed) continue;
|
|
discoverFromPath({
|
|
rawPath: trimmed,
|
|
origin: "config",
|
|
workspaceDir: workspaceDir?.trim() || void 0,
|
|
candidates,
|
|
diagnostics,
|
|
seen
|
|
});
|
|
}
|
|
if (workspaceDir) {
|
|
const workspaceRoot = resolveUserPath(workspaceDir);
|
|
const workspaceExtDirs = [path.join(workspaceRoot, ".openclaw", "extensions")];
|
|
for (const dir of workspaceExtDirs) discoverInDirectory({
|
|
dir,
|
|
origin: "workspace",
|
|
workspaceDir: workspaceRoot,
|
|
candidates,
|
|
diagnostics,
|
|
seen
|
|
});
|
|
}
|
|
discoverInDirectory({
|
|
dir: path.join(resolveConfigDir(), "extensions"),
|
|
origin: "global",
|
|
candidates,
|
|
diagnostics,
|
|
seen
|
|
});
|
|
const bundledDir = resolveBundledPluginsDir();
|
|
if (bundledDir) discoverInDirectory({
|
|
dir: bundledDir,
|
|
origin: "bundled",
|
|
candidates,
|
|
diagnostics,
|
|
seen
|
|
});
|
|
return {
|
|
candidates,
|
|
diagnostics
|
|
};
|
|
}
|
|
|
|
//#endregion
|
|
//#region src/plugins/manifest-registry.ts
|
|
const registryCache = /* @__PURE__ */ new Map();
|
|
const DEFAULT_MANIFEST_CACHE_MS = 200;
|
|
function resolveManifestCacheMs(env) {
|
|
const raw = env.OPENCLAW_PLUGIN_MANIFEST_CACHE_MS?.trim();
|
|
if (raw === "" || raw === "0") return 0;
|
|
if (!raw) return DEFAULT_MANIFEST_CACHE_MS;
|
|
const parsed = Number.parseInt(raw, 10);
|
|
if (!Number.isFinite(parsed)) return DEFAULT_MANIFEST_CACHE_MS;
|
|
return Math.max(0, parsed);
|
|
}
|
|
function shouldUseManifestCache(env) {
|
|
if (env.OPENCLAW_DISABLE_PLUGIN_MANIFEST_CACHE?.trim()) return false;
|
|
return resolveManifestCacheMs(env) > 0;
|
|
}
|
|
function buildCacheKey(params) {
|
|
return `${params.workspaceDir ? resolveUserPath(params.workspaceDir) : ""}::${JSON.stringify(params.plugins)}`;
|
|
}
|
|
function safeStatMtimeMs(filePath) {
|
|
try {
|
|
return fs.statSync(filePath).mtimeMs;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
function normalizeManifestLabel(raw) {
|
|
const trimmed = raw?.trim();
|
|
return trimmed ? trimmed : void 0;
|
|
}
|
|
function buildRecord(params) {
|
|
return {
|
|
id: params.manifest.id,
|
|
name: normalizeManifestLabel(params.manifest.name) ?? params.candidate.packageName,
|
|
description: normalizeManifestLabel(params.manifest.description) ?? params.candidate.packageDescription,
|
|
version: normalizeManifestLabel(params.manifest.version) ?? params.candidate.packageVersion,
|
|
kind: params.manifest.kind,
|
|
channels: params.manifest.channels ?? [],
|
|
providers: params.manifest.providers ?? [],
|
|
skills: params.manifest.skills ?? [],
|
|
origin: params.candidate.origin,
|
|
workspaceDir: params.candidate.workspaceDir,
|
|
rootDir: params.candidate.rootDir,
|
|
source: params.candidate.source,
|
|
manifestPath: params.manifestPath,
|
|
schemaCacheKey: params.schemaCacheKey,
|
|
configSchema: params.configSchema,
|
|
configUiHints: params.manifest.uiHints
|
|
};
|
|
}
|
|
function loadPluginManifestRegistry(params) {
|
|
const normalized = normalizePluginsConfig((params.config ?? {}).plugins);
|
|
const cacheKey = buildCacheKey({
|
|
workspaceDir: params.workspaceDir,
|
|
plugins: normalized
|
|
});
|
|
const env = params.env ?? process.env;
|
|
const cacheEnabled = params.cache !== false && shouldUseManifestCache(env);
|
|
if (cacheEnabled) {
|
|
const cached = registryCache.get(cacheKey);
|
|
if (cached && cached.expiresAt > Date.now()) return cached.registry;
|
|
}
|
|
const discovery = params.candidates ? {
|
|
candidates: params.candidates,
|
|
diagnostics: params.diagnostics ?? []
|
|
} : discoverOpenClawPlugins({
|
|
workspaceDir: params.workspaceDir,
|
|
extraPaths: normalized.loadPaths
|
|
});
|
|
const diagnostics = [...discovery.diagnostics];
|
|
const candidates = discovery.candidates;
|
|
const records = [];
|
|
const seenIds = /* @__PURE__ */ new Set();
|
|
for (const candidate of candidates) {
|
|
const manifestRes = loadPluginManifest(candidate.rootDir);
|
|
if (!manifestRes.ok) {
|
|
diagnostics.push({
|
|
level: "error",
|
|
message: manifestRes.error,
|
|
source: manifestRes.manifestPath
|
|
});
|
|
continue;
|
|
}
|
|
const manifest = manifestRes.manifest;
|
|
if (candidate.idHint && candidate.idHint !== manifest.id) diagnostics.push({
|
|
level: "warn",
|
|
pluginId: manifest.id,
|
|
source: candidate.source,
|
|
message: `plugin id mismatch (manifest uses "${manifest.id}", entry hints "${candidate.idHint}")`
|
|
});
|
|
if (seenIds.has(manifest.id)) diagnostics.push({
|
|
level: "warn",
|
|
pluginId: manifest.id,
|
|
source: candidate.source,
|
|
message: `duplicate plugin id detected; later plugin may be overridden (${candidate.source})`
|
|
});
|
|
else seenIds.add(manifest.id);
|
|
const configSchema = manifest.configSchema;
|
|
const manifestMtime = safeStatMtimeMs(manifestRes.manifestPath);
|
|
const schemaCacheKey = manifestMtime ? `${manifestRes.manifestPath}:${manifestMtime}` : manifestRes.manifestPath;
|
|
records.push(buildRecord({
|
|
manifest,
|
|
candidate,
|
|
manifestPath: manifestRes.manifestPath,
|
|
schemaCacheKey,
|
|
configSchema
|
|
}));
|
|
}
|
|
const registry = {
|
|
plugins: records,
|
|
diagnostics
|
|
};
|
|
if (cacheEnabled) {
|
|
const ttl = resolveManifestCacheMs(env);
|
|
if (ttl > 0) registryCache.set(cacheKey, {
|
|
expiresAt: Date.now() + ttl,
|
|
registry
|
|
});
|
|
}
|
|
return registry;
|
|
}
|
|
|
|
//#endregion
|
|
export { MANIFEST_KEY as a, resolveEnableState as c, LEGACY_MANIFEST_KEYS as i, resolveMemorySlotDecision as l, discoverOpenClawPlugins as n, isTestDefaultMemorySlotDisabled as o, loadPluginManifest as r, normalizePluginsConfig as s, loadPluginManifestRegistry as t, applyExclusiveSlotSelection as u }; |