Explorar o código

Merge remote-tracking branch 'origin/dev-liu'

max_liu hai 1 mes
pai
achega
c0dc97ede8

+ 14 - 0
.claude/settings.local.json

@@ -0,0 +1,14 @@
+{
+  "permissions": {
+    "allow": [
+      "Bash(cat:*)",
+      "Bash(python:*)",
+      "Bash(pip show:*)",
+      "Read(//usr/local/anaconda3/lib/python3.13/site-packages/browser_use/**)",
+      "Bash(tee:*)",
+      "Bash(browser-use:*)"
+    ],
+    "deny": [],
+    "ask": []
+  }
+}

+ 5 - 1
.gitignore

@@ -47,7 +47,11 @@ htmlcov/
 # Misc
 .DS_Store
 Thumbs.db
-output/
+
+.env
 debug.log
 info.log
 .browser_use_files
+output
+
+

+ 129 - 0
examples/test_skill.py

@@ -0,0 +1,129 @@
+import json
+import subprocess
+import time
+from pathlib import Path
+
+
+def run_cli(session: str, args: list[str]) -> dict:
+    command = ["browser-use", "--session", session, "--json"] + args
+    result = subprocess.run(command, capture_output=True, text=True)
+    if result.returncode != 0:
+        raise RuntimeError(result.stderr.strip() or "browser-use command failed")
+    payload = result.stdout.strip()
+    if not payload:
+        raise RuntimeError("browser-use returned empty output")
+    data = json.loads(payload)
+    if not data.get("success", False):
+        raise RuntimeError(data.get("error", "browser-use command error"))
+    return data.get("data", {})
+
+
+def stop_session_server(session: str) -> None:
+    subprocess.run(
+        ["browser-use", "--session", session, "server", "stop"],
+        capture_output=True,
+        text=True,
+    )
+
+
+def main():
+    project_root = Path(__file__).resolve().parents[1]
+    output_dir = project_root / "output"
+    output_dir.mkdir(parents=True, exist_ok=True)
+
+    json_file = output_dir / "skill_baidu.json"
+    html_file = output_dir / "skill_baidu_page.html"
+
+    session = "skill_baidu"
+    keyword = "瑜伽美女"
+
+    try:
+        stop_session_server(session)
+        try:
+            run_cli(session, ["open", "https://www.baidu.com"])
+        except RuntimeError:
+            stop_session_server(session)
+            run_cli(session, ["open", "https://www.baidu.com"])
+
+        search_js = (
+            "(function(){"
+            "const input=document.querySelector('#kw');"
+            "const btn=document.querySelector('#su');"
+            "if(input){input.value='" + keyword + "';}"
+            "if(btn){btn.click();}"
+            "else if(input&&input.form){input.form.submit();}"
+            "return {hasInput:!!input,hasButton:!!btn};"
+            "})()"
+        )
+        run_cli(session, ["eval", search_js])
+
+        wait_js = (
+            "(function(){"
+            "const items=document.querySelectorAll('#content_left .result, #content_left .c-container, #content_left .result-op');"
+            "const bodyReady=!!document.body;"
+            "const bodyLen=bodyReady?(document.body.innerText||'').length:0;"
+            "return {count:items.length, bodyReady:bodyReady, bodyLen:bodyLen};"
+            "})()"
+        )
+
+        count = 0
+        for _ in range(12):
+            data = run_cli(session, ["eval", wait_js])
+            result = data.get("result") if isinstance(data, dict) else {}
+            count = int(result.get("count") or 0)
+            body_len = int(result.get("bodyLen") or 0)
+            if count >= 3 or body_len > 1000:
+                break
+            time.sleep(1)
+
+        extract_js = (
+            "(function(){"
+            "const items=Array.from(document.querySelectorAll('#content_left .result, #content_left .c-container, #content_left .result-op'));"
+            "const results=[];"
+            "for(const item of items){"
+            "const a=item.querySelector('h3 a')||item.querySelector('a[data-click]')||item.querySelector('a');"
+            "if(!a) continue;"
+            "const title=(a.textContent||'').trim();"
+            "const link=a.href||'';"
+            "const summaryEl=item.querySelector('.c-abstract, .content-right_8Zs40, .content-right_8Zs40_2gVt2');"
+            "const summary=(summaryEl?summaryEl.textContent:'').trim();"
+            "results.push({index:results.length+1,title,link,summary});"
+            "if(results.length>=10) break;"
+            "}"
+            "return {success:true,keyword:'" + keyword + "',count:results.length,timestamp:new Date().toISOString(),results:results};"
+            "})()"
+        )
+
+        data = run_cli(session, ["eval", extract_js])
+        extracted = data.get("result") if isinstance(data, dict) else data
+
+        if not extracted:
+            extracted = {
+                "success": False,
+                "keyword": keyword,
+                "count": 0,
+                "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S"),
+                "results": [],
+            }
+
+        with open(json_file, "w", encoding="utf-8") as f:
+            json.dump(extracted, f, ensure_ascii=False, indent=2)
+
+        html_data = run_cli(session, ["eval", "document.documentElement.outerHTML"])
+        html_content = html_data.get("result") if isinstance(html_data, dict) else html_data
+
+        with open(html_file, "w", encoding="utf-8") as f:
+            f.write(html_content or "")
+
+        print(f"✅ 数据已保存到: {json_file}")
+        print(f"✅ HTML 已保存到: {html_file}")
+
+    finally:
+        try:
+            run_cli(session, ["close"])
+        except Exception:
+            pass
+
+
+if __name__ == "__main__":
+    main()

+ 141 - 0
examples/test_tools_baidu.py

@@ -0,0 +1,141 @@
+import asyncio
+import json
+import os
+import sys
+from datetime import datetime
+from pathlib import Path
+from urllib.parse import quote
+
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+from tools.baseClassTools import (
+    init_browser_session,
+    navigate_to_url,
+    wait,
+    get_page_html,
+    evaluate,
+    scroll_page,
+    cleanup_browser_session,
+)
+
+
+async def run_task():
+    project_root = Path(__file__).resolve().parents[1]
+    output_dir = project_root / "output"
+    output_dir.mkdir(parents=True, exist_ok=True)
+
+    json_file = output_dir / "baidu.json"
+    html_file = output_dir / "baidu_page.html"
+
+    try:
+        await init_browser_session(headless=False, profile_name="baidu_profile")
+
+        await navigate_to_url("https://www.baidu.com")
+        await wait(seconds=2)
+
+        keyword = "Python 教程"
+        search_url = f"https://www.baidu.com/s?wd={quote(keyword)}"
+        await navigate_to_url(search_url)
+        await wait(seconds=3)
+        await scroll_page(down=True, pages=1.0)
+        await wait(seconds=2)
+
+        extract_js = """
+        (function(){
+            try {
+                const results = [];
+                const resultItems = document.querySelectorAll('#content_left > div[class*="result"]');
+                resultItems.forEach((item, index) => {
+                    if (index >= 10) return;
+                    try {
+                        const titleEl = item.querySelector('h3 a, .t a');
+                        const title = titleEl ? titleEl.textContent.trim() : '';
+                        const link = titleEl ? titleEl.href : '';
+                        const summaryEl = item.querySelector('.c-abstract, .content-right_8Zs40');
+                        const summary = summaryEl ? summaryEl.textContent.trim() : '';
+                        const sourceEl = item.querySelector('.c-color-gray, .source_1Vdff');
+                        const source = sourceEl ? sourceEl.textContent.trim() : '';
+                        if (title || link) {
+                            results.push({
+                                index: index + 1,
+                                title: title,
+                                link: link,
+                                summary: summary.substring(0, 200),
+                                source: source
+                            });
+                        }
+                    } catch (e) {
+                    }
+                });
+                return {
+                    success: true,
+                    count: results.length,
+                    keyword: 'Python 教程',
+                    timestamp: new Date().toISOString(),
+                    results: results
+                };
+            } catch (e) {
+                return {
+                    success: false,
+                    error: e.message,
+                    stack: e.stack
+                };
+            }
+        })()
+        """
+
+        result = await evaluate(code=extract_js)
+        output = result.output
+        if output.startswith("Result: "):
+            output = output[8:]
+
+        try:
+            data = json.loads(output)
+        except json.JSONDecodeError:
+            data = {
+                "success": False,
+                "error": "JSON解析失败",
+                "raw_output": output[:1000],
+                "keyword": keyword,
+                "timestamp": datetime.now().isoformat(),
+            }
+
+        with open(json_file, "w", encoding="utf-8") as f:
+            json.dump(data, f, ensure_ascii=False, indent=2)
+
+        html_result = await get_page_html()
+        html_content = html_result.metadata.get("html", "")
+        page_url = html_result.metadata.get("url", "")
+        page_title = html_result.metadata.get("title", "")
+        meta_info = (
+            "\n".join(
+                [
+                    "<!--",
+                    f"    页面标题: {page_title}",
+                    f"    页面URL: {page_url}",
+                    f"    保存时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
+                    f"    搜索关键词: {keyword}",
+                    "-->",
+                    "",
+                ]
+            )
+            + "\n"
+        )
+
+        with open(html_file, "w", encoding="utf-8") as f:
+            f.write(meta_info)
+            f.write(html_content)
+
+        print(f"✅ 数据已保存到: {json_file}")
+        print(f"✅ HTML 已保存到: {html_file}")
+
+    finally:
+        await cleanup_browser_session()
+
+
+def main():
+    asyncio.run(run_task())
+
+
+if __name__ == "__main__":
+    main()

