mirror of
https://fastgit.cc/github.com/openclaw/openclaw
synced 2026-04-20 21:02:10 +08:00
refactor: cache provider tool runtimes
This commit is contained in:
@@ -6,6 +6,16 @@ import type {
|
||||
import { createWebSearchProviderContractFields } from "openclaw/plugin-sdk/provider-web-search-config-contract";
|
||||
|
||||
const BRAVE_CREDENTIAL_PATH = "plugins.entries.brave.config.webSearch.apiKey";
|
||||
|
||||
type BraveWebSearchRuntime = typeof import("./brave-web-search-provider.runtime.js");
|
||||
|
||||
let braveWebSearchRuntimePromise: Promise<BraveWebSearchRuntime> | undefined;
|
||||
|
||||
function loadBraveWebSearchRuntime(): Promise<BraveWebSearchRuntime> {
|
||||
braveWebSearchRuntimePromise ??= import("./brave-web-search-provider.runtime.js");
|
||||
return braveWebSearchRuntimePromise;
|
||||
}
|
||||
|
||||
const BraveSearchSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -111,7 +121,7 @@ function createBraveToolDefinition(
|
||||
: "Search the web using Brave Search API. Supports region-specific and localized search via country and language parameters. Returns titles, URLs, and snippets for fast research.",
|
||||
parameters: BraveSearchSchema,
|
||||
execute: async (args) => {
|
||||
const { executeBraveSearch } = await import("./brave-web-search-provider.runtime.js");
|
||||
const { executeBraveSearch } = await loadBraveWebSearchRuntime();
|
||||
return await executeBraveSearch(args, searchConfig);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import { readNumberParam, readStringParam } from "openclaw/plugin-sdk/param-readers";
|
||||
import {
|
||||
createWebSearchProviderContractFields,
|
||||
type WebSearchProviderPlugin,
|
||||
} from "openclaw/plugin-sdk/provider-web-search-contract";
|
||||
|
||||
type DuckDuckGoClientModule = typeof import("./ddg-client.js");
|
||||
|
||||
let duckDuckGoClientModulePromise: Promise<DuckDuckGoClientModule> | undefined;
|
||||
|
||||
function loadDuckDuckGoClientModule(): Promise<DuckDuckGoClientModule> {
|
||||
duckDuckGoClientModulePromise ??= import("./ddg-client.js");
|
||||
return duckDuckGoClientModulePromise;
|
||||
}
|
||||
|
||||
const DuckDuckGoSearchSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -47,10 +57,7 @@ export function createDuckDuckGoWebSearchProvider(): WebSearchProviderPlugin {
|
||||
"Search the web using DuckDuckGo. Returns titles, URLs, and snippets with no API key required.",
|
||||
parameters: DuckDuckGoSearchSchema,
|
||||
execute: async (args) => {
|
||||
const [{ runDuckDuckGoSearch }, { readNumberParam, readStringParam }] = await Promise.all([
|
||||
import("./ddg-client.js"),
|
||||
import("openclaw/plugin-sdk/provider-web-search"),
|
||||
]);
|
||||
const { runDuckDuckGoSearch } = await loadDuckDuckGoClientModule();
|
||||
return await runDuckDuckGoSearch({
|
||||
config: ctx.config,
|
||||
query: readStringParam(args, "query", { required: true }),
|
||||
|
||||
@@ -8,6 +8,15 @@ const EXA_SEARCH_TYPES = ["auto", "neural", "fast", "deep", "deep-reasoning", "i
|
||||
const EXA_FRESHNESS_VALUES = ["day", "week", "month", "year"] as const;
|
||||
const EXA_MAX_SEARCH_COUNT = 100;
|
||||
|
||||
type ExaWebSearchRuntime = typeof import("./exa-web-search-provider.runtime.js");
|
||||
|
||||
let exaWebSearchRuntimePromise: Promise<ExaWebSearchRuntime> | undefined;
|
||||
|
||||
function loadExaWebSearchRuntime(): Promise<ExaWebSearchRuntime> {
|
||||
exaWebSearchRuntimePromise ??= import("./exa-web-search-provider.runtime.js");
|
||||
return exaWebSearchRuntimePromise;
|
||||
}
|
||||
|
||||
const ExaSearchSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -81,8 +90,7 @@ export function createExaWebSearchProvider(): WebSearchProviderPlugin {
|
||||
"Search the web using Exa AI. Supports neural or keyword search, publication date filters, and optional highlights or text extraction.",
|
||||
parameters: ExaSearchSchema,
|
||||
execute: async (args) => {
|
||||
const { executeExaWebSearchProviderTool } =
|
||||
await import("./exa-web-search-provider.runtime.js");
|
||||
const { executeExaWebSearchProviderTool } = await loadExaWebSearchRuntime();
|
||||
return await executeExaWebSearchProviderTool(ctx, args);
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -4,6 +4,16 @@ import {
|
||||
} from "openclaw/plugin-sdk/provider-web-search-contract";
|
||||
|
||||
const FIRECRAWL_CREDENTIAL_PATH = "plugins.entries.firecrawl.config.webSearch.apiKey";
|
||||
|
||||
type FirecrawlClientModule = typeof import("./firecrawl-client.js");
|
||||
|
||||
let firecrawlClientModulePromise: Promise<FirecrawlClientModule> | undefined;
|
||||
|
||||
function loadFirecrawlClientModule(): Promise<FirecrawlClientModule> {
|
||||
firecrawlClientModulePromise ??= import("./firecrawl-client.js");
|
||||
return firecrawlClientModulePromise;
|
||||
}
|
||||
|
||||
const GenericFirecrawlSearchSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -42,7 +52,7 @@ export function createFirecrawlWebSearchProvider(): WebSearchProviderPlugin {
|
||||
"Search the web using Firecrawl. Returns structured results with snippets from Firecrawl Search. Use firecrawl_search for Firecrawl-specific knobs like sources or categories.",
|
||||
parameters: GenericFirecrawlSearchSchema,
|
||||
execute: async (args) => {
|
||||
const { runFirecrawlSearch } = await import("./firecrawl-client.js");
|
||||
const { runFirecrawlSearch } = await loadFirecrawlClientModule();
|
||||
return await runFirecrawlSearch({
|
||||
cfg: ctx.config,
|
||||
query: typeof args.query === "string" ? args.query : "",
|
||||
|
||||
@@ -8,6 +8,16 @@ import {
|
||||
import { resolveGeminiApiKey, resolveGeminiModel } from "./gemini-web-search-provider.shared.js";
|
||||
|
||||
const GEMINI_CREDENTIAL_PATH = "plugins.entries.google.config.webSearch.apiKey";
|
||||
|
||||
type GeminiWebSearchRuntime = typeof import("./gemini-web-search-provider.runtime.js");
|
||||
|
||||
let geminiWebSearchRuntimePromise: Promise<GeminiWebSearchRuntime> | undefined;
|
||||
|
||||
function loadGeminiWebSearchRuntime(): Promise<GeminiWebSearchRuntime> {
|
||||
geminiWebSearchRuntimePromise ??= import("./gemini-web-search-provider.runtime.js");
|
||||
return geminiWebSearchRuntimePromise;
|
||||
}
|
||||
|
||||
const GEMINI_TOOL_PARAMETERS = {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -35,7 +45,7 @@ function createGeminiToolDefinition(
|
||||
"Search the web using Gemini with Google Search grounding. Returns AI-synthesized answers with citations from Google Search.",
|
||||
parameters: GEMINI_TOOL_PARAMETERS,
|
||||
execute: async (args) => {
|
||||
const { executeGeminiSearch } = await import("./gemini-web-search-provider.runtime.js");
|
||||
const { executeGeminiSearch } = await loadGeminiWebSearchRuntime();
|
||||
return await executeGeminiSearch(args, searchConfig);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -6,6 +6,15 @@ import {
|
||||
const MINIMAX_CREDENTIAL_PATH = "plugins.entries.minimax.config.webSearch.apiKey";
|
||||
const MINIMAX_CODING_PLAN_ENV_VARS = ["MINIMAX_CODE_PLAN_KEY", "MINIMAX_CODING_API_KEY"] as const;
|
||||
|
||||
type MiniMaxWebSearchRuntime = typeof import("./minimax-web-search-provider.runtime.js");
|
||||
|
||||
let miniMaxWebSearchRuntimePromise: Promise<MiniMaxWebSearchRuntime> | undefined;
|
||||
|
||||
function loadMiniMaxWebSearchRuntime(): Promise<MiniMaxWebSearchRuntime> {
|
||||
miniMaxWebSearchRuntimePromise ??= import("./minimax-web-search-provider.runtime.js");
|
||||
return miniMaxWebSearchRuntimePromise;
|
||||
}
|
||||
|
||||
const MiniMaxSearchSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -41,8 +50,7 @@ export function createMiniMaxWebSearchProvider(): WebSearchProviderPlugin {
|
||||
"Search the web using MiniMax Search API. Returns titles, URLs, snippets, and related search suggestions.",
|
||||
parameters: MiniMaxSearchSchema,
|
||||
execute: async (args) => {
|
||||
const { executeMiniMaxWebSearchProviderTool } =
|
||||
await import("./minimax-web-search-provider.runtime.js");
|
||||
const { executeMiniMaxWebSearchProviderTool } = await loadMiniMaxWebSearchRuntime();
|
||||
return await executeMiniMaxWebSearchProviderTool(ctx, args);
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -9,6 +9,15 @@ import { resolvePerplexityRuntimeTransport } from "./perplexity-web-search-provi
|
||||
|
||||
const PERPLEXITY_CREDENTIAL_PATH = "plugins.entries.perplexity.config.webSearch.apiKey";
|
||||
|
||||
type PerplexityWebSearchRuntime = typeof import("./perplexity-web-search-provider.runtime.js");
|
||||
|
||||
let perplexityWebSearchRuntimePromise: Promise<PerplexityWebSearchRuntime> | undefined;
|
||||
|
||||
function loadPerplexityWebSearchRuntime(): Promise<PerplexityWebSearchRuntime> {
|
||||
perplexityWebSearchRuntimePromise ??= import("./perplexity-web-search-provider.runtime.js");
|
||||
return perplexityWebSearchRuntimePromise;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
@@ -95,8 +104,7 @@ function createPerplexityToolDefinition(
|
||||
: "Search the web using Perplexity. Runtime routing decides between native Search API and Sonar chat-completions compatibility. Structured filters are available on the native Search API path.",
|
||||
parameters: createPerplexityParameters(schemaTransport),
|
||||
execute: async (args) => {
|
||||
const { executePerplexitySearch } =
|
||||
await import("./perplexity-web-search-provider.runtime.js");
|
||||
const { executePerplexitySearch } = await loadPerplexityWebSearchRuntime();
|
||||
return await executePerplexitySearch(args, searchConfig);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -6,6 +6,15 @@ import {
|
||||
|
||||
const SEARXNG_CREDENTIAL_PATH = "plugins.entries.searxng.config.webSearch.baseUrl";
|
||||
|
||||
type SearxngClientModule = typeof import("./searxng-client.js");
|
||||
|
||||
let searxngClientModulePromise: Promise<SearxngClientModule> | undefined;
|
||||
|
||||
function loadSearxngClientModule(): Promise<SearxngClientModule> {
|
||||
searxngClientModulePromise ??= import("./searxng-client.js");
|
||||
return searxngClientModulePromise;
|
||||
}
|
||||
|
||||
const SearxngSearchSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -52,7 +61,7 @@ export function createSearxngWebSearchProvider(): WebSearchProviderPlugin {
|
||||
"Search the web using a self-hosted SearXNG instance. Returns titles, URLs, and snippets.",
|
||||
parameters: SearxngSearchSchema,
|
||||
execute: async (args) => {
|
||||
const { runSearxngSearch } = await import("./searxng-client.js");
|
||||
const { runSearxngSearch } = await loadSearxngClientModule();
|
||||
return await runSearxngSearch({
|
||||
config: ctx.config,
|
||||
query: readStringParam(args, "query", { required: true }),
|
||||
|
||||
@@ -4,6 +4,16 @@ import {
|
||||
} from "openclaw/plugin-sdk/provider-web-search-contract";
|
||||
|
||||
const TAVILY_CREDENTIAL_PATH = "plugins.entries.tavily.config.webSearch.apiKey";
|
||||
|
||||
type TavilyClientModule = typeof import("./tavily-client.js");
|
||||
|
||||
let tavilyClientModulePromise: Promise<TavilyClientModule> | undefined;
|
||||
|
||||
function loadTavilyClientModule(): Promise<TavilyClientModule> {
|
||||
tavilyClientModulePromise ??= import("./tavily-client.js");
|
||||
return tavilyClientModulePromise;
|
||||
}
|
||||
|
||||
const GenericTavilySearchSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -42,7 +52,7 @@ export function createTavilyWebSearchProvider(): WebSearchProviderPlugin {
|
||||
"Search the web using Tavily. Returns structured results with snippets. Use tavily_search for Tavily-specific options like search depth, topic filtering, or AI answers.",
|
||||
parameters: GenericTavilySearchSchema,
|
||||
execute: async (args) => {
|
||||
const { runTavilySearch } = await import("./tavily-client.js");
|
||||
const { runTavilySearch } = await loadTavilyClientModule();
|
||||
return await runTavilySearch({
|
||||
cfg: ctx.config,
|
||||
query: typeof args.query === "string" ? args.query : "",
|
||||
|
||||
@@ -24,6 +24,15 @@ import {
|
||||
} from "./x-search-tool-shared.js";
|
||||
|
||||
const PROVIDER_ID = "xai";
|
||||
type CodeExecutionModule = typeof import("./code-execution.js");
|
||||
|
||||
let codeExecutionModulePromise: Promise<CodeExecutionModule> | undefined;
|
||||
|
||||
function loadCodeExecutionModule(): Promise<CodeExecutionModule> {
|
||||
codeExecutionModulePromise ??= import("./code-execution.js");
|
||||
return codeExecutionModulePromise;
|
||||
}
|
||||
|
||||
function hasResolvableXaiApiKey(config: unknown): boolean {
|
||||
return Boolean(
|
||||
resolveFallbackXaiAuth(config as never)?.apiKey || readProviderEnvValue(["XAI_API_KEY"]),
|
||||
@@ -89,7 +98,7 @@ function createLazyCodeExecutionTool(ctx: {
|
||||
}),
|
||||
}),
|
||||
execute: async (toolCallId: string, args: Record<string, unknown>) => {
|
||||
const { createCodeExecutionTool } = await import("./code-execution.js");
|
||||
const { createCodeExecutionTool } = await loadCodeExecutionModule();
|
||||
const tool = createCodeExecutionTool({
|
||||
config: ctx.config as never,
|
||||
runtimeConfig: (ctx.runtimeConfig as never) ?? null,
|
||||
|
||||
@@ -40,6 +40,26 @@ function isTypeOnlyImportDeclaration(node) {
|
||||
);
|
||||
}
|
||||
|
||||
function readDeclarationName(node) {
|
||||
if (
|
||||
(ts.isFunctionDeclaration(node) ||
|
||||
ts.isMethodDeclaration(node) ||
|
||||
ts.isVariableDeclaration(node)) &&
|
||||
node.name &&
|
||||
ts.isIdentifier(node.name)
|
||||
) {
|
||||
return node.name.text;
|
||||
}
|
||||
|
||||
if (ts.isPropertyAssignment(node)) {
|
||||
if (ts.isIdentifier(node.name) || ts.isStringLiteral(node.name)) {
|
||||
return node.name.text;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function isIgnoredTestHelperContent(content) {
|
||||
return /\bfrom\s+["']vitest["']/.test(content) || /\bfrom\s+["']@vitest\//.test(content);
|
||||
}
|
||||
@@ -61,6 +81,8 @@ export function findDynamicImportAdvisories(content, fileName = "source.ts") {
|
||||
const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true);
|
||||
const staticRuntimeImports = new Map();
|
||||
const dynamicImports = new Map();
|
||||
const directExecuteImports = [];
|
||||
const declarationStack = [];
|
||||
|
||||
const addLine = (map, specifier, line) => {
|
||||
const lines = map.get(specifier) ?? [];
|
||||
@@ -69,6 +91,11 @@ export function findDynamicImportAdvisories(content, fileName = "source.ts") {
|
||||
};
|
||||
|
||||
const visit = (node) => {
|
||||
const declarationName = readDeclarationName(node);
|
||||
if (declarationName) {
|
||||
declarationStack.push(declarationName);
|
||||
}
|
||||
|
||||
if (
|
||||
ts.isImportDeclaration(node) &&
|
||||
ts.isStringLiteral(node.moduleSpecifier) &&
|
||||
@@ -84,16 +111,26 @@ export function findDynamicImportAdvisories(content, fileName = "source.ts") {
|
||||
) {
|
||||
const specifier = readStringLiteral(node.arguments[0]);
|
||||
if (specifier) {
|
||||
addLine(dynamicImports, specifier, toLine(sourceFile, node));
|
||||
const line = toLine(sourceFile, node);
|
||||
addLine(dynamicImports, specifier, line);
|
||||
if (declarationStack.includes("execute")) {
|
||||
directExecuteImports.push({
|
||||
line,
|
||||
reason: `direct dynamic import of "${specifier}" inside execute path; move it behind a cached loader`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ts.forEachChild(node, visit);
|
||||
if (declarationName) {
|
||||
declarationStack.pop();
|
||||
}
|
||||
};
|
||||
|
||||
visit(sourceFile);
|
||||
|
||||
const advisories = [];
|
||||
const advisories = [...directExecuteImports];
|
||||
for (const [specifier, dynamicLines] of dynamicImports) {
|
||||
const staticLines = staticRuntimeImports.get(specifier);
|
||||
if (staticLines?.length) {
|
||||
|
||||
@@ -54,4 +54,39 @@ describe("check-dynamic-import-warts", () => {
|
||||
`;
|
||||
expect(findDynamicImportAdvisories(source)).toEqual([]);
|
||||
});
|
||||
|
||||
it("flags direct dynamic imports inside execute paths", () => {
|
||||
const source = `
|
||||
export function createTool() {
|
||||
return {
|
||||
execute: async () => {
|
||||
return await import("./runtime.js");
|
||||
},
|
||||
};
|
||||
}
|
||||
`;
|
||||
expect(findDynamicImportAdvisories(source)).toEqual([
|
||||
{
|
||||
line: 5,
|
||||
reason:
|
||||
'direct dynamic import of "./runtime.js" inside execute path; move it behind a cached loader',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("allows execute paths that call cached loaders", () => {
|
||||
const source = `
|
||||
let runtimePromise: Promise<typeof import("./runtime.js")> | undefined;
|
||||
function loadRuntime() {
|
||||
runtimePromise ??= import("./runtime.js");
|
||||
return runtimePromise;
|
||||
}
|
||||
export function createTool() {
|
||||
return {
|
||||
execute: async () => await loadRuntime(),
|
||||
};
|
||||
}
|
||||
`;
|
||||
expect(findDynamicImportAdvisories(source)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user