mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 01:31:08 +00:00
plugins: load lightweight provider discovery entries
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
"id": "ollama",
|
||||
"enabledByDefault": true,
|
||||
"providers": ["ollama"],
|
||||
"providerDiscoveryEntry": "./provider-discovery.ts",
|
||||
"providerAuthEnvVars": {
|
||||
"ollama": ["OLLAMA_API_KEY"]
|
||||
},
|
||||
|
||||
30
extensions/ollama/provider-discovery.import-guard.test.ts
Normal file
30
extensions/ollama/provider-discovery.import-guard.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const repoRoot = path.resolve(import.meta.dirname, "../..");
|
||||
|
||||
function readPluginSource(relativePath: string): string {
|
||||
return fs.readFileSync(path.join(repoRoot, relativePath), "utf8");
|
||||
}
|
||||
|
||||
describe("ollama provider discovery import surface", () => {
|
||||
it("stays off the full provider runtime graph", () => {
|
||||
const source = readPluginSource("extensions/ollama/provider-discovery.ts");
|
||||
|
||||
for (const forbidden of [
|
||||
"./index",
|
||||
"./api",
|
||||
"./runtime-api",
|
||||
"./src/setup",
|
||||
"./src/stream",
|
||||
"./src/embedding-provider",
|
||||
"./src/memory-embedding-adapter",
|
||||
"./src/web-search-provider",
|
||||
"openclaw/plugin-sdk/text-runtime",
|
||||
"openclaw/plugin-sdk/plugin-entry",
|
||||
]) {
|
||||
expect(source, `provider discovery must not import ${forbidden}`).not.toContain(forbidden);
|
||||
}
|
||||
});
|
||||
});
|
||||
199
extensions/ollama/provider-discovery.ts
Normal file
199
extensions/ollama/provider-discovery.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import type { ProviderCatalogContext } from "openclaw/plugin-sdk/provider-catalog-shared";
|
||||
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { OLLAMA_DEFAULT_BASE_URL } from "./src/defaults.js";
|
||||
import {
|
||||
buildOllamaModelDefinition,
|
||||
enrichOllamaModelsWithContext,
|
||||
fetchOllamaModels,
|
||||
resolveOllamaApiBase,
|
||||
} from "./src/provider-models.js";
|
||||
|
||||
const PROVIDER_ID = "ollama";
|
||||
const DEFAULT_API_KEY = "ollama-local";
|
||||
const OLLAMA_CONTEXT_ENRICH_LIMIT = 200;
|
||||
|
||||
type OllamaPluginConfig = {
|
||||
discovery?: {
|
||||
enabled?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
type OllamaProviderLikeConfig = ModelProviderConfig;
|
||||
type OllamaProviderPlugin = {
|
||||
id: string;
|
||||
label: string;
|
||||
docsPath: string;
|
||||
envVars: string[];
|
||||
auth: [];
|
||||
discovery: {
|
||||
order: "late";
|
||||
run: (ctx: ProviderCatalogContext) => ReturnType<typeof runOllamaDiscovery>;
|
||||
};
|
||||
};
|
||||
|
||||
function normalizeOptionalString(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
function readStringValue(value: unknown): string | undefined {
|
||||
if (typeof value === "string") {
|
||||
return normalizeOptionalString(value);
|
||||
}
|
||||
if (value && typeof value === "object" && "value" in value) {
|
||||
return normalizeOptionalString((value as { value?: unknown }).value);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveOllamaDiscoveryApiKey(params: {
|
||||
env: NodeJS.ProcessEnv;
|
||||
explicitApiKey?: string;
|
||||
resolvedApiKey?: string;
|
||||
}): string {
|
||||
const envApiKey = params.env.OLLAMA_API_KEY?.trim() ? "OLLAMA_API_KEY" : undefined;
|
||||
return envApiKey ?? params.explicitApiKey ?? params.resolvedApiKey ?? DEFAULT_API_KEY;
|
||||
}
|
||||
|
||||
function shouldSkipAmbientOllamaDiscovery(env: NodeJS.ProcessEnv): boolean {
|
||||
return Boolean(env.VITEST) || env.NODE_ENV === "test";
|
||||
}
|
||||
|
||||
function hasMeaningfulExplicitOllamaConfig(
|
||||
providerConfig: OllamaProviderLikeConfig | undefined,
|
||||
): boolean {
|
||||
if (!providerConfig) {
|
||||
return false;
|
||||
}
|
||||
if (Array.isArray(providerConfig.models) && providerConfig.models.length > 0) {
|
||||
return true;
|
||||
}
|
||||
if (typeof providerConfig.baseUrl === "string" && providerConfig.baseUrl.trim()) {
|
||||
return resolveOllamaApiBase(providerConfig.baseUrl) !== OLLAMA_DEFAULT_BASE_URL;
|
||||
}
|
||||
if (readStringValue(providerConfig.apiKey)) {
|
||||
return true;
|
||||
}
|
||||
if (providerConfig.auth) {
|
||||
return true;
|
||||
}
|
||||
if (typeof providerConfig.authHeader === "boolean") {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
providerConfig.headers &&
|
||||
typeof providerConfig.headers === "object" &&
|
||||
Object.keys(providerConfig.headers).length > 0
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (providerConfig.request) {
|
||||
return true;
|
||||
}
|
||||
if (typeof providerConfig.injectNumCtxForOpenAICompat === "boolean") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function buildOllamaProvider(
|
||||
configuredBaseUrl?: string,
|
||||
opts?: { quiet?: boolean },
|
||||
): Promise<ModelProviderConfig> {
|
||||
const apiBase = resolveOllamaApiBase(configuredBaseUrl);
|
||||
const { reachable, models } = await fetchOllamaModels(apiBase);
|
||||
if (!reachable && !opts?.quiet) {
|
||||
console.warn(`Ollama could not be reached at ${apiBase}.`);
|
||||
}
|
||||
const discovered = await enrichOllamaModelsWithContext(
|
||||
apiBase,
|
||||
models.slice(0, OLLAMA_CONTEXT_ENRICH_LIMIT),
|
||||
);
|
||||
return {
|
||||
baseUrl: apiBase,
|
||||
api: "ollama",
|
||||
models: discovered.map((model) =>
|
||||
buildOllamaModelDefinition(model.name, model.contextWindow, model.capabilities),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveOllamaPluginConfig(ctx: ProviderCatalogContext): OllamaPluginConfig {
|
||||
const entries = (ctx.config.plugins?.entries ?? {}) as Record<
|
||||
string,
|
||||
{ config?: OllamaPluginConfig }
|
||||
>;
|
||||
return entries.ollama?.config ?? {};
|
||||
}
|
||||
|
||||
async function runOllamaDiscovery(ctx: ProviderCatalogContext) {
|
||||
const pluginConfig = resolveOllamaPluginConfig(ctx);
|
||||
const explicit = ctx.config.models?.providers?.ollama;
|
||||
const hasExplicitModels = Array.isArray(explicit?.models) && explicit.models.length > 0;
|
||||
const hasMeaningfulExplicitConfig = hasMeaningfulExplicitOllamaConfig(explicit);
|
||||
const discoveryEnabled =
|
||||
pluginConfig.discovery?.enabled ?? ctx.config.models?.ollamaDiscovery?.enabled;
|
||||
if (!hasExplicitModels && discoveryEnabled === false) {
|
||||
return null;
|
||||
}
|
||||
const ollamaKey = ctx.resolveProviderApiKey(PROVIDER_ID).apiKey;
|
||||
const hasRealOllamaKey =
|
||||
typeof ollamaKey === "string" &&
|
||||
ollamaKey.trim().length > 0 &&
|
||||
ollamaKey.trim() !== DEFAULT_API_KEY;
|
||||
const explicitApiKey = readStringValue(explicit?.apiKey);
|
||||
if (hasExplicitModels && explicit) {
|
||||
return {
|
||||
provider: {
|
||||
...explicit,
|
||||
baseUrl:
|
||||
typeof explicit.baseUrl === "string" && explicit.baseUrl.trim()
|
||||
? resolveOllamaApiBase(explicit.baseUrl)
|
||||
: OLLAMA_DEFAULT_BASE_URL,
|
||||
api: explicit.api ?? "ollama",
|
||||
apiKey: resolveOllamaDiscoveryApiKey({
|
||||
env: ctx.env,
|
||||
explicitApiKey,
|
||||
resolvedApiKey: ollamaKey,
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
if (
|
||||
!hasRealOllamaKey &&
|
||||
!hasMeaningfulExplicitConfig &&
|
||||
shouldSkipAmbientOllamaDiscovery(ctx.env)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const provider = await buildOllamaProvider(explicit?.baseUrl, {
|
||||
quiet: !hasRealOllamaKey && !hasMeaningfulExplicitConfig,
|
||||
});
|
||||
if (provider.models?.length === 0 && !ollamaKey && !explicit?.apiKey) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
provider: {
|
||||
...provider,
|
||||
apiKey: resolveOllamaDiscoveryApiKey({
|
||||
env: ctx.env,
|
||||
explicitApiKey,
|
||||
resolvedApiKey: ollamaKey,
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const ollamaProviderDiscovery: OllamaProviderPlugin = {
|
||||
id: PROVIDER_ID,
|
||||
label: "Ollama",
|
||||
docsPath: "/providers/ollama",
|
||||
envVars: ["OLLAMA_API_KEY"],
|
||||
auth: [],
|
||||
discovery: {
|
||||
order: "late",
|
||||
run: runOllamaDiscovery,
|
||||
},
|
||||
};
|
||||
|
||||
export default ollamaProviderDiscovery;
|
||||
@@ -341,15 +341,19 @@ export async function resolveImplicitProviders(
|
||||
): Promise<NonNullable<OpenClawConfig["models"]>["providers"]> {
|
||||
const providers: Record<string, ProviderConfig> = {};
|
||||
const env = params.env ?? process.env;
|
||||
const authStore = ensureAuthProfileStore(params.agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
});
|
||||
let authStore: ReturnType<typeof ensureAuthProfileStore> | undefined;
|
||||
const getAuthStore = () =>
|
||||
(authStore ??= ensureAuthProfileStore(params.agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
}));
|
||||
const context: ImplicitProviderContext = {
|
||||
...params,
|
||||
authStore,
|
||||
get authStore() {
|
||||
return getAuthStore();
|
||||
},
|
||||
env,
|
||||
resolveProviderApiKey: createProviderApiKeyResolver(env, authStore, params.config),
|
||||
resolveProviderAuth: createProviderAuthResolver(env, authStore, params.config),
|
||||
resolveProviderApiKey: createProviderApiKeyResolver(env, getAuthStore, params.config),
|
||||
resolveProviderAuth: createProviderAuthResolver(env, getAuthStore, params.config),
|
||||
};
|
||||
const discoveryProviders = await resolvePluginDiscoveryProviders({
|
||||
config: params.config,
|
||||
|
||||
@@ -46,6 +46,14 @@ export type ProviderAuthResolver = (
|
||||
profileId?: string;
|
||||
};
|
||||
|
||||
type AuthProfileStoreInput =
|
||||
| ReturnType<typeof ensureAuthProfileStore>
|
||||
| (() => ReturnType<typeof ensureAuthProfileStore>);
|
||||
|
||||
function resolveAuthProfileStoreInput(input: AuthProfileStoreInput) {
|
||||
return typeof input === "function" ? input() : input;
|
||||
}
|
||||
|
||||
const ENV_VAR_NAME_RE = /^[A-Z_][A-Z0-9_]*$/;
|
||||
|
||||
export function normalizeApiKeyConfig(value: string): string {
|
||||
@@ -321,7 +329,7 @@ export function resolveMissingProviderApiKey(params: {
|
||||
|
||||
export function createProviderApiKeyResolver(
|
||||
env: NodeJS.ProcessEnv,
|
||||
authStore: ReturnType<typeof ensureAuthProfileStore>,
|
||||
authStoreInput: AuthProfileStoreInput,
|
||||
config?: OpenClawConfig,
|
||||
): ProviderApiKeyResolver {
|
||||
return (provider: string): { apiKey: string | undefined; discoveryApiKey?: string } => {
|
||||
@@ -333,36 +341,40 @@ export function createProviderApiKeyResolver(
|
||||
discoveryApiKey: toDiscoveryApiKey(env[envVar]),
|
||||
};
|
||||
}
|
||||
const fromProfiles = resolveApiKeyFromProfiles({
|
||||
provider: authProvider,
|
||||
store: authStore,
|
||||
env,
|
||||
});
|
||||
if (fromProfiles?.apiKey) {
|
||||
return {
|
||||
apiKey: fromProfiles.apiKey,
|
||||
discoveryApiKey: fromProfiles.discoveryApiKey,
|
||||
};
|
||||
}
|
||||
const fromConfig = resolveConfigBackedProviderAuth({
|
||||
provider: authProvider,
|
||||
config,
|
||||
});
|
||||
return {
|
||||
apiKey: fromConfig?.apiKey,
|
||||
discoveryApiKey: fromConfig?.discoveryApiKey,
|
||||
};
|
||||
if (fromConfig?.apiKey) {
|
||||
return {
|
||||
apiKey: fromConfig.apiKey,
|
||||
discoveryApiKey: fromConfig.discoveryApiKey,
|
||||
};
|
||||
}
|
||||
const fromProfiles = resolveApiKeyFromProfiles({
|
||||
provider: authProvider,
|
||||
store: resolveAuthProfileStoreInput(authStoreInput),
|
||||
env,
|
||||
});
|
||||
return fromProfiles?.apiKey
|
||||
? {
|
||||
apiKey: fromProfiles.apiKey,
|
||||
discoveryApiKey: fromProfiles.discoveryApiKey,
|
||||
}
|
||||
: { apiKey: undefined, discoveryApiKey: undefined };
|
||||
};
|
||||
}
|
||||
|
||||
export function createProviderAuthResolver(
|
||||
env: NodeJS.ProcessEnv,
|
||||
authStore: ReturnType<typeof ensureAuthProfileStore>,
|
||||
authStoreInput: AuthProfileStoreInput,
|
||||
config?: OpenClawConfig,
|
||||
): ProviderAuthResolver {
|
||||
return (provider: string, options?: { oauthMarker?: string }) => {
|
||||
const authProvider = resolveProviderIdForAuth(provider, { config, env });
|
||||
const authStore = resolveAuthProfileStoreInput(authStoreInput);
|
||||
const ids = listProfilesForProvider(authStore, authProvider);
|
||||
|
||||
let oauthCandidate:
|
||||
| {
|
||||
apiKey: string | undefined;
|
||||
@@ -425,7 +437,6 @@ export function createProviderAuthResolver(
|
||||
source: "none",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
apiKey: undefined,
|
||||
discoveryApiKey: undefined,
|
||||
|
||||
@@ -189,6 +189,30 @@ export function clearPluginLoaderCache(): void {
|
||||
|
||||
const defaultLogger = () => createSubsystemLogger("plugins");
|
||||
|
||||
function shouldProfilePluginLoader(): boolean {
|
||||
return process.env.OPENCLAW_PLUGIN_LOAD_PROFILE === "1";
|
||||
}
|
||||
|
||||
function profilePluginLoaderSync<T>(params: {
|
||||
phase: string;
|
||||
pluginId?: string;
|
||||
source: string;
|
||||
run: () => T;
|
||||
}): T {
|
||||
if (!shouldProfilePluginLoader()) {
|
||||
return params.run();
|
||||
}
|
||||
const startMs = performance.now();
|
||||
try {
|
||||
return params.run();
|
||||
} finally {
|
||||
const elapsedMs = performance.now() - startMs;
|
||||
console.error(
|
||||
`[plugin-load-profile] phase=${params.phase} plugin=${params.pluginId ?? "(core)"} elapsedMs=${elapsedMs.toFixed(1)} source=${params.source}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On Windows, the Node.js ESM loader requires absolute paths to be expressed
|
||||
* as file:// URLs (e.g. file:///C:/Users/...). Raw drive-letter paths like
|
||||
@@ -1134,9 +1158,14 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
throw new Error("Unable to resolve plugin runtime module");
|
||||
}
|
||||
const safeRuntimePath = toSafeImportPath(runtimeModulePath);
|
||||
const runtimeModule = getJiti(runtimeModulePath)(safeRuntimePath) as {
|
||||
createPluginRuntime?: (options?: CreatePluginRuntimeOptions) => PluginRuntime;
|
||||
};
|
||||
const runtimeModule = profilePluginLoaderSync({
|
||||
phase: "runtime-module",
|
||||
source: runtimeModulePath,
|
||||
run: () =>
|
||||
getJiti(runtimeModulePath)(safeRuntimePath) as {
|
||||
createPluginRuntime?: (options?: CreatePluginRuntimeOptions) => PluginRuntime;
|
||||
},
|
||||
});
|
||||
if (typeof runtimeModule.createPluginRuntime !== "function") {
|
||||
throw new Error("Plugin runtime module missing createPluginRuntime export");
|
||||
}
|
||||
@@ -1550,7 +1579,12 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
// Track the plugin as imported once module evaluation begins. Top-level
|
||||
// code may have already executed even if evaluation later throws.
|
||||
recordImportedPluginId(record.id);
|
||||
mod = getJiti(safeSource)(safeImportSource) as OpenClawPluginModule;
|
||||
mod = profilePluginLoaderSync({
|
||||
phase: registrationMode,
|
||||
pluginId: record.id,
|
||||
source: safeSource,
|
||||
run: () => getJiti(safeSource)(safeImportSource) as OpenClawPluginModule,
|
||||
});
|
||||
} catch (err) {
|
||||
recordPluginError({
|
||||
logger,
|
||||
@@ -2006,7 +2040,12 @@ export async function loadOpenClawPluginCliRegistry(
|
||||
|
||||
let mod: OpenClawPluginModule | null = null;
|
||||
try {
|
||||
mod = getJiti(safeSource)(safeImportSource) as OpenClawPluginModule;
|
||||
mod = profilePluginLoaderSync({
|
||||
phase: "cli-metadata",
|
||||
pluginId: record.id,
|
||||
source: safeSource,
|
||||
run: () => getJiti(safeSource)(safeImportSource) as OpenClawPluginModule,
|
||||
});
|
||||
} catch (err) {
|
||||
recordPluginError({
|
||||
logger,
|
||||
|
||||
@@ -75,6 +75,7 @@ export type PluginManifestRecord = {
|
||||
kind?: PluginKind | PluginKind[];
|
||||
channels: string[];
|
||||
providers: string[];
|
||||
providerDiscoverySource?: string;
|
||||
modelSupport?: PluginManifestModelSupport;
|
||||
cliBackends: string[];
|
||||
providerAuthEnvVars?: Record<string, string[]>;
|
||||
@@ -309,6 +310,9 @@ function buildRecord(params: {
|
||||
kind: params.manifest.kind,
|
||||
channels: params.manifest.channels ?? [],
|
||||
providers: params.manifest.providers ?? [],
|
||||
providerDiscoverySource: params.manifest.providerDiscoveryEntry
|
||||
? path.resolve(params.candidate.rootDir, params.manifest.providerDiscoveryEntry)
|
||||
: undefined,
|
||||
modelSupport: params.manifest.modelSupport,
|
||||
cliBackends: params.manifest.cliBackends ?? [],
|
||||
providerAuthEnvVars: params.manifest.providerAuthEnvVars,
|
||||
|
||||
@@ -96,6 +96,11 @@ export type PluginManifest = {
|
||||
kind?: PluginKind | PluginKind[];
|
||||
channels?: string[];
|
||||
providers?: string[];
|
||||
/**
|
||||
* Optional lightweight module that exports provider plugin metadata for
|
||||
* auth/catalog discovery. It should not import the full plugin runtime.
|
||||
*/
|
||||
providerDiscoveryEntry?: string;
|
||||
/**
|
||||
* Cheap model-family ownership metadata used before plugin runtime loads.
|
||||
* Use this for shorthand model refs that omit an explicit provider prefix.
|
||||
@@ -531,6 +536,7 @@ export function loadPluginManifest(
|
||||
const version = normalizeOptionalString(raw.version);
|
||||
const channels = normalizeTrimmedStringList(raw.channels);
|
||||
const providers = normalizeTrimmedStringList(raw.providers);
|
||||
const providerDiscoveryEntry = normalizeOptionalString(raw.providerDiscoveryEntry);
|
||||
const modelSupport = normalizeManifestModelSupport(raw.modelSupport);
|
||||
const cliBackends = normalizeTrimmedStringList(raw.cliBackends);
|
||||
const providerAuthEnvVars = normalizeStringListRecord(raw.providerAuthEnvVars);
|
||||
@@ -560,6 +566,7 @@ export function loadPluginManifest(
|
||||
kind,
|
||||
channels,
|
||||
providers,
|
||||
providerDiscoveryEntry,
|
||||
modelSupport,
|
||||
cliBackends,
|
||||
providerAuthEnvVars,
|
||||
|
||||
@@ -1,13 +1,86 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { loadPluginManifestRegistry } from "./manifest-registry.js";
|
||||
import { resolveDiscoveredProviderPluginIds } from "./providers.js";
|
||||
import { resolvePluginProviders } from "./providers.runtime.js";
|
||||
import { createPluginSourceLoader } from "./source-loader.js";
|
||||
import type { ProviderPlugin } from "./types.js";
|
||||
|
||||
type ProviderDiscoveryModule =
|
||||
| ProviderPlugin
|
||||
| ProviderPlugin[]
|
||||
| {
|
||||
default?: ProviderPlugin | ProviderPlugin[];
|
||||
providers?: ProviderPlugin[];
|
||||
provider?: ProviderPlugin;
|
||||
};
|
||||
|
||||
function normalizeDiscoveryModule(value: ProviderDiscoveryModule): ProviderPlugin[] {
|
||||
const resolved =
|
||||
value && typeof value === "object" && "default" in value && value.default !== undefined
|
||||
? value.default
|
||||
: value;
|
||||
if (Array.isArray(resolved)) {
|
||||
return resolved;
|
||||
}
|
||||
if (resolved && typeof resolved === "object" && "id" in resolved) {
|
||||
return [resolved];
|
||||
}
|
||||
if (value && typeof value === "object" && !Array.isArray(value)) {
|
||||
const record = value as { providers?: ProviderPlugin[]; provider?: ProviderPlugin };
|
||||
if (Array.isArray(record.providers)) {
|
||||
return record.providers;
|
||||
}
|
||||
if (record.provider) {
|
||||
return [record.provider];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function resolveProviderDiscoveryEntryPlugins(params: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
onlyPluginIds?: string[];
|
||||
}): ProviderPlugin[] {
|
||||
const pluginIds = resolveDiscoveredProviderPluginIds(params);
|
||||
const pluginIdSet = new Set(pluginIds);
|
||||
const records = loadPluginManifestRegistry(params).plugins.filter(
|
||||
(plugin) => plugin.providerDiscoverySource && pluginIdSet.has(plugin.id),
|
||||
);
|
||||
if (records.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const loadSource = createPluginSourceLoader();
|
||||
const providers: ProviderPlugin[] = [];
|
||||
for (const manifest of records) {
|
||||
try {
|
||||
const moduleExport = loadSource(manifest.providerDiscoverySource!) as ProviderDiscoveryModule;
|
||||
providers.push(
|
||||
...normalizeDiscoveryModule(moduleExport).map((provider) => ({
|
||||
...provider,
|
||||
pluginId: manifest.id,
|
||||
})),
|
||||
);
|
||||
} catch {
|
||||
// Discovery fast path is optional. Fall back to the full plugin loader
|
||||
// below so existing plugin diagnostics/load behavior remains canonical.
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return providers;
|
||||
}
|
||||
|
||||
export function resolvePluginDiscoveryProvidersRuntime(params: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
onlyPluginIds?: string[];
|
||||
}): ProviderPlugin[] {
|
||||
const entryProviders = resolveProviderDiscoveryEntryPlugins(params);
|
||||
if (entryProviders.length > 0) {
|
||||
return entryProviders;
|
||||
}
|
||||
return resolvePluginProviders({
|
||||
...params,
|
||||
bundledProviderAllowlistCompat: true,
|
||||
|
||||
43
src/plugins/source-loader.ts
Normal file
43
src/plugins/source-loader.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { createJiti } from "jiti";
|
||||
import {
|
||||
buildPluginLoaderAliasMap,
|
||||
buildPluginLoaderJitiOptions,
|
||||
shouldPreferNativeJiti,
|
||||
} from "./sdk-alias.js";
|
||||
|
||||
export type PluginSourceLoader = (modulePath: string) => unknown;
|
||||
|
||||
function shouldProfilePluginSourceLoader(): boolean {
|
||||
return process.env.OPENCLAW_PLUGIN_LOAD_PROFILE === "1";
|
||||
}
|
||||
|
||||
export function createPluginSourceLoader(): PluginSourceLoader {
|
||||
const loaders = new Map<string, ReturnType<typeof createJiti>>();
|
||||
return (modulePath) => {
|
||||
const tryNative = shouldPreferNativeJiti(modulePath);
|
||||
const aliasMap = buildPluginLoaderAliasMap(modulePath, process.argv[1], import.meta.url);
|
||||
const cacheKey = JSON.stringify({
|
||||
tryNative,
|
||||
aliasMap: Object.entries(aliasMap).toSorted(([left], [right]) => left.localeCompare(right)),
|
||||
});
|
||||
let jiti = loaders.get(cacheKey);
|
||||
if (!jiti) {
|
||||
jiti = createJiti(import.meta.url, {
|
||||
...buildPluginLoaderJitiOptions(aliasMap),
|
||||
tryNative,
|
||||
});
|
||||
loaders.set(cacheKey, jiti);
|
||||
}
|
||||
if (!shouldProfilePluginSourceLoader()) {
|
||||
return jiti(modulePath);
|
||||
}
|
||||
const startMs = performance.now();
|
||||
try {
|
||||
return jiti(modulePath);
|
||||
} finally {
|
||||
console.error(
|
||||
`[plugin-load-profile] phase=source-loader plugin=(direct) elapsedMs=${(performance.now() - startMs).toFixed(1)} source=${modulePath}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user