From c5a51d5cd13277bead0189f519bf42441aa8114d Mon Sep 17 00:00:00 2001 From: Tony-ooo Date: Thu, 13 Aug 2026 18:11:02 +0800 Subject: [PATCH] fix: replace Bun globals with Node fs APIs for OpenCode 1.18 compatibility OpenCode 1.18 no longer exposes the Bun global to plugins, so Bun.file/Bun.write crash with 'ReferenceError: Bun is not defined', breaking pasted-image save/read. - vision-helper.ts: persist images via fs.access + fs.writeFile - vision.ts: read images via fs.stat + fs.readFile, derive MIME from extension --- plugins/vision-helper.ts | 11 +++++++---- tools/vision.ts | 40 ++++++++++++++++++++++++++++------------ 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/plugins/vision-helper.ts b/plugins/vision-helper.ts index ad88461..bd3b65b 100644 --- a/plugins/vision-helper.ts +++ b/plugins/vision-helper.ts @@ -255,12 +255,15 @@ export default (async () => { const seqDir = path.join(TMP_DIR, `image${seq}`) const filePath = path.join(seqDir, name) // P1-3: write failures degrade to a skip (don't throw, don't break the turn) - if (!(await Bun.file(filePath).exists())) { + let fileExists = false + try { + await fs.access(filePath) + fileExists = true + } catch {} + if (!fileExists) { try { - // H2 fix: ensure seqDir exists before writing — Bun.write may not - // auto-create intermediate directories depending on version. await fs.mkdir(seqDir, { recursive: true }).catch(() => {}) - await Bun.write(filePath, Buffer.from(base64, "base64")) + await fs.writeFile(filePath, Buffer.from(base64, "base64")) } catch (err) { console.error(`[vision-helper] Failed to write ${filePath}:`, err) continue diff --git a/tools/vision.ts b/tools/vision.ts index 1e89343..92d9448 100644 --- a/tools/vision.ts +++ b/tools/vision.ts @@ -1,6 +1,7 @@ import { tool } from "@opencode-ai/plugin" import { tmpdir } from "os" import path from "path" +import { promises as fs } from "fs" const TMP_DIR = path.join(tmpdir(), "opencode-vision") const TMP_DIR_RESOLVED = path.resolve(TMP_DIR) @@ -11,6 +12,19 @@ const MAX_FILE_SIZE = 50 * 1024 * 1024 // Allowlist image extensions accepted by the external VLM path. const IMAGE_EXTS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp"]) +function mimeType(filePath: string): string { + const ext = path.extname(filePath).toLowerCase() + switch (ext) { + case ".png": return "image/png" + case ".jpg": + case ".jpeg": return "image/jpeg" + case ".gif": return "image/gif" + case ".webp": return "image/webp" + case ".bmp": return "image/bmp" + default: return "image/png" + } +} + /** * Path sandbox: only read image files created under TMP_DIR. * This prevents prompt injection from exfiltrating sensitive local files by @@ -92,10 +106,14 @@ Override with VISION_API_TYPE=openai|minimax.`, for (const candidate of candidates) { const safe = sandboxPath(candidate) if (!safe) continue - const file = Bun.file(safe) - if (!(await file.exists())) continue - if (file.size > MAX_FILE_SIZE) { - rejected.push(`${safe} (too large: ${(file.size / 1024 / 1024).toFixed(1)}MB > ${MAX_FILE_SIZE / 1024 / 1024}MB)`) + let stat: { size: number } + try { + stat = await fs.stat(safe) + } catch { + continue + } + if (stat.size > MAX_FILE_SIZE) { + rejected.push(`${safe} (too large: ${(stat.size / 1024 / 1024).toFixed(1)}MB > ${MAX_FILE_SIZE / 1024 / 1024}MB)`) found = null break } @@ -167,10 +185,9 @@ async function callOpenAI(apiKey: string, baseUrl: string, resolved: string[], q } for (const filePath of resolved) { - const file = Bun.file(filePath) - const mime = file.type || "image/png" - const buffer = await file.arrayBuffer() - const base64 = Buffer.from(buffer).toString("base64") + const buffer = await fs.readFile(filePath) + const mime = mimeType(filePath) + const base64 = buffer.toString("base64") content.push({ type: "image_url", image_url: { url: `data:${mime};base64,${base64}` } }) } @@ -235,10 +252,9 @@ async function callMiniMax(apiKey: string, baseUrl: string, resolved: string[], const descriptions: string[] = [] for (const filePath of resolved) { - const file = Bun.file(filePath) - const mime = file.type || "image/png" - const buffer = await file.arrayBuffer() - const base64 = Buffer.from(buffer).toString("base64") + const buffer = await fs.readFile(filePath) + const mime = mimeType(filePath) + const base64 = buffer.toString("base64") const imageUrl = `data:${mime};base64,${base64}` const prompt = question || "Please describe this image in detail"