/** * 图片查看工具(1 个) * * view_image — 读取图片文件并返回 base64 data URL * * 支持格式:png, jpg, jpeg, gif, webp, bmp, svg * 文件大小限制:5MB */ import { readFile, stat } from 'fs/promises'; import { resolve, extname } from 'path'; import type { IMetonaTool, ToolExecutionContext } from '../../types/metona-tool'; import type { MetonaToolDef } from '../../../harness/types'; import { MetonaToolCategory, MetonaRiskLevel } from '../../../harness/types'; import { isPathWithinWorkspace } from './file-guard'; const MAX_IMAGE_BYTES = 5 * 1024 * 1024; // 5MB /** 扩展名 → MIME 映射 */ const SUPPORTED_FORMATS: Record = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', '.bmp': 'image/bmp', '.svg': 'image/svg+xml', }; const SUPPORTED_EXTENSIONS = Object.keys(SUPPORTED_FORMATS); export class ViewImageTool implements IMetonaTool { readonly definition: MetonaToolDef = { name: 'view_image', description: 'Read an image file and return its content as a base64 data URL. Supports png, jpg, jpeg, gif, webp, bmp, svg. File size limit: 5MB. Path must be within the workspace.', parameters: { type: 'object', properties: { path: { type: 'string', description: 'Image file path (relative to workspace or absolute)' }, }, required: ['path'], }, category: MetonaToolCategory.FILESYSTEM, riskLevel: MetonaRiskLevel.SAFE, requiresPermission: false, timeoutMs: 10_000, }; async execute(args: Record, context: ToolExecutionContext): Promise { try { const path = args.path as string; if (!path) { return { error: 'path is required', success: false }; } // 安全校验:路径必须在 workspace 内(防范路径遍历) if (!isPathWithinWorkspace(path, context.workspacePath)) { return { error: 'Path outside workspace', path, success: false }; } // 扩展名校验 const ext = extname(path).toLowerCase(); const mimeType = SUPPORTED_FORMATS[ext]; if (!mimeType) { return { error: 'Unsupported image format', path, supportedFormats: SUPPORTED_EXTENSIONS, success: false, }; } const resolvedPath = resolve(context.workspacePath, path); // 文件存在性 + 大小检查(先 stat 再读取,避免读取超大文件) let stats; try { stats = await stat(resolvedPath); } catch { return { error: 'File not found', path, success: false }; } if (stats.size > MAX_IMAGE_BYTES) { return { error: 'Image too large (max 5MB)', size: stats.size, success: false }; } // 读取文件并编码为 base64 data URL const buffer = await readFile(resolvedPath); const dataUrl = `data:${mimeType};base64,${buffer.toString('base64')}`; // v0.3.1 修复 WARN-3: 成功路径添加 success: true return { path, size: stats.size, mimeType, dataUrl, success: true, }; } catch (error) { const errMsg = error instanceof Error ? error.message : String(error); return { error: errMsg, success: false }; } } }