diff --git a/src/integration/builtin/sandbox.ts b/src/integration/builtin/sandbox.ts index 5cdcd2e..877b50c 100644 --- a/src/integration/builtin/sandbox.ts +++ b/src/integration/builtin/sandbox.ts @@ -681,7 +681,9 @@ export class BrowserToolSet extends SandboxToolSet { private async loadPlaywright(): Promise { try { return await import('playwright'); - } catch { + } catch (error) { + logger.error('Playwright is not installed. Please install it with: npm install playwright'); + logger.debug(`${error}`); throw new Error( 'Playwright is not installed. Please install it with: npm install playwright' ); @@ -1255,30 +1257,1122 @@ export class BrowserToolSet extends SandboxToolSet { } /** - * Create a sandbox toolset - * 创建沙箱工具集 + * Browser ToolSet + * 浏览器沙箱工具集 + * + * Provides browser automation capabilities compatible with Playwright-style APIs. + * Requires optional 'playwright' peer dependency for full functionality. */ -export async function sandboxToolset( - templateName: string, - options?: { - templateType?: TemplateType; +export class AIOToolSet extends SandboxToolSet { + private playwrightAIO: Browser | null = null; + private currentPage: Page | null = null; + private pages: Page[] = []; + + constructor(options: { + templateName: string; config?: Config; sandboxIdleTimeoutSeconds?: number; + }) { + super({ + templateName: options.templateName, + templateType: TemplateType.AIO, + sandboxIdleTimeoutSeconds: options.sandboxIdleTimeoutSeconds, + config: options.config, + }); + + // Initialize tools + this._tools = this._createTools(); } -) { - const client = new SandboxClient(); - const template = await client.getTemplate({ name: templateName }); - const templateType = template.templateType; + /** + * Load Playwright dynamically (optional dependency) + */ + private async loadPlaywright(): Promise { + try { + return await import('playwright'); + } catch (error) { + logger.error('Playwright is not installed. Please install it with: npm install playwright'); + logger.debug(`${error}`); + throw new Error( + 'Playwright is not installed. Please install it with: npm install playwright' + ); + } + } - if (templateType === TemplateType.BROWSER || templateType === TemplateType.AIO) - return new BrowserToolSet({ - templateName, - config: options?.config, - sandboxIdleTimeoutSeconds: options?.sandboxIdleTimeoutSeconds, - }); - else if (templateType === TemplateType.CODE_INTERPRETER) - return new CodeInterpreterToolSet({ + /** + * Ensure Playwright browser is connected + */ + private async ensurePlaywright(): Promise<{ + browser: Browser; + page: Page; + }> { + // Ensure sandbox is running first + const sb = await this.ensureSandbox(); + const browserSandbox = sb as BrowserSandbox; + + // Connect Playwright if not connected + if (!this.playwrightAIO) { + const playwright = await this.loadPlaywright(); + const cdpUrl = browserSandbox.getCdpUrl(); + this.playwrightAIO = await playwright.chromium.connectOverCDP(cdpUrl); + + // Get existing contexts/pages or create new ones + const contexts = this.playwrightAIO.contexts(); + if (contexts.length > 0) { + const existingPages = contexts[0].pages(); + if (existingPages.length > 0) { + this.pages = existingPages; + this.currentPage = existingPages[0]; + } else { + this.currentPage = await contexts[0].newPage(); + this.pages = [this.currentPage]; + } + } else { + throw new Error('No browser context available'); + } + } + + if (!this.currentPage) { + throw new Error('No page available'); + } + + return { + browser: this.playwrightAIO, + page: this.currentPage, + }; + } + + /** + * Close Playwright browser connection + */ + override close() { + if (this.playwrightAIO) { + this.playwrightAIO.close().catch(e => { + logger.debug('Failed to close Playwright browser:', e); + }); + this.playwrightAIO = null; + this.currentPage = null; + this.pages = []; + } + super.close(); + } + + private _createTools(): Tool[] { + return [ + // Health Check + createTool({ + name: 'health', + description: + 'Check the health status of the browser sandbox. Returns status="ok" if the browser is running normally.', + parameters: { type: 'object', properties: {} }, + func: async () => this.checkHealth(), + }), + + // Navigation + createTool({ + name: 'browser_navigate', + description: 'Navigate to the specified URL. This is the first step in browser automation.', + parameters: { + type: 'object', + properties: { + url: { type: 'string', description: 'URL to navigate to' }, + }, + required: ['url'], + }, + func: async (args: unknown) => { + const { url } = args as { url: string }; + return this.browserNavigate(url); + }, + }), + + createTool({ + name: 'browser_navigate_back', + description: + "Go back to the previous page, equivalent to clicking the browser's back button.", + parameters: { type: 'object', properties: {} }, + func: async () => this.browserNavigateBack(), + }), + + // Page Info + createTool({ + name: 'browser_snapshot', + description: + 'Get the HTML snapshot and title of the current page. Useful for analyzing page structure.', + parameters: { type: 'object', properties: {} }, + func: async () => this.browserSnapshot(), + }), + + createTool({ + name: 'browser_take_screenshot', + description: 'Capture a screenshot of the current page, returns base64 encoded image data.', + parameters: { + type: 'object', + properties: { + full_page: { + type: 'boolean', + description: 'Capture full page instead of viewport', + default: false, + }, + type: { + type: 'string', + description: 'Image format (png or jpeg)', + default: 'png', + }, + }, + }, + func: async (args: unknown) => { + const { full_page, type } = args as { + full_page?: boolean; + type?: string; + }; + return this.browserTakeScreenshot(full_page, type); + }, + }), + + // Interaction + createTool({ + name: 'browser_click', + description: + 'Click an element matching the selector on the page. Supports CSS selectors, text selectors, XPath, etc.', + parameters: { + type: 'object', + properties: { + selector: { + type: 'string', + description: 'Element selector', + }, + }, + required: ['selector'], + }, + func: async (args: unknown) => { + const { selector } = args as { selector: string }; + return this.browserClick(selector); + }, + }), + + createTool({ + name: 'browser_fill', + description: 'Fill a form input with a value. Clears existing content first.', + parameters: { + type: 'object', + properties: { + selector: { + type: 'string', + description: 'Input element selector', + }, + value: { + type: 'string', + description: 'Value to fill', + }, + }, + required: ['selector', 'value'], + }, + func: async (args: unknown) => { + const { selector, value } = args as { + selector: string; + value: string; + }; + return this.browserFill(selector, value); + }, + }), + + createTool({ + name: 'browser_type', + description: + 'Type text character by character in an element. Triggers keydown, keypress, keyup events.', + parameters: { + type: 'object', + properties: { + selector: { + type: 'string', + description: 'Input element selector', + }, + text: { + type: 'string', + description: 'Text to type', + }, + }, + required: ['selector', 'text'], + }, + func: async (args: unknown) => { + const { selector, text } = args as { selector: string; text: string }; + return this.browserType(selector, text); + }, + }), + + createTool({ + name: 'browser_hover', + description: + 'Hover the mouse over an element. Commonly used to trigger hover menus or tooltips.', + parameters: { + type: 'object', + properties: { + selector: { + type: 'string', + description: 'Element selector', + }, + }, + required: ['selector'], + }, + func: async (args: unknown) => { + const { selector } = args as { selector: string }; + return this.browserHover(selector); + }, + }), + + // Advanced + createTool({ + name: 'browser_evaluate', + description: 'Execute JavaScript code in the page context and return the result.', + parameters: { + type: 'object', + properties: { + expression: { + type: 'string', + description: 'JavaScript expression to evaluate', + }, + }, + required: ['expression'], + }, + func: async (args: unknown) => { + const { expression } = args as { expression: string }; + return this.browserEvaluate(expression); + }, + }), + + createTool({ + name: 'browser_wait_for', + description: 'Wait for the specified time in milliseconds.', + parameters: { + type: 'object', + properties: { + timeout: { + type: 'number', + description: 'Time to wait in milliseconds', + }, + }, + required: ['timeout'], + }, + func: async (args: unknown) => { + const { timeout } = args as { timeout: number }; + return this.browserWaitFor(timeout); + }, + }), + + // Tab Management + createTool({ + name: 'browser_tabs_list', + description: 'List all open browser tabs.', + parameters: { type: 'object', properties: {} }, + func: async () => this.browserTabsList(), + }), + + createTool({ + name: 'browser_tabs_new', + description: 'Create a new browser tab.', + parameters: { + type: 'object', + properties: { + url: { + type: 'string', + description: 'Initial URL for the new tab', + }, + }, + }, + func: async (args: unknown) => { + const { url } = args as { url?: string }; + return this.browserTabsNew(url); + }, + }), + + createTool({ + name: 'browser_tabs_select', + description: 'Switch to the tab at the specified index.', + parameters: { + type: 'object', + properties: { + index: { + type: 'integer', + description: 'Tab index (starting from 0)', + }, + }, + required: ['index'], + }, + func: async (args: unknown) => { + const { index } = args as { index: number }; + return this.browserTabsSelect(index); + }, + }), + + // Code Execution + createTool({ + name: 'run_code', + description: + 'Execute code in a secure isolated sandbox environment. Supports Python and JavaScript languages. Can specify context_id to execute in an existing context, preserving variable state.', + parameters: { + type: 'object', + properties: { + code: { type: 'string', description: 'Code to execute' }, + language: { + type: 'string', + description: 'Programming language (python or javascript)', + default: 'python', + }, + timeout: { + type: 'integer', + description: 'Execution timeout in seconds', + default: 60, + }, + context_id: { + type: 'string', + description: 'Context ID for stateful execution', + }, + }, + required: ['code'], + }, + func: async (args: unknown) => { + const { code, language, timeout, context_id } = args as { + code: string; + language?: string; + timeout?: number; + context_id?: string; + }; + return this.runCode(code, language, timeout, context_id); + }, + }), + + // Context Management + createTool({ + name: 'list_contexts', + description: + 'List all created execution contexts. Contexts preserve code execution state like variables and imported modules.', + parameters: { type: 'object', properties: {} }, + func: async () => this.listContexts(), + }), + + createTool({ + name: 'create_context', + description: + 'Create a new execution context for stateful code execution. Returns context_id for subsequent run_code calls.', + parameters: { + type: 'object', + properties: { + language: { + type: 'string', + description: 'Programming language', + default: 'python', + }, + cwd: { + type: 'string', + description: 'Working directory', + default: '/home/user', + }, + }, + }, + func: async (args: unknown) => { + const { language, cwd } = args as { language?: string; cwd?: string }; + return this.createContext(language, cwd); + }, + }), + + createTool({ + name: 'delete_context', + description: 'Delete a specific execution context and release related resources.', + parameters: { + type: 'object', + properties: { + context_id: { + type: 'string', + description: 'Context ID to delete', + }, + }, + required: ['context_id'], + }, + func: async (args: unknown) => { + const { context_id } = args as { context_id: string }; + return this.deleteContext(context_id); + }, + }), + + // File Operations + createTool({ + name: 'read_file', + description: 'Read the content of a file at the specified path in the sandbox.', + parameters: { + type: 'object', + properties: { + path: { type: 'string', description: 'File path to read' }, + }, + required: ['path'], + }, + func: async (args: unknown) => { + const { path } = args as { path: string }; + return this.readFile(path); + }, + }), + + createTool({ + name: 'write_file', + description: 'Write content to a file at the specified path in the sandbox.', + parameters: { + type: 'object', + properties: { + path: { type: 'string', description: 'File path to write' }, + content: { type: 'string', description: 'Content to write' }, + mode: { + type: 'string', + description: 'File permission mode', + default: '644', + }, + encoding: { + type: 'string', + description: 'File encoding', + default: 'utf-8', + }, + }, + required: ['path', 'content'], + }, + func: async (args: unknown) => { + const { path, content, mode, encoding } = args as { + path: string; + content: string; + mode?: string; + encoding?: string; + }; + return this.writeFile(path, content, mode, encoding); + }, + }), + + // File System Operations + createTool({ + name: 'file_system_list', + description: + 'List the contents of a directory in the sandbox, including files and subdirectories.', + parameters: { + type: 'object', + properties: { + path: { + type: 'string', + description: 'Directory path', + default: '/', + }, + depth: { + type: 'integer', + description: 'Traversal depth', + }, + }, + }, + func: async (args: unknown) => { + const { path, depth } = args as { path?: string; depth?: number }; + return this.fileSystemList(path, depth); + }, + }), + + createTool({ + name: 'file_system_stat', + description: 'Get detailed status information of a file or directory.', + parameters: { + type: 'object', + properties: { + path: { type: 'string', description: 'Path to stat' }, + }, + required: ['path'], + }, + func: async (args: unknown) => { + const { path } = args as { path: string }; + return this.fileSystemStat(path); + }, + }), + + createTool({ + name: 'file_system_mkdir', + description: 'Create a directory in the sandbox.', + parameters: { + type: 'object', + properties: { + path: { type: 'string', description: 'Directory path to create' }, + parents: { + type: 'boolean', + description: 'Create parent directories', + default: true, + }, + mode: { + type: 'string', + description: 'Directory permission mode', + default: '0755', + }, + }, + required: ['path'], + }, + func: async (args: unknown) => { + const { path, parents, mode } = args as { + path: string; + parents?: boolean; + mode?: string; + }; + return this.fileSystemMkdir(path, parents, mode); + }, + }), + + createTool({ + name: 'file_system_move', + description: 'Move or rename a file/directory.', + parameters: { + type: 'object', + properties: { + source: { type: 'string', description: 'Source path' }, + destination: { type: 'string', description: 'Destination path' }, + }, + required: ['source', 'destination'], + }, + func: async (args: unknown) => { + const { source, destination } = args as { + source: string; + destination: string; + }; + return this.fileSystemMove(source, destination); + }, + }), + + createTool({ + name: 'file_system_remove', + description: 'Delete a file or directory.', + parameters: { + type: 'object', + properties: { + path: { type: 'string', description: 'Path to delete' }, + }, + required: ['path'], + }, + func: async (args: unknown) => { + const { path } = args as { path: string }; + return this.fileSystemRemove(path); + }, + }), + + // Process Management + createTool({ + name: 'process_exec_cmd', + description: + 'Execute a shell command in the sandbox. Suitable for running system tools, installing packages, etc.', + parameters: { + type: 'object', + properties: { + command: { type: 'string', description: 'Command to execute' }, + cwd: { + type: 'string', + description: 'Working directory', + default: '/home/user', + }, + timeout: { + type: 'integer', + description: 'Execution timeout in seconds', + default: 30, + }, + }, + required: ['command'], + }, + func: async (args: unknown) => { + const { command, cwd, timeout } = args as { + command: string; + cwd?: string; + timeout?: number; + }; + return this.processExecCmd(command, cwd, timeout); + }, + }), + + createTool({ + name: 'process_list', + description: 'List all running processes in the sandbox.', + parameters: { type: 'object', properties: {} }, + func: async () => this.processList(), + }), + + createTool({ + name: 'process_kill', + description: 'Terminate a specific process.', + parameters: { + type: 'object', + properties: { + pid: { type: 'string', description: 'Process ID to kill' }, + }, + required: ['pid'], + }, + func: async (args: unknown) => { + const { pid } = args as { pid: string }; + return this.processKill(pid); + }, + }), + ]; + } + + // Tool implementations using Playwright + + checkHealth = async () => { + return this.runInSandbox(async sb => { + const browserSandbox = sb as BrowserSandbox; + return browserSandbox.checkHealth(); + }); + }; + + browserNavigate = async (url: string) => { + try { + const { page } = await this.ensurePlaywright(); + await page.goto(url, { timeout: 30000 }); + return { + url, + success: true, + title: await page.title(), + }; + } catch (error) { + return { + url, + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + }; + + browserNavigateBack = async () => { + try { + const { page } = await this.ensurePlaywright(); + await page.goBack({ timeout: 30000 }); + return { + success: true, + url: page.url(), + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + }; + + browserSnapshot = async () => { + try { + const { page } = await this.ensurePlaywright(); + const [title, content] = await Promise.all([page.title(), page.content()]); + return { + html: content, + title, + url: page.url(), + }; + } catch (error) { + return { + html: '', + title: '', + error: error instanceof Error ? error.message : String(error), + }; + } + }; + + browserTakeScreenshot = async (fullPage?: boolean, type?: string) => { + try { + const { page } = await this.ensurePlaywright(); + const buffer = await page.screenshot({ + fullPage: fullPage ?? false, + type: (type as 'png' | 'jpeg') ?? 'png', + }); + return { + screenshot: buffer.toString('base64'), + format: type ?? 'png', + full_page: fullPage ?? false, + }; + } catch (error) { + return { + screenshot: '', + format: type ?? 'png', + full_page: fullPage ?? false, + error: error instanceof Error ? error.message : String(error), + }; + } + }; + + browserClick = async (selector: string) => { + try { + const { page } = await this.ensurePlaywright(); + await page.click(selector, { timeout: 10000 }); + return { + selector, + success: true, + }; + } catch (error) { + return { + selector, + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + }; + + browserFill = async (selector: string, value: string) => { + try { + const { page } = await this.ensurePlaywright(); + await page.fill(selector, value); + return { + selector, + value, + success: true, + }; + } catch (error) { + return { + selector, + value, + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + }; + + browserType = async (selector: string, text: string) => { + try { + const { page } = await this.ensurePlaywright(); + await page.type(selector, text); + return { + selector, + text, + success: true, + }; + } catch (error) { + return { + selector, + text, + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + }; + + browserHover = async (selector: string) => { + try { + const { page } = await this.ensurePlaywright(); + await page.hover(selector); + return { + selector, + success: true, + }; + } catch (error) { + return { + selector, + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + }; + + browserEvaluate = async (expression: string) => { + try { + const { page } = await this.ensurePlaywright(); + // Create a function from the expression string + const fn = new Function(`return (${expression})`) as () => unknown; + const result = await page.evaluate(fn); + return { + result, + success: true, + }; + } catch (error) { + return { + result: null, + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + }; + + browserWaitFor = async (timeout: number) => { + try { + const { page } = await this.ensurePlaywright(); + await page.waitForTimeout(timeout); + return { success: true, waited_ms: timeout }; + } catch (error) { + return { + success: false, + waited_ms: 0, + error: error instanceof Error ? error.message : String(error), + }; + } + }; + + browserTabsList = async () => { + try { + await this.ensurePlaywright(); + return { + tabs: this.pages.map((p, i) => ({ + index: i, + url: p.url(), + active: p === this.currentPage, + })), + count: this.pages.length, + }; + } catch (error) { + return { + tabs: [], + count: 0, + error: error instanceof Error ? error.message : String(error), + }; + } + }; + + browserTabsNew = async (url?: string) => { + try { + const { browser } = await this.ensurePlaywright(); + const contexts = browser.contexts(); + if (contexts.length === 0) { + throw new Error('No browser context available'); + } + const newPage = await contexts[0].newPage(); + this.pages.push(newPage); + this.currentPage = newPage; + + if (url) { + await newPage.goto(url, { timeout: 30000 }); + } + + return { + success: true, + index: this.pages.length - 1, + url: url ?? '', + }; + } catch (error) { + return { + success: false, + url: url ?? '', + error: error instanceof Error ? error.message : String(error), + }; + } + }; + + browserTabsSelect = async (index: number) => { + try { + await this.ensurePlaywright(); + if (index < 0 || index >= this.pages.length) { + throw new Error(`Invalid tab index: ${index}`); + } + this.currentPage = this.pages[index]; + return { + success: true, + index, + url: this.currentPage.url(), + }; + } catch (error) { + return { + success: false, + index, + error: error instanceof Error ? error.message : String(error), + }; + } + }; + + runCode = async (code: string, language?: string, timeout?: number, contextId?: string) => { + return this.runInSandbox(async sb => { + const ciSandbox = sb as CodeInterpreterSandbox; + const lang = language === 'javascript' ? CodeLanguage.JAVASCRIPT : CodeLanguage.PYTHON; + + if (contextId) { + const result = await ciSandbox.context.execute({ + code, + contextId, + language: lang, + timeout: timeout ?? 60, + }); + return { + stdout: result?.stdout || '', + stderr: result?.stderr || '', + exit_code: result?.exitCode || 0, + result, + }; + } + + // Create temporary context + const ctx = await ciSandbox.context.create({ language: lang }); + try { + const result = await ctx.execute({ + code, + timeout: timeout ?? 60, + }); + return { + stdout: result?.stdout || '', + stderr: result?.stderr || '', + exit_code: result?.exitCode || 0, + result, + }; + } finally { + try { + await ctx.delete(); + } catch { + // Ignore cleanup errors + } + } + }); + }; + + listContexts = async () => { + return this.runInSandbox(async sb => { + const ciSandbox = sb as CodeInterpreterSandbox; + const contexts = await ciSandbox.context.list(); + return { contexts }; + }); + }; + + createContext = async (language?: string, cwd?: string) => { + return this.runInSandbox(async sb => { + const ciSandbox = sb as CodeInterpreterSandbox; + const lang = language === 'javascript' ? CodeLanguage.JAVASCRIPT : CodeLanguage.PYTHON; + const ctx = await ciSandbox.context.create({ + language: lang, + cwd: cwd ?? '/home/user', + }); + return { + context_id: ctx.contextId, + language: lang, + cwd: cwd ?? '/home/user', + }; + }); + }; + + deleteContext = async (contextId: string) => { + return this.runInSandbox(async sb => { + const ciSandbox = sb as CodeInterpreterSandbox; + const result = await ciSandbox.context.delete({ contextId }); + return { success: true, result }; + }); + }; + + readFile = async (path: string) => { + return this.runInSandbox(async sb => { + const ciSandbox = sb as CodeInterpreterSandbox; + const content = await ciSandbox.file.read({ path }); + return { path, content }; + }); + }; + + writeFile = async (path: string, content: string, mode?: string, encoding?: string) => { + return this.runInSandbox(async sb => { + const ciSandbox = sb as CodeInterpreterSandbox; + const result = await ciSandbox.file.write({ + path, + content, + mode: mode ?? '644', + encoding: encoding ?? 'utf-8', + }); + return { path, success: true, result }; + }); + }; + + fileSystemList = async (path?: string, depth?: number) => { + return this.runInSandbox(async sb => { + const ciSandbox = sb as CodeInterpreterSandbox; + const entries = await ciSandbox.fileSystem.list({ + path: path ?? '/', + depth, + }); + return { path: path ?? '/', entries }; + }); + }; + + fileSystemStat = async (path: string) => { + return this.runInSandbox(async sb => { + const ciSandbox = sb as CodeInterpreterSandbox; + const stat = await ciSandbox.fileSystem.stat({ path }); + return { path, stat }; + }); + }; + + fileSystemMkdir = async (path: string, parents?: boolean, mode?: string) => { + return this.runInSandbox(async sb => { + const ciSandbox = sb as CodeInterpreterSandbox; + const result = await ciSandbox.fileSystem.mkdir({ + path, + parents: parents ?? true, + mode: mode ?? '0755', + }); + return { path, success: true, result }; + }); + }; + + fileSystemMove = async (source: string, destination: string) => { + return this.runInSandbox(async sb => { + const ciSandbox = sb as CodeInterpreterSandbox; + const result = await ciSandbox.fileSystem.move({ source, destination }); + return { source, destination, success: true, result }; + }); + }; + + fileSystemRemove = async (path: string) => { + return this.runInSandbox(async sb => { + const ciSandbox = sb as CodeInterpreterSandbox; + const result = await ciSandbox.fileSystem.remove({ path }); + return { path, success: true, result }; + }); + }; + + processExecCmd = async (command: string, cwd?: string, timeout?: number) => { + return this.runInSandbox(async sb => { + const ciSandbox = sb as CodeInterpreterSandbox; + const result = await ciSandbox.process.cmd({ + command, + cwd: cwd ?? '/home/user', + timeout: timeout ?? 30, + }); + return { + command, + stdout: result?.stdout || '', + stderr: result?.stderr || '', + exit_code: result?.exitCode || 0, + result, + }; + }); + }; + + processList = () => { + return this.runInSandbox(async sb => { + const ciSandbox = sb as CodeInterpreterSandbox; + const processes = await ciSandbox.process.list(); + return { processes }; + }); + }; + + processKill = async (pid: string) => { + return this.runInSandbox(async sb => { + const ciSandbox = sb as CodeInterpreterSandbox; + const result = await ciSandbox.process.kill({ pid }); + return { pid, success: true, result }; + }); + }; +} + +/** + * Create a sandbox toolset + * 创建沙箱工具集 + */ +export async function sandboxToolset( + templateName: string, + options?: { + config?: Config; + sandboxIdleTimeoutSeconds?: number; + } +) { + const client = new SandboxClient(); + const template = await client.getTemplate({ name: templateName }); + + const templateType = template.templateType; + + if (templateType === TemplateType.BROWSER) + return new BrowserToolSet({ + templateName, + config: options?.config, + sandboxIdleTimeoutSeconds: options?.sandboxIdleTimeoutSeconds, + }); + else if (templateType === TemplateType.CODE_INTERPRETER) + return new CodeInterpreterToolSet({ + templateName, + config: options?.config, + sandboxIdleTimeoutSeconds: options?.sandboxIdleTimeoutSeconds, + }); + else if (templateType === TemplateType.AIO) + return new AIOToolSet({ templateName, config: options?.config, sandboxIdleTimeoutSeconds: options?.sandboxIdleTimeoutSeconds, diff --git a/src/integration/mastra/index.ts b/src/integration/mastra/index.ts index 4a905ea..7e7d72b 100644 --- a/src/integration/mastra/index.ts +++ b/src/integration/mastra/index.ts @@ -11,7 +11,7 @@ import '@/utils/version-check'; -import { TemplateType } from '@/sandbox'; +export { TemplateType } from '@/sandbox'; import type { Config } from '@/utils/config'; import { logger } from '@/utils/log'; import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; @@ -201,15 +201,13 @@ export async function toolset(params: { name: string; config?: Config }): Promis */ export async function sandbox(params: { templateName: string; - templateType?: TemplateType; sandboxIdleTimeoutSeconds?: number; config?: Config; }): Promise { - const { templateName, templateType, sandboxIdleTimeoutSeconds, config } = params; + const { templateName, sandboxIdleTimeoutSeconds, config } = params; // Use builtin sandboxToolset const toolsetInstance = await sandboxToolset(templateName, { - templateType, sandboxIdleTimeoutSeconds, config, }); @@ -218,63 +216,63 @@ export async function sandbox(params: { return convertToolSetToMastra(toolsetInstance); } -/** - * Create Mastra-compatible code interpreter tools - * 创建 Mastra 兼容的代码解释器工具 - * - * Shorthand for sandbox() with CODE_INTERPRETER type. - * - * @example - * ```typescript - * const tools = await codeInterpreter({ - * templateName: 'my-template', - * }); - * - * const agent = new Agent({ - * tools, - * model: await model({ name: 'qwen-max' }), - * }); - * ``` - */ -export async function codeInterpreter(params: { - templateName: string; - sandboxIdleTimeoutSeconds?: number; - config?: Config; -}): Promise { - return sandbox({ - ...params, - templateType: TemplateType.CODE_INTERPRETER, - }); -} +// /** +// * Create Mastra-compatible code interpreter tools +// * 创建 Mastra 兼容的代码解释器工具 +// * +// * Shorthand for sandbox() with CODE_INTERPRETER type. +// * +// * @example +// * ```typescript +// * const tools = await codeInterpreter({ +// * templateName: 'my-template', +// * }); +// * +// * const agent = new Agent({ +// * tools, +// * model: await model({ name: 'qwen-max' }), +// * }); +// * ``` +// */ +// export async function codeInterpreter(params: { +// templateName: string; +// sandboxIdleTimeoutSeconds?: number; +// config?: Config; +// }): Promise { +// return sandbox({ +// ...params, +// templateType: TemplateType.CODE_INTERPRETER, +// }); +// } -/** - * Create Mastra-compatible browser automation tools - * 创建 Mastra 兼容的浏览器自动化工具 - * - * Shorthand for sandbox() with BROWSER type. - * - * @example - * ```typescript - * const tools = await browser({ - * templateName: 'my-browser-template', - * }); - * - * const agent = new Agent({ - * tools, - * model: await model({ name: 'qwen-max' }), - * }); - * ``` - */ -export async function browser(params: { - templateName: string; - sandboxIdleTimeoutSeconds?: number; - config?: Config; -}): Promise { - return sandbox({ - ...params, - templateType: TemplateType.BROWSER, - }); -} +// /** +// * Create Mastra-compatible browser automation tools +// * 创建 Mastra 兼容的浏览器自动化工具 +// * +// * Shorthand for sandbox() with BROWSER type. +// * +// * @example +// * ```typescript +// * const tools = await browser({ +// * templateName: 'my-browser-template', +// * }); +// * +// * const agent = new Agent({ +// * tools, +// * model: await model({ name: 'qwen-max' }), +// * }); +// * ``` +// */ +// export async function browser(params: { +// templateName: string; +// sandboxIdleTimeoutSeconds?: number; +// config?: Config; +// }): Promise { +// return sandbox({ +// ...params, +// templateType: TemplateType.BROWSER, +// }); +// } // Export converter for event conversion export { MastraConverter, type AgentEventItem } from './converter'; diff --git a/src/utils/version-check.ts b/src/utils/version-check.ts index 923f55a..26ed373 100644 --- a/src/utils/version-check.ts +++ b/src/utils/version-check.ts @@ -9,6 +9,13 @@ if ( ) { (globalThis as any)._AGENTRUN_VERSION_WARNING_SHOWN = true; logger.warn( - `当前您正在使用 AgentRun Node.js SDK 版本 ${VERSION}。早期版本通常包含许多新功能,这些功能 可能引入不兼容的变更 。为避免潜在问题,我们强烈建议 将依赖锁定为此版本 。\nYou are currently using AgentRun Node.js SDK version ${VERSION}. Early versions often include many new features, which may introduce breaking changes. To avoid potential issues, we strongly recommend pinning the dependency to this version.\n npm install "@agentrun/sdk@${VERSION}"\n bun add "@agentrun/sdk@${VERSION}"\n\n增加 DISABLE_BREAKING_CHANGES_WARNING=1 到您的环境变量以关闭此警告。\nAdd DISABLE_BREAKING_CHANGES_WARNING=1 to your environment variables to disable this warning.\n\nReleases: https://github.com/Serverless-Devs/agentrun-sdk-nodejs/releases` + `当前您正在使用 AgentRun Python SDK 版本 ${VERSION}。早期版本通常包含许多新功能,这些功能\x1b[1;33m 可能引入不兼容的变更 \x1b[0m。为避免潜在问题,我们强烈建议\x1b[1;32m 将依赖锁定为此版本 \x1b[0m。 +You are currently using AgentRun Python SDK version ${VERSION}. Early versions often include many new features, which\x1b[1;33m may introduce breaking changes\x1b[0m. To avoid potential issues, we strongly recommend \x1b[1;32mpinning the dependency to this version\x1b[0m. +\x1b[2;3m pip install 'agentrun-sdk==${VERSION}' \x1b[0m + +增加\x1b[2;3m DISABLE_BREAKING_CHANGES_WARNING=1 \x1b[0m到您的环境变量以关闭此警告。 +Add\x1b[2;3m DISABLE_BREAKING_CHANGES_WARNING=1 \x1b[0mto your environment variables to disable this warning. + +Releases:\x1b[2;3m https://github.com/Serverless-Devs/agentrun-sdk-python/releases\x1b[0m` ); } diff --git a/tests/unittests/integration/mastra.test.ts b/tests/unittests/integration/mastra.test.ts index f0ded95..4334c57 100644 --- a/tests/unittests/integration/mastra.test.ts +++ b/tests/unittests/integration/mastra.test.ts @@ -7,19 +7,10 @@ * This test suite validates the new functional API for Mastra integration. */ -import { - model, - toolset, - sandbox, - codeInterpreter, - browser, - createMastraTool, -} from '@/integration/mastra'; +import { createMastraTool, model, sandbox, toolset } from '@/integration/mastra'; import { TemplateType } from '@/sandbox'; import { Config } from '@/utils/config'; -import type { LanguageModelV3 } from '@ai-sdk/provider'; import { z } from 'zod'; -import type { ToolsInput } from '@mastra/core/agent'; // Mock external dependencies jest.mock('@/integration/builtin'); @@ -261,11 +252,10 @@ describe('Mastra Integration', () => { await sandbox({ templateName: 'my-template', - templateType: TemplateType.CODE_INTERPRETER, }); expect(builtin.sandboxToolset).toHaveBeenCalledWith('my-template', { - templateType: TemplateType.CODE_INTERPRETER, + // templateType: TemplateType.CODE_INTERPRETER, sandboxIdleTimeoutSeconds: undefined, config: undefined, }); @@ -280,11 +270,10 @@ describe('Mastra Integration', () => { await sandbox({ templateName: 'browser-template', - templateType: TemplateType.BROWSER, }); expect(builtin.sandboxToolset).toHaveBeenCalledWith('browser-template', { - templateType: TemplateType.BROWSER, + // templateType: TemplateType.BROWSER, sandboxIdleTimeoutSeconds: undefined, config: undefined, }); @@ -329,113 +318,111 @@ describe('Mastra Integration', () => { }); }); - describe('codeInterpreter()', () => { - it('should create CODE_INTERPRETER sandbox tools', async () => { - const mockSandboxToolSet = { - tools: jest.fn().mockReturnValue([]), - }; - - (builtin.sandboxToolset as jest.Mock).mockResolvedValue(mockSandboxToolSet); - - await codeInterpreter({ - templateName: 'code-template', - }); - - expect(builtin.sandboxToolset).toHaveBeenCalledWith('code-template', { - templateType: TemplateType.CODE_INTERPRETER, - sandboxIdleTimeoutSeconds: undefined, - config: undefined, - }); - }); - - it('should be shorthand for sandbox()', async () => { - const mockSandboxToolSet = { - tools: jest.fn().mockReturnValue([]), - }; - - (builtin.sandboxToolset as jest.Mock).mockResolvedValue(mockSandboxToolSet); - - await codeInterpreter({ - templateName: 'test-template', - sandboxIdleTimeoutSeconds: 300, - config: mockConfig, - }); - - await sandbox({ - templateName: 'test-template', - templateType: TemplateType.CODE_INTERPRETER, - sandboxIdleTimeoutSeconds: 300, - config: mockConfig, - }); - - // Should call with same parameters - expect(builtin.sandboxToolset).toHaveBeenCalledTimes(2); - expect(builtin.sandboxToolset).toHaveBeenNthCalledWith(1, 'test-template', { - templateType: TemplateType.CODE_INTERPRETER, - sandboxIdleTimeoutSeconds: 300, - config: mockConfig, - }); - expect(builtin.sandboxToolset).toHaveBeenNthCalledWith(2, 'test-template', { - templateType: TemplateType.CODE_INTERPRETER, - sandboxIdleTimeoutSeconds: 300, - config: mockConfig, - }); - }); - }); - - describe('browser()', () => { - it('should create BROWSER sandbox tools', async () => { - const mockSandboxToolSet = { - tools: jest.fn().mockReturnValue([]), - }; - - (builtin.sandboxToolset as jest.Mock).mockResolvedValue(mockSandboxToolSet); - - await browser({ - templateName: 'browser-template', - }); - - expect(builtin.sandboxToolset).toHaveBeenCalledWith('browser-template', { - templateType: TemplateType.BROWSER, - sandboxIdleTimeoutSeconds: undefined, - config: undefined, - }); - }); - - it('should be shorthand for sandbox()', async () => { - const mockSandboxToolSet = { - tools: jest.fn().mockReturnValue([]), - }; - - (builtin.sandboxToolset as jest.Mock).mockResolvedValue(mockSandboxToolSet); - - await browser({ - templateName: 'test-browser', - sandboxIdleTimeoutSeconds: 300, - config: mockConfig, - }); - - await sandbox({ - templateName: 'test-browser', - templateType: TemplateType.BROWSER, - sandboxIdleTimeoutSeconds: 300, - config: mockConfig, - }); - - // Should call with same parameters - expect(builtin.sandboxToolset).toHaveBeenCalledTimes(2); - expect(builtin.sandboxToolset).toHaveBeenNthCalledWith(1, 'test-browser', { - templateType: TemplateType.BROWSER, - sandboxIdleTimeoutSeconds: 300, - config: mockConfig, - }); - expect(builtin.sandboxToolset).toHaveBeenNthCalledWith(2, 'test-browser', { - templateType: TemplateType.BROWSER, - sandboxIdleTimeoutSeconds: 300, - config: mockConfig, - }); - }); - }); + // describe('codeInterpreter()', () => { + // it('should create CODE_INTERPRETER sandbox tools', async () => { + // const mockSandboxToolSet = { + // tools: jest.fn().mockReturnValue([]), + // }; + + // (builtin.sandboxToolset as jest.Mock).mockResolvedValue(mockSandboxToolSet); + + // await codeInterpreter({ + // templateName: 'code-template', + // }); + + // expect(builtin.sandboxToolset).toHaveBeenCalledWith('code-template', { + // templateType: TemplateType.CODE_INTERPRETER, + // sandboxIdleTimeoutSeconds: undefined, + // config: undefined, + // }); + // }); + + // it('should be shorthand for sandbox()', async () => { + // const mockSandboxToolSet = { + // tools: jest.fn().mockReturnValue([]), + // }; + + // (builtin.sandboxToolset as jest.Mock).mockResolvedValue(mockSandboxToolSet); + + // await codeInterpreter({ + // templateName: 'test-template', + // sandboxIdleTimeoutSeconds: 300, + // config: mockConfig, + // }); + + // await sandbox({ + // templateName: 'test-template', + // sandboxIdleTimeoutSeconds: 300, + // config: mockConfig, + // }); + + // // Should call with same parameters + // expect(builtin.sandboxToolset).toHaveBeenCalledTimes(2); + // expect(builtin.sandboxToolset).toHaveBeenNthCalledWith(1, 'test-template', { + // templateType: TemplateType.CODE_INTERPRETER, + // sandboxIdleTimeoutSeconds: 300, + // config: mockConfig, + // }); + // expect(builtin.sandboxToolset).toHaveBeenNthCalledWith(2, 'test-template', { + // templateType: TemplateType.CODE_INTERPRETER, + // sandboxIdleTimeoutSeconds: 300, + // config: mockConfig, + // }); + // }); + // }); + + // describe('browser()', () => { + // it('should create BROWSER sandbox tools', async () => { + // const mockSandboxToolSet = { + // tools: jest.fn().mockReturnValue([]), + // }; + + // (builtin.sandboxToolset as jest.Mock).mockResolvedValue(mockSandboxToolSet); + + // await browser({ + // templateName: 'browser-template', + // }); + + // expect(builtin.sandboxToolset).toHaveBeenCalledWith('browser-template', { + // templateType: TemplateType.BROWSER, + // sandboxIdleTimeoutSeconds: undefined, + // config: undefined, + // }); + // }); + + // it('should be shorthand for sandbox()', async () => { + // const mockSandboxToolSet = { + // tools: jest.fn().mockReturnValue([]), + // }; + + // (builtin.sandboxToolset as jest.Mock).mockResolvedValue(mockSandboxToolSet); + + // await browser({ + // templateName: 'test-browser', + // sandboxIdleTimeoutSeconds: 300, + // config: mockConfig, + // }); + + // await sandbox({ + // templateName: 'test-browser', + // sandboxIdleTimeoutSeconds: 300, + // config: mockConfig, + // }); + + // // Should call with same parameters + // expect(builtin.sandboxToolset).toHaveBeenCalledTimes(2); + // expect(builtin.sandboxToolset).toHaveBeenNthCalledWith(1, 'test-browser', { + // templateType: TemplateType.BROWSER, + // sandboxIdleTimeoutSeconds: 300, + // config: mockConfig, + // }); + // expect(builtin.sandboxToolset).toHaveBeenNthCalledWith(2, 'test-browser', { + // templateType: TemplateType.BROWSER, + // sandboxIdleTimeoutSeconds: 300, + // config: mockConfig, + // }); + // }); + // }); describe('createMastraTool()', () => { it('should create Mastra tool from definition', async () => { @@ -519,20 +506,20 @@ describe('Mastra Integration', () => { // Create all components const llm = await model({ name: 'qwen-max' }); const tools = await toolset({ name: 'weather-toolset' }); - const sandboxTools = await codeInterpreter({ - templateName: 'python-sandbox', - }); + // const sandboxTools = await codeInterpreter({ + // templateName: 'python-sandbox', + // }); // Verify all components are created expect(llm).toBeDefined(); expect(tools).toBeDefined(); expect(tools.weatherTool).toBeDefined(); - expect(sandboxTools).toBeDefined(); - expect(sandboxTools.executeCode).toBeDefined(); + // expect(sandboxTools).toBeDefined(); + // expect(sandboxTools.executeCode).toBeDefined(); // Verify component structure expect(typeof tools).toBe('object'); - expect(typeof sandboxTools).toBe('object'); + // expect(typeof sandboxTools).toBe('object'); }); }); }); diff --git a/tsup.config.ts b/tsup.config.ts index d3e4081..39994c8 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -74,6 +74,7 @@ export default defineConfig({ 'zod', '@mastra/core', 'chromium-bidi', + 'playwright', ], treeshake: true, minify: false,