+ 0 - 581
examples/tools_examples.py

@@ -1,581 +0,0 @@
-"""
-工具系统完整示例
-
-本文件展示 @tool 装饰器的所有用法,包括:
-
-## 基础功能
-1. 最简形式
-2. 带 i18n 展示信息
-3. 带可编辑参数
-4. 需要用户确认
-5. 带 context 参数
-6. 同步工具
-7. 复杂返回类型
-
-## 高级功能
-8. 域名过滤(URL Patterns)
-9. 敏感数据处理(<secret> 占位符 + TOTP)
-10. 工具使用统计
-11. 组合所有功能
-
-注意:
-- uid 参数会由框架自动注入,不需要用户传递
-- context 参数用于传递额外上下文(如浏览器会话、当前 URL 等)
-- 返回值可以是字符串、字典或 ToolResult
-"""
-
-import asyncio
-import json
-from typing import List, Dict, Any, Optional
-from agent import tool, ToolResult, ToolContext, get_tool_registry
-
-
-# ============================================================
-# 基础功能示例
-# ============================================================
-
-# 1. 最简形式
-@tool()
-async def hello_world(name: str, uid: str = "") -> Dict[str, str]:
-	"""
-	最简单的工具示例
-
-	Args:
-		name: 要问候的名字
-		uid: 用户ID(自动注入)
-
-	Returns:
-		包含问候语的字典
-	"""
-	return {"greeting": f"Hello, {name}!"}
-
-
-# 2. 带 i18n 展示信息的工具
-@tool(
-	display={
-		"zh": {
-			"name": "搜索内容",
-			"params": {
-				"query": "搜索关键词",
-				"limit": "返回数量"
-			}
-		},
-		"en": {
-			"name": "Search Content",
-			"params": {
-				"query": "Search query",
-				"limit": "Number of results"
-			}
-		}
-	}
-)
-async def search_content(
-	query: str,
-	limit: int = 10,
-	uid: str = ""
-) -> List[Dict[str, Any]]:
-	"""
-	搜索用户的内容
-
-	使用语义搜索查找相关内容。display 参数用于前端展示:
-	- 工具名称会根据用户语言显示为"搜索内容"或"Search Content"
-	- 参数名称也会相应翻译
-
-	Args:
-		query: 搜索查询文本
-		limit: 返回结果数量(默认10)
-		uid: 用户ID(自动注入)
-
-	Returns:
-		搜索结果列表,每个包含 id, title, content, score
-	"""
-	# 实际实现中会调用向量搜索
-	return [
-		{
-			"id": "doc_001",
-			"title": f"关于 {query} 的文档",
-			"content": f"这是与 {query} 相关的内容...",
-			"score": 0.95
-		}
-	]
-
-
-# 3. 带可编辑参数的工具
-@tool(
-	editable_params=["query", "filters"],
-	display={
-		"zh": {
-			"name": "高级搜索",
-			"params": {
-				"query": "搜索关键词",
-				"filters": "过滤条件",
-				"sort_by": "排序方式"
-			}
-		}
-	}
-)
-async def advanced_search(
-	query: str,
-	filters: Optional[Dict[str, Any]] = None,
-	sort_by: str = "relevance",
-	limit: int = 20,
-	uid: str = ""
-) -> Dict[str, Any]:
-	"""
-	高级搜索工具(允许用户编辑参数)
-
-	editable_params 指定哪些参数允许用户在 LLM 生成后编辑:
-	- LLM 会先生成 query 和 filters
-	- 用户可以在确认前修改这些参数
-	- 适用于搜索、创建等需要用户微调的场景
-
-	Args:
-		query: 搜索查询
-		filters: 过滤条件(如 {"type": "note", "date_range": "7d"})
-		sort_by: 排序方式(relevance/date/title)
-		limit: 返回数量
-		uid: 用户ID(自动注入)
-
-	Returns:
-		搜索结果和元数据
-	"""
-	return {
-		"results": [
-			{"id": "1", "title": "Result 1", "score": 0.9},
-			{"id": "2", "title": "Result 2", "score": 0.8},
-		],
-		"total": 42,
-		"query": query,
-		"filters_applied": filters or {},
-		"sort_by": sort_by
-	}
-
-
-# 4. 需要用户确认的危险操作
-@tool(
-	requires_confirmation=True,
-	display={
-		"zh": {
-			"name": "删除内容",
-			"params": {
-				"content_id": "内容ID",
-				"permanent": "永久删除"
-			}
-		}
-	}
-)
-async def delete_content(
-	content_id: str,
-	permanent: bool = False,
-	uid: str = ""
-) -> Dict[str, Any]:
-	"""
-	删除内容(需要用户确认)
-
-	requires_confirmation=True 表示这是一个危险操作:
-	- LLM 调用此工具时,不会立即执行
-	- 会先向用户展示操作详情,等待确认
-	- 用户确认后才会真正执行
-
-	适用场景:删除操作、发送消息、修改重要设置、任何不可逆操作
-
-	Args:
-		content_id: 要删除的内容ID
-		permanent: 是否永久删除(False=移到回收站)
-		uid: 用户ID(自动注入)
-
-	Returns:
-		删除结果
-	"""
-	return {
-		"success": True,
-		"content_id": content_id,
-		"permanent": permanent,
-		"message": f"内容 {content_id} 已{'永久删除' if permanent else '移到回收站'}"
-	}
-
-
-# 5. 带 context 参数的工具
-@tool(
-	display={
-		"zh": {"name": "获取相关推荐", "params": {"top_k": "推荐数量"}}
-	}
-)
-async def get_recommendations(
-	top_k: int = 5,
-	uid: str = "",
-	context: Optional[Dict[str, Any]] = None
-) -> List[Dict[str, Any]]:
-	"""
-	获取相关推荐(使用 context 获取额外信息)
-
-	context 参数用于传递执行上下文,由框架自动注入:
-	- 当前阅读位置 (current_location)
-	- 当前会话 ID (session_id)
-	- 排除的内容 ID (exclude_ids)
-
-	Args:
-		top_k: 返回推荐数量
-		uid: 用户ID(自动注入)
-		context: 执行上下文(自动注入)
-
-	Returns:
-		推荐列表
-	"""
-	current_location = None
-	if context:
-		current_location = context.get("current_location")
-
-	return [
-		{
-			"id": "rec_001",
-			"title": "推荐内容 1",
-			"reason": f"基于当前位置 {current_location}" if current_location else "基于您的兴趣"
-		}
-	]
-
-
-# 6. 同步工具(非 async)
-@tool()
-def format_text(
-	text: str,
-	format_type: str = "markdown",
-	uid: str = ""
-) -> str:
-	"""
-	格式化文本(同步工具)
-
-	不需要 async 的工具可以定义为普通函数。
-	框架会自动检测并正确调用。
-
-	适用于:纯计算操作、文本处理、不需要 I/O 的操作
-
-	Args:
-		text: 要格式化的文本
-		format_type: 格式类型(markdown/plain/html)
-		uid: 用户ID(自动注入)
-
-	Returns:
-		格式化后的文本
-	"""
-	if format_type == "markdown":
-		return f"**{text}**"
-	elif format_type == "html":
-		return f"<p>{text}</p>"
-	else:
-		return text
-
-
-# 7. 使用 ToolResult 的工具
-@tool()
-async def analyze_content(
-	content_id: str,
-	analysis_types: Optional[List[str]] = None,
-	uid: str = ""
-) -> ToolResult:
-	"""
-	分析内容(使用 ToolResult)
-
-	ToolResult 支持双层记忆管理:
-	- output: 完整结果(可能很长)
-	- long_term_memory: 简短摘要(永久保存)
-
-	Args:
-		content_id: 要分析的内容ID
-		analysis_types: 分析类型列表(sentiment/keywords/summary)
-		uid: 用户ID(自动注入)
-
-	Returns:
-		ToolResult 包含分析结果
-	"""
-	types = analysis_types or ["sentiment", "keywords"]
-
-	result = {
-		"content_id": content_id,
-		"analyses": {}
-	}
-
-	if "sentiment" in types:
-		result["analyses"]["sentiment"] = {
-			"score": 0.8,
-			"label": "positive",
-			"confidence": 0.92
-		}
-
-	if "keywords" in types:
-		result["analyses"]["keywords"] = [
-			{"word": "AI", "weight": 0.9},
-			{"word": "学习", "weight": 0.7}
-		]
-
-	return ToolResult(
-		title=f"Analysis of {content_id}",
-		output=json.dumps(result, indent=2, ensure_ascii=False),
-		long_term_memory=f"Analyzed {content_id}: {', '.join(types)}",
-		metadata={"types": types}
-	)
-
-
-# ============================================================
-# 高级功能示例
-# ============================================================
-
-# 8. 域名过滤示例
-@tool(url_patterns=["*.google.com", "www.google.*"])
-async def google_search(query: str, uid: str = "") -> ToolResult:
-	"""
-	Google 搜索(仅在 Google 页面可用)
-
-	使用 url_patterns 限制工具只在特定域名显示。
-	在 Google 页面时,此工具会出现在可用工具列表中。
-	在其他页面时,此工具会被过滤掉。
-
-	Args:
-		query: 搜索查询
-		uid: 用户ID(自动注入)
-
-	Returns:
-		搜索结果
-	"""
-	return ToolResult(
-		title="Google Search",
-		output=f"Searching Google for: {query}",
-		long_term_memory=f"Searched Google for '{query}'"
-	)
-
-
-@tool(url_patterns=["*.github.com"])
-async def create_github_issue(
-	title: str,
-	body: str,
-	uid: str = ""
-) -> ToolResult:
-	"""
-	创建 GitHub Issue(仅在 GitHub 页面可用)
-
-	Args:
-		title: Issue 标题
-		body: Issue 内容
-		uid: 用户ID(自动注入)
-
-	Returns:
-		创建结果
-	"""
-	return ToolResult(
-		title="Issue Created",
-		output=f"Created issue: {title}",
-		long_term_memory=f"Created GitHub issue: {title}"
-	)
-
-
-@tool()  # 无 url_patterns,所有页面都可用
-async def take_screenshot(uid: str = "") -> ToolResult:
-	"""截图(所有页面都可用)"""
-	return ToolResult(
-		title="Screenshot",
-		output="Screenshot taken",
-		attachments=["screenshot_001.png"]
-	)
-
-
-# 9. 敏感数据处理示例
-@tool(url_patterns=["*.github.com"])
-async def github_login(
-	username: str,
-	password: str,
-	totp_code: str,
-	uid: str = ""
-) -> ToolResult:
-	"""
-	GitHub 登录(支持敏感数据占位符)
-
-	LLM 会输出类似:
-	{
-		"username": "user@example.com",
-		"password": "<secret>github_password</secret>",
-		"totp_code": "<secret>github_2fa_bu_2fa_code</secret>"
-	}
-
-	执行时会自动替换为实际值。
-
-	Args:
-		username: 用户名
-		password: 密码(可以是占位符)
-		totp_code: TOTP 验证码(可以是占位符,自动生成)
-		uid: 用户ID(自动注入)
-
-	Returns:
-		登录结果
-	"""
-	# 注意:password 和 totp_code 在到达这里时已经被替换
-	return ToolResult(
-		title="Login Successful",
-		output=f"Logged in as {username}",
-		long_term_memory=f"Logged in to GitHub as {username}"
-	)
-
-
-# 10. 组合所有功能
-@tool(
-	url_patterns=["*.example.com"],
-	requires_confirmation=True,
-	editable_params=["message"],
-	display={
-		"zh": {
-			"name": "发送认证消息",
-			"params": {
-				"recipient": "接收者",
-				"message": "消息内容",
-				"api_key": "API密钥"
-			}
-		}
-	}
-)
-async def send_authenticated_message(
-	recipient: str,
-	message: str,
-	api_key: str,
-	ctx: ToolContext,
-	uid: str = ""
-) -> ToolResult:
-	"""
-	发送消息(组合多个功能)
-
-	展示所有高级功能:
-	- 仅在 example.com 可用(域名过滤)
-	- 需要用户确认(危险操作)
-	- 消息可编辑(用户微调)
-	- API key 使用敏感数据占位符
-	- 使用 ToolContext 获取上下文
-
-	Args:
-		recipient: 接收者
-		message: 消息内容
-		api_key: API密钥(可以是占位符)
-		ctx: 工具上下文
-		uid: 用户ID(自动注入)
-
-	Returns:
-		发送结果
-	"""
-	# api_key 会从 <secret>api_key</secret> 替换为实际值
-	# ctx 包含 page_url, browser_session 等信息
-
-	return ToolResult(
-		title="Message Sent",
-		output=f"Sent to {recipient}: {message}",
-		long_term_memory=f"Sent message to {recipient} on {ctx.page_url}",
-		metadata={"recipient": recipient}
-	)
-
-
-# ============================================================
-# 使用示例
-# ============================================================
-
-async def main():
-	registry = get_tool_registry()
-
-	print("=" * 60)
-	print("工具系统完整示例")
-	print("=" * 60)
-
-	# ============================================================
-	# 示例 1:基础工具调用
-	# ============================================================
-	print("\n1. 基础工具调用")
-	print("-" * 60)
-
-	result = await registry.execute("hello_world", {"name": "Alice"})
-	print(f"hello_world: {result}")
-
-	result = await registry.execute("search_content", {"query": "Python", "limit": 5})
-	print(f"search_content: {result}")
-
-	# ============================================================
-	# 示例 2:域名过滤
-	# ============================================================
-	print("\n\n2. 域名过滤示例")
-	print("-" * 60)
-
-	# 在 Google 页面
-	google_url = "https://www.google.com/search?q=test"
-	google_tools = registry.get_tool_names(google_url)
-	print(f"在 {google_url} 可用的工具:")
-	print(f"  包含 google_search: {'google_search' in google_tools}")
-
-	# 在 GitHub 页面
-	github_url = "https://github.com/user/repo"
-	github_tools = registry.get_tool_names(github_url)
-	print(f"\n在 {github_url} 可用的工具:")
-	print(f"  包含 create_github_issue: {'create_github_issue' in github_tools}")
-	print(f"  包含 google_search: {'google_search' in github_tools}")
-
-	# ============================================================
-	# 示例 3:敏感数据处理
-	# ============================================================
-	print("\n\n3. 敏感数据处理示例")
-	print("-" * 60)
-
-	# 配置敏感数据
-	sensitive_data = {
-		"*.github.com": {
-			"github_password": "my_secret_password",
-			"github_2fa_bu_2fa_code": "JBSWY3DPEHPK3PXP"  # TOTP secret
-		}
-	}
-
-	# 模拟 LLM 输出(包含占位符)
-	llm_output_args = {
-		"username": "user@example.com",
-		"password": "<secret>github_password</secret>",
-		"totp_code": "<secret>github_2fa_bu_2fa_code</secret>"
-	}
-
-	print("LLM 输出的参数(包含占位符):")
-	print(f"  {llm_output_args}")
-
-	# 执行工具(自动替换敏感数据)
-	result = await registry.execute(
-		"github_login",
-		llm_output_args,
-		context={"page_url": "https://github.com/login"},
-		sensitive_data=sensitive_data
-	)
-
-	print(f"\n执行结果(密码已替换):")
-	print(f"  {result}")
-
-	# ============================================================
-	# 示例 4:工具统计
-	# ============================================================
-	print("\n\n4. 工具统计示例")
-	print("-" * 60)
-
-	# 模拟多次调用
-	for i in range(5):
-		await registry.execute("google_search", {"query": f"test {i}"})
-
-	await registry.execute("take_screenshot", {})
-	await registry.execute("take_screenshot", {})
-
-	# 查看统计
-	stats = registry.get_stats()
-	print("工具使用统计:")
-	for tool_name, tool_stats in stats.items():
-		if tool_stats["call_count"] > 0:
-			print(f"\n  {tool_name}:")
-			print(f"    调用次数: {tool_stats['call_count']}")
-			print(f"    成功率: {tool_stats['success_rate']:.1%}")
-			print(f"    平均执行时间: {tool_stats['average_duration']:.3f}s")
-
-	# 获取 Top 工具
-	print("\n\nTop 3 最常用工具:")
-	top_tools = registry.get_top_tools(limit=3, by="call_count")
-	for i, tool_name in enumerate(top_tools, 1):
-		tool_stats = stats[tool_name]
-		print(f"  {i}. {tool_name} ({tool_stats['call_count']} 次调用)")
-
-
-if __name__ == "__main__":
-	asyncio.run(main())

