openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
263 lines (262 loc) • 10.5 kB
JavaScript
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 };