MagicCode/packages/ext-claudecode/src/tools/webfetch.ts

132 lines
4.3 KiB
TypeScript
Raw Normal View History

2026-10-11 22:41:17 +00:00
import type { Tool } from "@magiccode/core";
import { optNum, optStr, str } from "./helpers";
const DEFAULT_TIMEOUT = 30_000; // 30 secondes
const OUTPUT_MAX = 100_000; // 100 KB
interface FetchOptions {
method?: string;
headers?: Record<string, string>;
body?: string;
timeout?: number;
}
export const webfetchTool: Tool = {
spec: {
name: "WebFetch",
description:
"Récupère le contenu d'une URL (HTTP/HTTPS). Retourne le statut, les en-têtes et le corps de la réponse (texte ou JSON).",
inputSchema: {
type: "object",
properties: {
url: { type: "string", description: "URL à récupérer (http:// ou https://)" },
method: { type: "string", description: 'Méthode HTTP (GET par défaut, ou POST, PUT, DELETE, etc.)' },
headers: {
type: "object",
description: "En-têtes HTTP personnalisés",
additionalProperties: { type: "string" },
},
body: { type: "string", description: "Corps de la requête (pour POST, PUT, etc.)" },
timeout: { type: "number", description: "Timeout en millisecondes (30000 par défaut)" },
},
required: ["url"],
},
},
permission: "ask",
describe: (i) => `WebFetch ${String(i.method ?? "GET")} ${String(i.url ?? "")}`,
async execute(input) {
const url = str(input, "url");
const method = optStr(input, "method")?.toUpperCase() ?? "GET";
const headers = (input.headers as Record<string, string>) ?? {};
const body = optStr(input, "body");
const timeout = optNum(input, "timeout") ?? DEFAULT_TIMEOUT;
// Validation basique de l'URL
let parsedUrl: URL;
try {
parsedUrl = new URL(url);
if (!["http:", "https:"].includes(parsedUrl.protocol)) {
throw new Error(`Protocole non supporté : ${parsedUrl.protocol}`);
}
} catch (error) {
throw new Error(`URL invalide : ${url} (${(error as Error).message})`);
}
// Préparer les options de fetch
const fetchOptions: FetchOptions = {
method,
timeout,
};
if (Object.keys(headers).length > 0) {
fetchOptions.headers = headers;
}
if (body) {
fetchOptions.body = body;
// Ajouter Content-Type par défaut si non défini
if (!fetchOptions.headers) {
fetchOptions.headers = {};
}
if (!fetchOptions.headers["Content-Type"]) {
fetchOptions.headers["Content-Type"] = "application/json";
}
}
try {
const response = await fetch(url, fetchOptions);
// Construire l'en-tête de réponse
const responseHeaders: Record<string, string> = {};
response.headers.forEach((value, key) => {
responseHeaders[key] = value;
});
// Déterminer le type de contenu
const contentType = responseHeaders["content-type"] || "";
let bodyText: string;
if (contentType.includes("application/json")) {
// Pour JSON, retourner le texte formaté
const text = await response.text();
try {
const parsed = JSON.parse(text);
bodyText = JSON.stringify(parsed, null, 2);
} catch {
bodyText = text;
}
} else if (contentType.includes("text/") || contentType.includes("application/xml")) {
bodyText = await response.text();
} else {
// Pour les autres types (images, binaires), retourner un résumé
const buffer = await response.arrayBuffer();
bodyText = `[Contenu binaire : ${buffer.byteLength} bytes (type: ${contentType || "inconnu"})]`;
}
// Tronquer si trop volumineux
if (bodyText.length > OUTPUT_MAX) {
bodyText = `${bodyText.slice(0, OUTPUT_MAX)}\n… (tronqué, ${bodyText.length} bytes au total)`;
}
// Assembler la réponse
const result = [
`Status: ${response.status} ${response.statusText}`,
`\n[Headers]`,
Object.entries(responseHeaders)
.map(([k, v]) => `${k}: ${v}`)
.join("\n"),
`\n[Body]`,
bodyText,
].join("\n");
return result;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (message.includes("timeout") || message.includes("Timeout")) {
throw new Error(`Timeout après ${timeout}ms pour ${url}`);
}
throw new Error(`Erreur lors de la récupération de ${url}: ${message}`);
}
},
};