Files
metona-ai-desktop/electron/harness/tools/built-in/browser.ts
T
thzxx 8cdb93f8bf feat: MEMORY.md 访问保护、格式简化及预存问题修复
MEMORY.md 保护: 新建 file-guard.ts 共享守卫模块; read_file/write_file/search_files/run_command 禁止访问根目录 MEMORY.md; permissions.ts 增加 deniedPatterns 深度防御。格式简化: 元数据从 # 注释改为 > 引用语法; 校验规则从 6 条简化为 3 条; context-builder extractContent 适配。预存问题: 修复路径遍历前缀碰撞漏洞; 移除 browser_extract 内容截断; SearXNG 配置实时读取; web_search/web_fetch 移除截断; 侧边栏动态获取工具列表; 版本号 0.1.1
2026-07-05 21:24:22 +08:00

389 lines
13 KiB
TypeScript

/**
* 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<string, unknown>, _context: ToolExecutionContext): Promise<unknown> {
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<string, unknown>, _context: ToolExecutionContext): Promise<unknown> {
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<string, unknown>, _context: ToolExecutionContext): Promise<unknown> {
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<string, unknown>, _context: ToolExecutionContext): Promise<unknown> {
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<string, unknown>, _context: ToolExecutionContext): Promise<unknown> {
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<string, unknown>, _context: ToolExecutionContext): Promise<unknown> {
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<string, unknown>, _context: ToolExecutionContext): Promise<unknown> {
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<string, unknown>, _context: ToolExecutionContext): Promise<unknown> {
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<string, unknown>, _context: ToolExecutionContext): Promise<unknown> {
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(),
];