cloudaxe-opencode/packages/opencode/src/tool/webfetch.ts

112 lines
3.2 KiB
TypeScript
Raw Normal View History

2025-05-31 18:41:00 +00:00
import { z } from "zod"
import { Tool } from "./tool"
import TurndownService from "turndown"
2025-06-04 17:12:13 +00:00
import DESCRIPTION from "./webfetch.txt"
2025-05-26 18:09:17 +00:00
2025-05-31 18:41:00 +00:00
const MAX_RESPONSE_SIZE = 5 * 1024 * 1024 // 5MB
const DEFAULT_TIMEOUT = 30 * 1000 // 30 seconds
const MAX_TIMEOUT = 120 * 1000 // 2 minutes
2025-05-26 18:09:17 +00:00
2025-06-04 17:12:13 +00:00
export const WebFetchTool = Tool.define({
id: "opencode.webfetch",
2025-05-26 18:09:17 +00:00
description: DESCRIPTION,
parameters: z.object({
url: z.string().describe("The URL to fetch content from"),
format: z
.enum(["text", "markdown", "html"])
.describe(
"The format to return the content in (text, markdown, or html)",
),
timeout: z
.number()
.min(0)
.max(MAX_TIMEOUT / 1000)
.describe("Optional timeout in seconds (max 120)")
.optional(),
}),
async execute(params) {
2025-05-26 18:09:17 +00:00
// Validate URL
if (
!params.url.startsWith("http://") &&
!params.url.startsWith("https://")
) {
2025-05-31 18:41:00 +00:00
throw new Error("URL must start with http:// or https://")
2025-05-26 18:09:17 +00:00
}
const timeout = Math.min(
(params.timeout ?? DEFAULT_TIMEOUT / 1000) * 1000,
MAX_TIMEOUT,
2025-05-31 18:41:00 +00:00
)
2025-05-26 18:09:17 +00:00
2025-05-31 18:41:00 +00:00
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), timeout)
2025-05-26 18:09:17 +00:00
const response = await fetch(params.url, {
signal: controller.signal,
headers: {
"User-Agent": "opencode/1.0",
},
2025-05-31 18:41:00 +00:00
})
2025-05-26 18:09:17 +00:00
2025-05-31 18:41:00 +00:00
clearTimeout(timeoutId)
2025-05-26 18:09:17 +00:00
if (!response.ok) {
2025-05-31 18:41:00 +00:00
throw new Error(`Request failed with status code: ${response.status}`)
2025-05-26 18:09:17 +00:00
}
// Check content length
2025-05-31 18:41:00 +00:00
const contentLength = response.headers.get("content-length")
2025-05-26 18:09:17 +00:00
if (contentLength && parseInt(contentLength) > MAX_RESPONSE_SIZE) {
2025-05-31 18:41:00 +00:00
throw new Error("Response too large (exceeds 5MB limit)")
2025-05-26 18:09:17 +00:00
}
2025-05-31 18:41:00 +00:00
const arrayBuffer = await response.arrayBuffer()
2025-05-26 18:09:17 +00:00
if (arrayBuffer.byteLength > MAX_RESPONSE_SIZE) {
2025-05-31 18:41:00 +00:00
throw new Error("Response too large (exceeds 5MB limit)")
2025-05-26 18:09:17 +00:00
}
2025-05-31 18:41:00 +00:00
const content = new TextDecoder().decode(arrayBuffer)
const contentType = response.headers.get("content-type") || ""
2025-05-26 18:09:17 +00:00
switch (params.format) {
case "text":
if (contentType.includes("text/html")) {
2025-05-31 18:41:00 +00:00
const text = extractTextFromHTML(content)
2025-05-31 21:12:16 +00:00
return { output: text, metadata: {} }
2025-05-26 18:09:17 +00:00
}
2025-05-31 21:12:16 +00:00
return { output: content, metadata: {} }
2025-05-26 18:09:17 +00:00
case "markdown":
if (contentType.includes("text/html")) {
2025-05-31 18:41:00 +00:00
const markdown = convertHTMLToMarkdown(content)
2025-05-31 21:12:16 +00:00
return { output: markdown, metadata: {} }
2025-05-26 18:09:17 +00:00
}
return { output: "```\n" + content + "\n```", metadata: {} }
2025-05-26 18:09:17 +00:00
case "html":
2025-05-31 21:12:16 +00:00
return { output: content, metadata: {} }
2025-05-26 18:09:17 +00:00
default:
2025-05-31 21:12:16 +00:00
return { output: content, metadata: {} }
2025-05-26 18:09:17 +00:00
}
},
2025-05-31 18:41:00 +00:00
})
2025-05-26 18:09:17 +00:00
function extractTextFromHTML(html: string): string {
2025-05-31 18:41:00 +00:00
const doc = new DOMParser().parseFromString(html, "text/html")
const text = doc.body.textContent || doc.body.innerText || ""
return text.replace(/\s+/g, " ").trim()
2025-05-26 18:09:17 +00:00
}
function convertHTMLToMarkdown(html: string): string {
2025-06-05 19:59:09 +00:00
const turndownService = new TurndownService({
headingStyle: "atx",
hr: "---",
bulletListMarker: "-",
codeBlockStyle: "fenced",
emDelimiter: "*",
})
turndownService.remove(["script", "style", "meta", "link"])
2025-05-31 18:41:00 +00:00
return turndownService.turndown(html)
2025-05-26 18:09:17 +00:00
}