/** * browser — 浏览器交互工具集(9 个函数) * * 基于单例 BrowserWindowManager 实现网页加载、截图、JS 执行、 * 内容提取、点击、输入、滚动、等待、关闭等操作。 * * 工具列表: * 1. browser_open — 打开 URL * 2. browser_screenshot — 截图(视口/元素/全页三模式) * 3. browser_evaluate — 执行 JavaScript * 4. browser_extract — 提取页面文本与链接 * 5. browser_click — 点击元素 * 6. browser_type — 输入文本 * 7. browser_scroll — 滚动页面 * 8. browser_wait — 等待条件 * 9. browser_close — 关闭窗口 * * @see docs/Agent网络工具通用设计-v2.md — 第 4 章 browser 浏览器设计 */ import type { IMetonaTool, ToolExecutionContext } from '../../types/metona-tool'; import type { MetonaToolDef } from '../../../harness/types'; import { MetonaToolCategory, MetonaRiskLevel } from '../../../harness/types'; import { BrowserWindowManager } from './browser-window-manager'; import { logTool } from './network-utils'; // ===== 单例 Manager ===== let managerInstance: BrowserWindowManager | null = null; function getManager(): BrowserWindowManager { if (!managerInstance) { managerInstance = new BrowserWindowManager(); } return managerInstance; } /** 应用退出时清理(由 main.ts 调用) */ export function cleanupBrowser(): void { BrowserWindowManager.cleanup(managerInstance); managerInstance = null; } // ===== 1. browser_open ===== export class BrowserOpenTool implements IMetonaTool { readonly definition: MetonaToolDef = { name: 'browser_open', description: 'Open a URL in a hidden browser window. Loads the page and optionally waits for a selector to appear. Returns the page title and URL.', parameters: { type: 'object', properties: { url: { type: 'string', description: 'Target URL (http/https only)' }, wait_selector: { type: 'string', description: 'CSS selector to wait for before returning (optional)' }, }, required: ['url'], }, category: MetonaToolCategory.NETWORK, riskLevel: MetonaRiskLevel.MEDIUM, requiresPermission: true, timeoutMs: 60_000, }; async execute(args: Record, _context: ToolExecutionContext): Promise { const url = args.url as string; if (!url || !/^https?:\/\//i.test(url)) { return { success: false, error: 'URL must start with http:// or https://' }; } const waitSelector = args.wait_selector as string | undefined; logTool('browser_open', `Opening: ${url}`); try { const result = await getManager().open({ url, waitSelector }); return { success: true, ...result }; } catch (err) { return { success: false, error: (err as Error).message }; } } } // ===== 2. browser_screenshot ===== export class BrowserScreenshotTool implements IMetonaTool { readonly definition: MetonaToolDef = { name: 'browser_screenshot', description: 'Capture a screenshot of the current browser window. Three modes: viewport (default), full page, or specific element by CSS selector. Returns base64-encoded PNG image.', parameters: { type: 'object', properties: { full_page: { type: 'boolean', description: 'Capture entire scrollable page (default false)' }, selector: { type: 'string', description: 'CSS selector to capture a specific element (overrides full_page)' }, }, required: [], }, category: MetonaToolCategory.NETWORK, riskLevel: MetonaRiskLevel.LOW, requiresPermission: false, timeoutMs: 30_000, }; async execute(args: Record, _context: ToolExecutionContext): Promise { const fullPage = (args.full_page as boolean) ?? false; const selector = args.selector as string | undefined; logTool('browser_screenshot', `full_page=${fullPage}, selector=${selector ?? 'none'}`); try { const result = await getManager().screenshot({ fullPage, selector }); return { success: true, image: result.data, width: result.width, height: result.height, mime_type: 'image/png', }; } catch (err) { return { success: false, error: (err as Error).message }; } } } // ===== 3. browser_evaluate ===== export class BrowserEvaluateTool implements IMetonaTool { readonly definition: MetonaToolDef = { name: 'browser_evaluate', description: 'Execute JavaScript code in the current browser page context. Returns the result of the last expression. Use for data extraction, DOM queries, or triggering page actions.', parameters: { type: 'object', properties: { script: { type: 'string', description: 'JavaScript code to execute (must return a value)' }, }, required: ['script'], }, category: MetonaToolCategory.NETWORK, riskLevel: MetonaRiskLevel.HIGH, requiresPermission: true, timeoutMs: 30_000, }; async execute(args: Record, _context: ToolExecutionContext): Promise { const script = args.script as string; if (!script) { return { success: false, error: 'No script provided' }; } logTool('browser_evaluate', `Executing ${script.length} chars of JS`); try { const result = await getManager().evaluate(script); return { success: true, result }; } catch (err) { return { success: false, error: (err as Error).message }; } } } // ===== 4. browser_extract ===== export class BrowserExtractTool implements IMetonaTool { readonly definition: MetonaToolDef = { name: 'browser_extract', description: 'Extract text content and links from the current browser page. Optionally target a specific element by CSS selector. Strips scripts, styles, and navigation elements. Returns clean text and up to 50 links.', parameters: { type: 'object', properties: { selector: { type: 'string', description: 'CSS selector to extract from (default: entire body)' }, }, required: [], }, category: MetonaToolCategory.NETWORK, riskLevel: MetonaRiskLevel.LOW, requiresPermission: false, timeoutMs: 30_000, }; async execute(args: Record, _context: ToolExecutionContext): Promise { const selector = args.selector as string | undefined; logTool('browser_extract', `selector=${selector ?? 'body'}`); try { const result = await getManager().extract(selector); return { success: true, text: result.text, links: result.links, link_count: result.links.length, }; } catch (err) { return { success: false, error: (err as Error).message }; } } } // ===== 5. browser_click ===== export class BrowserClickTool implements IMetonaTool { readonly definition: MetonaToolDef = { name: 'browser_click', description: 'Click an element on the page by CSS selector. Scrolls the element into view before clicking. Optionally wait for the selector to appear first.', parameters: { type: 'object', properties: { selector: { type: 'string', description: 'CSS selector of the element to click' }, wait: { type: 'boolean', description: 'Wait for selector to appear before clicking (default false)' }, }, required: ['selector'], }, category: MetonaToolCategory.NETWORK, riskLevel: MetonaRiskLevel.MEDIUM, requiresPermission: true, timeoutMs: 30_000, }; async execute(args: Record, _context: ToolExecutionContext): Promise { const selector = args.selector as string; const wait = (args.wait as boolean) ?? false; if (!selector) { return { success: false, error: 'No selector provided' }; } logTool('browser_click', `selector=${selector}, wait=${wait}`); try { await getManager().click(selector, wait); return { success: true, selector, clicked: true }; } catch (err) { return { success: false, error: (err as Error).message }; } } } // ===== 6. browser_type ===== export class BrowserTypeTool implements IMetonaTool { readonly definition: MetonaToolDef = { name: 'browser_type', description: 'Type text into an input element by CSS selector. Focuses the element, optionally clears it first, and dispatches input/change events for React/Vue compatibility. Can optionally submit the form.', parameters: { type: 'object', properties: { selector: { type: 'string', description: 'CSS selector of the input element' }, text: { type: 'string', description: 'Text to type into the element' }, clear: { type: 'boolean', description: 'Clear the field before typing (default true)' }, submit: { type: 'boolean', description: 'Submit the form after typing (default false)' }, }, required: ['selector', 'text'], }, category: MetonaToolCategory.NETWORK, riskLevel: MetonaRiskLevel.MEDIUM, requiresPermission: true, timeoutMs: 30_000, }; async execute(args: Record, _context: ToolExecutionContext): Promise { const selector = args.selector as string; const text = args.text as string; const clear = (args.clear as boolean) ?? true; const submit = (args.submit as boolean) ?? false; if (!selector) { return { success: false, error: 'No selector provided' }; } if (text === undefined || text === null) { return { success: false, error: 'No text provided' }; } logTool('browser_type', `selector=${selector}, len=${text.length}, submit=${submit}`); try { await getManager().type(selector, text, { clear, submit }); return { success: true, selector, typed: text.length }; } catch (err) { return { success: false, error: (err as Error).message }; } } } // ===== 7. browser_scroll ===== export class BrowserScrollTool implements IMetonaTool { readonly definition: MetonaToolDef = { name: 'browser_scroll', description: 'Scroll the browser page. Modes: down/up (500px increment), top/bottom (absolute), or scroll to a specific element by CSS selector.', parameters: { type: 'object', properties: { direction: { type: 'string', description: 'Scroll direction: down, up, top, bottom (default down)' }, selector: { type: 'string', description: 'CSS selector to scroll to (overrides direction)' }, }, required: [], }, category: MetonaToolCategory.NETWORK, riskLevel: MetonaRiskLevel.LOW, requiresPermission: false, timeoutMs: 15_000, }; async execute(args: Record, _context: ToolExecutionContext): Promise { const direction = (args.direction as string) ?? 'down'; const selector = args.selector as string | undefined; logTool('browser_scroll', `direction=${direction}, selector=${selector ?? 'none'}`); try { await getManager().scroll({ direction: direction as 'down' | 'up' | 'top' | 'bottom', selector }); return { success: true, direction, selector }; } catch (err) { return { success: false, error: (err as Error).message }; } } } // ===== 8. browser_wait ===== export class BrowserWaitTool implements IMetonaTool { readonly definition: MetonaToolDef = { name: 'browser_wait', description: 'Wait for a condition on the browser page. Either wait for a CSS selector to appear (with timeout) or wait for a fixed duration.', parameters: { type: 'object', properties: { selector: { type: 'string', description: 'CSS selector to wait for (mutually exclusive with time_ms)' }, time_ms: { type: 'number', description: 'Fixed wait time in milliseconds (default 1000)' }, }, required: [], }, category: MetonaToolCategory.NETWORK, riskLevel: MetonaRiskLevel.LOW, requiresPermission: false, timeoutMs: 30_000, }; async execute(args: Record, _context: ToolExecutionContext): Promise { const selector = args.selector as string | undefined; const timeMs = (args.time_ms as number) ?? 1_000; logTool('browser_wait', `selector=${selector ?? 'none'}, time_ms=${timeMs}`); try { await getManager().wait({ selector, timeMs }); return { success: true, waited_for: selector ?? `${timeMs}ms` }; } catch (err) { return { success: false, error: (err as Error).message }; } } } // ===== 9. browser_close ===== export class BrowserCloseTool implements IMetonaTool { readonly definition: MetonaToolDef = { name: 'browser_close', description: 'Close the current browser window and release resources. Call this when browser interaction is complete to free memory.', parameters: { type: 'object', properties: {}, required: [], }, category: MetonaToolCategory.NETWORK, riskLevel: MetonaRiskLevel.LOW, requiresPermission: false, timeoutMs: 10_000, }; async execute(_args: Record, _context: ToolExecutionContext): Promise { logTool('browser_close', 'Closing browser window'); getManager().close(); return { success: true, closed: true }; } } // ===== 导出所有 Browser 工具 ===== export const browserTools: IMetonaTool[] = [ new BrowserOpenTool(), new BrowserScreenshotTool(), new BrowserEvaluateTool(), new BrowserExtractTool(), new BrowserClickTool(), new BrowserTypeTool(), new BrowserScrollTool(), new BrowserWaitTool(), new BrowserCloseTool(), ];