diff --git a/app.py b/app.py index bd35fdb..df767ff 100644 --- a/app.py +++ b/app.py @@ -7,7 +7,7 @@ import asyncio from typing import Optional from fastapi import FastAPI, HTTPException from pydantic import BaseModel, Field -from browser_use import Agent, Browser, BrowserProfile, ChatOpenAI +from browser_use import Agent, Browser, BrowserProfile, ChatOpenAI, ActionResult from browser_use.agent.views import AgentHistoryList import uvicorn from fastapi.responses import HTMLResponse @@ -27,6 +27,99 @@ import logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) +# Создаём Tools с кастомными действиями +try: + from browser_use import Tools, BrowserSession + + custom_tools = Tools() + + @custom_tools.action(description='Paste text from clipboard into the currently focused element using Ctrl+V. Use this instead of regular typing when a website requires a paste event to trigger special behavior (e.g., URL preview generation, link formatting). Parameters: text - the text to paste, index - optional index of element to click/focus first.') + async def paste_text(text: str, index: int = None, browser_session: BrowserSession = None): + """Вставляет текст через Ctrl+V (эмуляция вставки из буфера обмена)""" + import json as json_util + try: + page = await browser_session.get_current_page() + + # Если указан индекс элемента — кликаем на него для фокуса + if index is not None: + dom_element = await browser_session.get_dom_element_by_index(index) + if dom_element: + await browser_session.click_element_node(dom_element) + await asyncio.sleep(0.3) + + # Экранируем текст для безопасной вставки в JS + safe_text = json_util.dumps(text) + + # Записываем текст в буфер обмена через JavaScript + await page.evaluate(f'''() => {{ + const textArea = document.createElement('textarea'); + textArea.value = {safe_text}; + textArea.style.position = 'fixed'; + textArea.style.left = '-9999px'; + document.body.appendChild(textArea); + textArea.select(); + document.execCommand('copy'); + document.body.removeChild(textArea); + }}''') + + # Также пробуем через Clipboard API + try: + await page.evaluate(f'''async () => {{ + await navigator.clipboard.writeText({safe_text}); + }}''') + except Exception: + pass + + # Нажимаем Ctrl+V для вставки + await page.keyboard.down('Control') + await page.keyboard.press('v') + await page.keyboard.up('Control') + + await asyncio.sleep(0.5) + + return ActionResult( + extracted_content=f"Text pasted successfully: {text[:100]}...", + include_in_memory=True + ) + except Exception as e: + # Fallback: эмулируем paste event через JavaScript + try: + page = await browser_session.get_current_page() + safe_text = json_util.dumps(text) + await page.evaluate(f'''() => {{ + const el = document.activeElement; + if (el) {{ + const clipboardData = new DataTransfer(); + clipboardData.setData('text/plain', {safe_text}); + const pasteEvent = new ClipboardEvent('paste', {{ + bubbles: true, + cancelable: true, + clipboardData: clipboardData + }}); + el.dispatchEvent(pasteEvent); + + if ('value' in el) {{ + el.value = {safe_text}; + el.dispatchEvent(new Event('input', {{ bubbles: true }})); + el.dispatchEvent(new Event('change', {{ bubbles: true }})); + }} else if (el.isContentEditable) {{ + document.execCommand('insertText', false, {safe_text}); + }} + }} + }}''') + return ActionResult( + extracted_content=f"Text pasted via fallback: {text[:100]}...", + include_in_memory=True + ) + except Exception as e2: + return ActionResult(error=f"Failed to paste text: {e2}") + + logger.info("Custom tools (paste_text) registered successfully") +except ImportError as e: + custom_tools = None + logger.warning(f"Could not import Tools/BrowserSession from browser_use: {e}. Custom tools disabled.") + + def patch_chrome_binary(): """Патчит бинарный файл Chrome для удаления строки cdc_, которую ищут антифроды""" possible_paths = [ @@ -224,9 +317,11 @@ async def run_browser_task(task_id: str, task: str, max_steps: int, model: str): existing_prefs['safebrowsing'] = {} existing_prefs['safebrowsing']['enabled'] = False - # Разрешаем cookies для всех сайтов - existing_prefs['profile']['default_content_setting_values'] = existing_prefs.get('profile', {}).get('default_content_setting_values', {}) + # Разрешаем cookies и clipboard для всех сайтов + if 'default_content_setting_values' not in existing_prefs['profile']: + existing_prefs['profile']['default_content_setting_values'] = {} existing_prefs['profile']['default_content_setting_values']['cookies'] = 1 # 1 = Allow + existing_prefs['profile']['default_content_setting_values']['clipboard'] = 1 # 1 = Allow (для paste_text action) # Отключаем Tracking Protection if 'tracking_protection' not in existing_prefs: @@ -413,12 +508,16 @@ async def run_browser_task(task_id: str, task: str, max_steps: int, model: str): logger.info("[startup] Chrome started successfully and ready for connections.") return - # Создание агента - agent = Agent( - task=task, - llm=llm, - browser=browser, - ) + # Создание агента с кастомными действиями (paste_text и др.) + agent_kwargs = { + "task": task, + "llm": llm, + "browser": browser, + } + if custom_tools is not None: + agent_kwargs["tools"] = custom_tools + + agent = Agent(**agent_kwargs) # Выполнение задачи history: AgentHistoryList = await agent.run(max_steps=max_steps)