UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

263 lines (262 loc) 10.5 kB
import { b as readStringParam, h as readNumberParam, v as readStringArrayParam } from "./common-C4yy9V-D.js"; import { S as writeCachedSearchPayload, _ as resolveSearchTimeoutSeconds, f as readCachedSearchPayload, h as resolveSearchCacheTtlMs, x as withTrustedWebSearchEndpoint } from "./web-search-provider-common-DisZTXTb.js"; import { u as readPluginPackageVersion } from "./extension-shared-B8fkO3TV.js"; import { i as resolveProviderWebSearchPluginConfig, r as mergeScopedSearchConfig } from "./web-search-provider-config-BQzMMhw8.js"; import "./provider-web-search-9G87vZMY.js"; import { a as normalizeParallelClientModel, c as normalizeParallelSearchQueries, d as stripParallelGeneratedSessionId, i as mapParallelResults, l as normalizeParallelSessionId, n as buildParallelCacheKey, o as normalizeParallelObjective, r as invalidSearchQueriesPayload, u as resolveParallelSearchCount } from "./parallel-search-normalize-BrqPQAw_.js"; import { createRequire } from "node:module"; import { randomUUID } from "node:crypto"; //#region extensions/parallel/src/parallel-mcp-search.runtime.ts const PARALLEL_MCP_SEARCH_URL = "https://search.parallel.ai/mcp"; const MCP_PROTOCOL_VERSION = "2025-06-18"; const MCP_TIMEOUT_SECONDS = 30; const PLUGIN_VERSION = readPluginPackageVersion({ require: createRequire(import.meta.url) }); function isRecord(value) { return typeof value === "object" && value !== null && !Array.isArray(value); } function mcpHeaders(params) { const headers = { "Content-Type": "application/json", Accept: "application/json, text/event-stream" }; if (params.sessionId) headers["Mcp-Session-Id"] = params.sessionId; if (params.protocolVersion) headers["MCP-Protocol-Version"] = params.protocolVersion; return headers; } /** * Yield JSON-RPC message objects from a plain-JSON or SSE response body. * * Handles `application/json` (a single object) and `text/event-stream` (SSE: * events separated by blank lines; an event's one-or-more `data:` lines * concatenate into a single JSON payload). Streamable HTTP also allows batching * responses into a JSON array, so arrays are flattened. Unparseable chunks and * non-`data` SSE fields (`event:`/`id:`/comments) are skipped. */ function iterMcpMessages(text) { const out = []; const emit = (payload) => { if (Array.isArray(payload)) { for (const entry of payload) if (isRecord(entry)) out.push(entry); } else if (isRecord(payload)) out.push(payload); }; const body = (text ?? "").trim(); if (!body) return out; if (body.startsWith("{") || body.startsWith("[")) { try { emit(JSON.parse(body)); } catch {} return out; } let dataLines = []; const flush = () => { if (dataLines.length === 0) return; try { emit(JSON.parse(dataLines.join("\n"))); } catch {} dataLines = []; }; for (const raw of body.split("\n")) { const line = raw.replace(/\r$/, ""); if (line.startsWith("data:")) dataLines.push(line.slice(5).replace(/^ /, "")); else if (line.trim() === "") flush(); } flush(); return out; } /** * Select the JSON-RPC response for `requestId` from an MCP response body. * * Streamable-HTTP servers may emit progress/log notifications before the final * result, so scan the whole stream and return the result/error message whose * `id` matches. Falls back to the last result/error-bearing message if no id * matches; `{}` if none is present. */ function selectMcpEnvelope(text, requestId) { let fallback = {}; for (const msg of iterMcpMessages(text)) { if (!("result" in msg || "error" in msg)) continue; if (msg.id === requestId) return msg; fallback = msg; } return fallback; } /** * Extract the tool result payload from a `tools/call` envelope. * * Prefers `structuredContent` (authoritative machine-readable form); otherwise * scans text blocks for the first JSON-parseable one. Throws on a JSON-RPC * error or a tool-level `isError`. */ function extractMcpToolPayload(envelope) { if ("error" in envelope) throw new Error(`Parallel MCP error: ${JSON.stringify(envelope.error).slice(0, 500)}`); const result = isRecord(envelope.result) ? envelope.result : {}; if (result.isError) throw new Error(`Parallel MCP tool error: ${JSON.stringify(result).slice(0, 500)}`); if (isRecord(result.structuredContent)) return result.structuredContent; const content = Array.isArray(result.content) ? result.content : []; for (const block of content) if (isRecord(block) && block.type === "text" && typeof block.text === "string" && block.text) try { const parsed = JSON.parse(block.text); if (isRecord(parsed)) return parsed; } catch {} throw new Error(`Parallel MCP returned no parseable content: ${JSON.stringify(result).slice(0, 500)}`); } async function postMcp(params) { return withTrustedWebSearchEndpoint({ url: PARALLEL_MCP_SEARCH_URL, timeoutSeconds: params.timeoutSeconds, signal: params.signal, init: { method: "POST", headers: mcpHeaders({ sessionId: params.sessionId, protocolVersion: params.protocolVersion }), body: JSON.stringify(params.body) } }, async (response) => ({ ok: response.ok, status: response.status, statusText: response.statusText, text: await response.text(), sessionIdHeader: response.headers.get("mcp-session-id") })); } /** * Run the MCP handshake then a single `tools/call`, returning the tool payload. * * initialize -> (capture `Mcp-Session-Id` header + negotiated protocolVersion) * -> notifications/initialized -> tools/call. Anonymous (no bearer token). */ async function mcpCall(toolName, args, timeoutSeconds, signal) { const initId = randomUUID(); const init = await postMcp({ timeoutSeconds, signal, body: { jsonrpc: "2.0", id: initId, method: "initialize", params: { protocolVersion: MCP_PROTOCOL_VERSION, capabilities: {}, clientInfo: { name: "openclaw-parallel", version: PLUGIN_VERSION } } } }); if (!init.ok) throw new Error(`Parallel MCP initialize failed (${init.status}): ${init.text || init.statusText}`); const sessionId = init.sessionIdHeader ?? void 0; const initEnvelope = selectMcpEnvelope(init.text, initId); const negotiatedVersion = (isRecord(initEnvelope.result) && typeof initEnvelope.result.protocolVersion === "string" ? initEnvelope.result.protocolVersion : void 0) ?? MCP_PROTOCOL_VERSION; await postMcp({ body: { jsonrpc: "2.0", method: "notifications/initialized" }, sessionId, protocolVersion: negotiatedVersion, timeoutSeconds, signal }); const callId = randomUUID(); const call = await postMcp({ body: { jsonrpc: "2.0", id: callId, method: "tools/call", params: { name: toolName, arguments: args } }, sessionId, protocolVersion: negotiatedVersion, timeoutSeconds, signal }); if (!call.ok) throw new Error(`Parallel MCP tools/call failed (${call.status}): ${call.text || call.statusText}`); return extractMcpToolPayload(selectMcpEnvelope(call.text, callId)); } function normalizeMcpSessionId(value) { return value?.trim() || randomUUID(); } /** * Run a `web_search` tool call against the free hosted Search MCP and return a * `ParallelSearchResponse`-compatible object so the runtime's existing result * normalization (`normalizeParallelResults`) is reused verbatim. */ async function runParallelMcpSearch(params) { const sessionId = normalizeMcpSessionId(params.sessionId); const args = { objective: params.objective ?? params.searchQueries.join(" "), search_queries: [...params.searchQueries], session_id: sessionId }; if (params.modelName) args.model_name = params.modelName; const payload = await mcpCall("web_search", args, params.timeoutSeconds ?? MCP_TIMEOUT_SECONDS, params.signal); const results = (Array.isArray(payload.results) ? payload.results : []).slice(0, Math.max(params.maxResults, 1)); return { search_id: typeof payload.search_id === "string" ? payload.search_id : void 0, session_id: sessionId, results, warnings: payload.warnings, usage: payload.usage }; } //#endregion //#region extensions/parallel/src/parallel-free-web-search-provider.runtime.ts async function executeParallelFreeWebSearchProviderTool(ctx, args, signal) { const searchConfig = mergeScopedSearchConfig(ctx.searchConfig, "parallel-free", resolveProviderWebSearchPluginConfig(ctx.config, "parallel-free")); const objective = normalizeParallelObjective(readStringParam(args, "objective")); const cliQuery = normalizeParallelObjective(readStringParam(args, "query")); let searchQueries = normalizeParallelSearchQueries(readStringArrayParam(args, "search_queries")); if (searchQueries.length === 0 && cliQuery) searchQueries = normalizeParallelSearchQueries([cliQuery]); if (searchQueries.length === 0) return invalidSearchQueriesPayload(); const count = resolveParallelSearchCount(readNumberParam(args, "count", { integer: true }) ?? (typeof searchConfig?.maxResults === "number" ? searchConfig.maxResults : void 0) ?? 5); const sessionId = normalizeParallelSessionId(readStringParam(args, "session_id"), 100); const clientModel = normalizeParallelClientModel(readStringParam(args, "client_model")); const cacheKey = buildParallelCacheKey({ endpoint: PARALLEL_MCP_SEARCH_URL, objective, searchQueries, count, sessionId, clientModel }); const cached = readCachedSearchPayload(cacheKey); if (cached) return cached; const start = Date.now(); const response = await runParallelMcpSearch({ objective, searchQueries, maxResults: count, sessionId, modelName: clientModel, timeoutSeconds: resolveSearchTimeoutSeconds(searchConfig), signal }); const results = mapParallelResults(response); const payload = { ...objective ? { objective } : {}, searchQueries, provider: "parallel-free", count: results.length, tookMs: Date.now() - start, externalContent: { untrusted: true, source: "web_search", provider: "parallel-free", wrapped: true }, results }; if (typeof response.search_id === "string") payload.searchId = response.search_id; if (typeof response.session_id === "string") payload.sessionId = response.session_id; if (Array.isArray(response.warnings) && response.warnings.length > 0) payload.warnings = response.warnings; if (Array.isArray(response.usage) && response.usage.length > 0) payload.usage = response.usage; writeCachedSearchPayload(cacheKey, sessionId ? payload : stripParallelGeneratedSessionId(payload), resolveSearchCacheTtlMs(searchConfig)); return payload; } //#endregion export { executeParallelFreeWebSearchProviderTool };