mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-21 22:21:33 +00:00
feat: add Fireworks provider and simplify plugin setup loading
This commit is contained in:
@@ -47,6 +47,38 @@ export type BundledPluginCompatibleActivationInputs = PluginActivationInputs & {
|
||||
compatPluginIds: string[];
|
||||
};
|
||||
|
||||
export function withActivatedPluginIds(params: {
|
||||
config?: OpenClawConfig;
|
||||
pluginIds: readonly string[];
|
||||
}): OpenClawConfig | undefined {
|
||||
if (params.pluginIds.length === 0) {
|
||||
return params.config;
|
||||
}
|
||||
const allow = new Set(params.config?.plugins?.allow ?? []);
|
||||
const entries = {
|
||||
...params.config?.plugins?.entries,
|
||||
};
|
||||
for (const pluginId of params.pluginIds) {
|
||||
const normalized = pluginId.trim();
|
||||
if (!normalized) {
|
||||
continue;
|
||||
}
|
||||
allow.add(normalized);
|
||||
entries[normalized] = {
|
||||
...entries[normalized],
|
||||
enabled: true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...params.config,
|
||||
plugins: {
|
||||
...params.config?.plugins,
|
||||
...(allow.size > 0 ? { allow: [...allow] } : {}),
|
||||
entries,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function applyPluginCompatibilityOverrides(params: {
|
||||
config?: OpenClawConfig;
|
||||
compat?: PluginActivationCompatConfig;
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
// Auto-generated by scripts/generate-bundled-provider-auth-env-vars.mjs. Do not edit directly.
|
||||
|
||||
export const BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES = {
|
||||
anthropic: ["ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"],
|
||||
brave: ["BRAVE_API_KEY"],
|
||||
byteplus: ["BYTEPLUS_API_KEY"],
|
||||
chutes: ["CHUTES_API_KEY", "CHUTES_OAUTH_TOKEN"],
|
||||
"cloudflare-ai-gateway": ["CLOUDFLARE_AI_GATEWAY_API_KEY"],
|
||||
deepgram: ["DEEPGRAM_API_KEY"],
|
||||
deepseek: ["DEEPSEEK_API_KEY"],
|
||||
exa: ["EXA_API_KEY"],
|
||||
fal: ["FAL_KEY"],
|
||||
firecrawl: ["FIRECRAWL_API_KEY"],
|
||||
"github-copilot": ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"],
|
||||
google: ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
|
||||
groq: ["GROQ_API_KEY"],
|
||||
huggingface: ["HUGGINGFACE_HUB_TOKEN", "HF_TOKEN"],
|
||||
kilocode: ["KILOCODE_API_KEY"],
|
||||
kimi: ["KIMI_API_KEY", "KIMICODE_API_KEY"],
|
||||
"kimi-coding": ["KIMI_API_KEY", "KIMICODE_API_KEY"],
|
||||
litellm: ["LITELLM_API_KEY"],
|
||||
"microsoft-foundry": ["AZURE_OPENAI_API_KEY"],
|
||||
minimax: ["MINIMAX_API_KEY"],
|
||||
"minimax-portal": ["MINIMAX_OAUTH_TOKEN", "MINIMAX_API_KEY"],
|
||||
mistral: ["MISTRAL_API_KEY"],
|
||||
moonshot: ["MOONSHOT_API_KEY"],
|
||||
nvidia: ["NVIDIA_API_KEY"],
|
||||
ollama: ["OLLAMA_API_KEY"],
|
||||
openai: ["OPENAI_API_KEY"],
|
||||
opencode: ["OPENCODE_API_KEY", "OPENCODE_ZEN_API_KEY"],
|
||||
"opencode-go": ["OPENCODE_API_KEY", "OPENCODE_ZEN_API_KEY"],
|
||||
openrouter: ["OPENROUTER_API_KEY"],
|
||||
perplexity: ["PERPLEXITY_API_KEY", "OPENROUTER_API_KEY"],
|
||||
qianfan: ["QIANFAN_API_KEY"],
|
||||
qwen: ["QWEN_API_KEY", "MODELSTUDIO_API_KEY", "DASHSCOPE_API_KEY"],
|
||||
sglang: ["SGLANG_API_KEY"],
|
||||
stepfun: ["STEPFUN_API_KEY"],
|
||||
"stepfun-plan": ["STEPFUN_API_KEY"],
|
||||
synthetic: ["SYNTHETIC_API_KEY"],
|
||||
tavily: ["TAVILY_API_KEY"],
|
||||
together: ["TOGETHER_API_KEY"],
|
||||
venice: ["VENICE_API_KEY"],
|
||||
"vercel-ai-gateway": ["AI_GATEWAY_API_KEY"],
|
||||
vllm: ["VLLM_API_KEY"],
|
||||
volcengine: ["VOLCANO_ENGINE_API_KEY"],
|
||||
xai: ["XAI_API_KEY"],
|
||||
xiaomi: ["XIAOMI_API_KEY"],
|
||||
zai: ["ZAI_API_KEY", "Z_AI_API_KEY"],
|
||||
} as const satisfies Record<string, readonly string[]>;
|
||||
@@ -1,112 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
collectBundledProviderAuthEnvVars,
|
||||
writeBundledProviderAuthEnvVarModule,
|
||||
} from "../../scripts/generate-bundled-provider-auth-env-vars.mjs";
|
||||
import { BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES } from "./bundled-provider-auth-env-vars.js";
|
||||
import {
|
||||
createGeneratedPluginTempRoot,
|
||||
installGeneratedPluginTempRootCleanup,
|
||||
pluginTestRepoRoot as repoRoot,
|
||||
writeJson,
|
||||
} from "./generated-plugin-test-helpers.js";
|
||||
|
||||
installGeneratedPluginTempRootCleanup();
|
||||
|
||||
function expectGeneratedAuthEnvVarModuleState(params: {
|
||||
tempRoot: string;
|
||||
expectedChanged: boolean;
|
||||
expectedWrote: boolean;
|
||||
}) {
|
||||
const result = writeBundledProviderAuthEnvVarModule({
|
||||
repoRoot: params.tempRoot,
|
||||
outputPath: "src/plugins/bundled-provider-auth-env-vars.generated.ts",
|
||||
check: true,
|
||||
});
|
||||
expect(result.changed).toBe(params.expectedChanged);
|
||||
expect(result.wrote).toBe(params.expectedWrote);
|
||||
}
|
||||
|
||||
function expectGeneratedAuthEnvVarCheckMode(tempRoot: string) {
|
||||
expectGeneratedAuthEnvVarModuleState({
|
||||
tempRoot,
|
||||
expectedChanged: false,
|
||||
expectedWrote: false,
|
||||
});
|
||||
}
|
||||
|
||||
function expectBundledProviderEnvVars(expected: Record<string, readonly string[]>) {
|
||||
expect(
|
||||
Object.fromEntries(
|
||||
Object.keys(expected).map((providerId) => [
|
||||
providerId,
|
||||
BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES[
|
||||
providerId as keyof typeof BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES
|
||||
],
|
||||
]),
|
||||
),
|
||||
).toEqual(expected);
|
||||
}
|
||||
|
||||
function expectMissingBundledProviderEnvVars(providerIds: readonly string[]) {
|
||||
providerIds.forEach((providerId) => {
|
||||
expect(providerId in BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES).toBe(false);
|
||||
});
|
||||
}
|
||||
|
||||
describe("bundled provider auth env vars", () => {
|
||||
it("matches the generated manifest snapshot", () => {
|
||||
expect(BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES).toEqual(
|
||||
collectBundledProviderAuthEnvVars({ repoRoot }),
|
||||
);
|
||||
});
|
||||
|
||||
it("reads bundled provider auth env vars from plugin manifests", () => {
|
||||
expectBundledProviderEnvVars({
|
||||
brave: ["BRAVE_API_KEY"],
|
||||
deepgram: ["DEEPGRAM_API_KEY"],
|
||||
firecrawl: ["FIRECRAWL_API_KEY"],
|
||||
"github-copilot": ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"],
|
||||
groq: ["GROQ_API_KEY"],
|
||||
perplexity: ["PERPLEXITY_API_KEY", "OPENROUTER_API_KEY"],
|
||||
tavily: ["TAVILY_API_KEY"],
|
||||
"minimax-portal": ["MINIMAX_OAUTH_TOKEN", "MINIMAX_API_KEY"],
|
||||
openai: ["OPENAI_API_KEY"],
|
||||
fal: ["FAL_KEY"],
|
||||
});
|
||||
expectMissingBundledProviderEnvVars(["openai-codex"]);
|
||||
});
|
||||
|
||||
it("supports check mode for stale generated artifacts", () => {
|
||||
const tempRoot = createGeneratedPluginTempRoot("openclaw-provider-auth-env-vars-");
|
||||
|
||||
writeJson(path.join(tempRoot, "extensions", "alpha", "openclaw.plugin.json"), {
|
||||
id: "alpha",
|
||||
providerAuthEnvVars: {
|
||||
alpha: ["ALPHA_TOKEN"],
|
||||
},
|
||||
});
|
||||
|
||||
const initial = writeBundledProviderAuthEnvVarModule({
|
||||
repoRoot: tempRoot,
|
||||
outputPath: "src/plugins/bundled-provider-auth-env-vars.generated.ts",
|
||||
});
|
||||
expect(initial.wrote).toBe(true);
|
||||
|
||||
expectGeneratedAuthEnvVarCheckMode(tempRoot);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(tempRoot, "src/plugins/bundled-provider-auth-env-vars.generated.ts"),
|
||||
"// stale\n",
|
||||
"utf8",
|
||||
);
|
||||
|
||||
expectGeneratedAuthEnvVarModuleState({
|
||||
tempRoot,
|
||||
expectedChanged: true,
|
||||
expectedWrote: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
// Generated from extension manifests so core secrets/auth code does not need
|
||||
// static imports into extension source trees.
|
||||
export { BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES } from "./bundled-provider-auth-env-vars.generated.js";
|
||||
@@ -1,7 +1 @@
|
||||
import { BUNDLED_WEB_FETCH_PLUGIN_IDS as BUNDLED_WEB_FETCH_PLUGIN_IDS_FROM_METADATA } from "./bundled-capability-metadata.js";
|
||||
|
||||
export const BUNDLED_WEB_FETCH_PLUGIN_IDS = BUNDLED_WEB_FETCH_PLUGIN_IDS_FROM_METADATA;
|
||||
|
||||
export function listBundledWebFetchPluginIds(): string[] {
|
||||
return [...BUNDLED_WEB_FETCH_PLUGIN_IDS];
|
||||
}
|
||||
export { resolveBundledWebFetchPluginIds as listBundledWebFetchPluginIds } from "./bundled-web-fetch.js";
|
||||
|
||||
@@ -1,18 +1 @@
|
||||
import { BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS } from "./bundled-capability-metadata.js";
|
||||
|
||||
const bundledWebFetchProviderPluginIds = Object.fromEntries(
|
||||
BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS.flatMap((entry) =>
|
||||
entry.webFetchProviderIds.map((providerId) => [providerId, entry.pluginId] as const),
|
||||
).toSorted(([left], [right]) => left.localeCompare(right)),
|
||||
) as Readonly<Record<string, string>>;
|
||||
|
||||
export function resolveBundledWebFetchPluginId(providerId: string | undefined): string | undefined {
|
||||
if (!providerId) {
|
||||
return undefined;
|
||||
}
|
||||
const normalizedProviderId = providerId.trim().toLowerCase();
|
||||
if (!(normalizedProviderId in bundledWebFetchProviderPluginIds)) {
|
||||
return undefined;
|
||||
}
|
||||
return bundledWebFetchProviderPluginIds[normalizedProviderId];
|
||||
}
|
||||
export { resolveBundledWebFetchPluginId } from "./bundled-web-fetch.js";
|
||||
|
||||
@@ -1,25 +1,50 @@
|
||||
import { loadBundledCapabilityRuntimeRegistry } from "./bundled-capability-runtime.js";
|
||||
import { BUNDLED_WEB_FETCH_PLUGIN_IDS } from "./bundled-web-fetch-ids.js";
|
||||
import { resolveBundledWebFetchPluginId as resolveBundledWebFetchPluginIdFromMap } from "./bundled-web-fetch-provider-ids.js";
|
||||
import type { PluginLoadOptions } from "./loader.js";
|
||||
import { loadPluginManifestRegistry } from "./manifest-registry.js";
|
||||
import type { PluginWebFetchProviderEntry } from "./types.js";
|
||||
|
||||
type BundledWebFetchProviderEntry = PluginWebFetchProviderEntry & { pluginId: string };
|
||||
|
||||
let bundledWebFetchProvidersCache: BundledWebFetchProviderEntry[] | null = null;
|
||||
const bundledWebFetchProvidersCache = new Map<string, BundledWebFetchProviderEntry[]>();
|
||||
|
||||
function loadBundledWebFetchProviders(): BundledWebFetchProviderEntry[] {
|
||||
if (!bundledWebFetchProvidersCache) {
|
||||
bundledWebFetchProvidersCache = loadBundledCapabilityRuntimeRegistry({
|
||||
pluginIds: BUNDLED_WEB_FETCH_PLUGIN_IDS,
|
||||
pluginSdkResolution: "dist",
|
||||
}).webFetchProviders.map((entry) => ({
|
||||
pluginId: entry.pluginId,
|
||||
...entry.provider,
|
||||
}));
|
||||
function resolveBundledWebFetchManifestPlugins(params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
}) {
|
||||
return loadPluginManifestRegistry({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
}).plugins.filter(
|
||||
(plugin) =>
|
||||
plugin.origin === "bundled" && (plugin.contracts?.webFetchProviders?.length ?? 0) > 0,
|
||||
);
|
||||
}
|
||||
|
||||
function loadBundledWebFetchProviders(params?: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
}): BundledWebFetchProviderEntry[] {
|
||||
const pluginIds = resolveBundledWebFetchPluginIds(params ?? {});
|
||||
const cacheKey = pluginIds.join("\u0000");
|
||||
const cached = bundledWebFetchProvidersCache.get(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
return bundledWebFetchProvidersCache;
|
||||
const providers =
|
||||
pluginIds.length === 0
|
||||
? []
|
||||
: loadBundledCapabilityRuntimeRegistry({
|
||||
pluginIds,
|
||||
pluginSdkResolution: "dist",
|
||||
}).webFetchProviders.map((entry) => ({
|
||||
pluginId: entry.pluginId,
|
||||
...entry.provider,
|
||||
}));
|
||||
bundledWebFetchProvidersCache.set(cacheKey, providers);
|
||||
return providers;
|
||||
}
|
||||
|
||||
export function resolveBundledWebFetchPluginIds(params: {
|
||||
@@ -27,23 +52,41 @@ export function resolveBundledWebFetchPluginIds(params: {
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
}): string[] {
|
||||
const bundledWebFetchPluginIdSet = new Set<string>(BUNDLED_WEB_FETCH_PLUGIN_IDS);
|
||||
return loadPluginManifestRegistry({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
})
|
||||
.plugins.filter(
|
||||
(plugin) => plugin.origin === "bundled" && bundledWebFetchPluginIdSet.has(plugin.id),
|
||||
)
|
||||
return resolveBundledWebFetchManifestPlugins(params)
|
||||
.map((plugin) => plugin.id)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
export function listBundledWebFetchProviders(): PluginWebFetchProviderEntry[] {
|
||||
return loadBundledWebFetchProviders();
|
||||
export function listBundledWebFetchProviders(params?: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
}): PluginWebFetchProviderEntry[] {
|
||||
return loadBundledWebFetchProviders(params);
|
||||
}
|
||||
|
||||
export function resolveBundledWebFetchPluginId(providerId: string | undefined): string | undefined {
|
||||
return resolveBundledWebFetchPluginIdFromMap(providerId);
|
||||
export function resolveBundledWebFetchPluginId(
|
||||
providerId: string | undefined,
|
||||
params?: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
},
|
||||
): string | undefined {
|
||||
if (!providerId) {
|
||||
return undefined;
|
||||
}
|
||||
const normalizedProviderId = providerId.trim().toLowerCase();
|
||||
if (!normalizedProviderId) {
|
||||
return undefined;
|
||||
}
|
||||
return resolveBundledWebFetchManifestPlugins({
|
||||
config: params?.config,
|
||||
workspaceDir: params?.workspaceDir,
|
||||
env: params?.env,
|
||||
}).find((plugin) =>
|
||||
plugin.contracts?.webFetchProviders?.some(
|
||||
(candidate) => candidate.trim().toLowerCase() === normalizedProviderId,
|
||||
),
|
||||
)?.id;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BUNDLED_WEB_SEARCH_PLUGIN_IDS as BUNDLED_WEB_SEARCH_PLUGIN_IDS_FROM_METADATA } from "./bundled-capability-metadata.js";
|
||||
import { listBundledWebSearchPluginIds as listBundledWebSearchPluginIdsImpl } from "./bundled-web-search.js";
|
||||
|
||||
export const BUNDLED_WEB_SEARCH_PLUGIN_IDS = BUNDLED_WEB_SEARCH_PLUGIN_IDS_FROM_METADATA;
|
||||
export const BUNDLED_WEB_SEARCH_PLUGIN_IDS = listBundledWebSearchPluginIdsImpl();
|
||||
|
||||
export function listBundledWebSearchPluginIds(): string[] {
|
||||
return [...BUNDLED_WEB_SEARCH_PLUGIN_IDS];
|
||||
return listBundledWebSearchPluginIdsImpl();
|
||||
}
|
||||
|
||||
@@ -1,14 +1 @@
|
||||
import { BUNDLED_WEB_SEARCH_PROVIDER_PLUGIN_IDS } from "./bundled-capability-metadata.js";
|
||||
|
||||
export function resolveBundledWebSearchPluginId(
|
||||
providerId: string | undefined,
|
||||
): string | undefined {
|
||||
if (!providerId) {
|
||||
return undefined;
|
||||
}
|
||||
const normalizedProviderId = providerId.trim().toLowerCase();
|
||||
if (!(normalizedProviderId in BUNDLED_WEB_SEARCH_PROVIDER_PLUGIN_IDS)) {
|
||||
return undefined;
|
||||
}
|
||||
return BUNDLED_WEB_SEARCH_PROVIDER_PLUGIN_IDS[normalizedProviderId];
|
||||
}
|
||||
export { resolveBundledWebSearchPluginId } from "./bundled-web-search.js";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { loadBundledCapabilityRuntimeRegistry } from "./bundled-capability-runtime.js";
|
||||
import { BUNDLED_WEB_SEARCH_PLUGIN_IDS } from "./bundled-web-search-ids.js";
|
||||
import { hasBundledWebSearchCredential } from "./bundled-web-search-registry.js";
|
||||
import {
|
||||
listBundledWebSearchPluginIds,
|
||||
@@ -58,8 +57,9 @@ describe("bundled web search helpers", () => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(loadPluginManifestRegistry).mockReturnValue({
|
||||
plugins: [
|
||||
{ id: "xai", origin: "bundled" },
|
||||
{ id: "google", origin: "bundled" },
|
||||
{ id: "xai", origin: "bundled", contracts: { webSearchProviders: ["grok"] } },
|
||||
{ id: "google", origin: "bundled", contracts: { webSearchProviders: ["gemini"] } },
|
||||
{ id: "minimax", origin: "bundled", contracts: { webSearchProviders: ["minimax"] } },
|
||||
{ id: "noise", origin: "bundled" },
|
||||
{ id: "external-google", origin: "workspace" },
|
||||
] as never[],
|
||||
@@ -92,7 +92,7 @@ describe("bundled web search helpers", () => {
|
||||
} as never);
|
||||
});
|
||||
|
||||
it("filters bundled manifest entries down to known bundled web search plugins", () => {
|
||||
it("returns bundled manifest-derived web search plugins from the registry", () => {
|
||||
expect(
|
||||
resolveBundledWebSearchPluginIds({
|
||||
config: {
|
||||
@@ -103,7 +103,7 @@ describe("bundled web search helpers", () => {
|
||||
workspaceDir: "/tmp/workspace",
|
||||
env: { OPENCLAW_HOME: "/tmp/openclaw-home" },
|
||||
}),
|
||||
).toEqual(["google", "xai"]);
|
||||
).toEqual(["google", "minimax", "xai"]);
|
||||
expect(loadPluginManifestRegistry).toHaveBeenCalledWith({
|
||||
config: {
|
||||
plugins: {
|
||||
@@ -117,8 +117,8 @@ describe("bundled web search helpers", () => {
|
||||
|
||||
it("returns a copy of the bundled plugin id fast-path list", () => {
|
||||
const listed = listBundledWebSearchPluginIds();
|
||||
expect(listed).toEqual([...BUNDLED_WEB_SEARCH_PLUGIN_IDS]);
|
||||
expect(listed).not.toBe(BUNDLED_WEB_SEARCH_PLUGIN_IDS);
|
||||
expect(listed).toEqual(["google", "minimax", "xai"]);
|
||||
expect(listed).not.toBe(listBundledWebSearchPluginIds());
|
||||
});
|
||||
|
||||
it("maps bundled provider ids back to their owning plugins", () => {
|
||||
@@ -140,7 +140,7 @@ describe("bundled web search helpers", () => {
|
||||
]);
|
||||
expect(loadBundledCapabilityRuntimeRegistry).toHaveBeenCalledTimes(1);
|
||||
expect(loadBundledCapabilityRuntimeRegistry).toHaveBeenCalledWith({
|
||||
pluginIds: BUNDLED_WEB_SEARCH_PLUGIN_IDS,
|
||||
pluginIds: ["google", "minimax", "xai"],
|
||||
pluginSdkResolution: "dist",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,25 +1,50 @@
|
||||
import { BUNDLED_WEB_SEARCH_PLUGIN_IDS } from "./bundled-capability-metadata.js";
|
||||
import { loadBundledCapabilityRuntimeRegistry } from "./bundled-capability-runtime.js";
|
||||
import { resolveBundledWebSearchPluginId as resolveBundledWebSearchPluginIdFromMap } from "./bundled-web-search-provider-ids.js";
|
||||
import type { PluginLoadOptions } from "./loader.js";
|
||||
import { loadPluginManifestRegistry } from "./manifest-registry.js";
|
||||
import type { PluginWebSearchProviderEntry } from "./types.js";
|
||||
|
||||
type BundledWebSearchProviderEntry = PluginWebSearchProviderEntry & { pluginId: string };
|
||||
|
||||
let bundledWebSearchProvidersCache: BundledWebSearchProviderEntry[] | null = null;
|
||||
const bundledWebSearchProvidersCache = new Map<string, BundledWebSearchProviderEntry[]>();
|
||||
|
||||
function loadBundledWebSearchProviders(): BundledWebSearchProviderEntry[] {
|
||||
if (!bundledWebSearchProvidersCache) {
|
||||
bundledWebSearchProvidersCache = loadBundledCapabilityRuntimeRegistry({
|
||||
pluginIds: BUNDLED_WEB_SEARCH_PLUGIN_IDS,
|
||||
pluginSdkResolution: "dist",
|
||||
}).webSearchProviders.map((entry) => ({
|
||||
pluginId: entry.pluginId,
|
||||
...entry.provider,
|
||||
}));
|
||||
function resolveBundledWebSearchManifestPlugins(params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
}) {
|
||||
return loadPluginManifestRegistry({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
}).plugins.filter(
|
||||
(plugin) =>
|
||||
plugin.origin === "bundled" && (plugin.contracts?.webSearchProviders?.length ?? 0) > 0,
|
||||
);
|
||||
}
|
||||
|
||||
function loadBundledWebSearchProviders(params?: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
}): BundledWebSearchProviderEntry[] {
|
||||
const pluginIds = resolveBundledWebSearchPluginIds(params ?? {});
|
||||
const cacheKey = pluginIds.join("\u0000");
|
||||
const cached = bundledWebSearchProvidersCache.get(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
return bundledWebSearchProvidersCache;
|
||||
const providers =
|
||||
pluginIds.length === 0
|
||||
? []
|
||||
: loadBundledCapabilityRuntimeRegistry({
|
||||
pluginIds,
|
||||
pluginSdkResolution: "dist",
|
||||
}).webSearchProviders.map((entry) => ({
|
||||
pluginId: entry.pluginId,
|
||||
...entry.provider,
|
||||
}));
|
||||
bundledWebSearchProvidersCache.set(cacheKey, providers);
|
||||
return providers;
|
||||
}
|
||||
|
||||
export function resolveBundledWebSearchPluginIds(params: {
|
||||
@@ -27,29 +52,49 @@ export function resolveBundledWebSearchPluginIds(params: {
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
}): string[] {
|
||||
const bundledWebSearchPluginIdSet = new Set<string>(BUNDLED_WEB_SEARCH_PLUGIN_IDS);
|
||||
return loadPluginManifestRegistry({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
})
|
||||
.plugins.filter(
|
||||
(plugin) => plugin.origin === "bundled" && bundledWebSearchPluginIdSet.has(plugin.id),
|
||||
)
|
||||
return resolveBundledWebSearchManifestPlugins(params)
|
||||
.map((plugin) => plugin.id)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
export function listBundledWebSearchPluginIds(): string[] {
|
||||
return [...BUNDLED_WEB_SEARCH_PLUGIN_IDS];
|
||||
export function listBundledWebSearchPluginIds(params?: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
}): string[] {
|
||||
return resolveBundledWebSearchPluginIds(params ?? {});
|
||||
}
|
||||
|
||||
export function listBundledWebSearchProviders(): PluginWebSearchProviderEntry[] {
|
||||
return loadBundledWebSearchProviders();
|
||||
export function listBundledWebSearchProviders(params?: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
}): PluginWebSearchProviderEntry[] {
|
||||
return loadBundledWebSearchProviders(params);
|
||||
}
|
||||
|
||||
export function resolveBundledWebSearchPluginId(
|
||||
providerId: string | undefined,
|
||||
params?: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
},
|
||||
): string | undefined {
|
||||
return resolveBundledWebSearchPluginIdFromMap(providerId);
|
||||
if (!providerId) {
|
||||
return undefined;
|
||||
}
|
||||
const normalizedProviderId = providerId.trim().toLowerCase();
|
||||
if (!normalizedProviderId) {
|
||||
return undefined;
|
||||
}
|
||||
return resolveBundledWebSearchManifestPlugins({
|
||||
config: params?.config,
|
||||
workspaceDir: params?.workspaceDir,
|
||||
env: params?.env,
|
||||
}).find((plugin) =>
|
||||
plugin.contracts?.webSearchProviders?.some(
|
||||
(candidate) => candidate.trim().toLowerCase() === normalizedProviderId,
|
||||
),
|
||||
)?.id;
|
||||
}
|
||||
|
||||
@@ -25,8 +25,7 @@ export async function resolvePreferredProviderForAuthChoice(params: {
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
mode: "setup",
|
||||
});
|
||||
const pluginResolved = resolveProviderPluginChoice({
|
||||
providers,
|
||||
|
||||
@@ -174,8 +174,7 @@ export async function applyAuthChoiceLoadedPluginProvider(
|
||||
config: params.config,
|
||||
workspaceDir,
|
||||
env: params.env,
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
mode: "setup",
|
||||
});
|
||||
const resolved = resolveProviderPluginChoice({
|
||||
providers,
|
||||
@@ -256,8 +255,7 @@ export async function applyAuthChoicePluginProvider(
|
||||
config: nextConfig,
|
||||
workspaceDir,
|
||||
env: params.env,
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
mode: "setup",
|
||||
});
|
||||
const provider = resolveProviderMatch(providers, options.providerId);
|
||||
if (!provider) {
|
||||
|
||||
@@ -79,64 +79,6 @@ function createWizardRuntimeParams(params?: {
|
||||
};
|
||||
}
|
||||
|
||||
function expectWizardResolutionCount(params: {
|
||||
provider: ProviderPlugin;
|
||||
config?: object;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
expectedCount: number;
|
||||
}) {
|
||||
setResolvedProviders(params.provider);
|
||||
resolveProviderWizardOptions(
|
||||
createWizardRuntimeParams({
|
||||
config: params.config,
|
||||
env: params.env,
|
||||
}),
|
||||
);
|
||||
resolveProviderWizardOptions(
|
||||
createWizardRuntimeParams({
|
||||
config: params.config,
|
||||
env: params.env,
|
||||
}),
|
||||
);
|
||||
expectProviderResolutionCall({
|
||||
config: params.config,
|
||||
env: params.env,
|
||||
count: params.expectedCount,
|
||||
});
|
||||
}
|
||||
|
||||
function expectWizardCacheInvalidationCount(params: {
|
||||
provider: ProviderPlugin;
|
||||
config: { [key: string]: unknown };
|
||||
env: NodeJS.ProcessEnv;
|
||||
mutate: () => void;
|
||||
expectedCount?: number;
|
||||
}) {
|
||||
setResolvedProviders(params.provider);
|
||||
|
||||
resolveProviderWizardOptions(
|
||||
createWizardRuntimeParams({
|
||||
config: params.config,
|
||||
env: params.env,
|
||||
}),
|
||||
);
|
||||
|
||||
params.mutate();
|
||||
|
||||
resolveProviderWizardOptions(
|
||||
createWizardRuntimeParams({
|
||||
config: params.config,
|
||||
env: params.env,
|
||||
}),
|
||||
);
|
||||
|
||||
expectProviderResolutionCall({
|
||||
config: params.config,
|
||||
env: params.env,
|
||||
count: params.expectedCount ?? 2,
|
||||
});
|
||||
}
|
||||
|
||||
function expectProviderResolutionCall(params?: {
|
||||
config?: object;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
@@ -146,8 +88,7 @@ function expectProviderResolutionCall(params?: {
|
||||
expect(resolvePluginProviders).toHaveBeenCalledTimes(params?.count ?? 1);
|
||||
expect(resolvePluginProviders).toHaveBeenCalledWith({
|
||||
...createWizardRuntimeParams(params),
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
mode: "setup",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -351,7 +292,7 @@ describe("provider wizard boundaries", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("reuses provider resolution across wizard consumers for the same config and env", () => {
|
||||
it("resolves providers in setup mode across wizard consumers", () => {
|
||||
const provider = createSglangWizardProvider({ includeModelPicker: true });
|
||||
const config = {};
|
||||
const env = createHomeEnv();
|
||||
@@ -361,82 +302,9 @@ describe("provider wizard boundaries", () => {
|
||||
expect(resolveProviderWizardOptions(runtimeParams)).toHaveLength(1);
|
||||
expect(resolveProviderModelPickerEntries(runtimeParams)).toHaveLength(1);
|
||||
|
||||
expectProviderResolutionCall({ config, env });
|
||||
});
|
||||
|
||||
it("invalidates the wizard cache when config or env contents change in place", () => {
|
||||
const config = createSglangConfig();
|
||||
const env = createHomeEnv("-a");
|
||||
|
||||
expectWizardCacheInvalidationCount({
|
||||
provider: createSglangWizardProvider(),
|
||||
config,
|
||||
env,
|
||||
mutate: () => {
|
||||
config.plugins.allow = ["vllm"];
|
||||
env.OPENCLAW_HOME = "/tmp/openclaw-home-b";
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "skips provider-wizard memoization when plugin cache opt-outs are set",
|
||||
env: createHomeEnv("", {
|
||||
OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE: "1",
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "skips provider-wizard memoization when discovery cache ttl is zero",
|
||||
env: createHomeEnv("", {
|
||||
OPENCLAW_PLUGIN_DISCOVERY_CACHE_MS: "0",
|
||||
}),
|
||||
},
|
||||
] as const)("$name", ({ env }) => {
|
||||
expectWizardResolutionCount({
|
||||
provider: createSglangWizardProvider(),
|
||||
config: createSglangConfig(),
|
||||
env,
|
||||
expectedCount: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it("expires provider-wizard memoization after the shortest plugin cache ttl", () => {
|
||||
vi.useFakeTimers();
|
||||
const provider = createSglangWizardProvider();
|
||||
const config = {};
|
||||
const env = createHomeEnv("", {
|
||||
OPENCLAW_PLUGIN_DISCOVERY_CACHE_MS: "5",
|
||||
OPENCLAW_PLUGIN_MANIFEST_CACHE_MS: "20",
|
||||
});
|
||||
setResolvedProviders(provider);
|
||||
const runtimeParams = createWizardRuntimeParams({ config, env });
|
||||
|
||||
resolveProviderWizardOptions(runtimeParams);
|
||||
vi.advanceTimersByTime(4);
|
||||
resolveProviderWizardOptions(runtimeParams);
|
||||
vi.advanceTimersByTime(2);
|
||||
resolveProviderWizardOptions(runtimeParams);
|
||||
|
||||
expectProviderResolutionCall({ config, env, count: 2 });
|
||||
});
|
||||
|
||||
it("invalidates provider-wizard snapshots when cache-control env values change in place", () => {
|
||||
const config = {};
|
||||
const env = createHomeEnv("", {
|
||||
OPENCLAW_PLUGIN_DISCOVERY_CACHE_MS: "1000",
|
||||
});
|
||||
|
||||
expectWizardCacheInvalidationCount({
|
||||
provider: createSglangWizardProvider(),
|
||||
config,
|
||||
env,
|
||||
mutate: () => {
|
||||
env.OPENCLAW_PLUGIN_DISCOVERY_CACHE_MS = "5";
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("routes model-selected hooks only to the matching provider", async () => {
|
||||
const matchingHook = vi.fn(async () => {});
|
||||
const otherHook = vi.fn(async () => {});
|
||||
|
||||
@@ -2,11 +2,6 @@ import { DEFAULT_PROVIDER } from "../agents/defaults.js";
|
||||
import { normalizeProviderId } from "../agents/model-selection.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||
import {
|
||||
buildPluginSnapshotCacheEnvKey,
|
||||
resolvePluginSnapshotCacheTtlMs,
|
||||
shouldUsePluginSnapshotCache,
|
||||
} from "./cache-controls.js";
|
||||
import { resolvePluginProviders } from "./providers.runtime.js";
|
||||
import type {
|
||||
ProviderAuthMethod,
|
||||
@@ -16,26 +11,6 @@ import type {
|
||||
} from "./types.js";
|
||||
|
||||
export const PROVIDER_PLUGIN_CHOICE_PREFIX = "provider-plugin:";
|
||||
type ProviderWizardCacheEntry = {
|
||||
expiresAt: number;
|
||||
providers: ProviderPlugin[];
|
||||
};
|
||||
const providerWizardCache = new WeakMap<
|
||||
OpenClawConfig,
|
||||
WeakMap<NodeJS.ProcessEnv, Map<string, ProviderWizardCacheEntry>>
|
||||
>();
|
||||
|
||||
function buildProviderWizardCacheKey(params: {
|
||||
config: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): string {
|
||||
return JSON.stringify({
|
||||
workspaceDir: params.workspaceDir ?? "",
|
||||
config: params.config,
|
||||
env: buildPluginSnapshotCacheEnvKey(params.env),
|
||||
});
|
||||
}
|
||||
|
||||
export type ProviderWizardOption = {
|
||||
value: string;
|
||||
@@ -135,53 +110,12 @@ function resolveProviderWizardProviders(params: {
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): ProviderPlugin[] {
|
||||
if (!params.config) {
|
||||
return resolvePluginProviders(params);
|
||||
}
|
||||
const env = params.env ?? process.env;
|
||||
if (!shouldUsePluginSnapshotCache(env)) {
|
||||
return resolvePluginProviders({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env,
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
});
|
||||
}
|
||||
const cacheKey = buildProviderWizardCacheKey({
|
||||
return resolvePluginProviders({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env,
|
||||
env: params.env,
|
||||
mode: "setup",
|
||||
});
|
||||
const configCache = providerWizardCache.get(params.config);
|
||||
const envCache = configCache?.get(env);
|
||||
const cached = envCache?.get(cacheKey);
|
||||
if (cached && cached.expiresAt > Date.now()) {
|
||||
return cached.providers;
|
||||
}
|
||||
const providers = resolvePluginProviders({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env,
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
});
|
||||
const ttlMs = resolvePluginSnapshotCacheTtlMs(env);
|
||||
let nextConfigCache = configCache;
|
||||
if (!nextConfigCache) {
|
||||
nextConfigCache = new WeakMap<NodeJS.ProcessEnv, Map<string, ProviderWizardCacheEntry>>();
|
||||
providerWizardCache.set(params.config, nextConfigCache);
|
||||
}
|
||||
let nextEnvCache = nextConfigCache.get(env);
|
||||
if (!nextEnvCache) {
|
||||
nextEnvCache = new Map<string, ProviderWizardCacheEntry>();
|
||||
nextConfigCache.set(env, nextEnvCache);
|
||||
}
|
||||
nextEnvCache.set(cacheKey, {
|
||||
expiresAt: Date.now() + ttlMs,
|
||||
providers,
|
||||
});
|
||||
return providers;
|
||||
}
|
||||
|
||||
export function resolveProviderWizardOptions(params: {
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { withActivatedPluginIds } from "./activation-context.js";
|
||||
import { resolveBundledPluginCompatibleActivationInputs } from "./activation-context.js";
|
||||
import { resolveRuntimePluginRegistry, type PluginLoadOptions } from "./loader.js";
|
||||
import {
|
||||
loadOpenClawPlugins,
|
||||
resolveRuntimePluginRegistry,
|
||||
type PluginLoadOptions,
|
||||
} from "./loader.js";
|
||||
import { createPluginLoaderLogger } from "./logger.js";
|
||||
import {
|
||||
resolveDiscoveredProviderPluginIds,
|
||||
resolveEnabledProviderPluginIds,
|
||||
resolveBundledProviderCompatPluginIds,
|
||||
resolveOwningPluginIdsForModelRefs,
|
||||
@@ -12,38 +18,6 @@ import type { ProviderPlugin } from "./types.js";
|
||||
|
||||
const log = createSubsystemLogger("plugins");
|
||||
|
||||
function withRuntimeActivatedPluginIds(params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
pluginIds: readonly string[];
|
||||
}): PluginLoadOptions["config"] {
|
||||
if (params.pluginIds.length === 0) {
|
||||
return params.config;
|
||||
}
|
||||
const allow = new Set(params.config?.plugins?.allow ?? []);
|
||||
const entries = {
|
||||
...params.config?.plugins?.entries,
|
||||
};
|
||||
for (const pluginId of params.pluginIds) {
|
||||
const normalized = pluginId.trim();
|
||||
if (!normalized) {
|
||||
continue;
|
||||
}
|
||||
allow.add(normalized);
|
||||
entries[normalized] = {
|
||||
...entries[normalized],
|
||||
enabled: true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...params.config,
|
||||
plugins: {
|
||||
...params.config?.plugins,
|
||||
...(allow.size > 0 ? { allow: [...allow] } : {}),
|
||||
entries,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function resolvePluginProviders(params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
@@ -56,6 +30,7 @@ export function resolvePluginProviders(params: {
|
||||
activate?: boolean;
|
||||
cache?: boolean;
|
||||
pluginSdkResolution?: PluginLoadOptions["pluginSdkResolution"];
|
||||
mode?: "runtime" | "setup";
|
||||
}): ProviderPlugin[] {
|
||||
const env = params.env ?? process.env;
|
||||
const modelOwnedPluginIds = params.modelRefs?.length
|
||||
@@ -70,10 +45,40 @@ export function resolvePluginProviders(params: {
|
||||
params.onlyPluginIds || modelOwnedPluginIds.length > 0
|
||||
? [...new Set([...(params.onlyPluginIds ?? []), ...modelOwnedPluginIds])]
|
||||
: undefined;
|
||||
const runtimeConfig = withRuntimeActivatedPluginIds({
|
||||
const runtimeConfig = withActivatedPluginIds({
|
||||
config: params.config,
|
||||
pluginIds: modelOwnedPluginIds,
|
||||
});
|
||||
if (params.mode === "setup") {
|
||||
const providerPluginIds = resolveDiscoveredProviderPluginIds({
|
||||
config: runtimeConfig,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env,
|
||||
onlyPluginIds: requestedPluginIds,
|
||||
});
|
||||
if (providerPluginIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const registry = loadOpenClawPlugins({
|
||||
config: withActivatedPluginIds({
|
||||
config: runtimeConfig,
|
||||
pluginIds: providerPluginIds,
|
||||
}),
|
||||
activationSourceConfig: runtimeConfig,
|
||||
autoEnabledReasons: {},
|
||||
workspaceDir: params.workspaceDir,
|
||||
env,
|
||||
onlyPluginIds: providerPluginIds,
|
||||
pluginSdkResolution: params.pluginSdkResolution,
|
||||
cache: params.cache ?? false,
|
||||
activate: params.activate ?? false,
|
||||
logger: createPluginLoaderLogger(log),
|
||||
});
|
||||
return registry.providers.map((entry) => ({
|
||||
...entry.provider,
|
||||
pluginId: entry.pluginId,
|
||||
}));
|
||||
}
|
||||
const activation = resolveBundledPluginCompatibleActivationInputs({
|
||||
rawConfig: runtimeConfig,
|
||||
env,
|
||||
|
||||
@@ -6,11 +6,13 @@ import { createEmptyPluginRegistry } from "./registry-empty.js";
|
||||
import type { ProviderPlugin } from "./types.js";
|
||||
|
||||
type ResolveRuntimePluginRegistry = typeof import("./loader.js").resolveRuntimePluginRegistry;
|
||||
type LoadOpenClawPlugins = typeof import("./loader.js").loadOpenClawPlugins;
|
||||
type LoadPluginManifestRegistry =
|
||||
typeof import("./manifest-registry.js").loadPluginManifestRegistry;
|
||||
type ApplyPluginAutoEnable = typeof import("../config/plugin-auto-enable.js").applyPluginAutoEnable;
|
||||
|
||||
const resolveRuntimePluginRegistryMock = vi.fn<ResolveRuntimePluginRegistry>();
|
||||
const loadOpenClawPluginsMock = vi.fn<LoadOpenClawPlugins>();
|
||||
const loadPluginManifestRegistryMock = vi.fn<LoadPluginManifestRegistry>();
|
||||
const applyPluginAutoEnableMock = vi.fn<ApplyPluginAutoEnable>();
|
||||
|
||||
@@ -131,6 +133,20 @@ function expectLastRuntimeRegistryLoad(params?: {
|
||||
);
|
||||
}
|
||||
|
||||
function expectLastSetupRegistryLoad(params?: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
onlyPluginIds?: readonly string[];
|
||||
}) {
|
||||
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
cache: false,
|
||||
activate: false,
|
||||
...(params?.env ? { env: params.env } : {}),
|
||||
...(params?.onlyPluginIds ? { onlyPluginIds: params.onlyPluginIds } : {}),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function getLastResolvedPluginConfig() {
|
||||
return getLastRuntimeRegistryCall().config as
|
||||
| {
|
||||
@@ -142,6 +158,19 @@ function getLastResolvedPluginConfig() {
|
||||
| undefined;
|
||||
}
|
||||
|
||||
function getLastSetupLoadedPluginConfig() {
|
||||
const call = loadOpenClawPluginsMock.mock.calls.at(-1)?.[0];
|
||||
expect(call).toBeDefined();
|
||||
return (call?.config ?? undefined) as
|
||||
| {
|
||||
plugins?: {
|
||||
allow?: string[];
|
||||
entries?: Record<string, { enabled?: boolean }>;
|
||||
};
|
||||
}
|
||||
| undefined;
|
||||
}
|
||||
|
||||
function createBundledProviderCompatOptions(params?: { onlyPluginIds?: readonly string[] }) {
|
||||
return {
|
||||
config: {
|
||||
@@ -222,6 +251,8 @@ describe("resolvePluginProviders", () => {
|
||||
beforeAll(async () => {
|
||||
vi.resetModules();
|
||||
vi.doMock("./loader.js", () => ({
|
||||
loadOpenClawPlugins: (...args: Parameters<LoadOpenClawPlugins>) =>
|
||||
loadOpenClawPluginsMock(...args),
|
||||
resolveRuntimePluginRegistry: (...args: Parameters<ResolveRuntimePluginRegistry>) =>
|
||||
resolveRuntimePluginRegistryMock(...args),
|
||||
}));
|
||||
@@ -243,6 +274,7 @@ describe("resolvePluginProviders", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
resolveRuntimePluginRegistryMock.mockReset();
|
||||
loadOpenClawPluginsMock.mockReset();
|
||||
const provider: ProviderPlugin = {
|
||||
id: "demo-provider",
|
||||
label: "Demo Provider",
|
||||
@@ -251,6 +283,7 @@ describe("resolvePluginProviders", () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
registry.providers.push({ pluginId: "google", provider, source: "bundled" });
|
||||
resolveRuntimePluginRegistryMock.mockReturnValue(registry);
|
||||
loadOpenClawPluginsMock.mockReturnValue(registry);
|
||||
loadPluginManifestRegistryMock.mockReset();
|
||||
applyPluginAutoEnableMock.mockReset();
|
||||
applyPluginAutoEnableMock.mockImplementation(
|
||||
@@ -452,6 +485,43 @@ describe("resolvePluginProviders", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("loads all discovered provider plugins in setup mode", () => {
|
||||
resolvePluginProviders({
|
||||
config: {
|
||||
plugins: {
|
||||
allow: ["openrouter"],
|
||||
entries: {
|
||||
google: { enabled: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
mode: "setup",
|
||||
});
|
||||
|
||||
expectLastSetupRegistryLoad({
|
||||
onlyPluginIds: ["google", "kilocode", "moonshot", "workspace-provider"],
|
||||
});
|
||||
expect(getLastSetupLoadedPluginConfig()).toEqual(
|
||||
expect.objectContaining({
|
||||
plugins: expect.objectContaining({
|
||||
allow: expect.arrayContaining([
|
||||
"openrouter",
|
||||
"google",
|
||||
"kilocode",
|
||||
"moonshot",
|
||||
"workspace-provider",
|
||||
]),
|
||||
entries: expect.objectContaining({
|
||||
google: { enabled: true },
|
||||
kilocode: { enabled: true },
|
||||
moonshot: { enabled: true },
|
||||
"workspace-provider": { enabled: true },
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("loads provider plugins from the auto-enabled config snapshot", () => {
|
||||
const { rawConfig, autoEnabledConfig } = createAutoEnabledProviderConfig();
|
||||
applyPluginAutoEnableMock.mockReturnValue({
|
||||
|
||||
@@ -69,8 +69,29 @@ export function resolveEnabledProviderPluginIds(params: {
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
export function resolveDiscoveredProviderPluginIds(params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
onlyPluginIds?: readonly string[];
|
||||
}): string[] {
|
||||
const onlyPluginIdSet = params.onlyPluginIds ? new Set(params.onlyPluginIds) : null;
|
||||
return loadPluginManifestRegistry({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
})
|
||||
.plugins.filter(
|
||||
(plugin) =>
|
||||
plugin.providers.length > 0 && (!onlyPluginIdSet || onlyPluginIdSet.has(plugin.id)),
|
||||
)
|
||||
.map((plugin) => plugin.id)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
export const __testing = {
|
||||
resolveEnabledProviderPluginIds,
|
||||
resolveDiscoveredProviderPluginIds,
|
||||
resolveBundledProviderCompatPluginIds,
|
||||
withBundledProviderVitestCompat,
|
||||
} as const;
|
||||
|
||||
@@ -345,6 +345,32 @@ describe("resolvePluginWebSearchProviders", () => {
|
||||
expectLoaderCallCount(1);
|
||||
});
|
||||
|
||||
it("loads manifest-declared web-search providers in setup mode", () => {
|
||||
const providers = resolvePluginWebSearchProviders({
|
||||
config: {
|
||||
plugins: {
|
||||
allow: ["perplexity"],
|
||||
},
|
||||
},
|
||||
mode: "setup",
|
||||
});
|
||||
|
||||
expect(toRuntimeProviderKeys(providers)).toEqual(["brave:brave"]);
|
||||
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
onlyPluginIds: ["brave"],
|
||||
config: expect.objectContaining({
|
||||
plugins: expect.objectContaining({
|
||||
allow: ["perplexity", "brave"],
|
||||
entries: {
|
||||
brave: { enabled: true },
|
||||
},
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("loads plugin web-search providers from the auto-enabled config snapshot", () => {
|
||||
const rawConfig = createBraveAllowConfig();
|
||||
const autoEnabledConfig = {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { isRecord } from "../utils.js";
|
||||
import { withActivatedPluginIds } from "./activation-context.js";
|
||||
import {
|
||||
buildPluginSnapshotCacheEnvKey,
|
||||
resolvePluginSnapshotCacheTtlMs,
|
||||
@@ -155,8 +156,36 @@ export function resolvePluginWebSearchProviders(params: {
|
||||
onlyPluginIds?: readonly string[];
|
||||
activate?: boolean;
|
||||
cache?: boolean;
|
||||
mode?: "runtime" | "setup";
|
||||
}): PluginWebSearchProviderEntry[] {
|
||||
const env = params.env ?? process.env;
|
||||
if (params.mode === "setup") {
|
||||
const pluginIds =
|
||||
resolveWebSearchCandidatePluginIds({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env,
|
||||
onlyPluginIds: params.onlyPluginIds,
|
||||
}) ?? [];
|
||||
if (pluginIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const registry = loadOpenClawPlugins({
|
||||
config: withActivatedPluginIds({
|
||||
config: params.config,
|
||||
pluginIds,
|
||||
}),
|
||||
activationSourceConfig: params.config,
|
||||
autoEnabledReasons: {},
|
||||
workspaceDir: params.workspaceDir,
|
||||
env,
|
||||
onlyPluginIds: pluginIds,
|
||||
cache: params.cache ?? false,
|
||||
activate: params.activate ?? false,
|
||||
logger: createPluginLoaderLogger(log),
|
||||
});
|
||||
return mapRegistryWebSearchProviders({ registry, onlyPluginIds: pluginIds });
|
||||
}
|
||||
const cacheOwnerConfig = params.config;
|
||||
const shouldMemoizeSnapshot =
|
||||
params.activate !== true && params.cache !== true && shouldUsePluginSnapshotCache(env);
|
||||
|
||||
Reference in New Issue
Block a user