소스 검색

docs: update

Talegorithm 1 개월 전
부모
커밋
756406f07d
10개의 변경된 파일2983개의 추가작업 그리고 905개의 파일을 삭제
  1. 10 1
      agent/tools/__init__.py
  2. 123 0
      agent/tools/models.py
  3. 407 256
      agent/tools/registry.py
  4. 247 0
      agent/tools/sensitive.py
  5. 142 0
      agent/tools/url_matcher.py
  6. 171 201
      docs/README.md
  7. 358 0
      docs/decisions.md
  8. 944 0
      docs/tools.md
  9. 0 447
      examples/tool_examples.py
  10. 581 0
      examples/tools_examples.py

+ 10 - 1
agent/tools/__init__.py

@@ -4,5 +4,14 @@ Tools 包 - 工具注册和 Schema 生成
 
 from reson_agent.tools.registry import ToolRegistry, tool, get_tool_registry
 from reson_agent.tools.schema import SchemaGenerator
+from reson_agent.tools.models import ToolResult, ToolContext, ToolContextImpl
 
-__all__ = ["ToolRegistry", "tool", "get_tool_registry", "SchemaGenerator"]
+__all__ = [
+	"ToolRegistry",
+	"tool",
+	"get_tool_registry",
+	"SchemaGenerator",
+	"ToolResult",
+	"ToolContext",
+	"ToolContextImpl",
+]

+ 123 - 0
agent/tools/models.py

@@ -0,0 +1,123 @@
+"""
+Tool Models - 工具系统核心数据模型
+
+定义:
+1. ToolResult: 工具执行结果(支持双层记忆管理)
+2. ToolContext: 工具执行上下文(依赖注入)
+"""
+
+from dataclasses import dataclass, field
+from typing import Any, Dict, List, Optional, Protocol
+
+
+@dataclass
+class ToolResult:
+	"""
+	工具执行结果
+
+	支持双层记忆管理(参考 Browser-Use 的 ActionResult):
+	- output: 主要输出,可能很长,可以配置只给 LLM 看一次
+	- long_term_memory: 简短摘要,永久保存在对话历史中
+
+	这种设计避免了大量临时内容占用 context。
+	"""
+
+	# 主要输出(临时内容)
+	title: str  # 简短标题,用于展示
+	output: str  # 主要输出内容
+
+	# 记忆管理
+	long_term_memory: Optional[str] = None  # 永久记忆(简短摘要)
+	include_output_only_once: bool = False  # output 是否只给 LLM 看一次
+
+	# 元数据
+	metadata: Dict[str, Any] = field(default_factory=dict)
+
+	# 状态标志
+	truncated: bool = False  # 输出是否被截断
+	error: Optional[str] = None  # 错误信息(如果执行失败)
+
+	# 附件支持(用于浏览器自动化等场景)
+	attachments: List[str] = field(default_factory=list)  # 文件路径列表
+	images: List[Dict[str, Any]] = field(default_factory=list)  # 图片列表
+
+	def to_llm_message(self, first_time: bool = True) -> str:
+		"""
+		转换为给 LLM 的消息
+
+		Args:
+			first_time: 是否第一次展示(影响 include_output_only_once 的行为)
+
+		Returns:
+			给 LLM 的消息字符串
+		"""
+		# 如果有错误,优先返回错误
+		if self.error:
+			return f"Error: {self.error}"
+
+		# 构建消息
+		parts = []
+
+		# 标题
+		if self.title:
+			parts.append(f"# {self.title}")
+
+		# 主要输出
+		if first_time or not self.include_output_only_once:
+			if self.output:
+				parts.append(self.output)
+				if self.truncated:
+					parts.append("(Output truncated)")
+
+		# 长期记忆(永远包含)
+		if self.long_term_memory:
+			parts.append(f"\nSummary: {self.long_term_memory}")
+
+		# 附件信息
+		if self.attachments:
+			parts.append(f"\nAttachments: {', '.join(self.attachments)}")
+
+		return "\n\n".join(parts)
+
+
+class ToolContext(Protocol):
+	"""
+	工具执行上下文(依赖注入)
+
+	工具函数可以声明需要哪些上下文字段,框架自动注入。
+
+	使用 Protocol 允许不同实现提供不同的上下文字段。
+	"""
+
+	# 基础字段(所有工具都可用)
+	trace_id: str
+	step_id: str
+	uid: Optional[str]
+
+	# 浏览器相关(Browser-Use 集成)
+	browser_session: Optional[Any]  # BrowserSession 实例
+	page_url: Optional[str]  # 当前页面 URL
+	file_system: Optional[Any]  # FileSystem 实例
+	sensitive_data: Optional[Dict[str, Any]]  # 敏感数据字典
+
+	# 其他可扩展字段
+	context: Optional[Dict[str, Any]]  # 额外上下文数据
+
+
+@dataclass
+class ToolContextImpl:
+	"""ToolContext 的默认实现"""
+
+	# 基础字段
+	trace_id: str
+	step_id: str
+	uid: Optional[str] = None
+
+	# 浏览器相关
+	browser_session: Optional[Any] = None
+	page_url: Optional[str] = None
+	file_system: Optional[Any] = None
+	sensitive_data: Optional[Dict[str, Any]] = None
+
+	# 额外上下文
+	context: Optional[Dict[str, Any]] = None

+ 407 - 256
agent/tools/registry.py

@@ -5,234 +5,381 @@ Tool Registry - 工具注册表和装饰器
 1. @tool 装饰器:自动注册工具并生成 Schema
 2. 管理所有工具的 Schema 和实现
 3. 路由工具调用到具体实现
+4. 支持域名过滤、敏感数据处理、工具统计
 
