baseClass.py 67 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048
  1. """
  2. Browser-Use 原生工具适配器
  3. Native Browser-Use Tools Adapter
  4. 直接使用 browser-use 的原生类(BrowserSession, Tools)实现所有浏览器操作工具。
  5. 不依赖 Playwright,完全基于 CDP 协议。
  6. 核心特性:
  7. 1. 浏览器会话持久化 - 只启动一次浏览器
  8. 2. 状态自动保持 - 登录状态、Cookie、LocalStorage 等
  9. 3. 完整的底层访问 - 可以直接使用 CDP 协议
  10. 4. 性能优异 - 避免频繁创建/销毁浏览器实例
  11. 5. 多种浏览器类型 - 支持 local、cloud、container 三种模式
  12. 支持的浏览器类型:
  13. 1. Local (本地浏览器):
  14. - 在本地运行 Chrome
  15. - 支持可视化调试
  16. - 速度最快
  17. - 示例: init_browser_session(browser_type="local")
  18. 2. Cloud (云浏览器):
  19. - 在云端运行
  20. - 不占用本地资源
  21. - 适合生产环境
  22. - 示例: init_browser_session(browser_type="cloud")
  23. 3. Container (容器浏览器):
  24. - 在独立容器中运行
  25. - 隔离性好
  26. - 支持预配置账户
  27. - 示例: init_browser_session(browser_type="container", container_url="https://example.com")
  28. 使用方法:
  29. 1. 在 Agent 初始化时调用 init_browser_session() 并指定 browser_type
  30. 2. 使用各个工具函数执行浏览器操作
  31. 3. 任务结束时调用 cleanup_browser_session()
  32. 文件操作说明:
  33. - 浏览器专用文件目录:.cache/.browser_use_files/ (在当前工作目录下)
  34. 用于存储浏览器会话产生的临时文件(下载、上传、截图等)
  35. - 一般文件操作:请使用 agent.tools.builtin 中的文件工具 (read_file, write_file, edit_file)
  36. 这些工具功能更完善,支持diff预览、智能匹配、分页读取等
  37. """
  38. import sys
  39. import os
  40. import json
  41. import asyncio
  42. import aiohttp
  43. import re
  44. import base64
  45. from urllib.parse import urlparse, parse_qs, unquote
  46. from typing import Optional, List, Dict, Any, Tuple
  47. from pathlib import Path
  48. from langchain_core.runnables import RunnableLambda
  49. from argparse import Namespace # 使用 Namespace 快速构造带属性的对象
  50. from langchain_core.messages import AIMessage
  51. from ....llm.openrouter import openrouter_llm_call
  52. # 将项目根目录添加到 Python 路径
  53. sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
  54. # 导入框架的工具装饰器和结果类
  55. from agent.tools import tool, ToolResult
  56. from agent.tools.builtin.browser.sync_mysql_help import mysql
  57. # 导入 browser-use 的核心类
  58. from browser_use import BrowserSession, BrowserProfile
  59. from browser_use.tools.service import Tools
  60. try:
  61. from browser_use.tools.views import ReadContentAction # type: ignore
  62. except Exception:
  63. from pydantic import BaseModel
  64. class ReadContentAction(BaseModel):
  65. goal: str
  66. source: str = "page"
  67. context: str = ""
  68. from browser_use.agent.views import ActionResult
  69. from browser_use.filesystem.file_system import FileSystem
  70. # ============================================================
  71. # 无需注册的内部辅助函数
  72. # ============================================================
  73. # ============================================================
  74. # 全局浏览器会话管理
  75. # ============================================================
  76. # 全局变量:浏览器会话和工具实例
  77. _browser_session: Optional[BrowserSession] = None
  78. _browser_tools: Optional[Tools] = None
  79. _file_system: Optional[FileSystem] = None
  80. async def create_container(url: str, account_name: str = "liuwenwu") -> Dict[str, Any]:
  81. """
  82. 创建浏览器容器并导航到指定URL
  83. 按照 test.md 的要求:
  84. 1.1 调用接口创建容器
  85. 1.2 调用接口创建窗口并导航到URL
  86. Args:
  87. url: 要导航的URL地址
  88. account_name: 账户名称
  89. Returns:
  90. 包含容器信息的字典:
  91. - success: 是否成功
  92. - container_id: 容器ID
  93. - vnc: VNC访问URL
  94. - cdp: CDP协议URL(用于浏览器连接)
  95. - connection_id: 窗口连接ID
  96. - error: 错误信息(如果失败)
  97. """
  98. result = {
  99. "success": False,
  100. "container_id": None,
  101. "vnc": None,
  102. "cdp": None,
  103. "connection_id": None,
  104. "error": None
  105. }
  106. try:
  107. async with aiohttp.ClientSession() as session:
  108. # 步骤1.1: 创建容器
  109. print("📦 步骤1.1: 创建容器...")
  110. create_url = "http://47.84.182.56:8200/api/v1/container/create"
  111. create_payload = {
  112. "auto_remove": True,
  113. "need_port_binding": True,
  114. "max_lifetime_seconds": 900
  115. }
  116. async with session.post(create_url, json=create_payload) as resp:
  117. if resp.status != 200:
  118. raise RuntimeError(f"创建容器失败: HTTP {resp.status}")
  119. create_result = await resp.json()
  120. if create_result.get("code") != 0:
  121. raise RuntimeError(f"创建容器失败: {create_result.get('msg')}")
  122. data = create_result.get("data", {})
  123. result["container_id"] = data.get("container_id")
  124. result["vnc"] = data.get("vnc")
  125. result["cdp"] = data.get("cdp")
  126. print(f"✅ 容器创建成功")
  127. print(f" Container ID: {result['container_id']}")
  128. print(f" VNC: {result['vnc']}")
  129. print(f" CDP: {result['cdp']}")
  130. # 等待容器内的浏览器启动
  131. print(f"\n⏳ 等待容器内浏览器启动...")
  132. await asyncio.sleep(5)
  133. # 步骤1.2: 创建页面并导航
  134. print(f"\n📱 步骤1.2: 创建页面并导航到 {url}...")
  135. page_create_url = "http://47.84.182.56:8200/api/v1/browser/page/create"
  136. page_payload = {
  137. "container_id": result["container_id"],
  138. "url": url,
  139. "account_name": account_name,
  140. "need_wait": True,
  141. "timeout": 30
  142. }
  143. # 重试机制:最多尝试3次
  144. max_retries = 3
  145. page_created = False
  146. last_error = None
  147. for attempt in range(max_retries):
  148. try:
  149. if attempt > 0:
  150. print(f" 重试 {attempt + 1}/{max_retries}...")
  151. await asyncio.sleep(3) # 重试前等待
  152. async with session.post(page_create_url, json=page_payload, timeout=aiohttp.ClientTimeout(total=60)) as resp:
  153. if resp.status != 200:
  154. response_text = await resp.text()
  155. last_error = f"HTTP {resp.status}: {response_text[:200]}"
  156. continue
  157. page_result = await resp.json()
  158. if page_result.get("code") != 0:
  159. last_error = f"{page_result.get('msg')}"
  160. continue
  161. page_data = page_result.get("data", {})
  162. result["connection_id"] = page_data.get("connection_id")
  163. result["success"] = True
  164. page_created = True
  165. print(f"✅ 页面创建成功")
  166. print(f" Connection ID: {result['connection_id']}")
  167. break
  168. except asyncio.TimeoutError:
  169. last_error = "请求超时"
  170. continue
  171. except aiohttp.ClientError as e:
  172. last_error = f"网络错误: {str(e)}"
  173. continue
  174. except Exception as e:
  175. last_error = f"未知错误: {str(e)}"
  176. continue
  177. if not page_created:
  178. raise RuntimeError(f"创建页面失败(尝试{max_retries}次后): {last_error}")
  179. except Exception as e:
  180. result["error"] = str(e)
  181. print(f"❌ 错误: {str(e)}")
  182. return result
  183. async def init_browser_session(
  184. browser_type: str = "local",
  185. headless: bool = False,
  186. url: Optional[str] = None,
  187. profile_name: str = "default",
  188. user_data_dir: Optional[str] = None,
  189. browser_profile: Optional[BrowserProfile] = None,
  190. **kwargs
  191. ) -> tuple[BrowserSession, Tools]:
  192. """
  193. 初始化全局浏览器会话 - 支持三种浏览器类型
  194. Args:
  195. browser_type: 浏览器类型 ("local", "cloud", "container")
  196. headless: 是否无头模式
  197. url: 初始访问URL(可选)
  198. - local/cloud: 初始化后会自动导航到此URL
  199. - container: 必需,容器启动时访问的URL
  200. profile_name: 配置文件/账户名称(默认 "default")
  201. - local: 用于创建用户数据目录路径
  202. - cloud: 云浏览器配置ID
  203. - container: 容器账户名称
  204. user_data_dir: 用户数据目录(仅 local 模式,高级用法)
  205. 如果提供则覆盖 profile_name 生成的路径
  206. browser_profile: BrowserProfile 对象(通用,高级用法)
  207. 用于预设 cookies 等
  208. **kwargs: 其他 BrowserSession 参数
  209. Returns:
  210. (BrowserSession, Tools) 元组
  211. Examples:
  212. # 本地浏览器
  213. browser, tools = await init_browser_session(
  214. browser_type="local",
  215. url="https://www.baidu.com" # 可选
  216. )
  217. # 云浏览器
  218. browser, tools = await init_browser_session(
  219. browser_type="cloud",
  220. profile_name="my_cloud_profile" # 可选
  221. )
  222. # 容器浏览器
  223. browser, tools = await init_browser_session(
  224. browser_type="container",
  225. url="https://www.xiaohongshu.com", # 必需
  226. profile_name="my_account" # 可选
  227. )
  228. """
  229. global _browser_session, _browser_tools, _file_system
  230. if _browser_session is not None:
  231. return _browser_session, _browser_tools
  232. # 验证 browser_type
  233. valid_types = ["local", "cloud", "container"]
  234. if browser_type not in valid_types:
  235. raise ValueError(f"无效的 browser_type: {browser_type},必须是 {valid_types} 之一")
  236. # 创建浏览器会话参数
  237. session_params = {
  238. "headless": headless,
  239. }
  240. # === Container 模式 ===
  241. if browser_type == "container":
  242. print("🐳 使用容器浏览器模式")
  243. # container 模式必须提供 URL
  244. if not url:
  245. url = "about:blank" # 使用默认空白页
  246. print("⚠️ 未提供 url 参数,使用默认空白页")
  247. # 创建容器并获取 CDP URL
  248. print(f"📦 正在创建容器...")
  249. container_info = await create_container(
  250. url=url,
  251. account_name=profile_name
  252. )
  253. if not container_info["success"]:
  254. raise RuntimeError(f"容器创建失败: {container_info['error']}")
  255. cdp_url = container_info["cdp"]
  256. print(f"✅ 容器创建成功")
  257. print(f" CDP URL: {cdp_url}")
  258. print(f" Container ID: {container_info['container_id']}")
  259. print(f" Connection ID: {container_info.get('connection_id')}")
  260. # 使用容器的 CDP URL 连接
  261. session_params["cdp_url"] = cdp_url
  262. # 等待容器完全启动
  263. print("⏳ 等待容器浏览器启动...")
  264. await asyncio.sleep(3)
  265. # === Cloud 模式 ===
  266. elif browser_type == "cloud":
  267. print("🌐 使用云浏览器模式")
  268. session_params["use_cloud"] = True
  269. # profile_name 作为云配置ID
  270. if profile_name and profile_name != "default":
  271. session_params["cloud_profile_id"] = profile_name
  272. # === Local 模式 ===
  273. else: # local
  274. print("💻 使用本地浏览器模式")
  275. session_params["is_local"] = True
  276. # 设置用户数据目录(持久化登录状态)
  277. if user_data_dir is None and profile_name:
  278. user_data_dir = str(Path.home() / ".browser_use" / "profiles" / profile_name)
  279. Path(user_data_dir).mkdir(parents=True, exist_ok=True)
  280. # macOS 上显式指定 Chrome 路径
  281. import platform
  282. if platform.system() == "Darwin": # macOS
  283. chrome_path = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
  284. if Path(chrome_path).exists():
  285. session_params["executable_path"] = chrome_path
  286. # 只在有值时才添加 user_data_dir
  287. if user_data_dir:
  288. session_params["user_data_dir"] = user_data_dir
  289. # 只在有值时才添加 browser_profile (适用于所有模式)
  290. if browser_profile:
  291. session_params["browser_profile"] = browser_profile
  292. # 合并其他参数
  293. session_params.update(kwargs)
  294. # 创建浏览器会话
  295. _browser_session = BrowserSession(**session_params)
  296. # 启动浏览器
  297. await _browser_session.start()
  298. # 创建工具实例
  299. _browser_tools = Tools()
  300. # 创建文件系统实例(用于浏览器会话产生的文件)
  301. # 注意:这个目录仅用于浏览器操作相关的临时文件(下载、上传、截图等)
  302. # 对于一般文件读写操作,请使用 agent.tools.builtin 中的文件工具
  303. base_dir = Path.cwd() / ".cache/.browser_use_files"
  304. base_dir.mkdir(parents=True, exist_ok=True)
  305. _file_system = FileSystem(base_dir=str(base_dir))
  306. print("✅ 浏览器会话初始化成功")
  307. # 如果是 local 或 cloud 模式且提供了 URL,导航到该 URL
  308. if browser_type in ["local", "cloud"] and url:
  309. print(f"🔗 导航到: {url}")
  310. await _browser_tools.navigate(url=url, browser_session=_browser_session)
  311. return _browser_session, _browser_tools
  312. async def get_browser_session() -> tuple[BrowserSession, Tools]:
  313. """
  314. 获取当前浏览器会话,如果不存在则自动创建
  315. Returns:
  316. (BrowserSession, Tools) 元组
  317. """
  318. global _browser_session, _browser_tools
  319. if _browser_session is None:
  320. await init_browser_session()
  321. return _browser_session, _browser_tools
  322. async def cleanup_browser_session():
  323. """
  324. 清理浏览器会话
  325. 优雅地停止浏览器但保留会话状态
  326. """
  327. global _browser_session, _browser_tools, _file_system
  328. if _browser_session is not None:
  329. await _browser_session.stop()
  330. _browser_session = None
  331. _browser_tools = None
  332. _file_system = None
  333. async def kill_browser_session():
  334. """
  335. 强制终止浏览器会话
  336. 完全关闭浏览器进程
  337. """
  338. global _browser_session, _browser_tools, _file_system
  339. if _browser_session is not None:
  340. await _browser_session.kill()
  341. _browser_session = None
  342. _browser_tools = None
  343. _file_system = None
  344. # ============================================================
  345. # 辅助函数:ActionResult 转 ToolResult
  346. # ============================================================
  347. def action_result_to_tool_result(result: ActionResult, title: str = None) -> ToolResult:
  348. """
  349. 将 browser-use 的 ActionResult 转换为框架的 ToolResult
  350. Args:
  351. result: browser-use 的 ActionResult
  352. title: 可选的标题(如果不提供则从 result 推断)
  353. Returns:
  354. ToolResult
  355. """
  356. if result.error:
  357. return ToolResult(
  358. title=title or "操作失败",
  359. output="",
  360. error=result.error,
  361. long_term_memory=result.long_term_memory or result.error
  362. )
  363. return ToolResult(
  364. title=title or "操作成功",
  365. output=result.extracted_content or "",
  366. long_term_memory=result.long_term_memory or result.extracted_content or "",
  367. metadata=result.metadata or {}
  368. )
  369. def _cookie_domain_for_type(cookie_type: str, url: str) -> Tuple[str, str]:
  370. if cookie_type:
  371. key = cookie_type.lower()
  372. if key in {"xiaohongshu", "xhs"}:
  373. return ".xiaohongshu.com", "https://www.xiaohongshu.com"
  374. parsed = urlparse(url or "")
  375. domain = parsed.netloc or ""
  376. domain = domain.replace("www.", "")
  377. if domain:
  378. domain = f".{domain}"
  379. base_url = f"{parsed.scheme}://{parsed.netloc}" if parsed.scheme and parsed.netloc else url
  380. return domain, base_url
  381. def _parse_cookie_string(cookie_str: str, domain: str, url: str) -> List[Dict[str, Any]]:
  382. cookies: List[Dict[str, Any]] = []
  383. if not cookie_str:
  384. return cookies
  385. parts = cookie_str.split(";")
  386. for part in parts:
  387. if not part:
  388. continue
  389. if "=" not in part:
  390. continue
  391. name, value = part.split("=", 1)
  392. cookie = {
  393. "name": str(name).strip(),
  394. "value": str(value).strip(),
  395. "domain": domain,
  396. "path": "/",
  397. "expires": -1,
  398. "httpOnly": False,
  399. "secure": True,
  400. "sameSite": "None"
  401. }
  402. if url:
  403. cookie["url"] = url
  404. cookies.append(cookie)
  405. return cookies
  406. def _normalize_cookies(cookie_value: Any, domain: str, url: str) -> List[Dict[str, Any]]:
  407. if cookie_value is None:
  408. return []
  409. if isinstance(cookie_value, list):
  410. return cookie_value
  411. if isinstance(cookie_value, dict):
  412. if "cookies" in cookie_value:
  413. return _normalize_cookies(cookie_value.get("cookies"), domain, url)
  414. if "name" in cookie_value and "value" in cookie_value:
  415. return [cookie_value]
  416. return []
  417. if isinstance(cookie_value, (bytes, bytearray)):
  418. cookie_value = cookie_value.decode("utf-8", errors="ignore")
  419. if isinstance(cookie_value, str):
  420. text = cookie_value.strip()
  421. if not text:
  422. return []
  423. try:
  424. parsed = json.loads(text)
  425. except Exception:
  426. parsed = None
  427. if parsed is not None:
  428. return _normalize_cookies(parsed, domain, url)
  429. return _parse_cookie_string(text, domain, url)
  430. return []
  431. def _extract_cookie_value(row: Optional[Dict[str, Any]]) -> Any:
  432. if not row:
  433. return None
  434. # 优先使用 cookies 字段
  435. if "cookies" in row:
  436. return row["cookies"]
  437. # 兼容其他可能的字段名
  438. for key, value in row.items():
  439. if "cookie" in key.lower():
  440. return value
  441. return None
  442. def _fetch_cookie_row(cookie_type: str) -> Optional[Dict[str, Any]]:
  443. if not cookie_type:
  444. return None
  445. try:
  446. return mysql.fetchone(
  447. "select * from agent_channel_cookies where type=%s limit 1",
  448. (cookie_type,)
  449. )
  450. except Exception:
  451. return None
  452. def _fetch_profile_id(cookie_type: str) -> Optional[str]:
  453. """从数据库获取 cloud_profile_id"""
  454. if not cookie_type:
  455. return None
  456. try:
  457. row = mysql.fetchone(
  458. "select profileId from agent_channel_cookies where type=%s limit 1",
  459. (cookie_type,)
  460. )
  461. if row and "profileId" in row:
  462. return row["profileId"]
  463. return None
  464. except Exception:
  465. return None
  466. # ============================================================
  467. # 需要注册的工具
  468. # ============================================================
  469. # ============================================================
  470. # 导航类工具 (Navigation Tools)
  471. # ============================================================
  472. @tool()
  473. async def browser_navigate_to_url(url: str, new_tab: bool = False) -> ToolResult:
  474. """
  475. 导航到指定的 URL
  476. Navigate to a specific URL
  477. 使用 browser-use 的原生导航功能,支持在新标签页打开。
  478. Args:
  479. url: 要访问的 URL 地址
  480. new_tab: 是否在新标签页中打开(默认 False)
  481. Returns:
  482. ToolResult: 包含导航结果的工具返回对象
  483. Example:
  484. navigate_to_url("https://www.baidu.com")
  485. navigate_to_url("https://www.google.com", new_tab=True)
  486. """
  487. try:
  488. browser, tools = await get_browser_session()
  489. # 使用 browser-use 的 navigate 工具
  490. result = await tools.navigate(
  491. url=url,
  492. new_tab=new_tab,
  493. browser_session=browser
  494. )
  495. return action_result_to_tool_result(result, f"导航到 {url}")
  496. except Exception as e:
  497. return ToolResult(
  498. title="导航失败",
  499. output="",
  500. error=f"Failed to navigate to {url}: {str(e)}",
  501. long_term_memory=f"导航到 {url} 失败"
  502. )
  503. @tool()
  504. async def browser_search_web(query: str, engine: str = "bing") -> ToolResult:
  505. """
  506. 使用搜索引擎搜索
  507. Search the web using a search engine
  508. Args:
  509. query: 搜索关键词
  510. engine: 搜索引擎 (google, duckduckgo, bing) - 默认: google
  511. Returns:
  512. ToolResult: 搜索结果
  513. Example:
  514. search_web("Python async programming", engine="google")
  515. """
  516. try:
  517. browser, tools = await get_browser_session()
  518. # 使用 browser-use 的 search 工具
  519. result = await tools.search(
  520. query=query,
  521. engine=engine,
  522. browser_session=browser
  523. )
  524. return action_result_to_tool_result(result, f"搜索: {query}")
  525. except Exception as e:
  526. return ToolResult(
  527. title="搜索失败",
  528. output="",
  529. error=f"Search failed: {str(e)}",
  530. long_term_memory=f"搜索 '{query}' 失败"
  531. )
  532. @tool()
  533. async def browser_go_back() -> ToolResult:
  534. """
  535. 返回到上一个页面
  536. Go back to the previous page
  537. 模拟浏览器的"后退"按钮功能。
  538. Returns:
  539. ToolResult: 包含返回操作结果的工具返回对象
  540. """
  541. try:
  542. browser, tools = await get_browser_session()
  543. result = await tools.go_back(browser_session=browser)
  544. return action_result_to_tool_result(result, "返回上一页")
  545. except Exception as e:
  546. return ToolResult(
  547. title="返回失败",
  548. output="",
  549. error=f"Failed to go back: {str(e)}",
  550. long_term_memory="返回上一页失败"
  551. )
  552. @tool()
  553. async def browser_wait(seconds: int = 3) -> ToolResult:
  554. """
  555. 等待指定的秒数
  556. Wait for a specified number of seconds
  557. 用于等待页面加载、动画完成或其他异步操作。
  558. Args:
  559. seconds: 等待时间(秒),最大30秒
  560. Returns:
  561. ToolResult: 包含等待操作结果的工具返回对象
  562. Example:
  563. wait(5) # 等待5秒
  564. """
  565. try:
  566. browser, tools = await get_browser_session()
  567. result = await tools.wait(seconds=seconds, browser_session=browser)
  568. return action_result_to_tool_result(result, f"等待 {seconds} 秒")
  569. except Exception as e:
  570. return ToolResult(
  571. title="等待失败",
  572. output="",
  573. error=f"Failed to wait: {str(e)}",
  574. long_term_memory="等待失败"
  575. )
  576. # ============================================================
  577. # 元素交互工具 (Element Interaction Tools)
  578. # ============================================================
  579. @tool()
  580. async def browser_click_element(index: int) -> ToolResult:
  581. """
  582. 通过索引点击页面元素
  583. Click an element by index
  584. Args:
  585. index: 元素索引(从浏览器状态中获取)
  586. Returns:
  587. ToolResult: 包含点击操作结果的工具返回对象
  588. Example:
  589. click_element(index=5)
  590. Note:
  591. 需要先通过 get_selector_map 获取页面元素索引
  592. """
  593. try:
  594. browser, tools = await get_browser_session()
  595. result = await tools.click(
  596. index=index,
  597. browser_session=browser
  598. )
  599. return action_result_to_tool_result(result, f"点击元素 {index}")
  600. except Exception as e:
  601. return ToolResult(
  602. title="点击失败",
  603. output="",
  604. error=f"Failed to click element {index}: {str(e)}",
  605. long_term_memory=f"点击元素 {index} 失败"
  606. )
  607. @tool()
  608. async def browser_input_text(index: int, text: str, clear: bool = True) -> ToolResult:
  609. """
  610. 在指定元素中输入文本
  611. Input text into an element
  612. Args:
  613. index: 元素索引(从浏览器状态中获取)
  614. text: 要输入的文本内容
  615. clear: 是否先清除现有文本(默认 True)
  616. Returns:
  617. ToolResult: 包含输入操作结果的工具返回对象
  618. Example:
  619. input_text(index=0, text="Hello World", clear=True)
  620. """
  621. try:
  622. browser, tools = await get_browser_session()
  623. result = await tools.input(
  624. index=index,
  625. text=text,
  626. clear=clear,
  627. browser_session=browser
  628. )
  629. return action_result_to_tool_result(result, f"输入文本到元素 {index}")
  630. except Exception as e:
  631. return ToolResult(
  632. title="输入失败",
  633. output="",
  634. error=f"Failed to input text into element {index}: {str(e)}",
  635. long_term_memory=f"输入文本失败"
  636. )
  637. @tool()
  638. async def browser_send_keys(keys: str) -> ToolResult:
  639. """
  640. 发送键盘按键或快捷键
  641. Send keyboard keys or shortcuts
  642. 支持发送单个按键、组合键和快捷键。
  643. Args:
  644. keys: 要发送的按键字符串
  645. - 单个按键: "Enter", "Escape", "PageDown", "Tab"
  646. - 组合键: "Control+o", "Shift+Tab", "Alt+F4"
  647. - 功能键: "F1", "F2", ..., "F12"
  648. Returns:
  649. ToolResult: 包含按键操作结果的工具返回对象
  650. Example:
  651. send_keys("Enter")
  652. send_keys("Control+A")
  653. """
  654. try:
  655. browser, tools = await get_browser_session()
  656. result = await tools.send_keys(
  657. keys=keys,
  658. browser_session=browser
  659. )
  660. return action_result_to_tool_result(result, f"发送按键: {keys}")
  661. except Exception as e:
  662. return ToolResult(
  663. title="发送按键失败",
  664. output="",
  665. error=f"Failed to send keys: {str(e)}",
  666. long_term_memory="发送按键失败"
  667. )
  668. @tool()
  669. async def browser_upload_file(index: int, path: str) -> ToolResult:
  670. """
  671. 上传文件到文件输入元素
  672. Upload a file to a file input element
  673. Args:
  674. index: 文件输入框的元素索引
  675. path: 要上传的文件路径(绝对路径)
  676. Returns:
  677. ToolResult: 包含上传操作结果的工具返回对象
  678. Example:
  679. upload_file(index=7, path="/path/to/file.pdf")
  680. Note:
  681. 文件必须存在且路径必须是绝对路径
  682. """
  683. try:
  684. browser, tools = await get_browser_session()
  685. result = await tools.upload_file(
  686. index=index,
  687. path=path,
  688. browser_session=browser,
  689. available_file_paths=[path],
  690. file_system=_file_system
  691. )
  692. return action_result_to_tool_result(result, f"上传文件: {path}")
  693. except Exception as e:
  694. return ToolResult(
  695. title="上传失败",
  696. output="",
  697. error=f"Failed to upload file: {str(e)}",
  698. long_term_memory=f"上传文件 {path} 失败"
  699. )
  700. # ============================================================
  701. # 滚动和视图工具 (Scroll & View Tools)
  702. # ============================================================
  703. @tool()
  704. async def browser_scroll_page(down: bool = True, pages: float = 1.0, index: Optional[int] = None) -> ToolResult:
  705. try:
  706. browser, tools = await get_browser_session()
  707. # --- 核心修复 1: 必须先 await 拿到 session 实例 ---
  708. cdp_session = await browser.get_or_create_cdp_session()
  709. # 这里的执行方式建议参考你已有的 cdp 调用逻辑
  710. # 如果 cdp_session 没有直接封装 .eval(),使用 Runtime.evaluate
  711. before_y_result = await cdp_session.cdp_client.send.Runtime.evaluate(
  712. params={'expression': 'window.scrollY'},
  713. session_id=cdp_session.session_id
  714. )
  715. before_y = before_y_result.get('result', {}).get('value', 0)
  716. # 执行滚动
  717. result = await tools.scroll(down=down, pages=pages, index=index, browser_session=browser)
  718. # 等待渲染并检查偏移
  719. await asyncio.sleep(1)
  720. after_y_result = await cdp_session.cdp_client.send.Runtime.evaluate(
  721. params={'expression': 'window.scrollY'},
  722. session_id=cdp_session.session_id
  723. )
  724. after_y = after_y_result.get('result', {}).get('value', 0)
  725. # 3. 验证是否真的动了
  726. if before_y == after_y and index is None:
  727. return ToolResult(
  728. title="滚动无效",
  729. output="页面已到达边界或滚动被拦截",
  730. error="No movement detected"
  731. )
  732. return action_result_to_tool_result(result, f"已滚动")
  733. except Exception as e:
  734. # --- 核心修复 2: 必须补全 output 参数,否则框架会报错 ---
  735. return ToolResult(
  736. title="滚动失败",
  737. output="", # 补全这个缺失的必填参数
  738. error=str(e)
  739. )
  740. @tool()
  741. async def browser_find_text(text: str) -> ToolResult:
  742. """
  743. 查找页面中的文本并滚动到该位置
  744. Find text on the page and scroll to it
  745. 在页面中搜索指定的文本,找到后自动滚动到该位置。
  746. Args:
  747. text: 要查找的文本内容
  748. Returns:
  749. ToolResult: 包含查找结果的工具返回对象
  750. Example:
  751. find_text("Privacy Policy")
  752. """
  753. try:
  754. browser, tools = await get_browser_session()
  755. result = await tools.find_text(
  756. text=text,
  757. browser_session=browser
  758. )
  759. return action_result_to_tool_result(result, f"查找文本: {text}")
  760. except Exception as e:
  761. return ToolResult(
  762. title="查找失败",
  763. output="",
  764. error=f"Failed to find text: {str(e)}",
  765. long_term_memory=f"查找文本 '{text}' 失败"
  766. )
  767. @tool()
  768. async def browser_get_visual_selector_map() -> ToolResult:
  769. """
  770. 获取当前页面的视觉快照和交互元素索引映射。
  771. Get visual snapshot and selector map of interactive elements.
  772. 该工具会同时执行两个操作:
  773. 1. 捕捉当前页面的截图,并用 browser-use 内置方法在截图上标注元素索引号。
  774. 2. 生成页面所有可交互元素的索引字典(含 href、type 等属性信息)。
  775. Returns:
  776. ToolResult: 包含高亮截图(在 images 中)和元素列表的工具返回对象。
  777. """
  778. try:
  779. browser, _ = await get_browser_session()
  780. # 1. 构造同时包含 DOM 和 截图 的请求
  781. from browser_use.browser.events import BrowserStateRequestEvent
  782. from browser_use.browser.python_highlights import create_highlighted_screenshot_async
  783. event = browser.event_bus.dispatch(
  784. BrowserStateRequestEvent(
  785. include_dom=True,
  786. include_screenshot=True,
  787. include_recent_events=False
  788. )
  789. )
  790. # 2. 等待浏览器返回完整状态
  791. browser_state = await event.event_result(raise_if_none=True, raise_if_any=True)
  792. # 3. 提取 Selector Map
  793. selector_map = browser_state.dom_state.selector_map if browser_state.dom_state else {}
  794. # 4. 提取截图并生成带索引标注的高亮截图(通过 CDP 获取精确 DPI 和滚动偏移)
  795. screenshot_b64 = browser_state.screenshot or ""
  796. highlighted_b64 = ""
  797. if screenshot_b64 and selector_map:
  798. try:
  799. cdp_session = await browser.get_or_create_cdp_session()
  800. highlighted_b64 = await create_highlighted_screenshot_async(
  801. screenshot_b64, selector_map,
  802. cdp_session=cdp_session,
  803. filter_highlight_ids=False
  804. )
  805. except Exception:
  806. highlighted_b64 = screenshot_b64 # fallback to raw screenshot
  807. else:
  808. highlighted_b64 = screenshot_b64
  809. # 5. 构建供 Agent 阅读的完整元素列表,包含丰富的属性信息
  810. elements_info = []
  811. for index, node in selector_map.items():
  812. tag = node.tag_name
  813. attrs = node.attributes or {}
  814. desc = attrs.get('aria-label') or attrs.get('placeholder') or attrs.get('title') or node.get_all_children_text(max_depth=1) or ""
  815. # 收集有用的属性片段
  816. extra_parts = []
  817. if attrs.get('href'):
  818. extra_parts.append(f"href={attrs['href'][:60]}")
  819. if attrs.get('type'):
  820. extra_parts.append(f"type={attrs['type']}")
  821. if attrs.get('role'):
  822. extra_parts.append(f"role={attrs['role']}")
  823. if attrs.get('name'):
  824. extra_parts.append(f"name={attrs['name']}")
  825. extra = f" ({', '.join(extra_parts)})" if extra_parts else ""
  826. elements_info.append(f"Index {index}: <{tag}> \"{desc[:50]}\"{extra}")
  827. output = f"页面截图已捕获(含元素索引标注)\n找到 {len(selector_map)} 个交互元素\n\n"
  828. output += "元素列表:\n" + "\n".join(elements_info)
  829. # 6. 将高亮截图存入 images 字段,metadata 保留结构化数据
  830. images = []
  831. if highlighted_b64:
  832. images.append({"type": "base64", "media_type": "image/png", "data": highlighted_b64})
  833. return ToolResult(
  834. title="视觉元素观察",
  835. output=output,
  836. long_term_memory=f"在页面观察到 {len(selector_map)} 个元素并保存了截图",
  837. images=images,
  838. metadata={
  839. "selector_map": {k: str(v) for k, v in list(selector_map.items())[:100]},
  840. "url": browser_state.url,
  841. "title": browser_state.title
  842. }
  843. )
  844. except Exception as e:
  845. return ToolResult(
  846. title="视觉观察失败",
  847. output="",
  848. error=f"Failed to get visual selector map: {str(e)}",
  849. long_term_memory="获取视觉元素映射失败"
  850. )
  851. @tool()
  852. async def browser_screenshot() -> ToolResult:
  853. """
  854. 请求在下次观察中包含页面截图
  855. Request a screenshot to be included in the next observation
  856. 用于视觉检查页面状态,帮助理解页面布局和内容。
  857. Returns:
  858. ToolResult: 包含截图请求结果的工具返回对象
  859. Example:
  860. screenshot()
  861. Note:
  862. 截图会在下次页面观察时自动包含在结果中。
  863. """
  864. try:
  865. browser, tools = await get_browser_session()
  866. result = await tools.screenshot(browser_session=browser)
  867. return action_result_to_tool_result(result, "截图请求")
  868. except Exception as e:
  869. return ToolResult(
  870. title="截图失败",
  871. output="",
  872. error=f"Failed to capture screenshot: {str(e)}",
  873. long_term_memory="截图失败"
  874. )
  875. # ============================================================
  876. # 标签页管理工具 (Tab Management Tools)
  877. # ============================================================
  878. @tool()
  879. async def browser_switch_tab(tab_id: str) -> ToolResult:
  880. """
  881. 切换到指定标签页
  882. Switch to a different browser tab
  883. Args:
  884. tab_id: 4字符标签ID(target_id 的最后4位)
  885. Returns:
  886. ToolResult: 切换结果
  887. Example:
  888. switch_tab(tab_id="a3f2")
  889. """
  890. try:
  891. browser, tools = await get_browser_session()
  892. normalized_tab_id = tab_id[-4:] if tab_id else tab_id
  893. result = await tools.switch(
  894. tab_id=normalized_tab_id,
  895. browser_session=browser
  896. )
  897. return action_result_to_tool_result(result, f"切换到标签页 {normalized_tab_id}")
  898. except Exception as e:
  899. return ToolResult(
  900. title="切换标签页失败",
  901. output="",
  902. error=f"Failed to switch tab: {str(e)}",
  903. long_term_memory=f"切换到标签页 {tab_id} 失败"
  904. )
  905. @tool()
  906. async def browser_close_tab(tab_id: str) -> ToolResult:
  907. """
  908. 关闭指定标签页
  909. Close a browser tab
  910. Args:
  911. tab_id: 4字符标签ID
  912. Returns:
  913. ToolResult: 关闭结果
  914. Example:
  915. close_tab(tab_id="a3f2")
  916. """
  917. try:
  918. browser, tools = await get_browser_session()
  919. normalized_tab_id = tab_id[-4:] if tab_id else tab_id
  920. result = await tools.close(
  921. tab_id=normalized_tab_id,
  922. browser_session=browser
  923. )
  924. return action_result_to_tool_result(result, f"关闭标签页 {normalized_tab_id}")
  925. except Exception as e:
  926. return ToolResult(
  927. title="关闭标签页失败",
  928. output="",
  929. error=f"Failed to close tab: {str(e)}",
  930. long_term_memory=f"关闭标签页 {tab_id} 失败"
  931. )
  932. # ============================================================
  933. # 下拉框工具 (Dropdown Tools)
  934. # ============================================================
  935. @tool()
  936. async def browser_get_dropdown_options(index: int) -> ToolResult:
  937. """
  938. 获取下拉框的所有选项
  939. Get options from a dropdown element
  940. Args:
  941. index: 下拉框的元素索引
  942. Returns:
  943. ToolResult: 包含所有选项的结果
  944. Example:
  945. get_dropdown_options(index=8)
  946. """
  947. try:
  948. browser, tools = await get_browser_session()
  949. result = await tools.dropdown_options(
  950. index=index,
  951. browser_session=browser
  952. )
  953. return action_result_to_tool_result(result, f"获取下拉框选项: {index}")
  954. except Exception as e:
  955. return ToolResult(
  956. title="获取下拉框选项失败",
  957. output="",
  958. error=f"Failed to get dropdown options: {str(e)}",
  959. long_term_memory=f"获取下拉框 {index} 选项失败"
  960. )
  961. @tool()
  962. async def browser_select_dropdown_option(index: int, text: str) -> ToolResult:
  963. """
  964. 选择下拉框选项
  965. Select an option from a dropdown
  966. Args:
  967. index: 下拉框的元素索引
  968. text: 要选择的选项文本(精确匹配)
  969. Returns:
  970. ToolResult: 选择结果
  971. Example:
  972. select_dropdown_option(index=8, text="Option 2")
  973. """
  974. try:
  975. browser, tools = await get_browser_session()
  976. result = await tools.select_dropdown(
  977. index=index,
  978. text=text,
  979. browser_session=browser
  980. )
  981. return action_result_to_tool_result(result, f"选择下拉框选项: {text}")
  982. except Exception as e:
  983. return ToolResult(
  984. title="选择下拉框选项失败",
  985. output="",
  986. error=f"Failed to select dropdown option: {str(e)}",
  987. long_term_memory=f"选择选项 '{text}' 失败"
  988. )
  989. # ============================================================
  990. # 内容提取工具 (Content Extraction Tools)
  991. # ============================================================
  992. def scrub_search_redirect_url(url: str) -> str:
  993. """
  994. 自动检测并解析 Bing/Google 等搜索引擎的重定向链接,提取真实目标 URL。
  995. """
  996. if not url or not isinstance(url, str):
  997. return url
  998. try:
  999. parsed = urlparse(url)
  1000. # 1. 处理 Bing 重定向 (特征:u 参数带 Base64)
  1001. # 示例:...&u=a1aHR0cHM6Ly96aHVhbmxhbi56aGlodS5jb20vcC8zODYxMjgwOQ&...
  1002. if "bing.com" in parsed.netloc:
  1003. u_param = parse_qs(parsed.query).get('u', [None])[0]
  1004. if u_param:
  1005. # 移除开头的 'a1', 'a0' 等标识符
  1006. b64_str = u_param[2:]
  1007. # 补齐 Base64 填充符
  1008. padding = '=' * (4 - len(b64_str) % 4)
  1009. decoded = base64.b64decode(b64_str + padding).decode('utf-8', errors='ignore')
  1010. if decoded.startswith('http'):
  1011. return decoded
  1012. # 2. 处理 Google 重定向 (特征:url 参数)
  1013. if "google.com" in parsed.netloc:
  1014. url_param = parse_qs(parsed.query).get('url', [None])[0]
  1015. if url_param:
  1016. return unquote(url_param)
  1017. # 3. 兜底:处理常见的跳转参数
  1018. for param in ['target', 'dest', 'destination', 'link']:
  1019. found = parse_qs(parsed.query).get(param, [None])[0]
  1020. if found and found.startswith('http'):
  1021. return unquote(found)
  1022. except Exception:
  1023. pass # 解析失败则返回原链接
  1024. return url
  1025. async def extraction_adapter(input_data):
  1026. # 提取字符串
  1027. if isinstance(input_data, list):
  1028. prompt = input_data[-1].content if hasattr(input_data[-1], 'content') else str(input_data[-1])
  1029. else:
  1030. prompt = str(input_data)
  1031. response = await openrouter_llm_call(
  1032. messages=[{"role": "user", "content": prompt}]
  1033. )
  1034. content = response["content"]
  1035. # --- 核心改进:URL 自动修复 ---
  1036. # 使用正则表达式匹配内容中的所有 URL,并尝试进行洗涤
  1037. urls = re.findall(r'https?://[^\s<>"\']+', content)
  1038. for original_url in urls:
  1039. clean_url = scrub_search_redirect_url(original_url)
  1040. if clean_url != original_url:
  1041. content = content.replace(original_url, clean_url)
  1042. from argparse import Namespace
  1043. return Namespace(completion=content)
  1044. @tool()
  1045. async def browser_extract_content(query: str, extract_links: bool = False,
  1046. start_from_char: int = 0) -> ToolResult:
  1047. """
  1048. 使用 LLM 从页面提取结构化数据
  1049. Extract content from the current page using LLM
  1050. Args:
  1051. query: 提取查询(告诉 LLM 要提取什么内容)
  1052. extract_links: 是否提取链接(默认 False,节省 token)
  1053. start_from_char: 从哪个字符开始提取(用于分页提取大内容)
  1054. Returns:
  1055. ToolResult: 提取的内容
  1056. Example:
  1057. extract_content(query="提取页面上所有产品的名称和价格", extract_links=True)
  1058. Note:
  1059. 需要配置 page_extraction_llm,否则会失败
  1060. 支持分页提取,最大100k字符
  1061. """
  1062. try:
  1063. browser, tools = await get_browser_session()
  1064. # 注意:extract 需要 page_extraction_llm 参数
  1065. # 这里我们假设用户会在初始化时配置 LLM
  1066. # 如果没有配置,会抛出异常
  1067. result = await tools.extract(
  1068. query=query,
  1069. extract_links=extract_links,
  1070. start_from_char=start_from_char,
  1071. browser_session=browser,
  1072. page_extraction_llm=RunnableLambda(extraction_adapter), # 需要用户配置
  1073. file_system=_file_system
  1074. )
  1075. return action_result_to_tool_result(result, f"提取内容: {query}")
  1076. except Exception as e:
  1077. return ToolResult(
  1078. title="内容提取失败",
  1079. output="",
  1080. error=f"Failed to extract content: {str(e)}",
  1081. long_term_memory=f"提取内容失败: {query}"
  1082. )
  1083. async def _detect_and_download_pdf_via_cdp(browser) -> Optional[str]:
  1084. """
  1085. 检测当前页面是否为 PDF,如果是则通过 CDP(浏览器内 fetch)下载到本地。
  1086. 优势:自动携带浏览器的 cookies/session,可访问需要登录的 PDF。
  1087. 返回本地文件路径,非 PDF 页面返回 None。
  1088. """
  1089. try:
  1090. current_url = await browser.get_current_page_url()
  1091. if not current_url:
  1092. return None
  1093. parsed = urlparse(current_url)
  1094. is_pdf = parsed.path.lower().endswith('.pdf')
  1095. # URL 不明显是 PDF 时,通过 CDP 检查 content-type
  1096. if not is_pdf:
  1097. try:
  1098. cdp = await browser.get_or_create_cdp_session()
  1099. ct_result = await cdp.cdp_client.send.Runtime.evaluate(
  1100. params={'expression': 'document.contentType'},
  1101. session_id=cdp.session_id
  1102. )
  1103. content_type = ct_result.get('result', {}).get('value', '')
  1104. is_pdf = 'pdf' in content_type.lower()
  1105. except Exception:
  1106. pass
  1107. if not is_pdf:
  1108. return None
  1109. # 通过浏览器内 fetch API 下载 PDF(自动携带 cookies)
  1110. cdp = await browser.get_or_create_cdp_session()
  1111. js_code = """
  1112. (async () => {
  1113. try {
  1114. const resp = await fetch(window.location.href);
  1115. if (!resp.ok) return JSON.stringify({error: 'HTTP ' + resp.status});
  1116. const blob = await resp.blob();
  1117. return new Promise((resolve, reject) => {
  1118. const reader = new FileReader();
  1119. reader.onloadend = () => resolve(JSON.stringify({data: reader.result}));
  1120. reader.onerror = () => resolve(JSON.stringify({error: 'FileReader failed'}));
  1121. reader.readAsDataURL(blob);
  1122. });
  1123. } catch(e) {
  1124. return JSON.stringify({error: e.message});
  1125. }
  1126. })()
  1127. """
  1128. result = await cdp.cdp_client.send.Runtime.evaluate(
  1129. params={
  1130. 'expression': js_code,
  1131. 'awaitPromise': True,
  1132. 'returnByValue': True,
  1133. 'timeout': 60000
  1134. },
  1135. session_id=cdp.session_id
  1136. )
  1137. value = result.get('result', {}).get('value', '')
  1138. if not value:
  1139. print("⚠️ CDP fetch PDF: 无返回值")
  1140. return None
  1141. data = json.loads(value)
  1142. if 'error' in data:
  1143. print(f"⚠️ CDP fetch PDF 失败: {data['error']}")
  1144. return None
  1145. # 从 data URL 中提取 base64 并解码
  1146. data_url = data['data'] # data:application/pdf;base64,JVBERi0...
  1147. base64_data = data_url.split(',', 1)[1]
  1148. pdf_bytes = base64.b64decode(base64_data)
  1149. # 保存到本地
  1150. save_dir = Path.cwd() / ".cache/.browser_use_files"
  1151. save_dir.mkdir(parents=True, exist_ok=True)
  1152. filename = Path(parsed.path).name if parsed.path else ""
  1153. if not filename or not filename.lower().endswith('.pdf'):
  1154. import time
  1155. filename = f"downloaded_{int(time.time())}.pdf"
  1156. save_path = str(save_dir / filename)
  1157. with open(save_path, 'wb') as f:
  1158. f.write(pdf_bytes)
  1159. print(f"📄 PDF 已通过 CDP 下载到: {save_path} ({len(pdf_bytes)} bytes)")
  1160. return save_path
  1161. except Exception as e:
  1162. print(f"⚠️ PDF 检测/下载异常: {e}")
  1163. return None
  1164. @tool()
  1165. async def browser_read_long_content(
  1166. goal: Any,
  1167. source: str = "page",
  1168. context: Any = "",
  1169. **kwargs
  1170. ) -> ToolResult:
  1171. """
  1172. 智能读取长内容。支持自动检测并读取网页上的 PDF 文件。
  1173. 当 source="page" 且当前页面是 PDF 时,会通过 CDP 下载 PDF 并用 pypdf 解析,
  1174. 而非使用 DOM 提取(DOM 无法读取浏览器内置 PDF Viewer 的内容)。
  1175. 通过 CDP 下载可自动携带浏览器的 cookies/session,支持需要登录的 PDF。
  1176. """
  1177. try:
  1178. browser, tools = await get_browser_session()
  1179. # 1. 提取目标文本 (针对 GoalTree 字典结构)
  1180. final_goal_text = ""
  1181. if isinstance(goal, dict):
  1182. final_goal_text = goal.get("mission") or goal.get("goal") or str(goal)
  1183. else:
  1184. final_goal_text = str(goal)
  1185. # 2. 清洗业务背景 (过滤框架注入的 dict 类型 context)
  1186. business_context = context if isinstance(context, str) else ""
  1187. # 3. PDF 自动检测:当 source="page" 时检查是否为 PDF 页面
  1188. available_files = []
  1189. if source.lower() == "page":
  1190. pdf_path = await _detect_and_download_pdf_via_cdp(browser)
  1191. if pdf_path:
  1192. source = pdf_path
  1193. available_files.append(pdf_path)
  1194. # 4. 验证并实例化
  1195. action_params = ReadContentAction(
  1196. goal=final_goal_text,
  1197. source=source,
  1198. context=business_context
  1199. )
  1200. # 5. 解包参数调用底层方法
  1201. result = await tools.read_long_content(
  1202. **action_params.model_dump(),
  1203. browser_session=browser,
  1204. page_extraction_llm=RunnableLambda(extraction_adapter),
  1205. available_file_paths=available_files
  1206. )
  1207. return action_result_to_tool_result(result, f"深度读取: {source}")
  1208. except Exception as e:
  1209. return ToolResult(
  1210. title="深度读取失败",
  1211. output="",
  1212. error=f"Read long content failed: {str(e)}",
  1213. long_term_memory="参数解析或校验失败,请检查输入"
  1214. )
  1215. @tool()
  1216. async def browser_get_page_html() -> ToolResult:
  1217. """
  1218. 获取当前页面的完整 HTML
  1219. Get the full HTML of the current page
  1220. 返回当前页面的完整 HTML 源代码。
  1221. Returns:
  1222. ToolResult: 包含页面 HTML 的工具返回对象
  1223. Example:
  1224. get_page_html()
  1225. Note:
  1226. - 返回的是完整的 HTML 源代码
  1227. - 输出会被限制在 10000 字符以内(完整内容保存在 metadata 中)
  1228. """
  1229. try:
  1230. browser, tools = await get_browser_session()
  1231. # 使用 CDP 获取页面 HTML
  1232. cdp = await browser.get_or_create_cdp_session()
  1233. # 获取页面内容
  1234. result = await cdp.cdp_client.send.Runtime.evaluate(
  1235. params={'expression': 'document.documentElement.outerHTML'},
  1236. session_id=cdp.session_id
  1237. )
  1238. html = result.get('result', {}).get('value', '')
  1239. # 获取 URL 和标题
  1240. url = await browser.get_current_page_url()
  1241. title_result = await cdp.cdp_client.send.Runtime.evaluate(
  1242. params={'expression': 'document.title'},
  1243. session_id=cdp.session_id
  1244. )
  1245. title = title_result.get('result', {}).get('value', '')
  1246. # 限制输出大小
  1247. output_html = html
  1248. if len(html) > 10000:
  1249. output_html = html[:10000] + "... (truncated)"
  1250. return ToolResult(
  1251. title=f"获取 HTML: {url}",
  1252. output=f"页面: {title}\nURL: {url}\n\nHTML:\n{output_html}",
  1253. long_term_memory=f"获取 HTML: {url}",
  1254. metadata={"url": url, "title": title, "html": html}
  1255. )
  1256. except Exception as e:
  1257. return ToolResult(
  1258. title="获取 HTML 失败",
  1259. output="",
  1260. error=f"Failed to get page HTML: {str(e)}",
  1261. long_term_memory="获取 HTML 失败"
  1262. )
  1263. @tool()
  1264. async def browser_get_selector_map() -> ToolResult:
  1265. """
  1266. 获取当前页面的元素索引映射
  1267. Get the selector map of interactive elements on the current page
  1268. 返回页面所有可交互元素的索引字典,用于后续的元素操作。
  1269. Returns:
  1270. ToolResult: 包含元素映射的工具返回对象
  1271. Example:
  1272. get_selector_map()
  1273. Note:
  1274. 返回的索引可以用于 click_element, input_text 等操作
  1275. """
  1276. try:
  1277. browser, tools = await get_browser_session()
  1278. # 关键修复:先触发 BrowserStateRequestEvent 来更新 DOM 状态
  1279. # 这会触发 DOM watchdog 重新构建 DOM 树并更新 selector_map
  1280. from browser_use.browser.events import BrowserStateRequestEvent
  1281. # 触发事件并等待结果
  1282. event = browser.event_bus.dispatch(
  1283. BrowserStateRequestEvent(
  1284. include_dom=True,
  1285. include_screenshot=False, # 不需要截图,节省时间
  1286. include_recent_events=False
  1287. )
  1288. )
  1289. # 等待 DOM 更新完成
  1290. browser_state = await event.event_result(raise_if_none=True, raise_if_any=True)
  1291. # 从更新后的状态中获取 selector_map
  1292. selector_map = browser_state.dom_state.selector_map if browser_state.dom_state else {}
  1293. # 构建输出信息
  1294. elements_info = []
  1295. for index, node in list(selector_map.items())[:20]: # 只显示前20个
  1296. tag = node.tag_name
  1297. attrs = node.attributes or {}
  1298. text = attrs.get('aria-label') or attrs.get('placeholder') or attrs.get('value', '')
  1299. elements_info.append(f"索引 {index}: <{tag}> {text[:50]}")
  1300. output = f"找到 {len(selector_map)} 个交互元素\n\n"
  1301. output += "\n".join(elements_info)
  1302. if len(selector_map) > 20:
  1303. output += f"\n... 还有 {len(selector_map) - 20} 个元素"
  1304. return ToolResult(
  1305. title="获取元素映射",
  1306. output=output,
  1307. long_term_memory=f"获取到 {len(selector_map)} 个交互元素",
  1308. metadata={"selector_map": {k: str(v) for k, v in list(selector_map.items())[:100]}}
  1309. )
  1310. except Exception as e:
  1311. return ToolResult(
  1312. title="获取元素映射失败",
  1313. output="",
  1314. error=f"Failed to get selector map: {str(e)}",
  1315. long_term_memory="获取元素映射失败"
  1316. )
  1317. # ============================================================
  1318. # JavaScript 执行工具 (JavaScript Tools)
  1319. # ============================================================
  1320. @tool()
  1321. async def browser_evaluate(code: str) -> ToolResult:
  1322. """
  1323. 在页面中执行 JavaScript 代码
  1324. Execute JavaScript code in the page context
  1325. 允许在当前页面中执行任意 JavaScript 代码,用于复杂的页面操作或数据提取。
  1326. Args:
  1327. code: 要执行的 JavaScript 代码字符串
  1328. Returns:
  1329. ToolResult: 包含执行结果的工具返回对象
  1330. Example:
  1331. evaluate("document.title")
  1332. evaluate("document.querySelectorAll('a').length")
  1333. Note:
  1334. - 代码在页面上下文中执行,可以访问 DOM 和全局变量
  1335. - 返回值会被自动序列化为字符串
  1336. - 执行结果限制在 20k 字符以内
  1337. """
  1338. try:
  1339. browser, tools = await get_browser_session()
  1340. result = await tools.evaluate(
  1341. code=code,
  1342. browser_session=browser
  1343. )
  1344. return action_result_to_tool_result(result, "执行 JavaScript")
  1345. except Exception as e:
  1346. return ToolResult(
  1347. title="JavaScript 执行失败",
  1348. output="",
  1349. error=f"Failed to execute JavaScript: {str(e)}",
  1350. long_term_memory="JavaScript 执行失败"
  1351. )
  1352. @tool()
  1353. async def browser_ensure_login_with_cookies(cookie_type: str, url: str = "https://www.xiaohongshu.com") -> ToolResult:
  1354. """
  1355. 检查登录状态并在需要时注入 cookies
  1356. """
  1357. try:
  1358. browser, tools = await get_browser_session()
  1359. if url:
  1360. await tools.navigate(url=url, browser_session=browser)
  1361. await tools.wait(seconds=2, browser_session=browser)
  1362. check_login_js = """
  1363. (function() {
  1364. const loginBtn = document.querySelector('[class*="login"]') ||
  1365. document.querySelector('[href*="login"]') ||
  1366. Array.from(document.querySelectorAll('button, a')).find(el => (el.textContent || '').includes('登录'));
  1367. const userInfo = document.querySelector('[class*="user"]') ||
  1368. document.querySelector('[class*="avatar"]');
  1369. return {
  1370. needLogin: !!loginBtn && !userInfo,
  1371. hasLoginBtn: !!loginBtn,
  1372. hasUserInfo: !!userInfo
  1373. };
  1374. })()
  1375. """
  1376. result = await tools.evaluate(code=check_login_js, browser_session=browser)
  1377. status_output = result.extracted_content
  1378. if isinstance(status_output, str) and status_output.startswith("Result: "):
  1379. status_output = status_output[8:]
  1380. login_info: Dict[str, Any] = {}
  1381. if isinstance(status_output, str):
  1382. try:
  1383. login_info = json.loads(status_output)
  1384. except Exception:
  1385. login_info = {}
  1386. elif isinstance(status_output, dict):
  1387. login_info = status_output
  1388. if not login_info.get("needLogin"):
  1389. output = json.dumps({"need_login": False}, ensure_ascii=False)
  1390. return ToolResult(
  1391. title="已登录",
  1392. output=output,
  1393. long_term_memory=output
  1394. )
  1395. row = _fetch_cookie_row(cookie_type)
  1396. cookie_value = _extract_cookie_value(row)
  1397. if not cookie_value:
  1398. output = json.dumps({"need_login": True, "cookies_count": 0}, ensure_ascii=False)
  1399. return ToolResult(
  1400. title="未找到 cookies",
  1401. output=output,
  1402. error="未找到 cookies",
  1403. long_term_memory=output
  1404. )
  1405. domain, base_url = _cookie_domain_for_type(cookie_type, url)
  1406. cookies = _normalize_cookies(cookie_value, domain, base_url)
  1407. if not cookies:
  1408. output = json.dumps({"need_login": True, "cookies_count": 0}, ensure_ascii=False)
  1409. return ToolResult(
  1410. title="cookies 解析失败",
  1411. output=output,
  1412. error="cookies 解析失败",
  1413. long_term_memory=output
  1414. )
  1415. await browser._cdp_set_cookies(cookies)
  1416. if url:
  1417. await tools.navigate(url=url, browser_session=browser)
  1418. await tools.wait(seconds=2, browser_session=browser)
  1419. output = json.dumps({"need_login": True, "cookies_count": len(cookies)}, ensure_ascii=False)
  1420. return ToolResult(
  1421. title="已注入 cookies",
  1422. output=output,
  1423. long_term_memory=output
  1424. )
  1425. except Exception as e:
  1426. return ToolResult(
  1427. title="登录检查失败",
  1428. output="",
  1429. error=str(e),
  1430. long_term_memory="登录检查失败"
  1431. )
  1432. # ============================================================
  1433. # 等待用户操作工具 (Wait for User Action)
  1434. # ============================================================
  1435. @tool()
  1436. async def browser_wait_for_user_action(message: str = "Please complete the action in browser",
  1437. timeout: int = 300) -> ToolResult:
  1438. """
  1439. 等待用户在浏览器中完成操作(如登录)
  1440. Wait for user to complete an action in the browser (e.g., login)
  1441. 暂停自动化流程,等待用户手动完成某些操作(如登录、验证码等)。
  1442. Args:
  1443. message: 提示用户需要完成的操作
  1444. timeout: 最大等待时间(秒),默认 300 秒(5 分钟)
  1445. Returns:
  1446. ToolResult: 包含等待结果的工具返回对象
  1447. Example:
  1448. wait_for_user_action("Please login to Xiaohongshu", timeout=180)
  1449. wait_for_user_action("Please complete the CAPTCHA", timeout=60)
  1450. Note:
  1451. - 用户需要在浏览器窗口中手动完成操作
  1452. - 完成后按回车键继续
  1453. - 超时后会自动继续执行
  1454. """
  1455. try:
  1456. import asyncio
  1457. print(f"\n{'='*60}")
  1458. print(f"⏸️ WAITING FOR USER ACTION")
  1459. print(f"{'='*60}")
  1460. print(f"📝 {message}")
  1461. print(f"⏱️ Timeout: {timeout} seconds")
  1462. print(f"\n👉 Please complete the action in the browser window")
  1463. print(f"👉 Press ENTER when done, or wait for timeout")
  1464. print(f"{'='*60}\n")
  1465. # Wait for user input or timeout
  1466. try:
  1467. loop = asyncio.get_event_loop()
  1468. # Wait for either user input or timeout
  1469. await asyncio.wait_for(
  1470. loop.run_in_executor(None, input),
  1471. timeout=timeout
  1472. )
  1473. return ToolResult(
  1474. title="用户操作完成",
  1475. output=f"User completed: {message}",
  1476. long_term_memory=f"用户完成操作: {message}"
  1477. )
  1478. except asyncio.TimeoutError:
  1479. return ToolResult(
  1480. title="用户操作超时",
  1481. output=f"Timeout waiting for: {message}",
  1482. long_term_memory=f"等待用户操作超时: {message}"
  1483. )
  1484. except Exception as e:
  1485. return ToolResult(
  1486. title="等待用户操作失败",
  1487. output="",
  1488. error=f"Failed to wait for user action: {str(e)}",
  1489. long_term_memory="等待用户操作失败"
  1490. )
  1491. # ============================================================
  1492. # 任务完成工具 (Task Completion)
  1493. # ============================================================
  1494. @tool()
  1495. async def browser_done(text: str, success: bool = True,
  1496. files_to_display: Optional[List[str]] = None) -> ToolResult:
  1497. """
  1498. 标记任务完成并返回最终消息
  1499. Mark the task as complete and return final message to user
  1500. Args:
  1501. text: 给用户的最终消息
  1502. success: 任务是否成功完成
  1503. files_to_display: 可选的要显示的文件路径列表
  1504. Returns:
  1505. ToolResult: 完成结果
  1506. Example:
  1507. done("任务已完成,提取了10个产品信息", success=True)
  1508. """
  1509. try:
  1510. browser, tools = await get_browser_session()
  1511. result = await tools.done(
  1512. text=text,
  1513. success=success,
  1514. files_to_display=files_to_display,
  1515. file_system=_file_system
  1516. )
  1517. return action_result_to_tool_result(result, "任务完成")
  1518. except Exception as e:
  1519. return ToolResult(
  1520. title="标记任务完成失败",
  1521. output="",
  1522. error=f"Failed to complete task: {str(e)}",
  1523. long_term_memory="标记任务完成失败"
  1524. )
  1525. # ============================================================
  1526. # Cookie 持久化工具
  1527. # ============================================================
  1528. _COOKIES_DIR = Path(__file__).parent.parent.parent.parent.parent / ".cache/.cookies"
  1529. @tool()
  1530. async def browser_export_cookies(name: str = "", account: str = "") -> ToolResult:
  1531. """
  1532. 导出当前浏览器的所有 Cookie 到本地 .cookies/ 目录。
  1533. 文件命名格式:{域名}_{账号名}.json,如 bilibili.com_zhangsan.json
  1534. 登录成功后调用此工具,下次可通过 browser_load_cookies 恢复登录态。
  1535. Args:
  1536. name: 自定义文件名(可选,提供则忽略自动命名)
  1537. account: 账号名称(可选,用于区分同一网站的不同账号)
  1538. """
  1539. try:
  1540. browser, _ = await get_browser_session()
  1541. # 获取所有 Cookie(CDP 格式)
  1542. all_cookies = await browser._cdp_get_cookies()
  1543. if not all_cookies:
  1544. return ToolResult(title="Cookie 导出", output="当前浏览器没有 Cookie", long_term_memory="无 Cookie 可导出")
  1545. # 获取当前域名,用于过滤和命名
  1546. from urllib.parse import urlparse
  1547. current_url = await browser.get_current_page_url() or ''
  1548. domain = urlparse(current_url).netloc.replace("www.", "") or "default"
  1549. if not name:
  1550. name = f"{domain}_{account}" if account else domain
  1551. # 只保留当前域名的 cookie(过滤第三方)
  1552. cookies = [c for c in all_cookies if domain in c.get("domain", "").lstrip(".")]
  1553. # 保存
  1554. _COOKIES_DIR.mkdir(parents=True, exist_ok=True)
  1555. cookie_file = _COOKIES_DIR / f"{name}.json"
  1556. cookie_file.write_text(json.dumps(cookies, ensure_ascii=False, indent=2), encoding="utf-8")
  1557. return ToolResult(
  1558. title="Cookie 已导出",
  1559. output=f"已保存 {len(cookies)} 条 Cookie 到 .cookies/{name}.json(从 {len(all_cookies)} 条中过滤当前域名)",
  1560. long_term_memory=f"导出 {len(cookies)} 条 Cookie 到 .cookies/{name}.json"
  1561. )
  1562. except Exception as e:
  1563. return ToolResult(title="Cookie 导出失败", output="", error=str(e), long_term_memory="导出 Cookie 失败")
  1564. @tool()
  1565. async def browser_load_cookies(url: str, name: str = "") -> ToolResult:
  1566. """
  1567. 根据目标 URL 自动查找本地 Cookie 文件,注入浏览器并导航到目标页面恢复登录态。
  1568. 重要:此工具会自动完成导航,调用前不需要先调用 browser_navigate_to_url。
  1569. Args:
  1570. url: 目标 URL(必须提供,同时用于自动匹配 Cookie 文件)
  1571. name: Cookie 文件名(可选,不传则根据 URL 域名自动查找)
  1572. """
  1573. try:
  1574. browser, tools = await get_browser_session()
  1575. if not url.startswith("http"):
  1576. url = f"https://{url}"
  1577. # 根据域名自动查找 Cookie 文件
  1578. if not name:
  1579. from urllib.parse import urlparse
  1580. domain = urlparse(url).netloc.replace("www.", "")
  1581. if _COOKIES_DIR.exists():
  1582. matches = list(_COOKIES_DIR.glob(f"{domain}*.json"))
  1583. if matches:
  1584. cookie_file = matches[0] # 取第一个匹配的
  1585. else:
  1586. available = [f.stem for f in _COOKIES_DIR.glob("*.json")]
  1587. return ToolResult(title="未找到 Cookie", output=f"没有匹配 {domain} 的文件,可用: {available}", error=f"无 {domain} 的 Cookie 文件")
  1588. else:
  1589. return ToolResult(title="未找到 Cookie", output=".cookies 目录不存在", error="Cookie 目录不存在")
  1590. else:
  1591. cookie_file = _COOKIES_DIR / f"{name}.json"
  1592. if not cookie_file.exists():
  1593. available = [f.stem for f in _COOKIES_DIR.glob("*.json")] if _COOKIES_DIR.exists() else []
  1594. return ToolResult(title="文件不存在", output=f"可用: {available}", error=f"未找到 .cookies/{name}.json")
  1595. cookies = json.loads(cookie_file.read_text(encoding="utf-8"))
  1596. # 直接注入(export 和 load 使用相同的 CDP 格式,无需标准化)
  1597. await browser._cdp_set_cookies(cookies)
  1598. # 导航到目标页面(带上刚注入的 Cookie)
  1599. if url:
  1600. if not url.startswith("http"):
  1601. url = f"https://{url}"
  1602. await tools.navigate(url=url, browser_session=browser)
  1603. await tools.wait(seconds=3, browser_session=browser)
  1604. return ToolResult(
  1605. title="Cookie 注入并导航完成",
  1606. output=f"从 {cookie_file.name} 注入 {len(cookies)} 条 Cookie,已导航到 {url}",
  1607. long_term_memory=f"已从 {cookie_file.name} 注入 Cookie 并导航到 {url},登录态已恢复"
  1608. )
  1609. except Exception as e:
  1610. return ToolResult(title="Cookie 加载失败", output="", error=str(e), long_term_memory="加载 Cookie 失败")
  1611. # ============================================================
  1612. # 导出所有工具函数(供外部使用)
  1613. # ============================================================
  1614. __all__ = [
  1615. # 会话管理
  1616. 'init_browser_session',
  1617. 'get_browser_session',
  1618. 'cleanup_browser_session',
  1619. 'kill_browser_session',
  1620. # 导航类工具
  1621. 'browser_navigate_to_url',
  1622. 'browser_search_web',
  1623. 'browser_go_back',
  1624. 'browser_wait',
  1625. # 元素交互工具
  1626. 'browser_click_element',
  1627. 'browser_input_text',
  1628. 'browser_send_keys',
  1629. 'browser_upload_file',
  1630. # 滚动和视图工具
  1631. 'browser_scroll_page',
  1632. 'browser_find_text',
  1633. 'browser_screenshot',
  1634. # 标签页管理工具
  1635. 'browser_switch_tab',
  1636. 'browser_close_tab',
  1637. # 下拉框工具
  1638. 'browser_get_dropdown_options',
  1639. 'browser_select_dropdown_option',
  1640. # 内容提取工具
  1641. 'browser_extract_content',
  1642. 'browser_get_page_html',
  1643. 'browser_read_long_content',
  1644. 'browser_get_selector_map',
  1645. 'browser_get_visual_selector_map',
  1646. # JavaScript 执行工具
  1647. 'browser_evaluate',
  1648. 'browser_ensure_login_with_cookies',
  1649. # 等待用户操作
  1650. 'browser_wait_for_user_action',
  1651. # 任务完成
  1652. 'browser_done',
  1653. # Cookie 持久化
  1654. 'browser_export_cookies',
  1655. 'browser_load_cookies',
  1656. ]