From 9726ae84f368415a6611db036f1469f4f7894913 Mon Sep 17 00:00:00 2001 From: OhYee Date: Mon, 2 Feb 2026 17:22:35 +0800 Subject: [PATCH 1/2] feat(builtin): add AIOToolSet with Playwright integration and enhance error logging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit introduces a new AIOToolSet class that provides comprehensive browser automation capabilities through Playwright integration. The new toolset supports various browser operations including navigation, interaction, page info, advanced JavaScript evaluation, tab management, and file/code execution. It also includes enhanced error logging for Playwright installation issues and improved type safety across the sandbox integration. The changes also remove the templateType parameter from the sandbox function in mastra integration, update the version check warning to reflect Python SDK, adjust test cases accordingly, and add playwright to the build externals. build: add playwright to externals in tsup config feat(builtin): 添加具有 Playwright 集成的 AIOToolSet 并增强错误日志记录 此提交引入了一个新的 AIOToolSet 类,通过 Playwright 集成为浏览器自动化提供全面的功能。新工具集支持各种浏览器操作,包括导航、交互、页面信息、高级 JavaScript 评估、标签页管理和文件/代码执行。它还包括对 Playwright 安装问题的增强错误日志记录,以及在整个沙箱集成中改进的类型安全性。 更改还从 mastra 集成中的 sandbox 函数中移除了 templateType 参数,更新版本检查警告以反映 Python SDK,相应调整测试用例,并在构建外部依赖中添加 playwright。 build: 在 tsup 配置中将 playwright 添加到外部依赖 Signed-off-by: OhYee Change-Id: I9485e0df19ea33f96792644eb38e2c16af9a31b4 --- src/integration/builtin/sandbox.ts | 1132 +++++++++++++++++++- src/integration/mastra/index.ts | 116 +- src/utils/version-check.ts | 9 +- tests/unittests/integration/mastra.test.ts | 4 - tsup.config.ts | 1 + 5 files changed, 1179 insertions(+), 83 deletions(-) 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..6a47d75 100644 --- a/src/integration/mastra/index.ts +++ b/src/integration/mastra/index.ts @@ -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..3bb16b8 100644 --- a/tests/unittests/integration/mastra.test.ts +++ b/tests/unittests/integration/mastra.test.ts @@ -261,7 +261,6 @@ describe('Mastra Integration', () => { await sandbox({ templateName: 'my-template', - templateType: TemplateType.CODE_INTERPRETER, }); expect(builtin.sandboxToolset).toHaveBeenCalledWith('my-template', { @@ -280,7 +279,6 @@ describe('Mastra Integration', () => { await sandbox({ templateName: 'browser-template', - templateType: TemplateType.BROWSER, }); expect(builtin.sandboxToolset).toHaveBeenCalledWith('browser-template', { @@ -363,7 +361,6 @@ describe('Mastra Integration', () => { await sandbox({ templateName: 'test-template', - templateType: TemplateType.CODE_INTERPRETER, sandboxIdleTimeoutSeconds: 300, config: mockConfig, }); @@ -417,7 +414,6 @@ describe('Mastra Integration', () => { await sandbox({ templateName: 'test-browser', - templateType: TemplateType.BROWSER, sandboxIdleTimeoutSeconds: 300, config: mockConfig, }); 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, From b6df38fad96a0f411d782e3bcbf004485a4df4b2 Mon Sep 17 00:00:00 2001 From: OhYee Date: Mon, 2 Feb 2026 18:04:27 +0800 Subject: [PATCH 2/2] refactor(integration): change TemplateType import to export and remove unused imports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change modifies the TemplateType from an import to an export in the mastra integration file, and removes unused imports and related test code for codeInterpreter and browser functions. BREAKING CHANGE: TemplateType is now exported instead of imported, and codeInterpreter/browser functions are removed from tests --- refactor(integration): 更改TemplateType从导入为导出并移除未使用的导入 此更改将mastra集成文件中的TemplateType从导入改为导出,并删除了codeInterpreter和browser函数的未使用导入和相关测试代码。 重大变更:TemplateType现在是导出而不是导入,且测试中移除了codeInterpreter/browser函数 Change-Id: I8987d7da888f30340d25eedc97db600d3c3496e8 Signed-off-by: OhYee --- src/integration/mastra/index.ts | 2 +- tests/unittests/integration/mastra.test.ts | 237 ++++++++++----------- 2 files changed, 115 insertions(+), 124 deletions(-) diff --git a/src/integration/mastra/index.ts b/src/integration/mastra/index.ts index 6a47d75..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'; diff --git a/tests/unittests/integration/mastra.test.ts b/tests/unittests/integration/mastra.test.ts index 3bb16b8..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'); @@ -264,7 +255,7 @@ describe('Mastra Integration', () => { }); expect(builtin.sandboxToolset).toHaveBeenCalledWith('my-template', { - templateType: TemplateType.CODE_INTERPRETER, + // templateType: TemplateType.CODE_INTERPRETER, sandboxIdleTimeoutSeconds: undefined, config: undefined, }); @@ -282,7 +273,7 @@ describe('Mastra Integration', () => { }); expect(builtin.sandboxToolset).toHaveBeenCalledWith('browser-template', { - templateType: TemplateType.BROWSER, + // templateType: TemplateType.BROWSER, sandboxIdleTimeoutSeconds: undefined, config: undefined, }); @@ -327,111 +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', - 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('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 () => { @@ -515,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'); }); }); });