feat(tool): return image attachments from webfetch (#13331)
This commit is contained in:
@@ -3,6 +3,7 @@ import { Tool } from "./tool"
|
||||
import TurndownService from "turndown"
|
||||
import DESCRIPTION from "./webfetch.txt"
|
||||
import { abortAfterAny } from "../util/abort"
|
||||
import { Identifier } from "../id/id"
|
||||
|
||||
const MAX_RESPONSE_SIZE = 5 * 1024 * 1024 // 5MB
|
||||
const DEFAULT_TIMEOUT = 30 * 1000 // 30 seconds
|
||||
@@ -87,11 +88,34 @@ export const WebFetchTool = Tool.define("webfetch", {
|
||||
throw new Error("Response too large (exceeds 5MB limit)")
|
||||
}
|
||||
|
||||
const content = new TextDecoder().decode(arrayBuffer)
|
||||
const contentType = response.headers.get("content-type") || ""
|
||||
|
||||
const mime = contentType.split(";")[0]?.trim().toLowerCase() || ""
|
||||
const title = `${params.url} (${contentType})`
|
||||
|
||||
// Check if response is an image
|
||||
const isImage = mime.startsWith("image/") && mime !== "image/svg+xml" && mime !== "image/vnd.fastbidsheet"
|
||||
|
||||
if (isImage) {
|
||||
const base64Content = Buffer.from(arrayBuffer).toString("base64")
|
||||
return {
|
||||
title,
|
||||
output: "Image fetched successfully",
|
||||
metadata: {},
|
||||
attachments: [
|
||||
{
|
||||
id: Identifier.ascending("part"),
|
||||
sessionID: ctx.sessionID,
|
||||
messageID: ctx.messageID,
|
||||
type: "file",
|
||||
mime,
|
||||
url: `data:${mime};base64,${base64Content}`,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
const content = new TextDecoder().decode(arrayBuffer)
|
||||
|
||||
// Handle content based on requested format and actual content type
|
||||
switch (params.format) {
|
||||
case "markdown":
|
||||
|
||||
97
packages/opencode/test/tool/webfetch.test.ts
Normal file
97
packages/opencode/test/tool/webfetch.test.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import path from "path"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WebFetchTool } from "../../src/tool/webfetch"
|
||||
|
||||
const projectRoot = path.join(import.meta.dir, "../..")
|
||||
|
||||
const ctx = {
|
||||
sessionID: "test",
|
||||
messageID: "message",
|
||||
callID: "",
|
||||
agent: "build",
|
||||
abort: AbortSignal.any([]),
|
||||
messages: [],
|
||||
metadata: () => {},
|
||||
ask: async () => {},
|
||||
}
|
||||
|
||||
async function withFetch(
|
||||
mockFetch: (input: string | URL | Request, init?: RequestInit) => Promise<Response>,
|
||||
fn: () => Promise<void>,
|
||||
) {
|
||||
const originalFetch = globalThis.fetch
|
||||
globalThis.fetch = mockFetch as unknown as typeof fetch
|
||||
try {
|
||||
await fn()
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch
|
||||
}
|
||||
}
|
||||
|
||||
describe("tool.webfetch", () => {
|
||||
test("returns image responses as file attachments", async () => {
|
||||
const bytes = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10])
|
||||
await withFetch(
|
||||
async () => new Response(bytes, { status: 200, headers: { "content-type": "IMAGE/PNG; charset=binary" } }),
|
||||
async () => {
|
||||
await Instance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const webfetch = await WebFetchTool.init()
|
||||
const result = await webfetch.execute({ url: "https://example.com/image.png", format: "markdown" }, ctx)
|
||||
expect(result.output).toBe("Image fetched successfully")
|
||||
expect(result.attachments).toBeDefined()
|
||||
expect(result.attachments?.length).toBe(1)
|
||||
expect(result.attachments?.[0].type).toBe("file")
|
||||
expect(result.attachments?.[0].mime).toBe("image/png")
|
||||
expect(result.attachments?.[0].url.startsWith("data:image/png;base64,")).toBe(true)
|
||||
},
|
||||
})
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test("keeps svg as text output", async () => {
|
||||
const svg = '<svg xmlns="http://www.w3.org/2000/svg"><text>hello</text></svg>'
|
||||
await withFetch(
|
||||
async () =>
|
||||
new Response(svg, {
|
||||
status: 200,
|
||||
headers: { "content-type": "image/svg+xml; charset=UTF-8" },
|
||||
}),
|
||||
async () => {
|
||||
await Instance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const webfetch = await WebFetchTool.init()
|
||||
const result = await webfetch.execute({ url: "https://example.com/image.svg", format: "html" }, ctx)
|
||||
expect(result.output).toContain("<svg")
|
||||
expect(result.attachments).toBeUndefined()
|
||||
},
|
||||
})
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test("keeps text responses as text output", async () => {
|
||||
await withFetch(
|
||||
async () =>
|
||||
new Response("hello from webfetch", {
|
||||
status: 200,
|
||||
headers: { "content-type": "text/plain; charset=utf-8" },
|
||||
}),
|
||||
async () => {
|
||||
await Instance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const webfetch = await WebFetchTool.init()
|
||||
const result = await webfetch.execute({ url: "https://example.com/file.txt", format: "text" }, ctx)
|
||||
expect(result.output).toBe("hello from webfetch")
|
||||
expect(result.attachments).toBeUndefined()
|
||||
},
|
||||
})
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user