+ 8 - 8
tests/test_runner.py

@@ -13,7 +13,7 @@ from agent import (
     tool,
     get_tool_registry,
 )
-from reson_agent.storage import MemoryTraceStore, MemoryMemoryStore
+from agent.storage import MemoryTraceStore, MemoryMemoryStore
 
 
 # 测试工具
@@ -21,7 +21,7 @@ from reson_agent.storage import MemoryTraceStore, MemoryMemoryStore
     editable_params=["query"],
     display={"zh": {"name": "测试搜索", "params": {"query": "关键词"}}}
 )
-async def test_search(query: str, limit: int = 10, uid: str = "") -> dict:
+async def search_tool(query: str, limit: int = 10, uid: str = "") -> dict:
     """测试搜索工具"""
     return {"results": [f"结果: {query}"], "count": 1}
 
@@ -43,7 +43,7 @@ async def mock_llm_call(
             "tool_calls": [{
                 "id": "call_123",
                 "function": {
-                    "name": "test_search",
+                    "name": "search_tool",
                     "arguments": '{"query": "测试查询"}'
                 }
             }],
@@ -136,18 +136,18 @@ class TestToolRegistry:
 
     def test_tool_registered(self):
         registry = get_tool_registry()
-        assert registry.is_registered("test_search")
+        assert registry.is_registered("search_tool")
 
     def test_get_schemas(self):
         registry = get_tool_registry()
-        schemas = registry.get_schemas(["test_search"])
+        schemas = registry.get_schemas(["search_tool"])
         assert len(schemas) == 1
-        assert schemas[0]["function"]["name"] == "test_search"
+        assert schemas[0]["function"]["name"] == "search_tool"
 
     @pytest.mark.asyncio
     async def test_execute_tool(self):
         registry = get_tool_registry()
-        result = await registry.execute("test_search", {"query": "hello"}, uid="test")
+        result = await registry.execute("search_tool", {"query": "hello"}, uid="test")
         assert "结果" in result
 
 
@@ -202,7 +202,7 @@ class TestAgentRunner:
         events = []
         async for event in runner.run(
             task="请搜索相关内容",
-            tools=["test_search"],
+            tools=["search_tool"],
             agent_type="test"
         ):
             events.append(event)

+ 1299 - 0
tools/baseClassTools.py

@@ -0,0 +1,1299 @@
+"""
+Browser-Use 原生工具适配器
+Native Browser-Use Tools Adapter
+
+直接使用 browser-use 的原生类(BrowserSession, Tools)实现所有浏览器操作工具。
+不依赖 Playwright,完全基于 CDP 协议。
+
+核心特性:
+1. 浏览器会话持久化 - 只启动一次浏览器
+2. 状态自动保持 - 登录状态、Cookie、LocalStorage 等
+3. 完整的底层访问 - 可以直接使用 CDP 协议
+4. 性能优异 - 避免频繁创建/销毁浏览器实例
+
+使用方法:
+1. 在 Agent 初始化时调用 init_browser_session()
+2. 使用各个工具函数执行浏览器操作
+3. 任务结束时调用 cleanup_browser_session()
+"""
+
+import sys
+import os
+from typing import Optional, List
+from pathlib import Path
+
+# 将项目根目录添加到 Python 路径
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+# 导入框架的工具装饰器和结果类
+from agent.tools import tool, ToolResult
+
+# 导入 browser-use 的核心类
+from browser_use import BrowserSession, BrowserProfile
+from browser_use.tools.service import Tools
+from browser_use.agent.views import ActionResult
+from browser_use.filesystem.file_system import FileSystem
+
+# ============================================================
+# 全局浏览器会话管理
+# ============================================================
+
+# 全局变量:浏览器会话和工具实例
+_browser_session: Optional[BrowserSession] = None
+_browser_tools: Optional[Tools] = None
+_file_system: Optional[FileSystem] = None
+
+
+async def init_browser_session(
+    headless: bool = False,
+    user_data_dir: Optional[str] = None,
+    profile_name: str = "default",
+    browser_profile: Optional[BrowserProfile] = None,
+    **kwargs
+) -> tuple[BrowserSession, Tools]:
+    """
+    初始化全局浏览器会话
+
+    Args:
+        headless: 是否无头模式
+        user_data_dir: 用户数据目录(用于保存登录状态)
+        profile_name: 配置文件名称
+        browser_profile: BrowserProfile 对象(用于预设 cookies 等)
+        **kwargs: 其他 BrowserSession 参数
+
+    Returns:
+        (BrowserSession, Tools) 元组
+    """
+    global _browser_session, _browser_tools, _file_system
+
+    if _browser_session is not None:
+        return _browser_session, _browser_tools
+
+    # 设置用户数据目录(持久化登录状态)
+    if user_data_dir is None and profile_name:
+        user_data_dir = str(Path.home() / ".browser_use" / "profiles" / profile_name)
+        Path(user_data_dir).mkdir(parents=True, exist_ok=True)
+
+    # 创建浏览器会话
+    # 明确指定 is_local=True 以确保本地浏览器启动
+    session_params = {
+        "headless": headless,
+        "is_local": True,  # 明确指定本地浏览器
+    }
+
+    # macOS 上显式指定 Chrome 路径
+    import platform
+    if platform.system() == "Darwin":  # macOS
+        chrome_path = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
+        if Path(chrome_path).exists():
+            session_params["executable_path"] = chrome_path
+
+    # 只在有值时才添加 user_data_dir
+    if user_data_dir:
+        session_params["user_data_dir"] = user_data_dir
+
+    # 只在有值时才添加 browser_profile
+    if browser_profile:
+        session_params["browser_profile"] = browser_profile
+
+    # 合并其他参数
+    session_params.update(kwargs)
+
+    _browser_session = BrowserSession(**session_params)
+
+    # 启动浏览器
+    await _browser_session.start()
+
+    # 创建工具实例
+    _browser_tools = Tools()
+
+    # 创建文件系统实例(用于文件操作)
+    base_dir = Path.cwd() / ".browser_use_files"
+    base_dir.mkdir(parents=True, exist_ok=True)
+    _file_system = FileSystem(base_dir=str(base_dir))
+
+    return _browser_session, _browser_tools
+
+
+async def get_browser_session() -> tuple[BrowserSession, Tools]:
+    """
+    获取当前浏览器会话,如果不存在则自动创建
+
+    Returns:
+        (BrowserSession, Tools) 元组
+    """
+    global _browser_session, _browser_tools
+
+    if _browser_session is None:
+        await init_browser_session()
+
+    return _browser_session, _browser_tools
+
+
+async def cleanup_browser_session():
+    """
+    清理浏览器会话
+    优雅地停止浏览器但保留会话状态
+    """
+    global _browser_session, _browser_tools, _file_system
+
+    if _browser_session is not None:
+        await _browser_session.stop()
+        _browser_session = None
+        _browser_tools = None
+        _file_system = None
+
+
+async def kill_browser_session():
+    """
+    强制终止浏览器会话
+    完全关闭浏览器进程
+    """
+    global _browser_session, _browser_tools, _file_system
+
+    if _browser_session is not None:
+        await _browser_session.kill()
+        _browser_session = None
+        _browser_tools = None
+        _file_system = None
+
+
+# ============================================================
+# 辅助函数:ActionResult 转 ToolResult
+# ============================================================
+
+def action_result_to_tool_result(result: ActionResult, title: str = None) -> ToolResult:
+    """
+    将 browser-use 的 ActionResult 转换为框架的 ToolResult
+
+    Args:
+        result: browser-use 的 ActionResult
+        title: 可选的标题(如果不提供则从 result 推断)
+
+    Returns:
+        ToolResult
+    """
+    if result.error:
+        return ToolResult(
+            title=title or "操作失败",
+            output="",
+            error=result.error,
+            long_term_memory=result.long_term_memory or result.error
+        )
+
+    return ToolResult(
+        title=title or "操作成功",
+        output=result.extracted_content or "",
+        long_term_memory=result.long_term_memory or result.extracted_content or "",
+        metadata=result.metadata or {}
+    )
+
+
+# ============================================================
+# 导航类工具 (Navigation Tools)
+# ============================================================
+
+@tool()
+async def navigate_to_url(url: str, new_tab: bool = False, uid: str = "") -> ToolResult:
+    """
+    导航到指定的 URL
+    Navigate to a specific URL
+
+    使用 browser-use 的原生导航功能,支持在新标签页打开。
+
+    Args:
+        url: 要访问的 URL 地址
+        new_tab: 是否在新标签页中打开(默认 False)
+        uid: 用户 ID(由框架自动注入)
+
+    Returns:
+        ToolResult: 包含导航结果的工具返回对象
+
+    Example:
+        navigate_to_url("https://www.baidu.com")
+        navigate_to_url("https://www.google.com", new_tab=True)
+    """
+    try:
+        browser, tools = await get_browser_session()
+
+        # 使用 browser-use 的 navigate 工具
+        result = await tools.navigate(
+            url=url,
+            new_tab=new_tab,
+            browser_session=browser
+        )
+
+        return action_result_to_tool_result(result, f"导航到 {url}")
+
+    except Exception as e:
+        return ToolResult(
+            title="导航失败",
+            output="",
+            error=f"Failed to navigate to {url}: {str(e)}",
+            long_term_memory=f"导航到 {url} 失败"
+        )
+
+
+@tool()
+async def search_web(query: str, engine: str = "google", uid: str = "") -> ToolResult:
+    """
+    使用搜索引擎搜索
+    Search the web using a search engine
+
+    Args:
+        query: 搜索关键词
+        engine: 搜索引擎 (google, duckduckgo, bing) - 默认: google
+        uid: 用户 ID(由框架自动注入)
+
+    Returns:
+        ToolResult: 搜索结果
+
+    Example:
+        search_web("Python async programming", engine="google")
+    """
+    try:
+        browser, tools = await get_browser_session()
+
+        # 使用 browser-use 的 search 工具
+        result = await tools.search(
+            query=query,
+            engine=engine,
+            browser_session=browser
+        )
+
+        return action_result_to_tool_result(result, f"搜索: {query}")
+
+    except Exception as e:
+        return ToolResult(
+            title="搜索失败",
+            output="",
+            error=f"Search failed: {str(e)}",
+            long_term_memory=f"搜索 '{query}' 失败"
+        )
+
+
+@tool()
+async def go_back(uid: str = "") -> ToolResult:
+    """
+    返回到上一个页面
+    Go back to the previous page
+
+    模拟浏览器的"后退"按钮功能。
+
+    Args:
+        uid: 用户 ID(由框架自动注入)
+
+    Returns:
+        ToolResult: 包含返回操作结果的工具返回对象
+    """
+    try:
+        browser, tools = await get_browser_session()
+
+        result = await tools.go_back(browser_session=browser)
+
+        return action_result_to_tool_result(result, "返回上一页")
+
+    except Exception as e:
+        return ToolResult(
+            title="返回失败",
+            output="",
+            error=f"Failed to go back: {str(e)}",
+            long_term_memory="返回上一页失败"
+        )
+
+
+@tool()
+async def wait(seconds: int = 3, uid: str = "") -> ToolResult:
+    """
+    等待指定的秒数
+    Wait for a specified number of seconds
+
+    用于等待页面加载、动画完成或其他异步操作。
+
+    Args:
+        seconds: 等待时间(秒),最大30秒
+        uid: 用户 ID(由框架自动注入)
+
+    Returns:
+        ToolResult: 包含等待操作结果的工具返回对象
+
+    Example:
+        wait(5)  # 等待5秒
+    """
+    try:
+        browser, tools = await get_browser_session()
+
+        result = await tools.wait(seconds=seconds, browser_session=browser)
+
+        return action_result_to_tool_result(result, f"等待 {seconds} 秒")
+
+    except Exception as e:
+        return ToolResult(
+            title="等待失败",
+            output="",
+            error=f"Failed to wait: {str(e)}",
+            long_term_memory="等待失败"
+        )
+
+
+# ============================================================
+# 元素交互工具 (Element Interaction Tools)
+# ============================================================
+
+@tool()
+async def click_element(index: int, uid: str = "") -> ToolResult:
+    """
+    通过索引点击页面元素
+    Click an element by index
+
+    Args:
+        index: 元素索引(从浏览器状态中获取)
+        uid: 用户 ID(由框架自动注入)
+
+    Returns:
+        ToolResult: 包含点击操作结果的工具返回对象
+
+    Example:
+        click_element(index=5)
+
+    Note:
+        需要先通过 get_selector_map 获取页面元素索引
+    """
+    try:
+        browser, tools = await get_browser_session()
+
+        result = await tools.click(
+            index=index,
+            browser_session=browser
+        )
+
+        return action_result_to_tool_result(result, f"点击元素 {index}")
+
+    except Exception as e:
+        return ToolResult(
+            title="点击失败",
+            output="",
+            error=f"Failed to click element {index}: {str(e)}",
+            long_term_memory=f"点击元素 {index} 失败"
+        )
+
+
+@tool()
+async def input_text(index: int, text: str, clear: bool = True, uid: str = "") -> ToolResult:
+    """
+    在指定元素中输入文本
+    Input text into an element
+
+    Args:
+        index: 元素索引(从浏览器状态中获取)
+        text: 要输入的文本内容
+        clear: 是否先清除现有文本(默认 True)
+        uid: 用户 ID(由框架自动注入)
+
+    Returns:
+        ToolResult: 包含输入操作结果的工具返回对象
+
+    Example:
+        input_text(index=0, text="Hello World", clear=True)
+    """
+    try:
+        browser, tools = await get_browser_session()
+
+        result = await tools.input(
+            index=index,
+            text=text,
+            clear=clear,
+            browser_session=browser
+        )
+
+        return action_result_to_tool_result(result, f"输入文本到元素 {index}")
+
+    except Exception as e:
+        return ToolResult(
+            title="输入失败",
+            output="",
+            error=f"Failed to input text into element {index}: {str(e)}",
+            long_term_memory=f"输入文本失败"
+        )
+
+
+@tool()
+async def send_keys(keys: str, uid: str = "") -> ToolResult:
+    """
+    发送键盘按键或快捷键
+    Send keyboard keys or shortcuts
+
+    支持发送单个按键、组合键和快捷键。
+
+    Args:
+        keys: 要发送的按键字符串
+              - 单个按键: "Enter", "Escape", "PageDown", "Tab"
+              - 组合键: "Control+o", "Shift+Tab", "Alt+F4"
+              - 功能键: "F1", "F2", ..., "F12"
+        uid: 用户 ID(由框架自动注入)
+
+    Returns:
+        ToolResult: 包含按键操作结果的工具返回对象
+
+    Example:
+        send_keys("Enter")
+        send_keys("Control+A")
+    """
+    try:
+        browser, tools = await get_browser_session()
+
+        result = await tools.send_keys(
+            keys=keys,
+            browser_session=browser
+        )
+
+        return action_result_to_tool_result(result, f"发送按键: {keys}")
+
+    except Exception as e:
+        return ToolResult(
+            title="发送按键失败",
+            output="",
+            error=f"Failed to send keys: {str(e)}",
+            long_term_memory="发送按键失败"
+        )
+
+
+@tool()
+async def upload_file(index: int, path: str, uid: str = "") -> ToolResult:
+    """
+    上传文件到文件输入元素
+    Upload a file to a file input element
+
+    Args:
+        index: 文件输入框的元素索引
+        path: 要上传的文件路径(绝对路径)
+        uid: 用户 ID(由框架自动注入)
+
+    Returns:
+        ToolResult: 包含上传操作结果的工具返回对象
+
+    Example:
+        upload_file(index=7, path="/path/to/file.pdf")
+
+    Note:
+        文件必须存在且路径必须是绝对路径
+    """
+    try:
+        browser, tools = await get_browser_session()
+
+        result = await tools.upload_file(
+            index=index,
+            path=path,
+            browser_session=browser,
+            available_file_paths=[path],
+            file_system=_file_system
+        )
+
+        return action_result_to_tool_result(result, f"上传文件: {path}")
+
+    except Exception as e:
+        return ToolResult(
+            title="上传失败",
+            output="",
+            error=f"Failed to upload file: {str(e)}",
+            long_term_memory=f"上传文件 {path} 失败"
+        )
+
+
+# ============================================================
+# 滚动和视图工具 (Scroll & View Tools)
+# ============================================================
+
+@tool()
+async def scroll_page(down: bool = True, pages: float = 1.0,
+                     index: Optional[int] = None, uid: str = "") -> ToolResult:
+    """
+    滚动页面或元素
+    Scroll the page or a specific element
+
+    Args:
+        down: True 向下滚动,False 向上滚动
+        pages: 滚动页数(0.5=半页,1=全页,10=滚动到底部/顶部)
+        index: 可选,滚动特定元素(如下拉框内部)
+        uid: 用户 ID(由框架自动注入)
+
+    Returns:
+        ToolResult: 滚动结果
+
+    Example:
+        scroll_page(down=True, pages=2.0)  # 向下滚动2页
+        scroll_page(down=False, pages=1.0)  # 向上滚动1页
+    """
+    try:
+        browser, tools = await get_browser_session()
+
+        result = await tools.scroll(
+            down=down,
+            pages=pages,
+            index=index,
+            browser_session=browser
+        )
+
+        direction = "向下" if down else "向上"
+        return action_result_to_tool_result(result, f"{direction}滚动 {pages} 页")
+
+    except Exception as e:
+        return ToolResult(
+            title="滚动失败",
+            output="",
+            error=f"Failed to scroll: {str(e)}",
+            long_term_memory="滚动失败"
+        )
+
+
+@tool()
+async def find_text(text: str, uid: str = "") -> ToolResult:
+    """
+    查找页面中的文本并滚动到该位置
+    Find text on the page and scroll to it
+
+    在页面中搜索指定的文本,找到后自动滚动到该位置。
+
+    Args:
+        text: 要查找的文本内容
+        uid: 用户 ID(由框架自动注入)
+
+    Returns:
+        ToolResult: 包含查找结果的工具返回对象
+
+    Example:
+        find_text("Privacy Policy")
+    """
+    try:
+        browser, tools = await get_browser_session()
+
+        result = await tools.find_text(
+            text=text,
+            browser_session=browser
+        )
+
+        return action_result_to_tool_result(result, f"查找文本: {text}")
+
+    except Exception as e:
+        return ToolResult(
+            title="查找失败",
+            output="",
+            error=f"Failed to find text: {str(e)}",
+            long_term_memory=f"查找文本 '{text}' 失败"
+        )
+
+
+@tool()
+async def screenshot(uid: str = "") -> ToolResult:
+    """
+    请求在下次观察中包含页面截图
+    Request a screenshot to be included in the next observation
+
+    用于视觉检查页面状态,帮助理解页面布局和内容。
+
+    Args:
+        uid: 用户 ID(由框架自动注入)
+
+    Returns:
+        ToolResult: 包含截图请求结果的工具返回对象
+
+    Example:
+        screenshot()
+
+    Note:
+        截图会在下次页面观察时自动包含在结果中。
+    """
+    try:
+        browser, tools = await get_browser_session()
+
+        result = await tools.screenshot(browser_session=browser)
+
+        return action_result_to_tool_result(result, "截图请求")
+
+    except Exception as e:
+        return ToolResult(
+            title="截图失败",
+            output="",
+            error=f"Failed to capture screenshot: {str(e)}",
+            long_term_memory="截图失败"
+        )
+
+
+# ============================================================
+# 标签页管理工具 (Tab Management Tools)
+# ============================================================
+
+@tool()
+async def switch_tab(tab_id: str, uid: str = "") -> ToolResult:
+    """
+    切换到指定标签页
+    Switch to a different browser tab
+
+    Args:
+        tab_id: 4字符标签ID(target_id 的最后4位)
+        uid: 用户 ID(由框架自动注入)
+
+    Returns:
+        ToolResult: 切换结果
+
+    Example:
+        switch_tab(tab_id="a3f2")
+    """
+    try:
+        browser, tools = await get_browser_session()
+
+        result = await tools.switch(
+            tab_id=tab_id,
+            browser_session=browser
+        )
+
+        return action_result_to_tool_result(result, f"切换到标签页 {tab_id}")
+
+    except Exception as e:
+        return ToolResult(
+            title="切换标签页失败",
+            output="",
+            error=f"Failed to switch tab: {str(e)}",
+            long_term_memory=f"切换到标签页 {tab_id} 失败"
+        )
+
+
+@tool()
+async def close_tab(tab_id: str, uid: str = "") -> ToolResult:
+    """
+    关闭指定标签页
+    Close a browser tab
+
+    Args:
+        tab_id: 4字符标签ID
+        uid: 用户 ID(由框架自动注入)
+
+    Returns:
+        ToolResult: 关闭结果
+
+    Example:
+        close_tab(tab_id="a3f2")
+    """
+    try:
+        browser, tools = await get_browser_session()
+
+        result = await tools.close(
+            tab_id=tab_id,
+            browser_session=browser
+        )
+
+        return action_result_to_tool_result(result, f"关闭标签页 {tab_id}")
+
+    except Exception as e:
+        return ToolResult(
+            title="关闭标签页失败",
+            output="",
+            error=f"Failed to close tab: {str(e)}",
+            long_term_memory=f"关闭标签页 {tab_id} 失败"
+        )
+
+
+# ============================================================
+# 下拉框工具 (Dropdown Tools)
+# ============================================================
+
+@tool()
+async def get_dropdown_options(index: int, uid: str = "") -> ToolResult:
+    """
+    获取下拉框的所有选项
+    Get options from a dropdown element
+
+    Args:
+        index: 下拉框的元素索引
+        uid: 用户 ID(由框架自动注入)
+
+    Returns:
+        ToolResult: 包含所有选项的结果
+
+    Example:
+        get_dropdown_options(index=8)
+    """
+    try:
+        browser, tools = await get_browser_session()
+
+        result = await tools.dropdown_options(
+            index=index,
+            browser_session=browser
+        )
+
+        return action_result_to_tool_result(result, f"获取下拉框选项: {index}")
+
+    except Exception as e:
+        return ToolResult(
+            title="获取下拉框选项失败",
+            output="",
+            error=f"Failed to get dropdown options: {str(e)}",
+            long_term_memory=f"获取下拉框 {index} 选项失败"
+        )
+
+
+@tool()
+async def select_dropdown_option(index: int, text: str, uid: str = "") -> ToolResult:
+    """
+    选择下拉框选项
+    Select an option from a dropdown
+
+    Args:
+        index: 下拉框的元素索引
+        text: 要选择的选项文本(精确匹配)
+        uid: 用户 ID(由框架自动注入)
+
+    Returns:
+        ToolResult: 选择结果
+
+    Example:
+        select_dropdown_option(index=8, text="Option 2")
+    """
+    try:
+        browser, tools = await get_browser_session()
+
+        result = await tools.select_dropdown(
+            index=index,
+            text=text,
+            browser_session=browser
+        )
+
+        return action_result_to_tool_result(result, f"选择下拉框选项: {text}")
+
+    except Exception as e:
+        return ToolResult(
+            title="选择下拉框选项失败",
+            output="",
+            error=f"Failed to select dropdown option: {str(e)}",
+            long_term_memory=f"选择选项 '{text}' 失败"
+        )
+
+
+# ============================================================
+# 内容提取工具 (Content Extraction Tools)
+# ============================================================
+
+@tool()
+async def extract_content(query: str, extract_links: bool = False,
+                         start_from_char: int = 0, uid: str = "") -> ToolResult:
+    """
+    使用 LLM 从页面提取结构化数据
+    Extract content from the current page using LLM
+
+    Args:
+        query: 提取查询(告诉 LLM 要提取什么内容)
+        extract_links: 是否提取链接(默认 False,节省 token)
+        start_from_char: 从哪个字符开始提取(用于分页提取大内容)
+        uid: 用户 ID(由框架自动注入)
+
+    Returns:
+        ToolResult: 提取的内容
+
+    Example:
+        extract_content(query="提取页面上所有产品的名称和价格", extract_links=True)
+
+    Note:
+        需要配置 page_extraction_llm,否则会失败
+        支持分页提取,最大100k字符
+    """
+    try:
+        browser, tools = await get_browser_session()
+
+        # 注意:extract 需要 page_extraction_llm 参数
+        # 这里我们假设用户会在初始化时配置 LLM
+        # 如果没有配置,会抛出异常
+        result = await tools.extract(
+            query=query,
+            extract_links=extract_links,
+            start_from_char=start_from_char,
+            browser_session=browser,
+            page_extraction_llm=None,  # 需要用户配置
+            file_system=_file_system
+        )
+
+        return action_result_to_tool_result(result, f"提取内容: {query}")
+
+    except Exception as e:
+        return ToolResult(
+            title="内容提取失败",
+            output="",
+            error=f"Failed to extract content: {str(e)}",
+            long_term_memory=f"提取内容失败: {query}"
+        )
+
+
+@tool()
+async def get_page_html(uid: str = "") -> ToolResult:
+    """
+    获取当前页面的完整 HTML
+    Get the full HTML of the current page
+
+    返回当前页面的完整 HTML 源代码。
+
+    Args:
+        uid: 用户 ID(由框架自动注入)
+
+    Returns:
+        ToolResult: 包含页面 HTML 的工具返回对象
+
+    Example:
+        get_page_html()
+
+    Note:
+        - 返回的是完整的 HTML 源代码
+        - 输出会被限制在 10000 字符以内(完整内容保存在 metadata 中)
+    """
+    try:
+        browser, tools = await get_browser_session()
+
+        # 使用 CDP 获取页面 HTML
+        cdp = await browser.get_or_create_cdp_session()
+
+        # 获取页面内容
+        result = await cdp.cdp_client.send.Runtime.evaluate(
+            params={'expression': 'document.documentElement.outerHTML'},
+            session_id=cdp.session_id
+        )
+
+        html = result.get('result', {}).get('value', '')
+
+        # 获取 URL 和标题
+        url = await browser.get_current_page_url()
+
+        title_result = await cdp.cdp_client.send.Runtime.evaluate(
+            params={'expression': 'document.title'},
+            session_id=cdp.session_id
+        )
+        title = title_result.get('result', {}).get('value', '')
+
+        # 限制输出大小
+        output_html = html
+        if len(html) > 10000:
+            output_html = html[:10000] + "... (truncated)"
+
+        return ToolResult(
+            title=f"获取 HTML: {url}",
+            output=f"页面: {title}\nURL: {url}\n\nHTML:\n{output_html}",
+            long_term_memory=f"获取 HTML: {url}",
+            metadata={"url": url, "title": title, "html": html}
+        )
+
+    except Exception as e:
+        return ToolResult(
+            title="获取 HTML 失败",
+            output="",
+            error=f"Failed to get page HTML: {str(e)}",
+            long_term_memory="获取 HTML 失败"
+        )
+
+
+@tool()
+async def get_selector_map(uid: str = "") -> ToolResult:
+    """
+    获取当前页面的元素索引映射
+    Get the selector map of interactive elements on the current page
+
+    返回页面所有可交互元素的索引字典,用于后续的元素操作。
+
+    Args:
+        uid: 用户 ID(由框架自动注入)
+
+    Returns:
+        ToolResult: 包含元素映射的工具返回对象
+
+    Example:
+        get_selector_map()
+
+    Note:
+        返回的索引可以用于 click_element, input_text 等操作
+    """
+    try:
+        browser, tools = await get_browser_session()
+
+        # 获取选择器映射
+        selector_map = await browser.get_selector_map()
+
+        # 构建输出信息
+        elements_info = []
+        for index, node in list(selector_map.items())[:20]:  # 只显示前20个
+            tag = node.tag_name
+            attrs = node.attributes or {}
+            text = attrs.get('aria-label') or attrs.get('placeholder') or attrs.get('value', '')
+            elements_info.append(f"索引 {index}: <{tag}> {text[:50]}")
+
+        output = f"找到 {len(selector_map)} 个交互元素\n\n"
+        output += "\n".join(elements_info)
+        if len(selector_map) > 20:
+            output += f"\n... 还有 {len(selector_map) - 20} 个元素"
+
+        return ToolResult(
+            title="获取元素映射",
+            output=output,
+            long_term_memory=f"获取到 {len(selector_map)} 个交互元素",
+            metadata={"selector_map": {k: str(v) for k, v in list(selector_map.items())[:100]}}
+        )
+
+    except Exception as e:
+        return ToolResult(
+            title="获取元素映射失败",
+            output="",
+            error=f"Failed to get selector map: {str(e)}",
+            long_term_memory="获取元素映射失败"
+        )
+
+
+# ============================================================
+# JavaScript 执行工具 (JavaScript Tools)
+# ============================================================
+
+@tool()
+async def evaluate(code: str, uid: str = "") -> ToolResult:
+    """
+    在页面中执行 JavaScript 代码
+    Execute JavaScript code in the page context
+
+    允许在当前页面中执行任意 JavaScript 代码,用于复杂的页面操作或数据提取。
+
+    Args:
+        code: 要执行的 JavaScript 代码字符串
+        uid: 用户 ID(由框架自动注入)
+
+    Returns:
+        ToolResult: 包含执行结果的工具返回对象
+
+    Example:
+        evaluate("document.title")
+        evaluate("document.querySelectorAll('a').length")
+
+    Note:
+        - 代码在页面上下文中执行,可以访问 DOM 和全局变量
+        - 返回值会被自动序列化为字符串
+        - 执行结果限制在 20k 字符以内
+    """
+    try:
+        browser, tools = await get_browser_session()
+
+        result = await tools.evaluate(
+            code=code,
+            browser_session=browser
+        )
+
+        return action_result_to_tool_result(result, "执行 JavaScript")
+
+    except Exception as e:
+        return ToolResult(
+            title="JavaScript 执行失败",
+            output="",
+            error=f"Failed to execute JavaScript: {str(e)}",
+            long_term_memory="JavaScript 执行失败"
+        )
+
+
+# ============================================================
+# 文件系统工具 (File System Tools)
+# ============================================================
+
+@tool()
+async def write_file(file_name: str, content: str, append: bool = False, uid: str = "") -> ToolResult:
+    """
+    写入文件到本地文件系统
+    Write content to a local file
+
+    支持多种文件格式的写入操作。
+
+    Args:
+        file_name: 文件名(包含扩展名)
+        content: 要写入的文件内容
+        append: 是否追加模式(默认 False,覆盖写入)
+        uid: 用户 ID(由框架自动注入)
+
+    Returns:
+        ToolResult: 包含写入结果的工具返回对象
+
+    Example:
+        write_file("output.txt", "Hello World")
+        write_file("data.json", '{"key": "value"}')
+
+    Note:
+        支持的文件格式: .txt, .md, .json, .jsonl, .csv, .pdf
+    """
+    try:
+        browser, tools = await get_browser_session()
+
+        result = await tools.write_file(
+            file_name=file_name,
+            content=content,
+            append=append,
+            file_system=_file_system
+        )
+
+        return action_result_to_tool_result(result, f"写入文件: {file_name}")
+
+    except Exception as e:
+        return ToolResult(
+            title="写入文件失败",
+            output="",
+            error=f"Failed to write file: {str(e)}",
+            long_term_memory=f"写入文件 {file_name} 失败"
+        )
+
+
+@tool()
+async def read_file(file_name: str, uid: str = "") -> ToolResult:
+    """
+    读取文件内容
+    Read content from a local file
+
+    支持多种文件格式的读取操作。
+
+    Args:
+        file_name: 文件名(包含扩展名)
+        uid: 用户 ID(由框架自动注入)
+
+    Returns:
+        ToolResult: 包含文件内容的工具返回对象
+
+    Example:
+        read_file("input.txt")
+        read_file("data.json")
+
+    Note:
+        支持的文件格式: 文本文件、PDF、DOCX、图片等
+    """
+    try:
+        browser, tools = await get_browser_session()
+
+        result = await tools.read_file(
+            file_name=file_name,
+            available_file_paths=[],
+            file_system=_file_system
+        )
+
+        return action_result_to_tool_result(result, f"读取文件: {file_name}")
+
+    except Exception as e:
+        return ToolResult(
+            title="读取文件失败",
+            output="",
+            error=f"Failed to read file: {str(e)}",
+            long_term_memory=f"读取文件 {file_name} 失败"
+        )
+
+
+@tool()
+async def replace_file(file_name: str, old_str: str, new_str: str, uid: str = "") -> ToolResult:
+    """
+    替换文件中的特定文本
+    Replace specific text in a file
+
+    在文件中查找并替换指定的文本内容。
+
+    Args:
+        file_name: 文件名(包含扩展名)
+        old_str: 要替换的文本
+        new_str: 新文本
+        uid: 用户 ID(由框架自动注入)
+
+    Returns:
+        ToolResult: 包含替换结果的工具返回对象
+
+    Example:
+        replace_file("config.txt", "old_value", "new_value")
+
+    Note:
+        - 会替换文件中所有匹配的文本
+        - 如果找不到要替换的文本,会返回警告
+    """
+    try:
+        browser, tools = await get_browser_session()
+
+        result = await tools.replace_file(
+            file_name=file_name,
+            old_str=old_str,
+            new_str=new_str,
+            file_system=_file_system
+        )
+
+        return action_result_to_tool_result(result, f"替换文件内容: {file_name}")
+
+    except Exception as e:
+        return ToolResult(
+            title="替换文件失败",
+            output="",
+            error=f"Failed to replace file content: {str(e)}",
+            long_term_memory=f"替换文件 {file_name} 失败"
+        )
+
+
+# ============================================================
+# 等待用户操作工具 (Wait for User Action)
+# ============================================================
+
+@tool()
+async def wait_for_user_action(message: str = "Please complete the action in browser",
+                               timeout: int = 300, uid: str = "") -> ToolResult:
+    """
+    等待用户在浏览器中完成操作(如登录)
+    Wait for user to complete an action in the browser (e.g., login)
+
+    暂停自动化流程,等待用户手动完成某些操作(如登录、验证码等)。
+
+    Args:
+        message: 提示用户需要完成的操作
+        timeout: 最大等待时间(秒),默认 300 秒(5 分钟)
+        uid: 用户 ID(由框架自动注入)
+
+    Returns:
+        ToolResult: 包含等待结果的工具返回对象
+
+    Example:
+        wait_for_user_action("Please login to Xiaohongshu", timeout=180)
+        wait_for_user_action("Please complete the CAPTCHA", timeout=60)
+
+    Note:
+        - 用户需要在浏览器窗口中手动完成操作
+        - 完成后按回车键继续
+        - 超时后会自动继续执行
+    """
+    try:
+        import asyncio
+
+        print(f"\n{'='*60}")
+        print(f"⏸️  WAITING FOR USER ACTION")
+        print(f"{'='*60}")
+        print(f"📝 {message}")
+        print(f"⏱️  Timeout: {timeout} seconds")
+        print(f"\n👉 Please complete the action in the browser window")
+        print(f"👉 Press ENTER when done, or wait for timeout")
+        print(f"{'='*60}\n")
+
+        # Wait for user input or timeout
+        try:
+            loop = asyncio.get_event_loop()
+
+            # Wait for either user input or timeout
+            await asyncio.wait_for(
+                loop.run_in_executor(None, input),
+                timeout=timeout
+            )
+
+            return ToolResult(
+                title="用户操作完成",
+                output=f"User completed: {message}",
+                long_term_memory=f"用户完成操作: {message}"
+            )
+        except asyncio.TimeoutError:
+            return ToolResult(
+                title="用户操作超时",
+                output=f"Timeout waiting for: {message}",
+                long_term_memory=f"等待用户操作超时: {message}"
+            )
+
+    except Exception as e:
+        return ToolResult(
+            title="等待用户操作失败",
+            output="",
+            error=f"Failed to wait for user action: {str(e)}",
+            long_term_memory="等待用户操作失败"
+        )
+
+
+# ============================================================
+# 任务完成工具 (Task Completion)
+# ============================================================
+
+@tool()
+async def done(text: str, success: bool = True,
+              files_to_display: Optional[List[str]] = None, uid: str = "") -> ToolResult:
+    """
+    标记任务完成并返回最终消息
+    Mark the task as complete and return final message to user
+
+    Args:
+        text: 给用户的最终消息
+        success: 任务是否成功完成
+        files_to_display: 可选的要显示的文件路径列表
+        uid: 用户 ID(由框架自动注入)
+
+    Returns:
+        ToolResult: 完成结果
+
+    Example:
+        done("任务已完成,提取了10个产品信息", success=True)
+    """
+    try:
+        browser, tools = await get_browser_session()
+
+        result = await tools.done(
+            text=text,
+            success=success,
+            files_to_display=files_to_display,
+            file_system=_file_system
+        )
+
+        return action_result_to_tool_result(result, "任务完成")
+
+    except Exception as e:
+        return ToolResult(
+            title="标记任务完成失败",
+            output="",
+            error=f"Failed to complete task: {str(e)}",
+            long_term_memory="标记任务完成失败"
+        )
+
+
+# ============================================================
+# 导出所有工具函数(供外部使用)
+# ============================================================
+
+__all__ = [
+    # 会话管理
+    'init_browser_session',
+    'get_browser_session',
+    'cleanup_browser_session',
+    'kill_browser_session',
+
+    # 导航类工具
+    'navigate_to_url',
+    'search_web',
+    'go_back',
+    'wait',
+
+    # 元素交互工具
+    'click_element',
+    'input_text',
+    'send_keys',
+    'upload_file',
+
+    # 滚动和视图工具
+    'scroll_page',
+    'find_text',
+    'screenshot',
+
+    # 标签页管理工具
+    'switch_tab',
+    'close_tab',
+
+    # 下拉框工具
+    'get_dropdown_options',
+    'select_dropdown_option',
+
+    # 内容提取工具
+    'extract_content',
+    'get_page_html',
+    'get_selector_map',
+
+    # JavaScript 执行工具
+    'evaluate',
+
+    # 文件系统工具
+    'write_file',
+    'read_file',
+    'replace_file',
+
+    # 等待用户操作
+    'wait_for_user_action',
+
+    # 任务完成
+    'done',
+]