diff --git a/astrbot/core/tools/computer_tools/fs.py b/astrbot/core/tools/computer_tools/fs.py index b23296bdb..8b513913c 100644 --- a/astrbot/core/tools/computer_tools/fs.py +++ b/astrbot/core/tools/computer_tools/fs.py @@ -54,6 +54,13 @@ from astrbot.core.utils.astrbot_path import ( from ..registry import builtin_tool from .permissions import check_admin_permission +_COMPUTER_RUNTIME_TOOL_CONFIG = { + "provider_settings.computer_use_runtime": ("local", "sandbox"), +} +_SANDBOX_RUNTIME_TOOL_CONFIG = { + "provider_settings.computer_use_runtime": "sandbox", +} + def _normalize_umo_for_workspace(umo: str) -> str: normalized = re.sub(r"[^A-Za-z0-9._-]+", "_", umo.strip()) @@ -163,7 +170,7 @@ def _decode_escaped_text(value: str) -> str: ) -@builtin_tool +@builtin_tool(config=_COMPUTER_RUNTIME_TOOL_CONFIG) @dataclass class FileReadTool(FunctionTool): name: str = "astrbot_file_read_tool" @@ -243,7 +250,7 @@ class FileReadTool(FunctionTool): return f"Error reading file: {exc}" -@builtin_tool +@builtin_tool(config=_COMPUTER_RUNTIME_TOOL_CONFIG) @dataclass class FileWriteTool(FunctionTool): name: str = "astrbot_file_write_tool" @@ -310,7 +317,7 @@ class FileWriteTool(FunctionTool): return f"Error writing file: {exc}" -@builtin_tool +@builtin_tool(config=_COMPUTER_RUNTIME_TOOL_CONFIG) @dataclass class FileEditTool(FunctionTool): name: str = "astrbot_file_edit_tool" @@ -396,7 +403,7 @@ class FileEditTool(FunctionTool): return f"Error editing file: {exc}" -@builtin_tool +@builtin_tool(config=_COMPUTER_RUNTIME_TOOL_CONFIG) @dataclass class GrepTool(FunctionTool): name: str = "astrbot_grep_tool" @@ -602,7 +609,7 @@ class GrepTool(FunctionTool): return f"Error searching files: {exc}" -@builtin_tool +@builtin_tool(config=_SANDBOX_RUNTIME_TOOL_CONFIG) @dataclass class FileUploadTool(FunctionTool): name: str = "astrbot_upload_file" @@ -668,7 +675,7 @@ class FileUploadTool(FunctionTool): return f"Error uploading file: {str(e)}" -@builtin_tool +@builtin_tool(config=_SANDBOX_RUNTIME_TOOL_CONFIG) @dataclass class FileDownloadTool(FunctionTool): name: str = "astrbot_download_file" diff --git a/astrbot/core/tools/computer_tools/python.py b/astrbot/core/tools/computer_tools/python.py index 148beb7c5..1be2a6e1b 100644 --- a/astrbot/core/tools/computer_tools/python.py +++ b/astrbot/core/tools/computer_tools/python.py @@ -14,6 +14,12 @@ from ..registry import builtin_tool from .permissions import check_admin_permission _OS_NAME = platform.system() +_SANDBOX_PYTHON_TOOL_CONFIG = { + "provider_settings.computer_use_runtime": "sandbox", +} +_LOCAL_PYTHON_TOOL_CONFIG = { + "provider_settings.computer_use_runtime": "local", +} param_schema = { "type": "object", @@ -63,7 +69,7 @@ async def handle_result(result: dict, event: AstrMessageEvent) -> ToolExecResult return resp -@builtin_tool +@builtin_tool(config=_SANDBOX_PYTHON_TOOL_CONFIG) @dataclass class PythonTool(FunctionTool): name: str = "astrbot_execute_ipython" @@ -86,7 +92,7 @@ class PythonTool(FunctionTool): return f"Error executing code: {str(e)}" -@builtin_tool +@builtin_tool(config=_LOCAL_PYTHON_TOOL_CONFIG) @dataclass class LocalPythonTool(FunctionTool): name: str = "astrbot_execute_python" diff --git a/astrbot/core/tools/computer_tools/shell.py b/astrbot/core/tools/computer_tools/shell.py index 0c23dae43..98343bc1c 100644 --- a/astrbot/core/tools/computer_tools/shell.py +++ b/astrbot/core/tools/computer_tools/shell.py @@ -10,8 +10,12 @@ from astrbot.core.computer.computer_client import get_booter from ..registry import builtin_tool from .permissions import check_admin_permission +_COMPUTER_RUNTIME_TOOL_CONFIG = { + "provider_settings.computer_use_runtime": ("local", "sandbox"), +} -@builtin_tool + +@builtin_tool(config=_COMPUTER_RUNTIME_TOOL_CONFIG) @dataclass class ExecuteShellTool(FunctionTool): name: str = "astrbot_execute_shell" diff --git a/astrbot/core/tools/computer_tools/shipyard_neo/browser.py b/astrbot/core/tools/computer_tools/shipyard_neo/browser.py index f3cfb3449..72aa6076d 100644 --- a/astrbot/core/tools/computer_tools/shipyard_neo/browser.py +++ b/astrbot/core/tools/computer_tools/shipyard_neo/browser.py @@ -10,6 +10,11 @@ from astrbot.core.computer.computer_client import get_booter from astrbot.core.tools.computer_tools.permissions import check_admin_permission from astrbot.core.tools.registry import builtin_tool +_SHIPYARD_NEO_TOOL_CONFIG = { + "provider_settings.computer_use_runtime": "sandbox", + "provider_settings.sandbox.booter": "shipyard_neo", +} + def _to_json(data: Any) -> str: return json.dumps(data, ensure_ascii=False, default=str) @@ -29,7 +34,7 @@ async def _get_browser_component(context: ContextWrapper[AstrAgentContext]) -> A return browser -@builtin_tool +@builtin_tool(config=_SHIPYARD_NEO_TOOL_CONFIG) @dataclass class BrowserExecTool(FunctionTool): name: str = "astrbot_execute_browser" @@ -87,7 +92,7 @@ class BrowserExecTool(FunctionTool): return f"Error executing browser command: {str(e)}" -@builtin_tool +@builtin_tool(config=_SHIPYARD_NEO_TOOL_CONFIG) @dataclass class BrowserBatchExecTool(FunctionTool): name: str = "astrbot_execute_browser_batch" @@ -152,7 +157,7 @@ class BrowserBatchExecTool(FunctionTool): return f"Error executing browser batch command: {str(e)}" -@builtin_tool +@builtin_tool(config=_SHIPYARD_NEO_TOOL_CONFIG) @dataclass class RunBrowserSkillTool(FunctionTool): name: str = "astrbot_run_browser_skill" diff --git a/astrbot/core/tools/computer_tools/shipyard_neo/neo_skills.py b/astrbot/core/tools/computer_tools/shipyard_neo/neo_skills.py index 03ba1c4a4..edbe1ef6f 100644 --- a/astrbot/core/tools/computer_tools/shipyard_neo/neo_skills.py +++ b/astrbot/core/tools/computer_tools/shipyard_neo/neo_skills.py @@ -12,6 +12,11 @@ from astrbot.core.skills.neo_skill_sync import NeoSkillSyncManager from astrbot.core.tools.computer_tools.permissions import check_admin_permission from astrbot.core.tools.registry import builtin_tool +_SHIPYARD_NEO_TOOL_CONFIG = { + "provider_settings.computer_use_runtime": "sandbox", + "provider_settings.sandbox.booter": "shipyard_neo", +} + def _to_jsonable(model_like: Any) -> Any: if isinstance(model_like, dict): @@ -64,7 +69,7 @@ class NeoSkillToolBase(FunctionTool): return f"{self.error_prefix} {error_action}: {str(e)}" -@builtin_tool +@builtin_tool(config=_SHIPYARD_NEO_TOOL_CONFIG) @dataclass class GetExecutionHistoryTool(NeoSkillToolBase): name: str = "astrbot_get_execution_history" @@ -111,7 +116,7 @@ class GetExecutionHistoryTool(NeoSkillToolBase): ) -@builtin_tool +@builtin_tool(config=_SHIPYARD_NEO_TOOL_CONFIG) @dataclass class AnnotateExecutionTool(NeoSkillToolBase): name: str = "astrbot_annotate_execution" @@ -149,7 +154,7 @@ class AnnotateExecutionTool(NeoSkillToolBase): ) -@builtin_tool +@builtin_tool(config=_SHIPYARD_NEO_TOOL_CONFIG) @dataclass class CreateSkillPayloadTool(NeoSkillToolBase): name: str = "astrbot_create_skill_payload" @@ -197,7 +202,7 @@ class CreateSkillPayloadTool(NeoSkillToolBase): ) -@builtin_tool +@builtin_tool(config=_SHIPYARD_NEO_TOOL_CONFIG) @dataclass class GetSkillPayloadTool(NeoSkillToolBase): name: str = "astrbot_get_skill_payload" @@ -224,7 +229,7 @@ class GetSkillPayloadTool(NeoSkillToolBase): ) -@builtin_tool +@builtin_tool(config=_SHIPYARD_NEO_TOOL_CONFIG) @dataclass class CreateSkillCandidateTool(NeoSkillToolBase): name: str = "astrbot_create_skill_candidate" @@ -278,7 +283,7 @@ class CreateSkillCandidateTool(NeoSkillToolBase): ) -@builtin_tool +@builtin_tool(config=_SHIPYARD_NEO_TOOL_CONFIG) @dataclass class ListSkillCandidatesTool(NeoSkillToolBase): name: str = "astrbot_list_skill_candidates" @@ -316,7 +321,7 @@ class ListSkillCandidatesTool(NeoSkillToolBase): ) -@builtin_tool +@builtin_tool(config=_SHIPYARD_NEO_TOOL_CONFIG) @dataclass class EvaluateSkillCandidateTool(NeoSkillToolBase): name: str = "astrbot_evaluate_skill_candidate" @@ -357,7 +362,7 @@ class EvaluateSkillCandidateTool(NeoSkillToolBase): ) -@builtin_tool +@builtin_tool(config=_SHIPYARD_NEO_TOOL_CONFIG) @dataclass class PromoteSkillCandidateTool(NeoSkillToolBase): name: str = "astrbot_promote_skill_candidate" @@ -428,7 +433,7 @@ class PromoteSkillCandidateTool(NeoSkillToolBase): return f"Error promoting skill candidate: {str(e)}" -@builtin_tool +@builtin_tool(config=_SHIPYARD_NEO_TOOL_CONFIG) @dataclass class ListSkillReleasesTool(NeoSkillToolBase): name: str = "astrbot_list_skill_releases" @@ -469,7 +474,7 @@ class ListSkillReleasesTool(NeoSkillToolBase): ) -@builtin_tool +@builtin_tool(config=_SHIPYARD_NEO_TOOL_CONFIG) @dataclass class RollbackSkillReleaseTool(NeoSkillToolBase): name: str = "astrbot_rollback_skill_release" @@ -496,7 +501,7 @@ class RollbackSkillReleaseTool(NeoSkillToolBase): ) -@builtin_tool +@builtin_tool(config=_SHIPYARD_NEO_TOOL_CONFIG) @dataclass class SyncSkillReleaseTool(NeoSkillToolBase): name: str = "astrbot_sync_skill_release" diff --git a/astrbot/core/tools/cron_tools.py b/astrbot/core/tools/cron_tools.py index 599957e0a..638798852 100644 --- a/astrbot/core/tools/cron_tools.py +++ b/astrbot/core/tools/cron_tools.py @@ -9,6 +9,10 @@ from astrbot.core.agent.tool import FunctionTool, ToolExecResult from astrbot.core.astr_agent_context import AstrAgentContext from astrbot.core.tools.registry import builtin_tool +_CRON_TOOL_CONFIG = { + "provider_settings.proactive_capability.add_cron_tools": True, +} + def _extract_job_session(job: Any) -> str | None: payload = getattr(job, "payload", None) @@ -18,7 +22,7 @@ def _extract_job_session(job: Any) -> str | None: return str(session) if session is not None else None -@builtin_tool +@builtin_tool(config=_CRON_TOOL_CONFIG) @dataclass class CreateActiveCronTool(FunctionTool[AstrAgentContext]): name: str = "create_future_task" @@ -107,7 +111,7 @@ class CreateActiveCronTool(FunctionTool[AstrAgentContext]): return f"Scheduled future task {job.job_id} ({job.name}) {suffix}." -@builtin_tool +@builtin_tool(config=_CRON_TOOL_CONFIG) @dataclass class DeleteCronJobTool(FunctionTool[AstrAgentContext]): name: str = "delete_future_task" @@ -144,7 +148,7 @@ class DeleteCronJobTool(FunctionTool[AstrAgentContext]): return f"Deleted cron job {job_id}." -@builtin_tool +@builtin_tool(config=_CRON_TOOL_CONFIG) @dataclass class ListCronJobsTool(FunctionTool[AstrAgentContext]): name: str = "list_future_tasks" diff --git a/astrbot/core/tools/knowledge_base_tools.py b/astrbot/core/tools/knowledge_base_tools.py index e27a883d4..e082fd425 100644 --- a/astrbot/core/tools/knowledge_base_tools.py +++ b/astrbot/core/tools/knowledge_base_tools.py @@ -9,6 +9,10 @@ from astrbot.core.knowledge_base.kb_helper import KBHelper from astrbot.core.star.context import Context from astrbot.core.tools.registry import builtin_tool +_KNOWLEDGE_BASE_TOOL_CONFIG = { + "kb_agentic_mode": True, +} + def check_all_kb(kb_list: list[KBHelper | None]) -> bool: """检查是否所有的知识库都为空""" @@ -83,7 +87,7 @@ async def retrieve_knowledge_base( return None -@builtin_tool +@builtin_tool(config=_KNOWLEDGE_BASE_TOOL_CONFIG) @dataclass class KnowledgeBaseQueryTool(FunctionTool[AstrAgentContext]): name: str = "astr_kb_search" diff --git a/astrbot/core/tools/registry.py b/astrbot/core/tools/registry.py index 4a1fa8d66..c3b10d229 100644 --- a/astrbot/core/tools/registry.py +++ b/astrbot/core/tools/registry.py @@ -1,7 +1,9 @@ from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass from importlib import import_module -from typing import TypeVar +from typing import Any, TypeVar from astrbot.core.agent.tool import FunctionTool @@ -18,6 +20,182 @@ _BUILTIN_TOOL_MODULES = ( _builtin_tool_classes_by_name: dict[str, type[FunctionTool]] = {} _builtin_tool_names_by_class: dict[type[FunctionTool], str] = {} _builtin_tools_loaded = False +_MISSING = object() + + +@dataclass(frozen=True) +class BuiltinToolConfigCondition: + key: str + operator: str + expected: Any = None + message: str | None = None + + def evaluate(self, config: dict[str, Any]) -> dict[str, Any]: + actual = _get_config_value(config, self.key) + + if self.operator == "equals": + matched = actual == self.expected + elif self.operator == "in": + expected_values = tuple(self.expected or ()) + matched = actual in expected_values + elif self.operator == "truthy": + matched = bool(actual) + elif self.operator == "custom": + matched = bool(self.expected) + else: + raise ValueError( + f"Unsupported builtin tool config operator: {self.operator}" + ) + + return { + "key": self.key, + "operator": self.operator, + "expected": _json_safe(self.expected), + "actual": _json_safe(None if actual is _MISSING else actual), + "matched": matched, + "message": self.message, + } + + +@dataclass(frozen=True) +class BuiltinToolConfigRule: + conditions: tuple[BuiltinToolConfigCondition, ...] = () + evaluator: Callable[[dict[str, Any]], list[dict[str, Any]]] | None = None + + def evaluate(self, config: dict[str, Any]) -> list[dict[str, Any]]: + if self.evaluator is not None: + return self.evaluator(config) + return [condition.evaluate(config) for condition in self.conditions] + + +def _get_config_value(config: dict[str, Any], key_path: str) -> Any: + current: Any = config + for segment in key_path.split("."): + if not isinstance(current, dict) or segment not in current: + return _MISSING + current = current[segment] + return current + + +def _json_safe(value: Any) -> Any: + if isinstance(value, tuple): + return [_json_safe(item) for item in value] + if isinstance(value, list): + return [_json_safe(item) for item in value] + if isinstance(value, dict): + return {key: _json_safe(val) for key, val in value.items()} + return value + + +def _equals(key: str, expected: Any) -> BuiltinToolConfigCondition: + return BuiltinToolConfigCondition(key=key, operator="equals", expected=expected) + + +def _in(key: str, expected: tuple[Any, ...]) -> BuiltinToolConfigCondition: + return BuiltinToolConfigCondition(key=key, operator="in", expected=expected) + + +def _custom_condition(key: str, *, matched: bool, message: str) -> dict[str, Any]: + return { + "key": key, + "operator": "custom", + "expected": None, + "actual": None, + "matched": matched, + "message": message, + } + + +def _build_rule_from_config_map( + config_map: dict[str, Any], +) -> BuiltinToolConfigRule: + conditions: list[BuiltinToolConfigCondition] = [] + for key, expected in config_map.items(): + if isinstance(expected, tuple): + conditions.append(_in(key, expected)) + else: + conditions.append(_equals(key, expected)) + return BuiltinToolConfigRule(conditions=tuple(conditions)) + + +def _evaluate_send_message_tool(config: dict[str, Any]) -> list[dict[str, Any]]: + platform_configs = config.get("platform", []) + if not isinstance(platform_configs, list): + return [ + _custom_condition( + "platform", + matched=False, + message="No enabled platform in this config supports proactive messaging.", + ) + ] + + for platform_cfg in platform_configs: + if not isinstance(platform_cfg, dict): + continue + if platform_cfg.get("enable", False) is False: + continue + + platform_type = str(platform_cfg.get("type", "")).strip() + platform_id = str(platform_cfg.get("id", "")).strip() or platform_type + if not platform_type: + continue + + if platform_type in {"wecom", "weixin_official_account"}: + continue + + if platform_type == "wecom_ai_bot": + webhook = str(platform_cfg.get("msg_push_webhook_url", "")).strip() + if not webhook: + continue + return [ + _custom_condition( + "platform[].type", + matched=True, + message=( + f"Enabled platform `{platform_id}` uses `wecom_ai_bot`, which supports proactive messaging " + "when `platform[].msg_push_webhook_url` is configured." + ), + ), + BuiltinToolConfigCondition( + key="platform[].msg_push_webhook_url", + operator="truthy", + ).evaluate({"platform[]": {"msg_push_webhook_url": webhook}}), + ] + + return [ + _custom_condition( + "platform[].type", + matched=True, + message=( + f"Enabled platform `{platform_id}` (`{platform_type}`) supports proactive messaging." + ), + ) + ] + + return [ + _custom_condition( + "platform", + matched=False, + message="No enabled platform in this config supports proactive messaging.", + ) + ] + + +_BUILTIN_TOOL_CONFIG_RULES: dict[str, BuiltinToolConfigRule] = {} + + +def _register_builtin_tool_config_rule( + tool_names: tuple[str, ...], + rule: BuiltinToolConfigRule, +) -> None: + for tool_name in tool_names: + _BUILTIN_TOOL_CONFIG_RULES[tool_name] = rule + + +_register_builtin_tool_config_rule( + ("send_message_to_user",), + BuiltinToolConfigRule(evaluator=_evaluate_send_message_tool), +) def _resolve_builtin_tool_name(tool_cls: type[FunctionTool]) -> str: @@ -35,18 +213,29 @@ def _resolve_builtin_tool_name(tool_cls: type[FunctionTool]) -> str: ) -def builtin_tool(tool_cls: TFunctionTool) -> TFunctionTool: - tool_name = _resolve_builtin_tool_name(tool_cls) - existing = _builtin_tool_classes_by_name.get(tool_name) - if existing is not None and existing is not tool_cls: - raise ValueError( - f"Builtin tool name conflict detected: {tool_name} is already registered by " - f"{existing.__module__}.{existing.__name__}.", - ) +def builtin_tool( + tool_cls: TFunctionTool | None = None, + *, + config: dict[str, Any] | None = None, +) -> TFunctionTool | Callable[[TFunctionTool], TFunctionTool]: + def _register(cls: TFunctionTool) -> TFunctionTool: + tool_name = _resolve_builtin_tool_name(cls) + existing = _builtin_tool_classes_by_name.get(tool_name) + if existing is not None and existing is not cls: + raise ValueError( + f"Builtin tool name conflict detected: {tool_name} is already registered by " + f"{existing.__module__}.{existing.__name__}.", + ) - _builtin_tool_classes_by_name[tool_name] = tool_cls - _builtin_tool_names_by_class[tool_cls] = tool_name - return tool_cls + _builtin_tool_classes_by_name[tool_name] = cls + _builtin_tool_names_by_class[cls] = tool_name + if config is not None: + _BUILTIN_TOOL_CONFIG_RULES[tool_name] = _build_rule_from_config_map(config) + return cls + + if tool_cls is None: + return _register + return _register(tool_cls) def ensure_builtin_tools_loaded() -> None: @@ -75,9 +264,64 @@ def iter_builtin_tool_classes() -> tuple[type[FunctionTool], ...]: return tuple(_builtin_tool_classes_by_name.values()) +def get_builtin_tool_config_rule(name: str) -> BuiltinToolConfigRule | None: + ensure_builtin_tools_loaded() + return _BUILTIN_TOOL_CONFIG_RULES.get(name) + + +def get_builtin_tool_config_statuses( + tool_name: str, + config_entries: list[dict[str, Any]], +) -> list[dict[str, Any]]: + rule = get_builtin_tool_config_rule(tool_name) + if rule is None: + return [] + + statuses: list[dict[str, Any]] = [] + for entry in config_entries: + config = entry.get("config") + if not isinstance(config, dict): + continue + + conditions = rule.evaluate(config) + enabled = bool(conditions) and all( + bool(condition.get("matched")) for condition in conditions + ) + statuses.append( + { + "conf_id": entry.get("conf_id"), + "conf_name": entry.get("conf_name"), + "enabled": enabled, + "matched_conditions": [ + condition for condition in conditions if condition.get("matched") + ], + "failed_conditions": [ + condition + for condition in conditions + if not condition.get("matched") + ], + } + ) + return statuses + + +def get_builtin_tool_config_tags( + tool_name: str, + config_entries: list[dict[str, Any]], +) -> list[dict[str, Any]]: + return [ + status + for status in get_builtin_tool_config_statuses(tool_name, config_entries) + if status["enabled"] + ] + + __all__ = [ "builtin_tool", "ensure_builtin_tools_loaded", + "get_builtin_tool_config_rule", + "get_builtin_tool_config_statuses", + "get_builtin_tool_config_tags", "get_builtin_tool_class", "get_builtin_tool_name", "iter_builtin_tool_classes", diff --git a/astrbot/core/tools/web_search_tools.py b/astrbot/core/tools/web_search_tools.py index 5ca8c3e08..eacee9511 100644 --- a/astrbot/core/tools/web_search_tools.py +++ b/astrbot/core/tools/web_search_tools.py @@ -20,6 +20,22 @@ WEB_SEARCH_TOOL_NAMES = [ "web_search_bocha", "web_search_brave", ] +_TAVILY_WEB_SEARCH_TOOL_CONFIG = { + "provider_settings.web_search": True, + "provider_settings.websearch_provider": "tavily", +} +_BOCHA_WEB_SEARCH_TOOL_CONFIG = { + "provider_settings.web_search": True, + "provider_settings.websearch_provider": "bocha", +} +_BRAVE_WEB_SEARCH_TOOL_CONFIG = { + "provider_settings.web_search": True, + "provider_settings.websearch_provider": "brave", +} +_BAIDU_WEB_SEARCH_TOOL_CONFIG = { + "provider_settings.web_search": True, + "provider_settings.websearch_provider": "baidu_ai_search", +} @std_dataclass @@ -276,7 +292,7 @@ async def _baidu_search( ] -@builtin_tool +@builtin_tool(config=_TAVILY_WEB_SEARCH_TOOL_CONFIG) @pydantic_dataclass class TavilyWebSearchTool(FunctionTool[AstrAgentContext]): name: str = "web_search_tavily" @@ -359,7 +375,7 @@ class TavilyWebSearchTool(FunctionTool[AstrAgentContext]): return _search_result_payload(results) -@builtin_tool +@builtin_tool(config=_TAVILY_WEB_SEARCH_TOOL_CONFIG) @pydantic_dataclass class TavilyExtractWebPageTool(FunctionTool[AstrAgentContext]): name: str = "tavily_extract_web_page" @@ -406,7 +422,7 @@ class TavilyExtractWebPageTool(FunctionTool[AstrAgentContext]): return ret or "Error: Tavily web searcher does not return any results." -@builtin_tool +@builtin_tool(config=_BOCHA_WEB_SEARCH_TOOL_CONFIG) @pydantic_dataclass class BochaWebSearchTool(FunctionTool[AstrAgentContext]): name: str = "web_search_bocha" @@ -470,7 +486,7 @@ class BochaWebSearchTool(FunctionTool[AstrAgentContext]): return _search_result_payload(results) -@builtin_tool +@builtin_tool(config=_BRAVE_WEB_SEARCH_TOOL_CONFIG) @pydantic_dataclass class BraveWebSearchTool(FunctionTool[AstrAgentContext]): name: str = "web_search_brave" @@ -528,7 +544,7 @@ class BraveWebSearchTool(FunctionTool[AstrAgentContext]): return _search_result_payload(results) -@builtin_tool +@builtin_tool(config=_BAIDU_WEB_SEARCH_TOOL_CONFIG) @pydantic_dataclass class BaiduWebSearchTool(FunctionTool[AstrAgentContext]): name: str = "web_search_baidu" diff --git a/astrbot/dashboard/routes/tools.py b/astrbot/dashboard/routes/tools.py index 33b74deff..7273c5176 100644 --- a/astrbot/dashboard/routes/tools.py +++ b/astrbot/dashboard/routes/tools.py @@ -6,6 +6,7 @@ from astrbot.core import logger from astrbot.core.agent.mcp_client import MCPTool from astrbot.core.core_lifecycle import AstrBotCoreLifecycle from astrbot.core.star import star_map +from astrbot.core.tools.registry import get_builtin_tool_config_statuses from .route import Response, Route, RouteContext @@ -434,13 +435,36 @@ class ToolsRoute(Route): if tool.name not in existing_names: tools.append(tool) + conf_list = self.core_lifecycle.astrbot_config_mgr.get_conf_list() + conf_name_map = {conf["id"]: conf["name"] for conf in conf_list} + config_entries = [] + for conf_id, conf in self.core_lifecycle.astrbot_config_mgr.confs.items(): + config_entries.append( + { + "conf_id": conf_id, + "conf_name": conf_name_map.get(conf_id, conf_id), + "config": conf, + } + ) + tools_dict = [] for tool in tools: readonly = False + builtin_config_statuses = [] + builtin_config_tags = [] if self.tool_mgr.is_builtin_tool(tool.name): origin = "builtin" origin_name = "AstrBot Core" readonly = True + builtin_config_statuses = get_builtin_tool_config_statuses( + tool.name, + config_entries, + ) + builtin_config_tags = [ + status + for status in builtin_config_statuses + if status["enabled"] + ] elif isinstance(tool, MCPTool): origin = "mcp" origin_name = tool.mcp_server_name @@ -462,6 +486,8 @@ class ToolsRoute(Route): "origin": origin, "origin_name": origin_name, "readonly": readonly, + "builtin_config_statuses": builtin_config_statuses, + "builtin_config_tags": builtin_config_tags, } tools_dict.append(tool_info) return Response().ok(data=tools_dict).__dict__ diff --git a/dashboard/src/components/extension/componentPanel/components/ToolTable.vue b/dashboard/src/components/extension/componentPanel/components/ToolTable.vue index f0be7bbc8..9615e5ea8 100644 --- a/dashboard/src/components/extension/componentPanel/components/ToolTable.vue +++ b/dashboard/src/components/extension/componentPanel/components/ToolTable.vue @@ -1,7 +1,7 @@ @@ -38,7 +84,39 @@ const parameterEntries = (tool: ToolItem) => Object.entries(tool.parameters?.pro > - {{ item.name }} + + {{ item.name }} + + + + {{ tag.conf_name }} + + + + + + {{ tmTool('functionTools.configTags.tooltipTitle', { config: tag.conf_name }) }} + + + {{ formatCondition(condition) }} + + + + @@ -135,4 +213,16 @@ const parameterEntries = (tool: ToolItem) => Object.entries(tool.parameters?.pro font-size: 0.9rem; line-height: 1.35; } + +.tool-config-tooltip { + max-width: 360px; + padding: 4px 0; + color: rgba(255, 255, 255, 0.92); +} + +.tool-config-tooltip :deep(.text-body-2), +.tool-config-tooltip :deep(.text-medium-emphasis), +.tool-config-tooltip :deep(.font-weight-medium) { + color: inherit !important; +} diff --git a/dashboard/src/components/extension/componentPanel/types.ts b/dashboard/src/components/extension/componentPanel/types.ts index 1e581c5ec..4784cc8ee 100644 --- a/dashboard/src/components/extension/componentPanel/types.ts +++ b/dashboard/src/components/extension/componentPanel/types.ts @@ -89,6 +89,23 @@ export interface ToolParameter { description?: string; } +export interface ToolConfigCondition { + key: string; + operator: 'truthy' | 'equals' | 'in' | 'custom' | string; + expected?: unknown; + actual?: unknown; + matched: boolean; + message?: string | null; +} + +export interface BuiltinToolConfigTag { + conf_id: string; + conf_name: string; + enabled: boolean; + matched_conditions: ToolConfigCondition[]; + failed_conditions: ToolConfigCondition[]; +} + /** MCP/函数工具对象 */ export interface ToolItem { name: string; @@ -100,4 +117,6 @@ export interface ToolItem { }; origin?: string; origin_name?: string; + builtin_config_statuses?: BuiltinToolConfigTag[]; + builtin_config_tags?: BuiltinToolConfigTag[]; } diff --git a/dashboard/src/i18n/locales/en-US/features/tool-use.json b/dashboard/src/i18n/locales/en-US/features/tool-use.json index e0d762010..36b036f88 100644 --- a/dashboard/src/i18n/locales/en-US/features/tool-use.json +++ b/dashboard/src/i18n/locales/en-US/features/tool-use.json @@ -47,6 +47,15 @@ "originName": "Origin Name", "readonly": "Read-only", "actions": "Actions" + }, + "configTags": { + "tooltipTitle": "This tool is enabled in config file {config} because:", + "conditions": { + "truthy": "{key} is enabled", + "equals": "{key} = {expected}", + "in": "{key} matched {expected}", + "fallback": "{key} is currently {actual}" + } } }, "marketplace": { diff --git a/dashboard/src/i18n/locales/ru-RU/features/tool-use.json b/dashboard/src/i18n/locales/ru-RU/features/tool-use.json index b89474235..58181c5e5 100644 --- a/dashboard/src/i18n/locales/ru-RU/features/tool-use.json +++ b/dashboard/src/i18n/locales/ru-RU/features/tool-use.json @@ -47,6 +47,15 @@ "originName": "Имя источника", "readonly": "Только чтение", "actions": "Действия" + }, + "configTags": { + "tooltipTitle": "Этот инструмент включен в файле конфигурации {config}, потому что:", + "conditions": { + "truthy": "{key} включен", + "equals": "{key} = {expected}", + "in": "{key} соответствует {expected}", + "fallback": "Текущее значение {key}: {actual}" + } } }, "marketplace": { diff --git a/dashboard/src/i18n/locales/zh-CN/features/tool-use.json b/dashboard/src/i18n/locales/zh-CN/features/tool-use.json index 66244947b..aec458710 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/tool-use.json +++ b/dashboard/src/i18n/locales/zh-CN/features/tool-use.json @@ -47,6 +47,15 @@ "originName": "来源名称", "readonly": "只读", "actions": "操作" + }, + "configTags": { + "tooltipTitle": "该工具在配置文件 {config} 中启用,因为:", + "conditions": { + "truthy": "启用了 {key}", + "equals": "{key} = {expected}", + "in": "{key} 命中了 {expected}", + "fallback": "{key} 当前值为 {actual}" + } } }, "marketplace": {