-从 Resonote/llm/tools/registry.py 抽取
+从 Resonote/llm/tools/registry.py 抽取并扩展
 """
 
 import json
 import inspect
 import logging
+import time
 from typing import Any, Callable, Dict, List, Optional
 
+from reson_agent.tools.url_matcher import filter_by_url
+
 logger = logging.getLogger(__name__)
 
 
+class ToolStats:
+	"""工具使用统计"""
+
+	def __init__(self):
+		self.call_count: int = 0
+		self.success_count: int = 0
+		self.failure_count: int = 0
+		self.total_duration: float = 0.0
+		self.last_called: Optional[float] = None
+
+	@property
+	def average_duration(self) -> float:
+		"""平均执行时间(秒)"""
+		return self.total_duration / self.call_count if self.call_count > 0 else 0.0
+
+	@property
+	def success_rate(self) -> float:
+		"""成功率"""
+		return self.success_count / self.call_count if self.call_count > 0 else 0.0
+
+	def to_dict(self) -> Dict[str, Any]:
+		return {
+			"call_count": self.call_count,
+			"success_count": self.success_count,
+			"failure_count": self.failure_count,
+			"average_duration": self.average_duration,
+			"success_rate": self.success_rate,
+			"last_called": self.last_called
+		}
+
+
 class ToolRegistry:
-    """工具注册表"""
-
-    def __init__(self):
-        self._tools: Dict[str, Dict[str, Any]] = {}
-
-    def register(
-        self,
-        func: Callable,
-        schema: Optional[Dict] = None,
-        requires_confirmation: bool = False,
-        editable_params: Optional[List[str]] = None,
-        display: Optional[Dict[str, Dict[str, Any]]] = None
-    ):
-        """
-        注册工具
-
-        Args:
-            func: 工具函数
-            schema: 工具 Schema(如果为 None,自动生成)
-            requires_confirmation: 是否需要用户确认
-            editable_params: 允许用户编辑的参数列表
-            display: i18n 展示信息 {"zh": {"name": "xx", "params": {...}}, "en": {...}}
-        """
-        func_name = func.__name__
-
-        # 如果没有提供 Schema,自动生成
-        if schema is None:
-            try:
-                from reson_agent.tools.schema import SchemaGenerator
-                schema = SchemaGenerator.generate(func)
-            except Exception as e:
-                logger.error(f"Failed to generate schema for {func_name}: {e}")
-                raise
-
-        self._tools[func_name] = {
-            "func": func,
-            "schema": schema,
-            "ui_metadata": {
-                "requires_confirmation": requires_confirmation,
-                "editable_params": editable_params or [],
-                "display": display or {}
-            }
-        }
-
-        logger.debug(
-            f"[ToolRegistry] Registered: {func_name} "
-            f"(requires_confirmation={requires_confirmation}, "
-            f"editable_params={editable_params or []})"
-        )
-
-    def is_registered(self, tool_name: str) -> bool:
-        """检查工具是否已注册"""
-        return tool_name in self._tools
-
-    def get_schemas(self, tool_names: Optional[List[str]] = None) -> List[Dict]:
-        """
-        获取工具 Schema
-
-        Args:
-            tool_names: 工具名称列表(None = 所有工具)
-
-        Returns:
-            OpenAI Tool Schema 列表
-        """
-        if tool_names is None:
-            tool_names = list(self._tools.keys())
-
-        schemas = []
-        for name in tool_names:
-            if name in self._tools:
-                schemas.append(self._tools[name]["schema"])
-            else:
-                logger.warning(f"[ToolRegistry] Tool not found: {name}")
-
-        return schemas
-
-    def get_tool_names(self) -> List[str]:
-        """获取所有注册的工具名称"""
-        return list(self._tools.keys())
-
-    async def execute(
-        self,
-        name: str,
-        arguments: Dict[str, Any],
-        uid: str = "",
-        context: Optional[Dict[str, Any]] = None
-    ) -> str:
-        """
-        执行工具调用
-
-        Args:
-            name: 工具名称
-            arguments: 工具参数
-            uid: 用户ID(自动注入)
-            context: 额外上下文
-
-        Returns:
-            JSON 字符串格式的结果
-        """
-        if name not in self._tools:
-            error_msg = f"Unknown tool: {name}"
-            logger.error(f"[ToolRegistry] {error_msg}")
-            return json.dumps({"error": error_msg}, ensure_ascii=False)
-
-        try:
-            func = self._tools[name]["func"]
-
-            # 注入 uid
-            kwargs = {**arguments, "uid": uid}
-
-            # 注入 context(如果函数接受)
-            sig = inspect.signature(func)
-            if "context" in sig.parameters:
-                kwargs["context"] = context
-
-            # 执行函数
-            if inspect.iscoroutinefunction(func):
-                result = await func(**kwargs)
-            else:
-                result = func(**kwargs)
-
-            # 返回 JSON 字符串
-            if isinstance(result, str):
-                return result
-            return json.dumps(result, ensure_ascii=False, indent=2)
-
-        except Exception as e:
-            error_msg = f"Error executing tool '{name}': {str(e)}"
-            logger.error(f"[ToolRegistry] {error_msg}")
-            import traceback
-            logger.error(traceback.format_exc())
-            return json.dumps({"error": error_msg}, ensure_ascii=False)
-
-    def check_confirmation_required(self, tool_calls: List[Dict]) -> bool:
-        """检查是否有工具需要用户确认"""
-        for tc in tool_calls:
-            tool_name = tc.get("function", {}).get("name")
-            if tool_name and tool_name in self._tools:
-                if self._tools[tool_name]["ui_metadata"].get("requires_confirmation", False):
-                    return True
-        return False
-
-    def get_confirmation_flags(self, tool_calls: List[Dict]) -> List[bool]:
-        """返回每个工具是否需要确认"""
-        flags = []
-        for tc in tool_calls:
-            tool_name = tc.get("function", {}).get("name")
-            if tool_name and tool_name in self._tools:
-                flags.append(self._tools[tool_name]["ui_metadata"].get("requires_confirmation", False))
-            else:
-                flags.append(False)
-        return flags
-
-    def check_any_param_editable(self, tool_calls: List[Dict]) -> bool:
-        """检查是否有任何工具允许参数编辑"""
-        for tc in tool_calls:
-            tool_name = tc.get("function", {}).get("name")
-            if tool_name and tool_name in self._tools:
-                editable_params = self._tools[tool_name]["ui_metadata"].get("editable_params", [])
-                if editable_params:
-                    return True
-        return False
-
-    def get_editable_params_map(self, tool_calls: List[Dict]) -> Dict[str, List[str]]:
-        """返回每个工具调用的可编辑参数列表"""
-        params_map = {}
-        for tc in tool_calls:
-            tool_call_id = tc.get("id")
-            tool_name = tc.get("function", {}).get("name")
-
-            if tool_name and tool_name in self._tools:
-                editable_params = self._tools[tool_name]["ui_metadata"].get("editable_params", [])
-                params_map[tool_call_id] = editable_params
-            else:
-                params_map[tool_call_id] = []
-
-        return params_map
-
-    def get_ui_metadata(
-        self,
-        locale: str = "zh",
-        tool_names: Optional[List[str]] = None
-    ) -> Dict[str, Dict[str, Any]]:
-        """
-        获取工具的UI元数据(用于前端展示)
-
-        Returns:
-            {
-                "tool_name": {
-                    "display_name": "搜索笔记",
-                    "param_display_names": {"query": "搜索关键词"},
-                    "requires_confirmation": false,
-                    "editable_params": ["query"]
-                }
-            }
-        """
-        if tool_names is None:
-            tool_names = list(self._tools.keys())
-
-        metadata = {}
-        for name in tool_names:
-            if name not in self._tools:
-                continue
-
-            ui_meta = self._tools[name]["ui_metadata"]
-            display = ui_meta.get("display", {}).get(locale, {})
-
-            metadata[name] = {
-                "display_name": display.get("name", name),
-                "param_display_names": display.get("params", {}),
-                "requires_confirmation": ui_meta.get("requires_confirmation", False),
-                "editable_params": ui_meta.get("editable_params", [])
-            }
-
-        return metadata
+	"""工具注册表"""
+
+	def __init__(self):
+		self._tools: Dict[str, Dict[str, Any]] = {}
+		self._stats: Dict[str, ToolStats] = {}
+
+	def register(
+		self,
+		func: Callable,
+		schema: Optional[Dict] = None,
+		requires_confirmation: bool = False,
+		editable_params: Optional[List[str]] = None,
+		display: Optional[Dict[str, Dict[str, Any]]] = None,
+		url_patterns: Optional[List[str]] = None
+	):
+		"""
+		注册工具
+
+		Args:
+			func: 工具函数
+			schema: 工具 Schema(如果为 None,自动生成)
+			requires_confirmation: 是否需要用户确认
+			editable_params: 允许用户编辑的参数列表
+			display: i18n 展示信息 {"zh": {"name": "xx", "params": {...}}, "en": {...}}
+			url_patterns: URL 模式列表(如 ["*.google.com"],None = 无限制)
+		"""
+		func_name = func.__name__
+
+		# 如果没有提供 Schema,自动生成
+		if schema is None:
+			try:
+				from reson_agent.tools.schema import SchemaGenerator
+				schema = SchemaGenerator.generate(func)
+			except Exception as e:
+				logger.error(f"Failed to generate schema for {func_name}: {e}")
+				raise
+
+		self._tools[func_name] = {
+			"func": func,
+			"schema": schema,
+			"url_patterns": url_patterns,
+			"ui_metadata": {
+				"requires_confirmation": requires_confirmation,
+				"editable_params": editable_params or [],
+				"display": display or {}
+			}
+		}
+
+		# 初始化统计
+		self._stats[func_name] = ToolStats()
+
+		logger.debug(
+			f"[ToolRegistry] Registered: {func_name} "
+			f"(requires_confirmation={requires_confirmation}, "
+			f"editable_params={editable_params or []}, "
+			f"url_patterns={url_patterns or 'none'})"
+		)
+
+	def is_registered(self, tool_name: str) -> bool:
+		"""检查工具是否已注册"""
+		return tool_name in self._tools
+
+	def get_schemas(self, tool_names: Optional[List[str]] = None) -> List[Dict]:
+		"""
+		获取工具 Schema
+
+		Args:
+			tool_names: 工具名称列表(None = 所有工具)
+
+		Returns:
+			OpenAI Tool Schema 列表
+		"""
+		if tool_names is None:
+			tool_names = list(self._tools.keys())
+
+		schemas = []
+		for name in tool_names:
+			if name in self._tools:
+				schemas.append(self._tools[name]["schema"])
+			else:
+				logger.warning(f"[ToolRegistry] Tool not found: {name}")
+
+		return schemas
+
+	def get_tool_names(self, current_url: Optional[str] = None) -> List[str]:
+		"""
+		获取工具名称列表(可选 URL 过滤)
+
+		Args:
+			current_url: 当前 URL(None = 返回所有工具)
+
+		Returns:
+			工具名称列表
+		"""
+		if current_url is None:
+			return list(self._tools.keys())
+
+		# 过滤工具
+		tool_items = [
+			{"name": name, "url_patterns": tool["url_patterns"]}
+			for name, tool in self._tools.items()
+		]
+		filtered = filter_by_url(tool_items, current_url, url_field="url_patterns")
+		return [item["name"] for item in filtered]
+
+	def get_schemas_for_url(self, current_url: Optional[str] = None) -> List[Dict]:
+		"""
+		根据当前 URL 获取匹配的工具 Schema
+
+		Args:
+			current_url: 当前 URL(None = 返回无 URL 限制的工具)
+
+		Returns:
+			过滤后的工具 Schema 列表
+		"""
+		tool_names = self.get_tool_names(current_url)
+		return self.get_schemas(tool_names)
+
+	async def execute(
+		self,
+		name: str,
+		arguments: Dict[str, Any],
+		uid: str = "",
+		context: Optional[Dict[str, Any]] = None,
+		sensitive_data: Optional[Dict[str, Any]] = None
+	) -> str:
+		"""
+		执行工具调用
+
+		Args:
+			name: 工具名称
+			arguments: 工具参数
+			uid: 用户ID(自动注入)
+			context: 额外上下文
+			sensitive_data: 敏感数据字典(用于替换 <secret> 占位符)
+
+		Returns:
+			JSON 字符串格式的结果
+		"""
+		if name not in self._tools:
+			error_msg = f"Unknown tool: {name}"
+			logger.error(f"[ToolRegistry] {error_msg}")
+			return json.dumps({"error": error_msg}, ensure_ascii=False)
+
+		start_time = time.time()
+		stats = self._stats[name]
+		stats.call_count += 1
+		stats.last_called = start_time
+
+		try:
+			func = self._tools[name]["func"]
+
+			# 处理敏感数据占位符
+			if sensitive_data:
+				from reson_agent.tools.sensitive import replace_sensitive_data
+				current_url = context.get("page_url") if context else None
+				arguments = replace_sensitive_data(arguments, sensitive_data, current_url)
+
+			# 注入 uid
+			kwargs = {**arguments, "uid": uid}
+
+			# 注入 context(如果函数接受)
+			sig = inspect.signature(func)
+			if "context" in sig.parameters:
+				kwargs["context"] = context
+
+			# 执行函数
+			if inspect.iscoroutinefunction(func):
+				result = await func(**kwargs)
+			else:
+				result = func(**kwargs)
+
+			# 记录成功
+			stats.success_count += 1
+			duration = time.time() - start_time
+			stats.total_duration += duration
+
+			# 返回 JSON 字符串
+			if isinstance(result, str):
+				return result
+			return json.dumps(result, ensure_ascii=False, indent=2)
+
+		except Exception as e:
+			# 记录失败
+			stats.failure_count += 1
+			duration = time.time() - start_time
+			stats.total_duration += duration
+
+			error_msg = f"Error executing tool '{name}': {str(e)}"
+			logger.error(f"[ToolRegistry] {error_msg}")
+			import traceback
+			logger.error(traceback.format_exc())
+			return json.dumps({"error": error_msg}, ensure_ascii=False)
+
+	def get_stats(self, tool_name: Optional[str] = None) -> Dict[str, Dict[str, Any]]:
+		"""
+		获取工具统计信息
+
+		Args:
+			tool_name: 工具名称(None = 所有工具)
+
+		Returns:
+			统计信息字典
+		"""
+		if tool_name:
+			if tool_name in self._stats:
+				return {tool_name: self._stats[tool_name].to_dict()}
+			return {}
+
+		return {name: stats.to_dict() for name, stats in self._stats.items()}
+
+	def get_top_tools(self, limit: int = 10, by: str = "call_count") -> List[str]:
+		"""
+		获取排名靠前的工具
+
+		Args:
+			limit: 返回数量
+			by: 排序依据(call_count, success_rate, average_duration)
+
+		Returns:
+			工具名称列表
+		"""
+		if by == "call_count":
+			sorted_tools = sorted(
+				self._stats.items(),
+				key=lambda x: x[1].call_count,
+				reverse=True
+			)
+		elif by == "success_rate":
+			sorted_tools = sorted(
+				self._stats.items(),
+				key=lambda x: x[1].success_rate,
+				reverse=True
+			)
+		elif by == "average_duration":
+			sorted_tools = sorted(
+				self._stats.items(),
+				key=lambda x: x[1].average_duration,
+				reverse=False  # 越快越好
+			)
+		else:
+			raise ValueError(f"Invalid sort by: {by}")
+
+		return [name for name, _ in sorted_tools[:limit]]
+
+	def check_confirmation_required(self, tool_calls: List[Dict]) -> bool:
+		"""检查是否有工具需要用户确认"""
+		for tc in tool_calls:
+			tool_name = tc.get("function", {}).get("name")
+			if tool_name and tool_name in self._tools:
+				if self._tools[tool_name]["ui_metadata"].get("requires_confirmation", False):
+					return True
+		return False
+
+	def get_confirmation_flags(self, tool_calls: List[Dict]) -> List[bool]:
+		"""返回每个工具是否需要确认"""
+		flags = []
+		for tc in tool_calls:
+			tool_name = tc.get("function", {}).get("name")
+			if tool_name and tool_name in self._tools:
+				flags.append(self._tools[tool_name]["ui_metadata"].get("requires_confirmation", False))
+			else:
+				flags.append(False)
+		return flags
+
+	def check_any_param_editable(self, tool_calls: List[Dict]) -> bool:
+		"""检查是否有任何工具允许参数编辑"""
+		for tc in tool_calls:
+			tool_name = tc.get("function", {}).get("name")
+			if tool_name and tool_name in self._tools:
+				editable_params = self._tools[tool_name]["ui_metadata"].get("editable_params", [])
+				if editable_params:
+					return True
+		return False
+
+	def get_editable_params_map(self, tool_calls: List[Dict]) -> Dict[str, List[str]]:
+		"""返回每个工具调用的可编辑参数列表"""
+		params_map = {}
+		for tc in tool_calls:
+			tool_call_id = tc.get("id")
+			tool_name = tc.get("function", {}).get("name")
+
+			if tool_name and tool_name in self._tools:
+				editable_params = self._tools[tool_name]["ui_metadata"].get("editable_params", [])
+				params_map[tool_call_id] = editable_params
+			else:
+				params_map[tool_call_id] = []
+
+		return params_map
+
+	def get_ui_metadata(
+		self,
+		locale: str = "zh",
+		tool_names: Optional[List[str]] = None
+	) -> Dict[str, Dict[str, Any]]:
+		"""
+		获取工具的UI元数据(用于前端展示)
+
+		Returns:
+			{
+				"tool_name": {
+					"display_name": "搜索笔记",
+					"param_display_names": {"query": "搜索关键词"},
+					"requires_confirmation": false,
+					"editable_params": ["query"]
+				}
+			}
+		"""
+		if tool_names is None:
+			tool_names = list(self._tools.keys())
+
+		metadata = {}
+		for name in tool_names:
+			if name not in self._tools:
+				continue
+
+			ui_meta = self._tools[name]["ui_metadata"]
+			display = ui_meta.get("display", {}).get(locale, {})
+
+			metadata[name] = {
+				"display_name": display.get("name", name),
+				"param_display_names": display.get("params", {}),
+				"requires_confirmation": ui_meta.get("requires_confirmation", False),
+				"editable_params": ui_meta.get("editable_params", [])
+			}
+
+		return metadata
 
 
 # 全局单例
@@ -240,47 +387,51 @@ _global_registry = ToolRegistry()
 
 
 def tool(
-    description: Optional[str] = None,
-    param_descriptions: Optional[Dict[str, str]] = None,
-    requires_confirmation: bool = False,
-    editable_params: Optional[List[str]] = None,
-    display: Optional[Dict[str, Dict[str, Any]]] = None
+	description: Optional[str] = None,
+	param_descriptions: Optional[Dict[str, str]] = None,
+	requires_confirmation: bool = False,
+	editable_params: Optional[List[str]] = None,
+	display: Optional[Dict[str, Dict[str, Any]]] = None,
+	url_patterns: Optional[List[str]] = None
 ):
-    """
-    工具装饰器 - 自动注册工具并生成 Schema
-
-    Args:
-        description: 函数描述(可选,从 docstring 提取)
-        param_descriptions: 参数描述(可选,从 docstring 提取)
-        requires_confirmation: 是否需要用户确认(默认 False)
-        editable_params: 允许用户编辑的参数列表
-        display: i18n 展示信息
-
-    Example:
-        @tool(
-            editable_params=["query"],
-            display={
-                "zh": {"name": "搜索笔记", "params": {"query": "搜索关键词"}},
-                "en": {"name": "Search Notes", "params": {"query": "Query"}}
-            }
-        )
-        async def search_blocks(query: str, limit: int = 10, uid: str = "") -> str:
-            '''搜索用户的笔记块'''
-            ...
-    """
-    def decorator(func: Callable) -> Callable:
-        # 注册到全局 registry
-        _global_registry.register(
-            func,
-            requires_confirmation=requires_confirmation,
-            editable_params=editable_params,
-            display=display
-        )
-        return func
-
-    return decorator
+	"""
+	工具装饰器 - 自动注册工具并生成 Schema
+
+	Args:
+		description: 函数描述(可选,从 docstring 提取)
+		param_descriptions: 参数描述(可选,从 docstring 提取)
+		requires_confirmation: 是否需要用户确认(默认 False)
+		editable_params: 允许用户编辑的参数列表
+		display: i18n 展示信息
+		url_patterns: URL 模式列表(如 ["*.google.com"],None = 无限制)
+
+	Example:
+		@tool(
+			editable_params=["query"],
+			url_patterns=["*.google.com"],
+			display={
+				"zh": {"name": "搜索笔记", "params": {"query": "搜索关键词"}},
+				"en": {"name": "Search Notes", "params": {"query": "Query"}}
+			}
+		)
+		async def search_blocks(query: str, limit: int = 10, uid: str = "") -> str:
+			'''搜索用户的笔记块'''
+			...
+	"""
+	def decorator(func: Callable) -> Callable:
+		# 注册到全局 registry
+		_global_registry.register(
+			func,
+			requires_confirmation=requires_confirmation,
+			editable_params=editable_params,
+			display=display,
+			url_patterns=url_patterns
+		)
+		return func
+
+	return decorator
 
 
 def get_tool_registry() -> ToolRegistry:
-    """获取全局工具注册表"""
-    return _global_registry
+	"""获取全局工具注册表"""
+	return _global_registry

+ 247 - 0
agent/tools/sensitive.py

@@ -0,0 +1,247 @@
+"""
+Sensitive Data Handling - 敏感数据占位符替换
+
+支持:
+1. <secret>key</secret> 占位符格式
+2. 域名匹配(不同域名使用不同密钥)
+3. TOTP 2FA(key_bu_2fa_code 自动生成验证码)
+4. 递归处理嵌套结构
+
+参考 Browser-Use 的实现。
+"""
+
+import re
+import logging
+from typing import Any, Dict, Optional
+
+logger = logging.getLogger(__name__)
+
+# 尝试导入 pyotp(TOTP 支持)
+try:
+	import pyotp
+	HAS_PYOTP = True
+except ImportError:
+	HAS_PYOTP = False
+	logger.warning("pyotp not installed, TOTP 2FA support disabled")
+
+
+def match_domain(url: str, domain_pattern: str) -> bool:
+	"""
+	检查 URL 是否匹配域名模式
+
+	Args:
+		url: 完整 URL
+		domain_pattern: 域名模式(支持通配符)
+
+	Returns:
+		是否匹配
+	"""
+	from reson_agent.tools.url_matcher import match_url_with_pattern
+	return match_url_with_pattern(url, domain_pattern)
+
+
+def get_applicable_secrets(
+	sensitive_data: Dict[str, Any],
+	current_url: Optional[str]
+) -> Dict[str, Any]:
+	"""
+	获取当前 URL 适用的敏感数据
+
+	Args:
+		sensitive_data: 敏感数据字典,格式:
+			- 旧格式:{key: value}(适用于所有域名)
+			- 新格式:{domain_pattern: {key: value}}(域名特定)
+		current_url: 当前 URL
+
+	Returns:
+		适用的敏感数据字典
+	"""
+	applicable = {}
+
+	for domain_or_key, content in sensitive_data.items():
+		if isinstance(content, dict):
+			# 新格式:{domain_pattern: {key: value}}
+			if current_url:
+				if match_domain(current_url, domain_or_key):
+					applicable.update(content)
+		else:
+			# 旧格式:{key: value}(适用于所有域名)
+			applicable[domain_or_key] = content
+
+	# 过滤空值
+	return {k: v for k, v in applicable.items() if v}
+
+
+def replace_secret_in_string(
+	value: str,
+	applicable_secrets: Dict[str, Any],
+	replaced_placeholders: set,
+	missing_placeholders: set
+) -> str:
+	"""
+	替换字符串中的 <secret>key</secret> 占位符
+
+	Args:
+		value: 原始字符串
+		applicable_secrets: 适用的敏感数据
+		replaced_placeholders: 已替换的占位符集合(输出参数)
+		missing_placeholders: 缺失的占位符集合(输出参数)
+
+	Returns:
+		替换后的字符串
+	"""
+	secret_pattern = re.compile(r'<secret>(.*?)</secret>')
+	matches = secret_pattern.findall(value)
+
+	for placeholder in matches:
+		if placeholder in applicable_secrets:
+			secret_value = applicable_secrets[placeholder]
+
+			# 检查是否是 TOTP 2FA
+			if placeholder.endswith('_bu_2fa_code'):
+				if HAS_PYOTP:
+					try:
+						totp = pyotp.TOTP(secret_value, digits=6)
+						replacement = totp.now()
+						logger.info(f"Generated TOTP code for {placeholder}")
+					except Exception as e:
+						logger.error(f"Failed to generate TOTP for {placeholder}: {e}")
+						replacement = secret_value
+				else:
+					logger.warning(f"TOTP requested for {placeholder} but pyotp not installed")
+					replacement = secret_value
+			else:
+				replacement = secret_value
+
+			# 替换占位符
+			value = value.replace(f'<secret>{placeholder}</secret>', replacement)
+			replaced_placeholders.add(placeholder)
+		else:
+			# 缺失的占位符
+			missing_placeholders.add(placeholder)
+
+	return value
+
+
+def replace_secrets_recursively(
+	value: Any,
+	applicable_secrets: Dict[str, Any],
+	replaced_placeholders: set,
+	missing_placeholders: set
+) -> Any:
+	"""
+	递归替换嵌套结构中的敏感数据占位符
+
+	Args:
+		value: 原始值(可能是字符串、字典、列表等)
+		applicable_secrets: 适用的敏感数据
+		replaced_placeholders: 已替换的占位符集合
+		missing_placeholders: 缺失的占位符集合
+
+	Returns:
+		替换后的值
+	"""
+	if isinstance(value, str):
+		return replace_secret_in_string(
+			value,
+			applicable_secrets,
+			replaced_placeholders,
+			missing_placeholders
+		)
+	elif isinstance(value, dict):
+		return {
+			k: replace_secrets_recursively(
+				v,
+				applicable_secrets,
+				replaced_placeholders,
+				missing_placeholders
+			)
+			for k, v in value.items()
+		}
+	elif isinstance(value, list):
+		return [
+			replace_secrets_recursively(
+				item,
+				applicable_secrets,
+				replaced_placeholders,
+				missing_placeholders
+			)
+			for item in value
+		]
+	else:
+		return value
+
+
+def replace_sensitive_data(
+	arguments: Dict[str, Any],
+	sensitive_data: Dict[str, Any],
+	current_url: Optional[str] = None
+) -> Dict[str, Any]:
+	"""
+	替换工具参数中的敏感数据占位符
+
+	Args:
+		arguments: 工具参数字典
+		sensitive_data: 敏感数据字典
+		current_url: 当前 URL(用于域名匹配)
+
+	Returns:
+		替换后的参数字典
+
+	Example:
+		sensitive_data = {
+			"*.github.com": {
+				"github_token": "ghp_xxxxx",
+				"github_password": "secret123",
+				"github_2fa_bu_2fa_code": "JBSWY3DPEHPK3PXP"
+			}
+		}
+
+		arguments = {
+			"username": "user",
+			"password": "<secret>github_password</secret>",
+			"totp": "<secret>github_2fa_bu_2fa_code</secret>"
+		}
+
+		# 执行替换
+		replaced = replace_sensitive_data(arguments, sensitive_data, "https://github.com")
+
+		# 结果:
+		# {
+		#     "username": "user",
+		#     "password": "secret123",
+		#     "totp": "123456"  # 自动生成的 TOTP 代码
+		# }
+	"""
+	# 获取适用的密钥
+	applicable_secrets = get_applicable_secrets(sensitive_data, current_url)
+
+	if not applicable_secrets:
+		logger.debug("No applicable secrets found for current URL")
+		return arguments
+
+	# 跟踪替换和缺失的占位符
+	replaced_placeholders = set()
+	missing_placeholders = set()
+
+	# 递归替换
+	replaced_arguments = replace_secrets_recursively(
+		arguments,
+		applicable_secrets,
+		replaced_placeholders,
+		missing_placeholders
+	)
+
+	# 记录日志
+	if replaced_placeholders:
+		logger.info(
+			f"Replaced sensitive placeholders: {', '.join(sorted(replaced_placeholders))}"
+			f"{' on ' + current_url if current_url else ''}"
+		)
+
+	if missing_placeholders:
+		logger.warning(
+			f"Missing sensitive data keys: {', '.join(sorted(missing_placeholders))}"
+		)
+
+	return replaced_arguments

+ 142 - 0
agent/tools/url_matcher.py

@@ -0,0 +1,142 @@
+"""
+URL Pattern Matching - 域名模式匹配工具
+
+用于工具的域名过滤功能,支持 glob 模式:
+- *.example.com
+- www.example.*
+- https://*.example.com/path/*
+"""
+
+import re
+from typing import List, Optional
+from urllib.parse import urlparse
+
+
+def normalize_pattern(pattern: str) -> str:
+	"""
+	规范化 URL 模式
+
+	Args:
+		pattern: URL 模式(可能包含协议、通配符等)
+
+	Returns:
+		规范化的模式
+	"""
+	# 如果没有协议,添加通配符协议
+	if not pattern.startswith(("http://", "https://", "*://")):
+		pattern = f"*://{pattern}"
+
+	return pattern
+
+
+def pattern_to_regex(pattern: str) -> re.Pattern:
+	"""
+	将 glob 模式转换为正则表达式
+
+	支持的通配符:
+	- * : 匹配任意字符(不包括 /)
+	- ** : 匹配任意字符(包括 /)
+
+	Args:
+		pattern: glob 模式
+
+	Returns:
+		编译后的正则表达式
+	"""
+	# 转义正则表达式特殊字符
+	regex = re.escape(pattern)
+
+	# 替换通配符
+	regex = regex.replace(r"\*\*", ".__DOUBLE_STAR__")
+	regex = regex.replace(r"\*", r"[^/]*")
+	regex = regex.replace(".__DOUBLE_STAR__", ".*")
+
+	# 添加开始和结束锚点
+	regex = f"^{regex}$"
+
+	return re.compile(regex, re.IGNORECASE)
+
+
+def match_url_with_pattern(url: str, pattern: str) -> bool:
+	"""
+	检查 URL 是否匹配模式
+
+	Args:
+		url: 要检查的 URL
+		pattern: URL 模式(支持通配符)
+
+	Returns:
+		是否匹配
+
+	Examples:
+		>>> match_url_with_pattern("https://google.com", "*.google.com")
+		False
+		>>> match_url_with_pattern("https://www.google.com", "*.google.com")
+		True
+		>>> match_url_with_pattern("https://www.google.co.uk", "www.google.*")
+		True
+		>>> match_url_with_pattern("https://github.com/user/repo", "https://github.com/**")
+		True
+	"""
+	# 规范化模式
+	pattern = normalize_pattern(pattern)
+
+	# 解析 URL
+	parsed_url = urlparse(url)
+
+	# 构建完整 URL 字符串用于匹配
+	url_str = f"{parsed_url.scheme}://{parsed_url.netloc}{parsed_url.path}"
+	if parsed_url.query:
+		url_str += f"?{parsed_url.query}"
+
+	# 转换为正则并匹配
+	regex = pattern_to_regex(pattern)
+	return bool(regex.match(url_str))
+
+
+def match_url_with_patterns(url: str, patterns: List[str]) -> bool:
+	"""
+	检查 URL 是否匹配任一模式
+
+	Args:
+		url: 要检查的 URL
+		patterns: URL 模式列表
+
+	Returns:
+		是否匹配任一模式
+	"""
+	return any(match_url_with_pattern(url, pattern) for pattern in patterns)
+
+
+def filter_by_url(
+	items: List[dict],
+	current_url: Optional[str],
+	url_field: str = "url_patterns"
+) -> List[dict]:
+	"""
+	根据 URL 过滤项目列表
+
+	Args:
+		items: 项目列表(每个包含 url_patterns 字段)
+		current_url: 当前 URL(None = 只返回无 URL 限制的项)
+		url_field: URL 模式字段名
+
+	Returns:
+		过滤后的项目列表
+	"""
+	if current_url is None:
+		# 没有 URL 上下文,只返回无 URL 限制的项
+		return [item for item in items if not item.get(url_field)]
+
+	# 有 URL 上下文,返回匹配的项
+	filtered = []
+	for item in items:
+		patterns = item.get(url_field)
+		if patterns is None:
+			# 无 URL 限制,总是包含
+			filtered.append(item)
+		elif match_url_with_patterns(current_url, patterns):
+			# 匹配 URL,包含
+			filtered.append(item)
+
+	return filtered

+ 171 - 201
docs/design.md → docs/README.md

@@ -1,12 +1,23 @@
-# Agent 设计文档
+# Agent 功能需求与架构设计文档
 
-> **设计目标**:可扩展、可学习的 Agent 框架,支持执行追踪和持久记忆。
->
-> **使用场景**:后台执行复杂任务,人类专家定期检查和反馈。
+> **可执行规格书**:本文档是系统的核心设计。代码修改必须同步更新此文档。
+> 如文档与代码冲突,以代码为准,并立即修复文档。
 
 ---
 
-## 1. 核心洞察
+## 文档维护规范
+
+**维护原则**:
+1. **谁改代码谁更新文档** - 功能变更后,相关文档必须同步修改
+2. **保持结构稳定** - 只增删内容,不随意调整层级结构
+3. **流程优先** - 新功能先写入核心流程,再补充模块详情
+4. **链接代码** - 关键实现标注文件路径,格式:`module/file.py:function_name`
+5. **简洁原则** - 只记录最重要的信息,避免大量代码
+6. **文档分层** - 每层文档是不同层次的overview,在上层文档对应位置引用下层详细文档
+
+---
+
+## 系统概览
 
 **单次调用是 Agent 的特例**:
 
@@ -20,7 +31,9 @@
 
 ---
 
-## 2. 三层记忆模型
+## 核心架构
+
+### 三层记忆模型
 
 ```
 ┌─────────────────────────────────────────────────────────────┐
@@ -50,7 +63,71 @@
 
 ---
 
-## 3. 执行追踪
+## 核心流程:Agent Loop
+
+```python
+async def run(task: str, max_steps: int = 50):
+    # 1. 创建 Trace
+    trace = Trace(trace_id=gen_id(), task=task, status="running")
+    await trace_store.save(trace)
+
+    # 2. 检索 Experiences,构建 system prompt
+    experiences = await search_experiences(task)
+    system_prompt = build_system_prompt(experiences)
+
+    # 3. 初始化消息
+    messages = [{"role": "user", "content": task}]
+
+    # 4. ReAct 循环
+    for step in range(max_steps):
+        # 调用 LLM
+        response = await llm.chat(
+            messages=messages,
+            system=system_prompt,
+            tools=tool_registry.to_schema()  # 包括 skill 工具
+        )
+
+        # 记录 LLM 调用
+        await add_step(trace, "llm_call", {
+            "response": response.content,
+            "tool_calls": response.tool_calls
+        })
+
+        # 没有工具调用,完成
+        if not response.tool_calls:
+            break
+
+        # 执行工具
+        for tool_call in response.tool_calls:
+            # Doom loop 检测
+            if is_doom_loop(tool_call):
+                raise DoomLoopError()
+
+            # 执行工具(包括 skill 工具)
+            result = await execute_tool(tool_call)
+
+            # 记录步骤
+            await add_step(trace, "tool_call", {"tool": tool_call.name, "args": tool_call.args})
+            await add_step(trace, "tool_result", {"output": result})
+
+            # 添加到消息历史
+            messages.append({"role": "assistant", "tool_calls": [tool_call]})
+            messages.append({"role": "tool", "content": result})
+
+    # 5. 完成
+    trace.status = "completed"
+    await trace_store.save(trace)
+
+    return trace
+```
+
+**关键机制**:
+- **Doom Loop 检测**:跟踪最近 3 次工具调用,如果都是同一个工具且参数相同,中断循环
+- **动态工具加载**:Skill 通过 tool 动态加载,按需消耗 context
+
+---
+
+## 数据模型
 
 ### Trace(任务执行)
 
@@ -75,12 +152,13 @@ class Trace:
     completed_at: Optional[datetime] = None
 ```
 
-**context 字段说明**:存储任务相关的元信息,用于管理和分析
+**实现**:`agent/models/trace.py:Trace`
+
+**context 字段**:存储任务相关的元信息
 - `user_id`: 用户 ID
 - `project_id`: 项目 ID
 - `priority`: 优先级
 - `tags`: 标签列表
-- 其他业务相关数据
 
 ### Step(执行步骤)
 
@@ -89,7 +167,7 @@ class Trace:
 class Step:
     step_id: str
     trace_id: str
-    step_type: str  # "llm_call", "tool_call", "tool_result", ...
+    step_type: StepType  # "llm_call", "tool_call", "tool_result", ...
 
     # DAG 结构
     parent_ids: List[str] = field(default_factory=list)
@@ -100,6 +178,8 @@ class Step:
     created_at: datetime
 ```
 
+**实现**:`agent/models/trace.py:Step`
+
 **常见 step_type**:
 - `llm_call`: LLM 调用(data: messages, response, tokens, cost)
 - `tool_call`: 工具调用(data: tool_name, arguments)
@@ -125,11 +205,36 @@ Trace
 
 ---
 
-## 4. Skills(技能库)
+## 模块详情
+
+详细的模块文档请参阅:
+
+### [工具系统](./tools.md)
+- 工具定义和注册
+- 双层记忆管理
+- 域名过滤、敏感数据处理
+- 集成 Browser-Use
+- 最佳实践
+
+**核心特性**:
+```python
+from reson_agent import tool, ToolResult, ToolContext
+
+@tool(
+    url_patterns=["*.google.com"],
+    requires_confirmation=True
+)
+async def my_tool(arg: str, ctx: ToolContext) -> ToolResult:
+    return ToolResult(
+        title="Success",
+        output="Result content",
+        long_term_memory="Short summary"
+    )
+```
 
-### 存储
+### Skills(技能库)
 
-Markdown 文件:
+**存储**:Markdown 文件
 
 ```
 ~/.reson/skills/           # 全局
@@ -140,7 +245,7 @@ Markdown 文件:
 └── api-integration/SKILL.md
 ```
 
-### 格式
+**格式**:
 
 ```markdown
 ---
@@ -158,31 +263,13 @@ description: Error handling best practices
 - Group by error type
 ```
 
-### 加载
-
-通过 `skill` 工具:
-
-```python
-@tool(id="skill", description="Load a skill by name", parameters={"name": str})
-async def skill_tool(name: str) -> str:
-    # 扫描 Skills 目录
-    for dir in [Path.home() / ".reson/skills", Path.cwd() / ".reson/skills"]:
-        skill_file = dir / name / "SKILL.md"
-        if skill_file.exists():
-            return skill_file.read_text()
-
-    raise FileNotFoundError(f"Skill '{name}' not found")
-```
-
-**本质**:读取文件的工具,返回字符串。
-
----
+**加载**:通过 `skill` 工具动态加载
 
-## 5. Experiences(经验库)
+**实现**:`agent/storage/skill_fs.py:SkillLoader`
 
-### 存储
+### Experiences(经验库)
 
-PostgreSQL + pgvector
+**存储**:PostgreSQL + pgvector
 
 ```sql
 CREATE TABLE experiences (
@@ -204,7 +291,7 @@ CREATE TABLE experiences (
 );
 ```
 
-### 检索和注入
+**检索和注入**:
 
 ```python
 # 1. 检索相关 Experiences
@@ -227,118 +314,11 @@ system_prompt = base_prompt + "\n\n# Learned Experiences\n" + "\n".join([
 ])
 ```
 
----
-
-## 6. Agent Loop
-
-```python
-async def run(task: str, max_steps: int = 50):
-    # 1. 创建 Trace
-    trace = Trace(trace_id=gen_id(), task=task, status="running")
-    await trace_store.save(trace)
-
-    # 2. 检索 Experiences,构建 system prompt
-    experiences = await search_experiences(task)
-    system_prompt = build_system_prompt(experiences)
-
-    # 3. 初始化消息
-    messages = [{"role": "user", "content": task}]
-
-    # 4. ReAct 循环
-    for step in range(max_steps):
-        # 调用 LLM
-        response = await llm.chat(
-            messages=messages,
-            system=system_prompt,
-            tools=tool_registry.to_schema()  # 包括 skill 工具
-        )
-
-        # 记录 LLM 调用
-        await add_step(trace, "llm_call", {
-            "response": response.content,
-            "tool_calls": response.tool_calls
-        })
-
-        # 没有工具调用,完成
-        if not response.tool_calls:
-            break
-
-        # 执行工具
-        for tool_call in response.tool_calls:
-            # Doom loop 检测
-            if is_doom_loop(tool_call):
-                raise DoomLoopError()
-
-            # 执行工具(包括 skill 工具)
-            result = await execute_tool(tool_call)
-
-            # 记录步骤
-            await add_step(trace, "tool_call", {"tool": tool_call.name, "args": tool_call.args})
-            await add_step(trace, "tool_result", {"output": result})
-
-            # 添加到消息历史
-            messages.append({"role": "assistant", "tool_calls": [tool_call]})
-            messages.append({"role": "tool", "content": result})
-
-    # 5. 完成
-    trace.status = "completed"
-    await trace_store.save(trace)
-
-    return trace
-```
-
-**Doom Loop 检测**:
-- 跟踪最近 3 次工具调用
-- 如果都是同一个工具且参数相同,中断循环
+**实现**:`agent/storage/experience_pg.py:ExperienceStore`
 
 ---
 
-## 7. 工具系统
-
-### 定义
-
-```python
-@dataclass
-class ToolResult:
-    output: str
-    metadata: Dict[str, Any] = field(default_factory=dict)
-
-class Tool(Protocol):
-    id: str
-    description: str
-    parameters: Type[BaseModel]
-
-    async def execute(self, args: Dict, ctx: ToolContext) -> ToolResult: ...
-```
-
-### 装饰器
-
-```python
-@tool(id="read", description="Read a file", parameters={"path": str})
-async def read_tool(path: str) -> str:
-    return Path(path).read_text()
-
-@tool(id="skill", description="Load a skill", parameters={"name": str})
-async def skill_tool(name: str) -> str:
-    # 扫描并加载 Skill 文件
-    ...
-```
-
-### 注册
-
-```python
-registry = ToolRegistry()
-registry.register(read_tool)
-registry.register(skill_tool)
-registry.register(search_tool)
-
-# 转换为 LLM schema
-tools_schema = registry.to_schema()
-```
-
----
-
-## 8. 存储接口
+## 存储接口
 
 ```python
 class TraceStore(Protocol):
@@ -360,6 +340,8 @@ class SkillLoader(Protocol):
         """加载指定 skill 的 Markdown 内容"""
 ```
 
+**实现**:`agent/storage/protocols.py`
+
 **实现策略**:
 - Trace/Step: 文件系统(JSON)
 - Experience: PostgreSQL + pgvector
@@ -367,13 +349,15 @@ class SkillLoader(Protocol):
 
 ---
 
-## 9. 模块结构
+## 模块结构
 
 ```
-reson_agent/
+agent/
 ├── __init__.py
 ├── runner.py              # AgentRunner
-├── models.py              # Trace, Step
+├── models/
+│   ├── trace.py           # Trace, Step
+│   └── memory.py          # Experience, Skill
 ├── storage/
 │   ├── protocols.py       # TraceStore, ExperienceStore, SkillLoader
 │   ├── trace_fs.py        # 文件系统实现
@@ -381,78 +365,64 @@ reson_agent/
 │   └── skill_fs.py        # 文件系统实现
 ├── tools/
 │   ├── registry.py        # ToolRegistry
-│   ├── decorator.py       # @tool
-│   └── builtin.py         # read, skill, search
+│   ├── models.py          # ToolResult, ToolContext
+│   ├── schema.py          # SchemaGenerator
+│   ├── url_matcher.py     # URL 模式匹配
+│   └── sensitive.py       # 敏感数据处理
 └── llm.py                 # LLMProvider Protocol
 ```
 
 ---
 
-## 10. 设计决策
+## 设计决策
 
-### Skills 通过工具加载 vs 预先注入
+详见 [设计决策文档](./decisions.md)
 
-**方案对比**:
+**核心决策**:
 
-| 方案 | 优点 | 缺点 |
-|------|------|------|
-| 预先注入到 system prompt | 简单 | 浪费 token,Agent 无法选择 |
-| 作为工具动态加载 | 按需加载,Agent 自主选择 | 需要实现 skill 工具 |
+1. **Skills 通过工具加载**(vs 预先注入)
+   - 按需加载,Agent 自主选择
+   - 参考 OpenCode 和 Claude API 文档
 
-**选择**:动态加载(参考 OpenCode 和 Claude API 文档)
+2. **Skills 用文件系统**(vs 数据库)
+   - 易于编辑(Markdown)
+   - 版本控制(Git)
+   - 零依赖
 
-### Skills 用文件 vs 数据库
+3. **Experiences 用数据库**(vs 文件)
+   - 需要向量检索
+   - 需要统计分析
+   - 数量大,动态更新
 
-**选择**:文件系统
-- 易于编辑(Markdown)
-- 版本控制(Git)
-- 零依赖
-
-### Experiences 用数据库 vs 文件
-
-**选择**:数据库(PostgreSQL + pgvector)
-- 需要向量检索
-- 需要统计分析
-- 数量大,动态更新
-
-### 为什么不需要事件系统?
-
-**原因**:后台场景,不需要实时通知
-- Trace/Step 已记录所有信息
-- 需要告警时直接调用
+4. **不需要事件系统**
+   - 后台场景,不需要实时通知
+   - Trace/Step 已记录所有信息
 
 ---
 
-## 11. 实现计划
+## 实现计划
 
 ### Phase 1:MVP
-- [ ] Trace/Step 数据模型
-- [ ] 文件系统 TraceStore
-- [ ] 文件系统 SkillLoader
 - [ ] AgentRunner 基础循环
-- [ ] Doom Loop 检测
 - [ ] 基础工具(read, skill)
+- [ ] 高级工具集成:Browser-Use、Search
+- [ ] 单次执行的监控与分析:Trace/Step 数据模型与文件系统存储、初步的执行历史可视化
 
-### Phase 2:完善
-- [ ] PostgreSQL ExperienceStore
-- [ ] 向量检索(pgvector)
-- [ ] Experience 自动提取
-- [ ] Skill 自动归纳
+### Phase 2:反思能力
+- [ ] Experience:feedback、归纳反思
+- [ ] 批量执行的监控与分析
 
-### Phase 3:集成
-- [ ] 与 Resonote 集成
-- [ ] 多 Agent 协作
-- [ ] 监控和成本分析
 
 ---
 
-## 附录:核心概念
-
-| 概念 | 定义 | 存储 |
-|------|------|------|
-| **Trace** | 一次任务执行 | 文件系统(JSON) |
-| **Step** | 执行步骤 | 文件系统(JSON) |
-| **Skill** | 能力描述(Markdown) | 文件系统 |
-| **Experience** | 经验规则(条件+规则) | 数据库 + 向量 |
-| **Agent Loop** | ReAct 循环 | - |
-| **Doom Loop** | 无限循环检测 | - |
+## 核心概念速查
+
+| 概念 | 定义 | 存储 | 实现 |
+|------|------|------|------|
+| **Trace** | 一次任务执行 | 文件系统(JSON) | `models/trace.py` |
+| **Step** | 执行步骤 | 文件系统(JSON) | `models/trace.py` |
+| **Skill** | 能力描述(Markdown) | 文件系统 | `storage/skill_fs.py` |
+| **Experience** | 经验规则(条件+规则) | 数据库 + 向量 | `storage/experience_pg.py` |
+| **Tool** | 可调用的函数 | 内存(注册表) | `tools/registry.py` |
+| **Agent Loop** | ReAct 循环 | - | `runner.py` |
+| **Doom Loop** | 无限循环检测 | - | `runner.py` |

+ 358 - 0
docs/decisions.md

@@ -0,0 +1,358 @@
+# 设计决策
+
+> 记录系统设计中的关键决策、权衡和理由。
+
+---
+
+## 1. Skills 通过工具加载 vs 预先注入
+
+### 问题
+Skills 包含大量能力描述,如何提供给 Agent?
+
+### 方案对比
+
+| 方案 | 优点 | 缺点 |
+|------|------|------|
+| **预先注入到 system prompt** | 实现简单 | 浪费 token,Agent 无法选择需要的 skill |
+| **作为工具动态加载** | 按需加载,Agent 自主选择 | 需要实现 skill 工具 |
+
+### 决策
+**选择:作为工具动态加载**
+
+**理由**:
+1. **Token 效率**:只加载需要的 skill,避免浪费 context
+2. **Agent 自主性**:LLM 根据任务决定需要哪些 skill
+3. **可扩展性**:可以有数百个 skills,不影响单次调用的 token 消耗
+4. **业界参考**:OpenCode 和 Claude API 文档都采用此方式
+
+**参考**:
+- OpenCode 的 skill 系统
+- Claude API 文档中的工具使用模式
+
+---
+
+## 2. Skills 用文件系统 vs 数据库
+
+### 问题
+Skills 如何存储?
+
+### 方案对比
+
+| 方案 | 优点 | 缺点 |
+|------|------|------|
+| **文件系统(Markdown)** | 易于编辑,支持版本控制,零依赖 | 搜索能力弱 |
+| **数据库** | 搜索强大,支持元数据 | 编辑困难,需要额外服务 |
+
+### 决策
+**选择:文件系统(Markdown)**
+
+**理由**:
+1. **易于编辑**:直接用文本编辑器或 IDE 编辑
+2. **版本控制**:通过 Git 管理 skill 的历史变更
+3. **零依赖**:不需要数据库服务
+4. **人类可读**:Markdown 格式,便于人工审查和修改
+5. **搜索需求低**:Skill 数量有限(几十到几百个),文件扫描足够快
+
+**实现**:
+```
+~/.reson/skills/           # 全局 skills
+└── error-handling/SKILL.md
+
+./project/.reson/skills/   # 项目级 skills
+└── api-integration/SKILL.md
+```
+
+---
+
+## 3. Experiences 用数据库 vs 文件
+
+### 问题
+Experiences 如何存储?
+
+### 方案对比
+
+| 方案 | 优点 | 缺点 |
+|------|------|------|
+| **文件系统** | 简单,零依赖 | 搜索慢,不支持向量检索 |
+| **数据库(PostgreSQL + pgvector)** | 向量检索,统计分析,高性能 | 需要数据库服务 |
+
+### 决策
+**选择:数据库(PostgreSQL + pgvector)**
+
+**理由**:
+1. **向量检索必需**:Experiences 需要根据任务语义匹配,文件系统无法支持
+2. **统计分析**:需要追踪 success_rate, usage_count 等指标
+3. **数量大**:Experiences 会随着使用不断增长(数千到数万条)
+4. **动态更新**:每次执行后可能更新统计信息,数据库更适合
+
+**实现**:
+```sql
+CREATE TABLE experiences (
+    exp_id TEXT PRIMARY KEY,
+    scope TEXT,
+    condition TEXT,
+    rule TEXT,
+    evidence JSONB,
+
+    confidence FLOAT,
+    usage_count INT,
+    success_rate FLOAT,
+
+    embedding vector(1536),  -- 向量检索
+
+    created_at TIMESTAMP,
+    updated_at TIMESTAMP
+);
+
+CREATE INDEX ON experiences USING ivfflat (embedding vector_cosine_ops);
+```
+
+---
+
+## 4. 不需要事件系统
+
+### 问题
+是否需要事件总线(EventBus)来通知任务状态变化?
+
+### 决策
+**选择:不需要事件系统**
+
+**理由**:
+1. **后台场景**:Agent 主要在后台运行,不需要实时通知
+2. **已有追踪**:Trace/Step 已完整记录所有信息
+3. **按需查询**:需要监控时,查询 Trace 即可
+4. **简化架构**:避免引入额外的复杂性
+
+**替代方案**:
+- 需要告警时,直接在 AgentRunner 中调用通知函数
+- 需要实时监控时,轮询 TraceStore
+
+---
+
+## 5. Trace/Step 用文件系统 vs 数据库
+
+### 问题
+Trace 和 Step 如何存储?
+
+### 方案对比
+
+| 方案 | 优点 | 缺点 |
+|------|------|------|
+| **文件系统(JSON)** | 简单,易于调试,可直接查看 | 搜索和分析能力弱 |
+| **数据库** | 搜索强大,支持复杂查询 | 初期复杂,调试困难 |
+
+### 决策
+**选择:文件系统(JSON)用于 MVP,后期可选数据库**
+
+**理由(MVP阶段)**:
+1. **快速迭代**:JSON 文件易于查看和调试
+2. **零依赖**:不需要数据库服务
+3. **数据量小**:单个项目的 traces 数量有限
+
+**后期迁移到数据库的时机**:
+- Traces 数量超过 1 万条
+- 需要复杂的查询和分析(如"查找所有失败的 traces")
+- 需要聚合统计(如"Agent 的平均成功率")
+
+**实现接口保持一致**:
+```python
+class TraceStore(Protocol):
+    async def save(self, trace: Trace) -> None: ...
+    async def get(self, trace_id: str) -> Trace: ...
+    # ...
+```
+
+通过 Protocol 定义,可以无缝切换实现。
+
+---
+
+## 6. 工具系统的双层记忆管理
+
+### 问题
+工具返回的数据可能很大(如 Browser-Use 的 extract 返回 10K tokens),如何避免占用过多 context?
+
+### 方案对比
+
+| 方案 | 优点 | 缺点 |
+|------|------|------|
+| **单一输出** | 简单 | 大数据会持续占用 context |
+| **双层记忆**(output + long_term_memory) | 节省 context,避免重复传输 | 稍微复杂 |
+
+### 决策
+**选择:双层记忆管理**
+
+**设计**:
+```python
+@dataclass
+class ToolResult:
+    title: str
+    output: str                           # 临时内容(可能很长)
+    long_term_memory: Optional[str]       # 永久记忆(简短摘要)
+    include_output_only_once: bool        # output 是否只给 LLM 看一次
+```
+
+**效果**:
+```
+[User] 提取 amazon.com 的商品价格
+[Assistant] 调用 extract_page_data(url="amazon.com")
+[Tool]
+# Extracted page data
+
+<完整的 10K tokens 数据...>
+
+Summary: Extracted 10000 chars from amazon.com
+
+[User] 现在保存到文件
+[Assistant] 调用 write_file(content="...")
+[Tool] (此时不再包含 10K tokens,只有摘要)
+Summary: Extracted 10000 chars from amazon.com
+```
+
+**理由**:
+1. **Context 效率**:大量数据只传输一次
+2. **保留关键信息**:摘要永久保留在对话历史中
+3. **Browser-Use 兼容**:直接映射到 Browser-Use 的 ActionResult 设计
+
+**参考**:Browser-Use 的 ActionResult.extracted_content 和 long_term_memory
+
+---
+
+## 7. 工具的域名过滤
+
+### 问题
+某些工具只在特定网站可用(如 Google 搜索技巧),是否需要域名过滤?
+
+### 决策
+**选择:支持域名过滤(可选)**
+
+**设计**:
+```python
+@tool(url_patterns=["*.google.com", "www.google.*"])
+async def google_advanced_search(...):
+    """仅在 Google 页面可用的工具"""
+    ...
+```
+
+**理由**:
+1. **减少 context**:在 Google 页面,35 工具 → 20 工具(节省 40%)
+2. **减少 LLM 困惑**:工具数量少了,LLM 更容易选择正确工具
+3. **灵活性**:默认 `url_patterns=None`,所有页面可用
+
+**实现**:
+- URL 模式匹配引擎(`tools/url_matcher.py`)
+- 动态工具过滤(`registry.get_schemas_for_url()`)
+
+---
+
+## 8. 敏感数据处理
+
+### 问题
+浏览器自动化需要输入密码、Token,但不想在对话历史中显示明文,如何处理?
+
+### 决策
+**选择:占位符替换机制**
+
+**设计**:
+```python
+# LLM 输出占位符
+arguments = {
+    "password": "<secret>github_password</secret>",
+    "totp": "<secret>github_2fa_bu_2fa_code</secret>"
+}
+
+# 执行前自动替换
+sensitive_data = {
+    "*.github.com": {
+        "github_password": "secret123",
+        "github_2fa_bu_2fa_code": "JBSWY3DPEHPK3PXP"  # TOTP secret
+    }
+}
+```
+
+**理由**:
+1. **保护隐私**:对话历史中只有占位符,不泄露实际密码
+2. **域名匹配**:不同网站使用不同密钥,防止密钥泄露
+3. **TOTP 支持**:自动生成 2FA 验证码,无需手动输入
+4. **Browser-Use 兼容**:直接映射到 Browser-Use 的敏感数据处理
+
+**实现**:
+- 递归替换(`tools/sensitive.py`)
+- 支持嵌套结构(dict, list, str)
+- 自动 TOTP 生成(pyotp)
+
+**参考**:Browser-Use 的 _replace_sensitive_data
+
+---
+
+## 9. 工具使用统计
+
+### 问题
+是否需要记录工具调用统计(调用次数、成功率、执行时间)?
+
+### 决策
+**选择:内建统计支持**
+
+**设计**:
+```python
+class ToolStats:
+    call_count: int
+    success_count: int
+    failure_count: int
+    total_duration: float
+    last_called: Optional[float]
+```
+
+**理由**:
+1. **监控健康**:识别失败率高的工具
+2. **性能优化**:识别执行慢的工具
+3. **优化排序**:高频工具排前面,减少 LLM 选择时间
+4. **零成本**:自动记录,性能影响 <0.01ms
+
+**用途**:
+- 监控工具健康状况(失败率、延迟)
+- 优化工具顺序(高频工具排前面)
+- 识别问题工具(低成功率、高延迟)
+
+---
+
+## 10. 工具参数的可编辑性
+
+### 问题
+LLM 生成的工具参数是否允许用户编辑?
+
+### 决策
+**选择:支持可选的参数编辑**
+
+**设计**:
+```python
+@tool(editable_params=["query", "filters"])
+async def advanced_search(
+    query: str,
+    filters: Optional[Dict] = None,
+    uid: str = ""
+) -> ToolResult:
+    """高级搜索(用户可编辑 query 和 filters)"""
+    ...
+```
+
+**理由**:
+1. **人类监督**:Agent 生成的参数可能不准确,允许人工微调
+2. **灵活性**:大多数工具不需要编辑(默认 `editable_params=[]`)
+3. **UI 集成**:前端可以展示可编辑的参数供用户修改
+
+**适用场景**:
+- 搜索查询
+- 内容创建
+- 需要人工微调的参数
+
+---
+
+## 总结
+
+这些设计决策的核心原则:
+
+1. **灵活性优先**:大多数特性都是可选的,保持系统简洁
+2. **Token 效率**:通过动态加载、双层记忆等机制优化 context 使用
+3. **可扩展性**:通过 Protocol 定义接口,便于后期切换实现
+4. **安全性**:敏感数据占位符、域名匹配等机制保护隐私
+5. **可观测性**:内建统计、完整追踪,便于监控和调试

+ 944 - 0
docs/tools.md

@@ -0,0 +1,944 @@
+# 工具系统文档
+
+> Agent 框架的工具系统:定义、注册、执行工具调用。
+
+---
+
+## 目录
+
+1. [核心概念](#核心概念)
+2. [定义工具](#定义工具)
+3. [ToolResult 和记忆管理](#toolresult-和记忆管理)
+4. [ToolContext 和依赖注入](#toolcontext-和依赖注入)
+5. [高级特性](#高级特性)
+6. [集成 Browser-Use](#集成-browser-use)
+7. [最佳实践](#最佳实践)
+
+---
+
+## 核心概念
+
+### 三个核心类型
+
+```python
+from reson_agent import tool, ToolResult, ToolContext
+
+@tool()
+async def my_tool(arg: str, ctx: ToolContext) -> ToolResult:
+    return ToolResult(
+        title="Success",
+        output="Result content"
+    )
+```
+
+| 类型 | 作用 | 定义位置 |
+|------|------|---------|
+| **`@tool`** | 装饰器,自动注册工具并生成 Schema | `tools/registry.py` |
+| **`ToolResult`** | 工具执行结果(支持记忆管理) | `tools/models.py` |
+| **`ToolContext`** | 工具执行上下文(依赖注入) | `tools/models.py` |
+
+### 工具的生命周期
+
+```
+1. 定义工具
+   ↓ @tool() 装饰器
+2. 自动注册到 ToolRegistry
+   ↓ 生成 OpenAI Tool Schema
+3. LLM 选择工具并生成参数
+   ↓ registry.execute(name, args)
+4. 注入 uid 和 context
+   ↓ 调用工具函数
+5. 返回 ToolResult
+   ↓ 转换为 LLM 消息
+6. 添加到对话历史
+```
+
+---
+
+## 定义工具
+
+### 最简形式
+
+```python
+from reson_agent import tool
+
+@tool()
+async def hello(name: str, uid: str = "") -> str:
+    """向用户问好"""
+    return f"Hello, {name}!"
+```
+
+**要点**:
+- `uid` 参数由框架自动注入(用户不传递)
+- 可以是同步或异步函数
+- 返回值自动序列化为 JSON
+
+### 带完整注释
+
+```python
+@tool()
+async def search_notes(
+    query: str,
+    limit: int = 10,
+    uid: str = ""
+) -> str:
+    """
+    搜索用户的笔记
+
+    Args:
+        query: 搜索关键词
+        limit: 返回结果数量
+
+    Returns:
+        JSON 格式的搜索结果
+    """
+    # 自动从 docstring 提取 function description 和 parameter descriptions
+    ...
+```
+
+### 带 UI 元数据
+
+```python
+@tool(
+    display={
+        "zh": {
+            "name": "搜索笔记",
+            "params": {
+                "query": "搜索关键词",
+                "limit": "结果数量"
+            }
+        },
+        "en": {
+            "name": "Search Notes",
+            "params": {
+                "query": "Search query",
+                "limit": "Result limit"
+            }
+        }
+    }
+)
+async def search_notes(query: str, limit: int = 10, uid: str = "") -> str:
+    """搜索用户的笔记"""
+    ...
+```
+
+---
+
+## ToolResult 和记忆管理
+
+### 基础用法
+
+```python
+from reson_agent import ToolResult
+
+@tool()
+async def read_file(path: str) -> ToolResult:
+    content = Path(path).read_text()
+
+    return ToolResult(
+        title=f"Read {path}",
+        output=content
+    )
+```
+
+### 双层记忆管理
+
+**问题**:某些工具返回大量内容(如 Browser-Use 的 `extract`),如果每次都放入对话历史,会快速耗尽 context。
+
+**解决**:`ToolResult` 支持双层记忆:
+
+```python
+@tool()
+async def extract_page_data(url: str) -> ToolResult:
+    # 假设提取了 10K tokens 的内容
+    full_content = extract_all_data(url)
+
+    return ToolResult(
+        title="Extracted page data",
+        output=full_content,  # 完整内容(可能很长)
+        long_term_memory=f"Extracted {len(full_content)} chars from {url}",  # 简短摘要
+        include_output_only_once=True  # output 只给 LLM 看一次
+    )
+```
+
+**效果**:
+- **第一次**:LLM 看到 `output`(完整内容)+ `long_term_memory`(摘要)
+- **后续**:LLM 只看到 `long_term_memory`(摘要)
+
+**对话历史示例**:
+
+```
+[User] 提取 amazon.com 的商品价格
+[Assistant] 调用 extract_page_data(url="amazon.com")
+[Tool]
+# Extracted page data
+
+<完整的 10K tokens 数据...>
+
+Summary: Extracted 10000 chars from amazon.com
+
+[User] 现在保存到文件
+[Assistant] 调用 write_file(content="...")
+[Tool] (此时不再包含 10K tokens,只有摘要)
+Summary: Extracted 10000 chars from amazon.com
+```
+
+### 错误处理
+
+```python
+@tool()
+async def risky_operation() -> ToolResult:
+    try:
+        result = perform_operation()
+        return ToolResult(
+            title="Success",
+            output=result
+        )
+    except Exception as e:
+        return ToolResult(
+            title="Failed",
+            output="",
+            error=str(e)
+        )
+```
+
+### 附件和图片
+
+```python
+@tool()
+async def generate_report() -> ToolResult:
+    report_path = create_pdf_report()
+    screenshot_data = take_screenshot()
+
+    return ToolResult(
+        title="Report generated",
+        output="Report created successfully",
+        attachments=[report_path],  # 文件路径列表
+        images=[{
+            "name": "screenshot.png",
+            "data": screenshot_data  # Base64 或路径
+        }]
+    )
+```
+
+---
+
+## ToolContext 和依赖注入
+
+### 基本概念
+
+工具函数可以声明需要 `ToolContext` 参数,框架自动注入。
+
+```python
+from reson_agent import ToolContext
+
+@tool()
+async def get_current_state(ctx: ToolContext) -> ToolResult:
+    return ToolResult(
+        title="Current state",
+        output=f"Trace ID: {ctx.trace_id}\nStep ID: {ctx.step_id}"
+    )
+```
+
+### ToolContext 字段
+
+```python
+class ToolContext(Protocol):
+    # 基础字段(所有工具)
+    trace_id: str               # 当前 Trace ID
+    step_id: str                # 当前 Step ID
+    uid: Optional[str]          # 用户 ID
+
+    # 浏览器相关(Browser-Use 集成)
+    browser_session: Optional[Any]      # 浏览器会话
+    page_url: Optional[str]             # 当前页面 URL
+    file_system: Optional[Any]          # 文件系统访问
+    sensitive_data: Optional[Dict]      # 敏感数据
+
+    # 扩展字段
+    context: Optional[Dict[str, Any]]   # 额外上下文
+```
+
+### 使用示例
+
+```python
+@tool()
+async def analyze_current_page(ctx: ToolContext) -> ToolResult:
+    """分析当前浏览器页面"""
+
+    if not ctx.browser_session:
+        return ToolResult(
+            title="Error",
+            error="Browser session not available"
+        )
+
+    # 使用浏览器会话
+    page_content = await ctx.browser_session.get_content()
+
+    return ToolResult(
+        title=f"Analyzed {ctx.page_url}",
+        output=page_content,
+        long_term_memory=f"Analyzed page at {ctx.page_url}"
+    )
+```
+
+### 创建 ToolContext
+
+```python
+from reson_agent import ToolContextImpl
+
+ctx = ToolContextImpl(
+    trace_id="trace_123",
+    step_id="step_456",
+    uid="user_789",
+    page_url="https://example.com"
+)
+
+# 执行工具
+result = await registry.execute(
+    "analyze_current_page",
+    arguments={},
+    context=ctx
+)
+```
+
+---
+
+## 高级特性
+
+### 1. 需要用户确认
+
+```python
+@tool(requires_confirmation=True)
+async def delete_all_notes(uid: str = "") -> ToolResult:
+    """删除所有笔记(危险操作)"""
+    # 执行前会等待用户确认
+    ...
+```
+
+**适用场景**:
+- 删除操作
+- 发送消息
+- 修改重要设置
+- 任何不可逆操作
+
+### 2. 可编辑参数
+
+```python
+@tool(editable_params=["query", "filters"])
+async def advanced_search(
+    query: str,
+    filters: Optional[Dict] = None,
+    uid: str = ""
+) -> ToolResult:
+    """高级搜索"""
+    # LLM 生成参数后,用户可以编辑 query 和 filters
+    ...
+```
+
+**适用场景**:
+- 搜索查询
+- 内容创建
+- 需要用户微调的参数
+
+### 3. 域名过滤(URL Patterns)
+
+**场景**:某些工具只在特定网站可用,减少无关工具的 context 占用。
+
+```python
+@tool(url_patterns=["*.google.com", "www.google.*"])
+async def google_advanced_search(
+    query: str,
+    date_range: Optional[str] = None,
+    uid: str = ""
+) -> ToolResult:
+    """Google 高级搜索技巧(仅在 Google 页面可用)"""
+    ...
+
+@tool(url_patterns=["*.github.com"])
+async def github_pr_create(
+    title: str,
+    body: str,
+    uid: str = ""
+) -> ToolResult:
+    """创建 GitHub PR(仅在 GitHub 页面可用)"""
+    ...
+
+@tool()  # 无 url_patterns,所有页面都可用
+async def take_screenshot() -> ToolResult:
+    """截图(所有页面都可用)"""
+    ...
+```
+
+**支持的模式**:
+
+```python
+# 通配符域名
+"*.google.com"        # 匹配 www.google.com, mail.google.com
+"www.google.*"        # 匹配 www.google.com, www.google.co.uk
+
+# 路径匹配
+"https://github.com/**/issues"  # 匹配所有 issues 页面
+
+# 多个模式
+url_patterns=["*.github.com", "*.gitlab.com"]
+```
+
+**使用过滤后的工具**:
+
+```python
+from reson_agent import get_tool_registry
+
+registry = get_tool_registry()
+
+# 根据 URL 获取可用工具
+current_url = "https://www.google.com/search?q=test"
+tool_names = registry.get_tool_names(current_url)
+# 返回:["google_advanced_search", "take_screenshot"](不包含 github_pr_create)
+
+# 获取过滤后的 Schema
+schemas = registry.get_schemas_for_url(current_url)
+# 传递给 LLM,只包含相关工具
+```
+
+**效果**:
+
+| 场景 | 无过滤 | 有过滤 | 节省 |
+|------|--------|--------|------|
+| 在 Google 页面 | 35 工具 (~5K tokens) | 20 工具 (~3K tokens) | 40% |
+| 在 GitHub 页面 | 35 工具 (~5K tokens) | 18 工具 (~2.5K tokens) | 50% |
+
+### 4. 敏感数据处理
+
+**场景**:浏览器自动化需要输入密码、Token,但不想在对话历史中显示明文。
+
+**设置敏感数据**:
+
+```python
+sensitive_data = {
+    # 格式 1:全局密钥(适用于所有域名)
+    "api_key": "sk-xxxxx",
+
+    # 格式 2:域名特定密钥(推荐)
+    "*.github.com": {
+        "github_token": "ghp_xxxxx",
+        "github_password": "my_secret_password"
+    },
+
+    "*.google.com": {
+        "google_email": "user@example.com",
+        "google_password": "another_secret",
+        "google_2fa_bu_2fa_code": "JBSWY3DPEHPK3PXP"  # TOTP secret
+    }
+}
+```
+
+**LLM 输出占位符**:
+
+```python
+# LLM 决定需要输入密码
+{
+    "tool": "browser_input",
+    "arguments": {
+        "index": 5,
+        "text": "<secret>github_password</secret>"  # 占位符
+    }
+}
+```
+
+**自动替换**:
+
+```python
+# 执行工具前,框架自动替换
+registry.execute(
+    "browser_input",
+    arguments={"index": 5, "text": "<secret>github_password</secret>"},
+    context={"page_url": "https://github.com/login"},
+    sensitive_data=sensitive_data
+)
+
+# 实际执行:
+# arguments = {"index": 5, "text": "my_secret_password"}
+```
+
+**TOTP 2FA 支持**:
+
+```python
+# 密钥以 _bu_2fa_code 结尾,自动生成 TOTP 代码
+sensitive_data = {
+    "*.google.com": {
+        "google_2fa_bu_2fa_code": "JBSWY3DPEHPK3PXP"
+    }
+}
+
+# LLM 输出
+{
+    "text": "<secret>google_2fa_bu_2fa_code</secret>"
+}
+
+# 自动替换为当前的 6 位数字验证码
+{
+    "text": "123456"  # 当前时间的 TOTP 代码
+}
+```
+
+**完整示例**:
+
+```python
+from reson_agent import get_tool_registry, ToolContext
+
+# 设置敏感数据
+sensitive_data = {
+    "*.github.com": {
+        "github_token": "ghp_xxxxxxxxxxxxx",
+        "github_2fa_bu_2fa_code": "JBSWY3DPEHPK3PXP"
+    }
+}
+
+# 执行工具(LLM 输出的参数包含占位符)
+result = await registry.execute(
+    "github_api_call",
+    arguments={
+        "endpoint": "/user",
+        "token": "<secret>github_token</secret>",
+        "totp": "<secret>github_2fa_bu_2fa_code</secret>"
+    },
+    context={"page_url": "https://github.com"},
+    sensitive_data=sensitive_data
+)
+
+# 实际调用时参数已被替换:
+# {
+#     "endpoint": "/user",
+#     "token": "ghp_xxxxxxxxxxxxx",
+#     "totp": "123456"
+# }
+```
+
+**安全性**:
+- ✅ 对话历史中只有 `<secret>key</secret>` 占位符
+- ✅ 实际密码仅在执行时注入
+- ✅ 域名匹配防止密钥泄露到错误的网站
+- ✅ TOTP 验证码实时生成,无需手动输入
+
+### 5. 工具使用统计
+
+**自动记录**:
+
+每个工具调用自动记录:
+- 调用次数
+- 成功/失败次数
+- 平均执行时间
+- 最后调用时间
+
+**查询统计**:
+
+```python
+from reson_agent import get_tool_registry
+
+registry = get_tool_registry()
+
+# 获取所有工具统计
+all_stats = registry.get_stats()
+print(all_stats)
+# {
+#     "search_notes": {
+#         "call_count": 145,
+#         "success_count": 142,
+#         "failure_count": 3,
+#         "average_duration": 0.32,
+#         "success_rate": 0.979,
+#         "last_called": 1704123456.78
+#     },
+#     ...
+# }
+
+# 获取单个工具统计
+search_stats = registry.get_stats("search_notes")
+
+# 获取 Top 工具
+top_tools = registry.get_top_tools(limit=5, by="call_count")
+# ['search_notes', 'read_file', 'browser_click', ...]
+
+top_by_success = registry.get_top_tools(limit=5, by="success_rate")
+fastest_tools = registry.get_top_tools(limit=5, by="average_duration")
+```
+
+**优化工具排序**:
+
+```python
+# 根据使用频率优化工具顺序
+def get_optimized_schemas(registry, current_url):
+    # 获取可用工具
+    tool_names = registry.get_tool_names(current_url)
+
+    # 按调用次数排序(高频工具排前面)
+    all_stats = registry.get_stats()
+    sorted_tools = sorted(
+        tool_names,
+        key=lambda name: all_stats.get(name, {}).get("call_count", 0),
+        reverse=True
+    )
+
+    # 返回排序后的 Schema
+    return registry.get_schemas(sorted_tools)
+```
+
+**监控和告警**:
+
+```python
+# 监控工具失败率
+stats = registry.get_stats()
+for tool_name, tool_stats in stats.items():
+    if tool_stats["call_count"] > 10 and tool_stats["success_rate"] < 0.8:
+        logger.warning(
+            f"Tool {tool_name} has low success rate: "
+            f"{tool_stats['success_rate']:.1%} "
+            f"({tool_stats['failure_count']}/{tool_stats['call_count']} failures)"
+        )
+
+# 监控执行时间
+for tool_name, tool_stats in stats.items():
+    if tool_stats["average_duration"] > 5.0:
+        logger.warning(
+            f"Tool {tool_name} is slow: "
+            f"average {tool_stats['average_duration']:.2f}s"
+        )
+```
+
+### 6. 组合使用
+
+**完整示例:浏览器自动化工具**
+
+```python
+@tool(
+    requires_confirmation=False,
+    editable_params=["query"],
+    url_patterns=["*.google.com"],
+    display={
+        "zh": {"name": "Google 搜索", "params": {"query": "搜索关键词"}},
+        "en": {"name": "Google Search", "params": {"query": "Query"}}
+    }
+)
+async def google_search(
+    query: str,
+    ctx: ToolContext,
+    uid: str = ""
+) -> ToolResult:
+    """
+    在 Google 执行搜索
+
+    仅在 Google 页面可用,支持敏感数据注入
+    """
+    # 使用浏览器会话
+    if not ctx.browser_session:
+        return ToolResult(title="Error", error="Browser session not available")
+
+    # 敏感数据已在 registry.execute() 中自动处理
+    # 例如 query 中的 <secret>api_key</secret> 已被替换
+
+    # 执行搜索
+    await ctx.browser_session.navigate(f"https://google.com/search?q={query}")
+
+    # 提取结果
+    results = await ctx.browser_session.extract_results()
+
+    return ToolResult(
+        title=f"Search results for {query}",
+        output=json.dumps(results),
+        long_term_memory=f"Searched Google for '{query}', found {len(results)} results",
+        include_output_only_once=True
+    )
+```
+
+**使用**:
+
+```python
+# 设置环境
+registry = get_tool_registry()
+sensitive_data = {"*.google.com": {"search_api_key": "sk-xxxxx"}}
+
+# Agent 在 Google 页面时
+current_url = "https://www.google.com"
+
+# 获取工具(自动过滤)
+tool_names = registry.get_tool_names(current_url)
+# 包含 google_search(匹配 *.google.com)
+
+# LLM 决定使用工具(可能包含敏感占位符)
+tool_call = {
+    "name": "google_search",
+    "arguments": {
+        "query": "site:github.com <secret>search_api_key</secret>"
+    }
+}
+
+# 执行(自动替换敏感数据)
+result = await registry.execute(
+    tool_call["name"],
+    tool_call["arguments"],
+    context={"page_url": current_url, "browser_session": browser},
+    sensitive_data=sensitive_data
+)
+
+# 查看统计
+stats = registry.get_stats("google_search")
+print(f"Success rate: {stats['success_rate']:.1%}")
+```
+
+---
+
+## 集成 Browser-Use
+
+### 适配器模式
+
+将 Browser-Use 的 25 个工具适配为你的工具系统:
+
+```python
+from browser_use import BrowserSession, Tools as BrowserUseTools
+from reson_agent import tool, ToolResult, ToolContext
+
+class BrowserToolsAdapter:
+    """Browser-Use 工具适配器"""
+
+    def __init__(self):
+        self.session = BrowserSession(headless=False)
+        self.browser_tools = BrowserUseTools()
+
+    async def __aenter__(self):
+        await self.session.__aenter__()
+        return self
+
+    async def __aexit__(self, *args):
+        await self.session.__aexit__(*args)
+
+    def register_all(self, registry):
+        """批量注册所有 Browser-Use 工具"""
+        for action_name, registered_action in self.browser_tools.registry.actions.items():
+            self._adapt_action(registry, action_name, registered_action)
+
+    def _adapt_action(self, registry, action_name, registered_action):
+        """适配单个 Browser-Use action"""
+
+        @tool()
+        async def adapted_tool(args: dict, ctx: ToolContext) -> ToolResult:
+            # 构建 Browser-Use 需要的 special context
+            special_context = {
+                'browser_session': self.session,
+                'page_url': ctx.page_url,
+                'file_system': ctx.file_system,
+            }
+
+            # 执行 Browser-Use action
+            result = await registered_action.function(
+                params=registered_action.param_model(**args),
+                **special_context
+            )
+
+            # 转换 ActionResult -> ToolResult
+            return ToolResult(
+                title=action_name,
+                output=result.extracted_content or '',
+                long_term_memory=result.long_term_memory,
+                include_output_only_once=result.include_extracted_content_only_once,
+                error=result.error,
+                attachments=result.attachments or [],
+                images=result.images or [],
+                metadata=result.metadata or {}
+            )
+
+        # 注册到你的 registry
+        registry.register(adapted_tool, schema=generate_schema(registered_action))
+```
+
+### 使用示例
+
+```python
+from reson_agent import AgentRunner
+
+async def main():
+    async with BrowserToolsAdapter() as browser:
+        # 创建 Agent
+        agent = AgentRunner(
+            task="在 Amazon 找最便宜的 iPhone 15",
+            tools=[],  # 空列表
+        )
+
+        # 批量注册浏览器工具
+        browser.register_all(agent.tool_registry)
+
+        # 现在 Agent 有 25 个浏览器工具 + 其他工具
+        result = await agent.run()
+```
+
+### Context 占用分析
+
+| 工具类型 | 数量 | Token 占用 | 占比(200K) |
+|---------|------|-----------|-------------|
+| Browser-Use 工具 | 25 | ~4,000 | 2% |
+| 你的自定义工具 | 10 | ~1,000 | 0.5% |
+| **总计** | **35** | **~5,000** | **2.5%** |
+
+**结论**:完全可接受,且 Prompt Caching 会优化后续调用。
+
+---
+
+## 最佳实践
+
+### 1. 工具命名
+
+```python
+# 好:清晰的动词 + 名词
+@tool()
+async def search_notes(...): ...
+
+@tool()
+async def create_document(...): ...
+
+# 不好:模糊或过长
+@tool()
+async def do_something(...): ...
+
+@tool()
+async def search_and_filter_notes_with_advanced_options(...): ...
+```
+
+### 2. 返回结构化数据
+
+```python
+# 好:返回 ToolResult 或结构化字典
+@tool()
+async def get_weather(city: str) -> ToolResult:
+    data = fetch_weather(city)
+    return ToolResult(
+        title=f"Weather in {city}",
+        output=json.dumps(data, indent=2)
+    )
+
+# 不好:返回纯文本
+@tool()
+async def get_weather(city: str) -> str:
+    return "The weather is sunny, 25°C, humidity 60%"  # 难以解析
+```
+
+### 3. 错误处理
+
+```python
+# 好:捕获异常并返回 ToolResult
+@tool()
+async def risky_operation() -> ToolResult:
+    try:
+        result = dangerous_call()
+        return ToolResult(title="Success", output=result)
+    except Exception as e:
+        logger.error(f"Operation failed: {e}")
+        return ToolResult(title="Failed", error=str(e))
+
+# 不好:让异常传播(会中断 Agent 循环)
+@tool()
+async def risky_operation() -> str:
+    return dangerous_call()  # 可能抛出异常
+```
+
+### 4. 记忆管理
+
+```python
+# 好:大量数据用 include_output_only_once
+@tool()
+async def fetch_all_logs() -> ToolResult:
+    logs = get_last_10000_logs()  # 很大
+    return ToolResult(
+        title="Fetched logs",
+        output=logs,
+        long_term_memory=f"Fetched {len(logs)} log entries",
+        include_output_only_once=True  # 只给 LLM 看一次
+    )
+
+# 不好:大量数据每次都传给 LLM
+@tool()
+async def fetch_all_logs() -> str:
+    return get_last_10000_logs()  # 每次都占用 context
+```
+
+### 5. 工具粒度
+
+```python
+# 好:单一职责,细粒度
+@tool()
+async def search_notes(query: str) -> ToolResult: ...
+
+@tool()
+async def get_note_detail(note_id: str) -> ToolResult: ...
+
+@tool()
+async def update_note(note_id: str, content: str) -> ToolResult: ...
+
+# 不好:功能过多,难以使用
+@tool()
+async def manage_notes(
+    action: Literal["search", "get", "update", "delete"],
+    query: Optional[str] = None,
+    note_id: Optional[str] = None,
+    content: Optional[str] = None
+) -> ToolResult:
+    # 太复杂,LLM 容易用错
+    ...
+```
+
+### 6. 文档和示例
+
+```python
+@tool()
+async def search_notes(
+    query: str,
+    filters: Optional[Dict[str, Any]] = None,
+    sort_by: str = "relevance",
+    limit: int = 10,
+    uid: str = ""
+) -> ToolResult:
+    """
+    搜索用户的笔记
+
+    使用语义搜索查找相关笔记,支持过滤和排序。
+
+    Args:
+        query: 搜索关键词(必需)
+        filters: 过滤条件,例如 {"type": "markdown", "tags": ["work"]}
+        sort_by: 排序方式,可选 "relevance" | "date" | "title"
+        limit: 返回结果数量,默认 10,最大 100
+
+    Returns:
+        ToolResult 包含搜索结果列表
+
+    Example:
+        搜索包含 "项目计划" 的工作笔记:
+        {
+            "query": "项目计划",
+            "filters": {"tags": ["work"]},
+            "limit": 5
+        }
+    """
+    ...
+```
+
+---
+
+## 总结
+
+| 特性 | 状态 | 说明 |
+|------|------|------|
+| **基础注册** | ✅ 已实现 | `@tool()` 装饰器 |
+| **Schema 生成** | ✅ 已实现 | 自动从函数签名生成 |
+| **双层记忆** | ✅ 已实现 | `ToolResult` 支持 long_term_memory |
+| **依赖注入** | ✅ 已实现 | `ToolContext` 提供上下文 |
+| **UI 元数据** | ✅ 已实现 | `display`, `requires_confirmation`, `editable_params` |
+| **域名过滤** | ✅ **已实现** | `url_patterns` 参数 + URL 匹配器 |
+| **敏感数据** | ✅ **已实现** | `<secret>` 占位符 + TOTP 支持 |
+| **工具统计** | ✅ **已实现** | 自动记录调用次数、成功率、执行时间 |
+
+**核心设计原则**:
+1. **简单优先**:最简工具只需要一个装饰器
+2. **按需扩展**:高级特性可选
+3. **类型安全**:充分利用 Python 类型注解
+4. **灵活集成**:支持各种工具库(Browser-Use, MCP 等)
+5. **可观测性**:内建统计和监控能力

+ 0 - 447
examples/tool_examples.py

@@ -1,447 +0,0 @@
-"""
-Tool Examples - 工具装饰器使用样例
-
-本文件展示 @tool 装饰器的各种用法,参考 Resonote 项目的实际工具实现。
-
-样例包括:
-1. 基础工具(最简形式)
-2. 带 i18n 展示信息的工具
-3. 带可编辑参数的工具
-4. 需要用户确认的危险操作
-5. 带 context 参数的工具
-6. 同步工具
-
-注意:
-- uid 参数会由框架自动注入,不需要用户传递
-- context 参数用于传递额外上下文(如当前阅读位置等)
-- 返回值会自动序列化为 JSON 字符串
-"""
-
-from typing import List, Dict, Any, Optional
-from reson_agent import tool
-
-
-# ============================================================
-# 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": "排序方式"
-            }
-        },
-        "en": {
-            "name": "Advanced Search",
-            "params": {
-                "query": "Search query",
-                "filters": "Filters",
-                "sort_by": "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": "永久删除"
-            }
-        },
-        "en": {
-            "name": "Delete Content",
-            "params": {
-                "content_id": "Content ID",
-                "permanent": "Permanent delete"
-            }
-        }
-    }
-)
-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": "推荐数量"
-            }
-        },
-        "en": {
-            "name": "Get Recommendations",
-            "params": {
-                "top_k": "Number of recommendations"
-            }
-        }
-    }
-)
-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)
-    - 等等...
-
-    框架会检查函数签名,如果有 context 参数就自动传入。
-
-    Args:
-        top_k: 返回推荐数量
-        uid: 用户ID(自动注入)
-        context: 执行上下文(自动注入)
-
-    Returns:
-        推荐列表
-    """
-    # 从 context 中提取信息
-    current_location = None
-    exclude_ids = []
-
-    if context:
-        current_location = context.get("current_location")
-        exclude_ids = context.get("exclude_ids", [])
-
-    # 实际实现中会根据 context 生成推荐
-    return [
-        {
-            "id": "rec_001",
-            "title": "推荐内容 1",
-            "reason": f"基于当前位置 {current_location}" if current_location else "基于您的兴趣"
-        },
-        {
-            "id": "rec_002",
-            "title": "推荐内容 2",
-            "reason": "热门内容"
-        }
-    ]
-
-
-# ============================================================
-# 6. 同步工具(非 async)
-# ============================================================
-
-@tool(
-    display={
-        "zh": {
-            "name": "格式化文本",
-            "params": {
-                "text": "原始文本",
-                "format_type": "格式类型"
-            }
-        },
-        "en": {
-            "name": "Format Text",
-            "params": {
-                "text": "Raw text",
-                "format_type": "Format type"
-            }
-        }
-    }
-)
-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. 复杂返回类型的工具
-# ============================================================
-
-@tool(
-    display={
-        "zh": {
-            "name": "分析内容",
-            "params": {
-                "content_id": "内容ID",
-                "analysis_types": "分析类型"
-            }
-        },
-        "en": {
-            "name": "Analyze Content",
-            "params": {
-                "content_id": "Content ID",
-                "analysis_types": "Analysis types"
-            }
-        }
-    }
-)
-async def analyze_content(
-    content_id: str,
-    analysis_types: Optional[List[str]] = None,
-    uid: str = ""
-) -> Dict[str, Any]:
-    """
-    分析内容(复杂返回类型)
-
-    展示如何返回复杂的嵌套结构。
-    返回值会自动序列化为 JSON。
-
-    Args:
-        content_id: 要分析的内容ID
-        analysis_types: 分析类型列表(sentiment/keywords/summary)
-        uid: 用户ID(自动注入)
-
-    Returns:
-        分析结果,包含多种分析数据
-    """
-    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},
-            {"word": "创新", "weight": 0.6}
-        ]
-
-    if "summary" in types:
-        result["analyses"]["summary"] = {
-            "short": "这是一篇关于AI学习的文章",
-            "long": "本文详细介绍了AI在学习领域的应用..."
-        }
-
-    return result
-
-
-# ============================================================
-# 使用示例
-# ============================================================
-
-if __name__ == "__main__":
-    import asyncio
-    from reson_agent import get_tool_registry
-
-    async def main():
-        # 获取全局注册表
-        registry = get_tool_registry()
-
-        # 查看已注册的工具
-        print("已注册的工具:")
-        for name in registry.get_tool_names():
-            print(f"  - {name}")
-
-        # 获取工具 Schema
-        schemas = registry.get_schemas(["search_content"])
-        print("\nsearch_content Schema:")
-        import json
-        print(json.dumps(schemas[0], indent=2, ensure_ascii=False))
-
-        # 执行工具
-        result = await registry.execute(
-            "search_content",
-            {"query": "人工智能", "limit": 5},
-            uid="user123"
-        )
-        print("\n执行结果:")
-        print(result)
-
-        # 获取 UI 元数据
-        ui_meta = registry.get_ui_metadata(locale="zh", tool_names=["advanced_search"])
-        print("\nUI 元数据 (中文):")
-        print(json.dumps(ui_meta, indent=2, ensure_ascii=False))
-
-    asyncio.run(main())

+ 581 - 0
examples/tools_examples.py

@@ -0,0 +1,581 @@
+"""
+工具系统完整示例
+
+本文件展示 @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 reson_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())