UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

249 lines (248 loc) 8.16 kB
import { l as normalizeStringEntries } from "../string-normalization-WNUDCpXX.js"; import { n as readResponseWithLimit } from "../read-response-with-limit-MDCSJrlg.js"; import { a as shouldLogVerbose, r as logVerbose } from "../globals-GTrXU4s9.js"; import { r as runCommandWithTimeout } from "../exec-DubsSJS2.js"; import { c as isBlockedHostnameOrIp } from "../ssrf-CvPEXMGn.js"; import { r as fetchWithSsrFGuard, t as GUARDED_FETCH_MODE } from "../fetch-guard-BttkNCLm.js"; import "../defaults-BK9rbdLc.js"; import { t as CLI_OUTPUT_MAX_BUFFER } from "../defaults.constants-A49pIMjc.js"; import { c as resolveTimeoutMs, l as normalizeMediaUnderstandingChatType, u as resolveMediaUnderstandingScope } from "../resolve-BmY6318x.js"; import { t as applyTemplate } from "../templating-CLmjS51i.js"; import { t as finalizeInboundContext } from "../inbound-context-CZx-NgvC.js"; //#region src/link-understanding/format.ts /** Appends normalized link-understanding outputs to the agent-visible body. */ function formatLinkUnderstandingBody(params) { const outputs = normalizeStringEntries(params.outputs); if (outputs.length === 0) return params.body ?? ""; const base = (params.body ?? "").trim(); if (!base) return outputs.join("\n"); return `${base}\n\n${outputs.join("\n")}`; } //#endregion //#region src/link-understanding/detect.ts const MARKDOWN_LINK_RE = /\[[^\]]*]\((https?:\/\/\S+?)\)/gi; const BARE_LINK_RE = /https?:\/\/\S+/gi; function stripMarkdownLinks(message) { return message.replace(MARKDOWN_LINK_RE, " "); } function resolveMaxLinks(value) { if (typeof value === "number" && Number.isFinite(value) && value > 0) return Math.floor(value); return 3; } function isAllowedUrl(raw) { try { const parsed = new URL(raw); if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false; if (isBlockedHostnameOrIp(parsed.hostname)) return false; return true; } catch { return false; } } /** * Extracts unique, SSRF-filtered bare HTTP(S) links from inbound text. * Markdown links are ignored so display-only citations do not trigger fetches. */ function extractLinksFromMessage(message, opts) { const source = message?.trim(); if (!source) return []; const maxLinks = resolveMaxLinks(opts?.maxLinks); const sanitized = stripMarkdownLinks(source); const seen = /* @__PURE__ */ new Set(); const results = []; for (const match of sanitized.matchAll(BARE_LINK_RE)) { const raw = match[0]?.trim(); if (!raw) continue; if (!isAllowedUrl(raw)) continue; if (seen.has(raw)) continue; seen.add(raw); results.push(raw); if (results.length >= maxLinks) break; } return results; } //#endregion //#region src/link-understanding/runner.ts function resolveScopeDecision(params) { return resolveMediaUnderstandingScope({ scope: params.config?.scope, sessionKey: params.ctx.SessionKey, channel: params.ctx.Surface ?? params.ctx.Provider, chatType: normalizeMediaUnderstandingChatType(params.ctx.ChatType) }); } function resolveTimeoutMsFromConfig(params) { return resolveTimeoutMs(params.entry.timeoutSeconds ?? params.config?.timeoutSeconds, 30); } function isLinkUrlTemplate(value) { return value.includes("LinkUrl") || value.includes("LinkFinalUrl"); } function commandName(command) { return (command.split(/[\\/]/).pop() ?? command).toLowerCase(); } function isUrlFetcherCommand(command) { return commandName(command) === "curl" || commandName(command) === "wget"; } function buildLinkCliArgs(params) { const templCtx = { ...params.ctx, LinkFinalUrl: params.finalUrl, LinkUrl: params.url }; return params.args.filter((arg) => !isLinkUrlTemplate(arg)).map((arg) => applyTemplate(arg, templCtx)); } async function fetchLinkContent(params) { const { response, finalUrl, release } = await fetchWithSsrFGuard({ url: params.url, timeoutMs: params.timeoutMs, mode: GUARDED_FETCH_MODE.STRICT, auditContext: "link-understanding", init: { headers: { Accept: "text/*,application/json,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "User-Agent": "OpenClaw-LinkUnderstanding/1.0" } } }); try { if (!response.ok) throw new Error(`Link fetch failed with HTTP ${response.status}`); const buffer = await readResponseWithLimit(response, CLI_OUTPUT_MAX_BUFFER); const content = new TextDecoder().decode(buffer).trim(); if (!content) return null; return { content, finalUrl }; } finally { await release(); } } async function runCliEntry(params) { if ((params.entry.type ?? "cli") !== "cli") return null; const command = params.entry.command.trim(); if (!command) return null; const args = params.entry.args ?? []; const timeoutMs = resolveTimeoutMsFromConfig({ config: params.config, entry: params.entry }); if (isUrlFetcherCommand(command) && args.some(isLinkUrlTemplate)) return params.content; const argv = [command, ...buildLinkCliArgs({ args, ctx: params.ctx, finalUrl: params.finalUrl, url: params.url })]; if (shouldLogVerbose()) logVerbose(`Link understanding via CLI: ${argv.join(" ")}`); const result = await runCommandWithTimeout(argv, { timeoutMs, input: params.content, env: { OPENCLAW_LINK_FINAL_URL: params.finalUrl, OPENCLAW_LINK_URL: params.url } }); if (result.code !== 0) throw new Error(`Link understanding command exited with code ${result.code ?? "unknown"}`); return result.stdout.trim() || null; } async function runLinkEntries(params) { let lastError; for (const entry of params.entries) try { const output = await runCliEntry({ content: params.content, entry, finalUrl: params.finalUrl, ctx: params.ctx, url: params.url, config: params.config }); if (output) return output; } catch (err) { lastError = err; if (shouldLogVerbose()) logVerbose(`Link understanding failed for ${params.url}: ${String(err)}`); } if (lastError && shouldLogVerbose()) logVerbose(`Link understanding exhausted for ${params.url}`); return null; } /** * Fetches detected links through the SSRF guard and runs configured CLI processors. * Returns detected URLs even when processors are absent so callers can report discovery. */ async function runLinkUnderstanding(params) { const config = params.cfg.tools?.links; if (!config || config.enabled === false) return { urls: [], outputs: [] }; if (resolveScopeDecision({ config, ctx: params.ctx }) === "deny") { if (shouldLogVerbose()) logVerbose("Link understanding disabled by scope policy."); return { urls: [], outputs: [] }; } const links = extractLinksFromMessage(params.message ?? params.ctx.CommandBody ?? params.ctx.RawBody ?? params.ctx.Body ?? "", { maxLinks: config?.maxLinks }); if (links.length === 0) return { urls: [], outputs: [] }; const entries = config?.models ?? []; if (entries.length === 0) return { urls: links, outputs: [] }; const outputs = []; for (const url of links) { const timeoutMs = resolveTimeoutMsFromConfig({ config, entry: entries[0] }); let fetched; try { fetched = await fetchLinkContent({ url, timeoutMs }); } catch (err) { if (shouldLogVerbose()) logVerbose(`Link understanding fetch blocked or failed for ${url}: ${String(err)}`); continue; } if (!fetched) continue; const output = await runLinkEntries({ content: fetched.content, entries, finalUrl: fetched.finalUrl, ctx: params.ctx, url, config }) ?? fetched.content; if (output) outputs.push(output); } return { urls: links, outputs }; } //#endregion //#region src/link-understanding/apply.ts /** Runs link understanding and folds successful outputs into the inbound context. */ async function applyLinkUnderstanding(params) { const result = await runLinkUnderstanding({ cfg: params.cfg, ctx: params.ctx }); if (result.outputs.length === 0) return result; params.ctx.LinkUnderstanding = [...params.ctx.LinkUnderstanding ?? [], ...result.outputs]; params.ctx.Body = formatLinkUnderstandingBody({ body: params.ctx.Body, outputs: result.outputs }); finalizeInboundContext(params.ctx, { forceBodyForAgent: true, forceBodyForCommands: true }); return result; } //#endregion export { applyLinkUnderstanding };