feat: supports to display enabled builtin tools in configs

This commit is contained in:
Soulter
2026-04-09 23:49:22 +08:00
parent efc93a37b1
commit adc01e0c9d
15 changed files with 504 additions and 47 deletions

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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",

View File

@@ -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"

View File

@@ -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__

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useModuleI18n } from '@/i18n/composables';
import type { ToolItem } from '../types';
import type { BuiltinToolConfigTag, ToolConfigCondition, ToolItem } from '../types';
const { tm: tmTool } = useModuleI18n('features/tooluse');
@@ -15,7 +15,7 @@ const emit = defineEmits<{
}>();
const toolHeaders = computed(() => [
{ title: tmTool('functionTools.title'), key: 'name', minWidth: '240px' },
{ title: tmTool('functionTools.title'), key: 'name', minWidth: '320px' },
{ title: tmTool('functionTools.description'), key: 'description' },
{ title: tmTool('functionTools.table.origin'), key: 'origin', sortable: false, width: '120px' },
{ title: tmTool('functionTools.table.originName'), key: 'origin_name', sortable: false, width: '160px' },
@@ -23,6 +23,52 @@ const toolHeaders = computed(() => [
]);
const parameterEntries = (tool: ToolItem) => Object.entries(tool.parameters?.properties || {});
const formatConfigValue = (value: unknown) => {
if (Array.isArray(value)) {
return value.map(item => String(item)).join(', ');
}
if (typeof value === 'boolean') {
return value ? 'true' : 'false';
}
if (value === null || value === undefined || value === '') {
return '-';
}
return String(value);
};
const formatCondition = (condition: ToolConfigCondition) => {
if (condition.message) {
return condition.message;
}
switch (condition.operator) {
case 'truthy':
return tmTool('functionTools.configTags.conditions.truthy', {
key: condition.key
});
case 'equals':
return tmTool('functionTools.configTags.conditions.equals', {
key: condition.key,
expected: formatConfigValue(condition.expected)
});
case 'in':
return tmTool('functionTools.configTags.conditions.in', {
key: condition.key,
expected: formatConfigValue(condition.expected)
});
default:
return tmTool('functionTools.configTags.conditions.fallback', {
key: condition.key,
actual: formatConfigValue(condition.actual)
});
}
};
const enabledConfigTags = (tool: ToolItem): BuiltinToolConfigTag[] => {
if (tool.origin !== 'builtin') return [];
return (tool.builtin_config_tags || []).filter(tag => tag.enabled);
};
</script>
<template>
@@ -38,7 +84,39 @@ const parameterEntries = (tool: ToolItem) => Object.entries(tool.parameters?.pro
>
<template #item.name="{ item }">
<div class="py-2">
<div class="tool-name text-body-2 font-weight-medium">{{ item.name }}</div>
<div class="d-flex flex-wrap align-center ga-1">
<div class="tool-name text-body-2 font-weight-medium">{{ item.name }}</div>
<v-tooltip
v-for="tag in enabledConfigTags(item)"
:key="`${item.name}-${tag.conf_id}`"
location="top"
>
<template #activator="{ props: tooltipProps }">
<v-chip
v-bind="tooltipProps"
size="x-small"
variant="tonal"
color="secondary"
class="text-caption font-weight-medium"
>
{{ tag.conf_name }}
</v-chip>
</template>
<div class="tool-config-tooltip">
<div class="text-body-2 font-weight-medium mb-2">
{{ tmTool('functionTools.configTags.tooltipTitle', { config: tag.conf_name }) }}
</div>
<div
v-for="(condition, index) in tag.matched_conditions"
:key="`${tag.conf_id}-${index}-${condition.key}`"
class="text-body-2 text-medium-emphasis mb-1"
>
{{ formatCondition(condition) }}
</div>
</div>
</v-tooltip>
</div>
</div>
</template>
@@ -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;
}
</style>

View File

@@ -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[];
}

View File

@@ -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": {

View File

@@ -47,6 +47,15 @@
"originName": "Имя источника",
"readonly": "Только чтение",
"actions": "Действия"
},
"configTags": {
"tooltipTitle": "Этот инструмент включен в файле конфигурации {config}, потому что:",
"conditions": {
"truthy": "{key} включен",
"equals": "{key} = {expected}",
"in": "{key} соответствует {expected}",
"fallback": "Текущее значение {key}: {actual}"
}
}
},
"marketplace": {

View File

@@ -47,6 +47,15 @@
"originName": "来源名称",
"readonly": "只读",
"actions": "操作"
},
"configTags": {
"tooltipTitle": "该工具在配置文件 {config} 中启用,因为:",
"conditions": {
"truthy": "启用了 {key}",
"equals": "{key} = {expected}",
"in": "{key} 命中了 {expected}",
"fallback": "{key} 当前值为 {actual}"
}
}
},
"marketplace": {