























@@ -3,13 +3,15 @@ import { applyTemplate } from "../auto-reply/templating.js";
33import type { OpenClawConfig } from "../config/types.openclaw.js";
44import type { LinkModelConfig, LinkToolsConfig } from "../config/types.tools.js";
55import { logVerbose, shouldLogVerbose } from "../globals.js";
6+import { fetchWithSsrFGuard, GUARDED_FETCH_MODE } from "../infra/net/fetch-guard.js";
67import { CLI_OUTPUT_MAX_BUFFER } from "../media-understanding/defaults.js";
78import { resolveTimeoutMs } from "../media-understanding/resolve.js";
89import {
910normalizeMediaUnderstandingChatType,
1011resolveMediaUnderstandingScope,
1112} from "../media-understanding/scope.js";
12-import { runExec } from "../process/exec.js";
13+import { readResponseWithLimit } from "../media/read-response-with-limit.js";
14+import { runCommandWithTimeout } from "../process/exec.js";
1315import { DEFAULT_LINK_TIMEOUT_SECONDS } from "./defaults.js";
1416import { extractLinksFromMessage } from "./detect.js";
1517@@ -38,8 +40,69 @@ function resolveTimeoutMsFromConfig(params: {
3840return resolveTimeoutMs(configured, DEFAULT_LINK_TIMEOUT_SECONDS);
3941}
404243+function isLinkUrlTemplate(value: string): boolean {
44+return value.includes("LinkUrl") || value.includes("LinkFinalUrl");
45+}
46+47+function commandName(command: string): string {
48+return (command.split(/[\\/]/).pop() ?? command).toLowerCase();
49+}
50+51+function isUrlFetcherCommand(command: string): boolean {
52+return commandName(command) === "curl" || commandName(command) === "wget";
53+}
54+55+function buildLinkCliArgs(params: {
56+args: string[];
57+ctx: MsgContext;
58+finalUrl: string;
59+url: string;
60+}): string[] {
61+const templCtx = {
62+ ...params.ctx,
63+LinkFinalUrl: params.finalUrl,
64+LinkUrl: params.url,
65+};
66+return params.args
67+.filter((arg) => !isLinkUrlTemplate(arg))
68+.map((arg) => applyTemplate(arg, templCtx));
69+}
70+71+async function fetchLinkContent(params: {
72+timeoutMs: number;
73+url: string;
74+}): Promise<{ content: string; finalUrl: string } | null> {
75+const { response, finalUrl, release } = await fetchWithSsrFGuard({
76+url: params.url,
77+timeoutMs: params.timeoutMs,
78+mode: GUARDED_FETCH_MODE.STRICT,
79+auditContext: "link-understanding",
80+init: {
81+headers: {
82+Accept: "text/*,application/json,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
83+"User-Agent": "OpenClaw-LinkUnderstanding/1.0",
84+},
85+},
86+});
87+try {
88+if (!response.ok) {
89+throw new Error(`Link fetch failed with HTTP ${response.status}`);
90+}
91+const buffer = await readResponseWithLimit(response, CLI_OUTPUT_MAX_BUFFER);
92+const content = new TextDecoder().decode(buffer).trim();
93+if (!content) {
94+return null;
95+}
96+return { content, finalUrl };
97+} finally {
98+await release();
99+}
100+}
101+41102async function runCliEntry(params: {
103+content: string;
42104entry: LinkModelConfig;
105+finalUrl: string;
43106ctx: MsgContext;
44107url: string;
45108config?: LinkToolsConfig;
@@ -53,28 +116,43 @@ async function runCliEntry(params: {
53116}
54117const args = params.entry.args ?? [];
55118const timeoutMs = resolveTimeoutMsFromConfig({ config: params.config, entry: params.entry });
56-const templCtx = {
57- ...params.ctx,
58-LinkUrl: params.url,
59-};
60-const argv = [command, ...args].map((part, index) =>
61-index === 0 ? part : applyTemplate(part, templCtx),
62-);
119+if (isUrlFetcherCommand(command) && args.some(isLinkUrlTemplate)) {
120+return params.content;
121+}
122+123+const argv = [
124+command,
125+ ...buildLinkCliArgs({
126+ args,
127+ctx: params.ctx,
128+finalUrl: params.finalUrl,
129+url: params.url,
130+}),
131+];
6313264133if (shouldLogVerbose()) {
65134logVerbose(`Link understanding via CLI: ${argv.join(" ")}`);
66135}
6713668-const { stdout } = await runExec(argv[0], argv.slice(1), {
137+const result = await runCommandWithTimeout(argv, {
69138 timeoutMs,
70-maxBuffer: CLI_OUTPUT_MAX_BUFFER,
139+input: params.content,
140+env: {
141+OPENCLAW_LINK_FINAL_URL: params.finalUrl,
142+OPENCLAW_LINK_URL: params.url,
143+},
71144});
72-const trimmed = stdout.trim();
145+if (result.code !== 0) {
146+throw new Error(`Link understanding command exited with code ${result.code ?? "unknown"}`);
147+}
148+const trimmed = result.stdout.trim();
73149return trimmed || null;
74150}
7515176152async function runLinkEntries(params: {
153+content: string;
77154entries: LinkModelConfig[];
155+finalUrl: string;
78156ctx: MsgContext;
79157url: string;
80158config?: LinkToolsConfig;
@@ -83,7 +161,9 @@ async function runLinkEntries(params: {
83161for (const entry of params.entries) {
84162try {
85163const output = await runCliEntry({
164+content: params.content,
86165 entry,
166+finalUrl: params.finalUrl,
87167ctx: params.ctx,
88168url: params.url,
89169config: params.config,
@@ -135,12 +215,28 @@ export async function runLinkUnderstanding(params: {
135215136216const outputs: string[] = [];
137217for (const url of links) {
138-const output = await runLinkEntries({
139- entries,
140-ctx: params.ctx,
141- url,
142- config,
143-});
218+const timeoutMs = resolveTimeoutMsFromConfig({ config, entry: entries[0] });
219+let fetched: Awaited<ReturnType<typeof fetchLinkContent>>;
220+try {
221+fetched = await fetchLinkContent({ url, timeoutMs });
222+} catch (err) {
223+if (shouldLogVerbose()) {
224+logVerbose(`Link understanding fetch blocked or failed for ${url}: ${String(err)}`);
225+}
226+continue;
227+}
228+if (!fetched) {
229+continue;
230+}
231+const output =
232+(await runLinkEntries({
233+content: fetched.content,
234+ entries,
235+finalUrl: fetched.finalUrl,
236+ctx: params.ctx,
237+ url,
238+ config,
239+})) ?? fetched.content;
144240if (output) {
145241outputs.push(output);
146242}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。