From b4ea2ab948dd5bc3d4873f5e28dfc95ea6eb5482 Mon Sep 17 00:00:00 2001 From: Wenjie Zhang Date: Tue, 3 Mar 2026 23:44:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=87=8D=E6=9E=84=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=B9=B6=E9=9B=86=E6=88=90=E6=96=B0=E5=B7=A5?= =?UTF-8?q?=E5=85=B7API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 `get_tavily_search` 导入移至新的工具包结构中。 - 更新 `skill_service.py` 以获取所有工具而不仅仅是内置工具。 - 引入 `tool_service.py` 用于管理工具元数据和初始化。 - 从 `models.py` 中删除了旧的数据库模型。 - 添加了新的 `tool_api.js` 用于工具管理API端点。 - 创建了 `ToolsManagerComponent.vue` 用于在UI中显示和管理工具。 - 更新了 `ExtensionsView.vue` 以包含工具管理的新标签页。 --- server/routers/__init__.py | 2 + server/routers/tool_router.py | 25 + src/agents/common/__init__.py | 15 +- src/agents/common/context.py | 3 +- .../middlewares/runtime_config_middleware.py | 3 +- src/agents/common/subagents/__init__.py | 3 - src/agents/common/subagents/calc_agent.py | 19 - src/agents/common/toolkits/__init__.py | 54 +++ .../common/toolkits/buildin/__init__.py | 7 + .../common/{ => toolkits/buildin}/tools.py | 109 +---- src/agents/common/toolkits/mysql/__init__.py | 1 + src/agents/common/toolkits/mysql/tools.py | 8 +- src/agents/common/toolkits/registry.py | 123 +++++ src/agents/common/toolkits/utils.py | 55 +++ src/agents/deep_agent/graph.py | 2 +- src/services/skill_service.py | 25 +- src/services/tool_service.py | 77 ++++ src/storage/db/models.py | 430 ------------------ web/src/apis/index.js | 1 + web/src/apis/tool_api.js | 37 ++ web/src/assets/css/extensions.less | 8 + web/src/components/AgentConfigSidebar.vue | 35 +- web/src/components/McpServersComponent.vue | 45 +- web/src/components/SkillsManagerComponent.vue | 39 +- web/src/components/ToolsManagerComponent.vue | 294 ++++++++++++ web/src/views/ExtensionsView.vue | 31 ++ 26 files changed, 830 insertions(+), 621 deletions(-) create mode 100644 server/routers/tool_router.py delete mode 100644 src/agents/common/subagents/__init__.py delete mode 100644 src/agents/common/subagents/calc_agent.py create mode 100644 src/agents/common/toolkits/__init__.py create mode 100644 src/agents/common/toolkits/buildin/__init__.py rename src/agents/common/{ => toolkits/buildin}/tools.py (56%) create mode 100644 src/agents/common/toolkits/registry.py create mode 100644 src/agents/common/toolkits/utils.py create mode 100644 src/services/tool_service.py delete mode 100644 src/storage/db/models.py create mode 100644 web/src/apis/tool_api.js create mode 100644 web/src/components/ToolsManagerComponent.vue diff --git a/server/routers/__init__.py b/server/routers/__init__.py index a66420ec..2f4ae1fe 100644 --- a/server/routers/__init__.py +++ b/server/routers/__init__.py @@ -12,6 +12,7 @@ from server.routers.mindmap_router import mindmap from server.routers.skill_router import skills from server.routers.system_router import system from server.routers.task_router import tasks +from server.routers.tool_router import tools router = APIRouter() @@ -28,3 +29,4 @@ router.include_router(graph) # /api/graph/* router.include_router(tasks) # /api/tasks/* router.include_router(mcp) # /api/system/mcp-servers/* router.include_router(skills) # /api/system/skills/* +router.include_router(tools) # /api/system/tools/* diff --git a/server/routers/tool_router.py b/server/routers/tool_router.py new file mode 100644 index 00000000..1221610b --- /dev/null +++ b/server/routers/tool_router.py @@ -0,0 +1,25 @@ +from fastapi import APIRouter, Depends + +from src.services.tool_service import get_tool_metadata +from server.utils.auth_middleware import get_admin_user +from src.storage.postgres.models_business import User + +tools = APIRouter(prefix="/system/tools", tags=["tools"]) + + +@tools.get("") +async def list_tools( + category: str = None, + user: User = Depends(get_admin_user), +): + """获取工具列表""" + return {"success": True, "data": get_tool_metadata(category)} + + +@tools.get("/options") +async def get_tool_options( + user: User = Depends(get_admin_user), +): + """获取工具选项(前端下拉框用)""" + all_tools = get_tool_metadata() + return {"success": True, "data": [{"label": t["name"], "value": t["id"]} for t in all_tools]} diff --git a/src/agents/common/__init__.py b/src/agents/common/__init__.py index 5a21c7f6..dd71683e 100644 --- a/src/agents/common/__init__.py +++ b/src/agents/common/__init__.py @@ -1,15 +1,3 @@ -""" -Common utilities and base classes for agents. - -This module provides a unified namespace for commonly used base classes and utilities, -allowing simplified imports like: - from src.agents.common import BaseAgent, BaseContext, BaseState - -For other specific functions, use the original import style: - from src.agents.common.tools import query_knowledge_graph - from src.services.mcp_service import MCP_SERVERS -""" - # Base classes - 核心基类 from src.agents.common.base import BaseAgent from src.agents.common.context import BaseContext @@ -19,7 +7,8 @@ from src.agents.common.models import load_chat_model from src.agents.common.state import BaseState # Tools - 核心工具函数 -from src.agents.common.tools import gen_tool_info, get_buildin_tools +from src.agents.common.toolkits.buildin import get_buildin_tools +from src.agents.common.toolkits.utils import gen_tool_info # MCP - Agent 层统一入口(自动过滤 disabled_tools) from src.services.mcp_service import get_enabled_mcp_tools diff --git a/src/agents/common/context.py b/src/agents/common/context.py index 6e5ba3aa..4a609c3e 100644 --- a/src/agents/common/context.py +++ b/src/agents/common/context.py @@ -12,7 +12,8 @@ from src import config as sys_config from src.services.mcp_service import get_mcp_server_names from src.utils import logger -from .tools import gen_tool_info, get_buildin_tools +from .toolkits.buildin import get_buildin_tools +from .toolkits.utils import gen_tool_info @dataclass(kw_only=True) diff --git a/src/agents/common/middlewares/runtime_config_middleware.py b/src/agents/common/middlewares/runtime_config_middleware.py index 518d4487..ea217edc 100644 --- a/src/agents/common/middlewares/runtime_config_middleware.py +++ b/src/agents/common/middlewares/runtime_config_middleware.py @@ -12,7 +12,8 @@ from langchain_core.messages import SystemMessage, ToolMessage from langgraph.types import Command from src.agents.common import load_chat_model -from src.agents.common.tools import get_buildin_tools, get_kb_based_tools +from src.agents.common.toolkits.buildin import get_buildin_tools +from src.agents.common.toolkits.kbs import get_kb_based_tools from src.services.mcp_service import get_enabled_mcp_tools from src.services.skill_resolver import ( SkillSessionSnapshot, diff --git a/src/agents/common/subagents/__init__.py b/src/agents/common/subagents/__init__.py deleted file mode 100644 index c54d1b00..00000000 --- a/src/agents/common/subagents/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .calc_agent import calc_agent, calc_agent_tool - -__all__ = ["calc_agent", "calc_agent_tool"] diff --git a/src/agents/common/subagents/calc_agent.py b/src/agents/common/subagents/calc_agent.py deleted file mode 100644 index 6fac054a..00000000 --- a/src/agents/common/subagents/calc_agent.py +++ /dev/null @@ -1,19 +0,0 @@ -from langchain.agents import create_agent -from langchain.tools import tool - -from src import config -from src.agents.common import load_chat_model -from src.agents.common.tools import calculator - -calc_agent = create_agent( - model=load_chat_model(config.default_model), - tools=[calculator], - system_prompt="你可以使用计算器工具,处理各种数学计算任务。最终仅返回计算结果,不需要任何额外的解释。", -) - - -@tool(name_or_callable="calc_agent_tool", description="进行计算任务,输入是数学表达式或描述,输出计算结果。") -async def calc_agent_tool(description: str) -> str: - """CalcAgent 工具 - 使用子智能体 CalcAgent 进行计算任务""" - response = await calc_agent.ainvoke({"messages": [("user", description)]}) - return response["messages"][-1].content diff --git a/src/agents/common/toolkits/__init__.py b/src/agents/common/toolkits/__init__.py new file mode 100644 index 00000000..5a1be101 --- /dev/null +++ b/src/agents/common/toolkits/__init__.py @@ -0,0 +1,54 @@ +# toolkits 包 +from .registry import ( + ToolExtraMetadata, + get_all_extra_metadata, + get_all_tool_instances, + get_extra_metadata, + register_tool, + tool, +) + +# 工具获取函数 +from .buildin import get_buildin_tools +from .kbs import get_kb_based_tools +from src.services.mcp_service import get_enabled_mcp_tools + + +async def get_tools_from_context(context, extra_tools=None): + """从上下文配置中获取工具列表""" + # 1. 基础工具 (从 context.tools 中筛选) + all_basic_tools = get_buildin_tools() + (extra_tools or []) + selected_tools = [] + + if context.tools: + # 创建工具映射表 + tools_map = {t.name: t for t in all_basic_tools} + for tool_name in context.tools: + if tool_name in tools_map: + selected_tools.append(tools_map[tool_name]) + + # 2. 知识库工具 + if context.knowledges: + kb_tools = get_kb_based_tools(db_names=context.knowledges) + selected_tools.extend(kb_tools) + + # 3. MCP 工具(使用统一入口,自动过滤 disabled_tools) + if context.mcps: + for server_name in context.mcps: + mcp_tools = await get_enabled_mcp_tools(server_name) + selected_tools.extend(mcp_tools) + + return selected_tools + + +__all__ = [ + "register_tool", + "get_extra_metadata", + "get_all_extra_metadata", + "get_all_tool_instances", + "ToolExtraMetadata", + "tool", + "get_buildin_tools", + "get_kb_based_tools", + "get_tools_from_context", +] diff --git a/src/agents/common/toolkits/buildin/__init__.py b/src/agents/common/toolkits/buildin/__init__.py new file mode 100644 index 00000000..1321c601 --- /dev/null +++ b/src/agents/common/toolkits/buildin/__init__.py @@ -0,0 +1,7 @@ +# buildin 工具包 +from .tools import get_buildin_tools, query_knowledge_graph + +__all__ = [ + "get_buildin_tools", + "query_knowledge_graph", +] diff --git a/src/agents/common/tools.py b/src/agents/common/toolkits/buildin/tools.py similarity index 56% rename from src/agents/common/tools.py rename to src/agents/common/toolkits/buildin/tools.py index c8568cfc..1efed55e 100644 --- a/src/agents/common/tools.py +++ b/src/agents/common/toolkits/buildin/tools.py @@ -4,12 +4,10 @@ import uuid from typing import Annotated, Any import requests -from langchain.tools import tool from langgraph.types import interrupt from src import config, graph_base -from src.agents.common.toolkits.kbs import get_kb_based_tools -from src.services.mcp_service import get_enabled_mcp_tools +from src.agents.common.toolkits import tool from src.storage.minio import aupload_file_to_minio from src.utils import logger @@ -24,12 +22,15 @@ def get_tavily_search(): from langchain_tavily import TavilySearch _tavily_search_instance = TavilySearch() - _tavily_search_instance.metadata = {"name": "Tavily 网页搜索"} + _tavily_search_instance.metadata = {"name": "Tavily 网页搜索", "category": "buildin", "tags": ["搜索"]} + + # 即使没有配置 API_KEY 也返回实例,调用时会出错 return _tavily_search_instance -@tool(name_or_callable="calculator", description="可以对给定的2个数字选择进行 add, subtract, multiply, divide 运算") +@tool(category="buildin", tags=["计算"], display_name="计算器") def calculator(a: float, b: float, operation: str) -> float: + """计算器:对给定的2个数字进行基本数学运算""" try: if operation == "add": return a + b @@ -48,7 +49,7 @@ def calculator(a: float, b: float, operation: str) -> float: raise -@tool +@tool(category="buildin", tags=["图片", "测试"], display_name="文生图测试") async def text_to_img_demo(text: str) -> str: """【测试用】使用模型生成图片, 会返回图片的URL""" @@ -85,7 +86,7 @@ async def text_to_img_demo(text: str) -> str: return image_url -@tool(name_or_callable="human_in_the_loop_debug", description="请求人工审批工具,用于在执行重要操作前获得人类确认。") +@tool(category="debug", tags=["内置", "审批"], display_name="人工审批") def get_approved_user_goal( operation_description: str, ) -> dict: @@ -129,7 +130,7 @@ KG_QUERY_DESCRIPTION = """ """ -@tool(name_or_callable="查询知识图谱", description=KG_QUERY_DESCRIPTION) +@tool(category="buildin", tags=["图谱"], display_name="查询知识图谱", description=KG_QUERY_DESCRIPTION) def query_knowledge_graph(query: Annotated[str, "The keyword to query knowledge graph."]) -> Any: """使用这个工具可以查询知识图谱中包含的三元组信息。关键词(query),使用可能帮助回答这个问题的关键词进行查询,不要直接使用用户的原始输入去查询。""" try: @@ -145,59 +146,8 @@ def query_knowledge_graph(query: Annotated[str, "The keyword to query knowledge return f"知识图谱查询失败: {str(e)}" -def gen_tool_info(tools) -> list[dict[str, Any]]: - """获取所有工具的信息(用于前端展示)""" - tools_info = [] - - try: - # 获取注册的工具信息 - for tool_obj in tools: - try: - metadata = getattr(tool_obj, "metadata", {}) or {} - info = { - "id": tool_obj.name, - "name": metadata.get("name", tool_obj.name), - "description": tool_obj.description, - "metadata": metadata, - "args": [], - # "is_async": is_async # Include async information - } - - if hasattr(tool_obj, "args_schema") and tool_obj.args_schema: - if isinstance(tool_obj.args_schema, dict): - schema = tool_obj.args_schema - else: - schema = tool_obj.args_schema.schema() - - for arg_name, arg_info in schema.get("properties", {}).items(): - info["args"].append( - { - "name": arg_name, - "type": arg_info.get("type", ""), - "description": arg_info.get("description", ""), - } - ) - - tools_info.append(info) - # logger.debug(f"Successfully processed tool info for {tool_obj.name}") - - except Exception as e: - logger.error( - f"Failed to process tool {getattr(tool_obj, 'name', 'unknown')}: {e}\n{traceback.format_exc()}. " - f"Details: {dict(tool_obj.__dict__)}" - ) - continue - - except Exception as e: - logger.error(f"Failed to get tools info: {e}\n{traceback.format_exc()}") - return [] - - logger.info(f"Successfully extracted info for {len(tools_info)} tools") - return tools_info - - def get_buildin_tools() -> list: - """注册静态工具""" + """获取内置工具列表""" static_tools = [ query_knowledge_graph, get_approved_user_goal, @@ -205,42 +155,7 @@ def get_buildin_tools() -> list: text_to_img_demo, ] - # subagents 工具 - from .subagents import calc_agent_tool - - static_tools.append(calc_agent_tool) - - # 检查是否启用网页搜索(即是否配置了 API_KEY) - if config.enable_web_search: - tavily_search = get_tavily_search() - if tavily_search: - static_tools.append(tavily_search) + # 始终添加 tavily_search,无论是否配置 API_KEY(调用时会出错) + static_tools.append(get_tavily_search()) return static_tools - - -async def get_tools_from_context(context, extra_tools=None) -> list: - """从上下文配置中获取工具列表""" - # 1. 基础工具 (从 context.tools 中筛选) - all_basic_tools = get_buildin_tools() + (extra_tools or []) - selected_tools = [] - - if context.tools: - # 创建工具映射表 - tools_map = {t.name: t for t in all_basic_tools} - for tool_name in context.tools: - if tool_name in tools_map: - selected_tools.append(tools_map[tool_name]) - - # 2. 知识库工具 - if context.knowledges: - kb_tools = get_kb_based_tools(db_names=context.knowledges) - selected_tools.extend(kb_tools) - - # 3. MCP 工具(使用统一入口,自动过滤 disabled_tools) - if context.mcps: - for server_name in context.mcps: - mcp_tools = await get_enabled_mcp_tools(server_name) - selected_tools.extend(mcp_tools) - - return selected_tools diff --git a/src/agents/common/toolkits/mysql/__init__.py b/src/agents/common/toolkits/mysql/__init__.py index 0480f381..1c75ed60 100644 --- a/src/agents/common/toolkits/mysql/__init__.py +++ b/src/agents/common/toolkits/mysql/__init__.py @@ -1,3 +1,4 @@ +# mysql 工具包 from .tools import get_mysql_tools __all__ = ["get_mysql_tools"] diff --git a/src/agents/common/toolkits/mysql/tools.py b/src/agents/common/toolkits/mysql/tools.py index f163b24d..beca4bc5 100644 --- a/src/agents/common/toolkits/mysql/tools.py +++ b/src/agents/common/toolkits/mysql/tools.py @@ -1,8 +1,8 @@ from typing import Annotated, Any -from langchain.tools import tool from pydantic import BaseModel, Field +from src.agents.common.toolkits import tool from src.utils import logger from .connection import ( @@ -52,7 +52,7 @@ class TableListModel(BaseModel): pass -@tool(name_or_callable="mysql_list_tables", args_schema=TableListModel) +@tool(category="mysql", tags=["数据库", "查询"], display_name="列出MySQL表", name_or_callable="mysql_list_tables", args_schema=TableListModel) def mysql_list_tables() -> str: """【查询表名及说明】获取数据库中的所有表名 @@ -107,7 +107,7 @@ class TableDescribeModel(BaseModel): table_name: str = Field(description="要查询的表名", example="users") -@tool(name_or_callable="mysql_describe_table", args_schema=TableDescribeModel) +@tool(category="mysql", tags=["数据库", "结构"], display_name="描述MySQL表结构", name_or_callable="mysql_describe_table", args_schema=TableDescribeModel) def mysql_describe_table(table_name: Annotated[str, "要查询结构的表名"]) -> str: """【描述表】获取指定表的详细结构信息 @@ -203,7 +203,7 @@ class QueryModel(BaseModel): timeout: int | None = Field(default=60, description="查询超时时间(秒),默认60秒,最大600秒", ge=1, le=600) -@tool(name_or_callable="mysql_query", args_schema=QueryModel) +@tool(category="mysql", tags=["数据库", "SQL"], display_name="执行MySQL查询", name_or_callable="mysql_query", args_schema=QueryModel) def mysql_query( sql: Annotated[str, "要执行的SQL查询语句(只能是SELECT语句)"], timeout: Annotated[int | None, "查询超时时间(秒),默认60秒,最大600秒"] = 60, diff --git a/src/agents/common/toolkits/registry.py b/src/agents/common/toolkits/registry.py new file mode 100644 index 00000000..e7ed03c9 --- /dev/null +++ b/src/agents/common/toolkits/registry.py @@ -0,0 +1,123 @@ +from collections.abc import Callable +from dataclasses import dataclass, field + + +@dataclass +class ToolExtraMetadata: + """附加元数据(用装饰器注册)""" + + category: str = "" # 分类: buildin, mysql, subagents, debug + tags: list[str] = field(default_factory=list) + display_name: str = "" # 显示名称(给人看的名字) + icon: str = "" + + +# 全局注册表: tool_name -> ToolExtraMetadata +_extra_registry: dict[str, ToolExtraMetadata] = {} + +# 全局工具实例列表(由 @tool 装饰器自动收集) +_all_tool_instances: list = [] + + +def register_tool( + category: str, + tags: list[str] = None, + display_name: str = "", + icon: str = "", +): + """装饰器:注册工具附加元数据 + + 使用方式: + @register_tool(category="buildin", tags=["内置"], display_name="计算器") + def calculator(...): + ... + """ + + def decorator(func_or_tool: Callable) -> Callable: + # 获取工具名称(优先用 metadata.name) + tool_name = getattr(func_or_tool, "name", None) or getattr( + func_or_tool, "__name__", "" + ) + _extra_registry[tool_name] = ToolExtraMetadata( + category=category, + tags=tags or [], + display_name=display_name, + icon=icon, + ) + return func_or_tool + + return decorator + + +def get_extra_metadata(tool_name: str) -> ToolExtraMetadata | None: + """获取工具附加元数据""" + return _extra_registry.get(tool_name) + + +def get_all_extra_metadata() -> dict[str, ToolExtraMetadata]: + """获取所有附加元数据""" + return _extra_registry.copy() + + +def get_all_tool_instances() -> list: + """获取所有工具实例(由 @tool 装饰器自动收集)""" + return _all_tool_instances + + +# 基于 langchain.tool 的拓展装饰器 +def tool( + category: str = "", + tags: list[str] = None, + display_name: str = "", + icon: str = "", + name_or_callable: str | Callable | None = None, + description: str | None = None, + args_schema: type | None = None, + return_direct: bool = True, +): + """基于 langchain.tool 的拓展装饰器,同时注册元数据 + + 使用方式: + @tool(category="buildin", tags=["计算"], display_name="计算器") + def calculator(a: float, b: float, operation: str) -> float: + ... + + 或者保留原有的 name_or_callable 和 description: + @tool( + category="buildin", + display_name="查询知识图谱", + name_or_callable="查询知识图谱", + description=KG_QUERY_DESCRIPTION, + ) + def query_knowledge_graph(query: str) -> str: + ... + """ + from langchain.tools import tool as langchain_tool + + # 先应用 langchain tool 装饰器 + langchain_decorator = langchain_tool( + name_or_callable=name_or_callable, + description=description, + args_schema=args_schema, + return_direct=return_direct, + ) + + def decorator(func: Callable) -> Callable: + # 应用 langchain 装饰器 + tool_obj = langchain_decorator(func) + + # 注册附加元数据 + tool_name = tool_obj.name + _extra_registry[tool_name] = ToolExtraMetadata( + category=category, + tags=tags or [], + display_name=display_name, + icon=icon, + ) + + # 自动收集工具实例 + _all_tool_instances.append(tool_obj) + + return tool_obj + + return decorator diff --git a/src/agents/common/toolkits/utils.py b/src/agents/common/toolkits/utils.py new file mode 100644 index 00000000..b1a9475a --- /dev/null +++ b/src/agents/common/toolkits/utils.py @@ -0,0 +1,55 @@ +import traceback +from typing import Any + +from src.utils import logger + + +def gen_tool_info(tools) -> list[dict[str, Any]]: + """获取所有工具的信息(用于前端展示)""" + tools_info = [] + + try: + # 获取注册的工具信息 + for tool_obj in tools: + try: + metadata = getattr(tool_obj, "metadata", {}) or {} + info = { + "id": tool_obj.name, + "name": metadata.get("name", tool_obj.name), + "description": tool_obj.description, + "metadata": metadata, + "args": [], + # "is_async": is_async # Include async information + } + + if hasattr(tool_obj, "args_schema") and tool_obj.args_schema: + if isinstance(tool_obj.args_schema, dict): + schema = tool_obj.args_schema + else: + schema = tool_obj.args_schema.schema() + + for arg_name, arg_info in schema.get("properties", {}).items(): + info["args"].append( + { + "name": arg_name, + "type": arg_info.get("type", ""), + "description": arg_info.get("description", ""), + } + ) + + tools_info.append(info) + # logger.debug(f"Successfully processed tool info for {tool_obj.name}") + + except Exception as e: + logger.error( + f"Failed to process tool {getattr(tool_obj, 'name', 'unknown')}: {e}\n{traceback.format_exc()}. " + f"Details: {dict(tool_obj.__dict__)}" + ) + continue + + except Exception as e: + logger.error(f"Failed to get tools info: {e}\n{traceback.format_exc()}") + return [] + + logger.info(f"Successfully extracted info for {len(tools_info)} tools") + return tools_info diff --git a/src/agents/deep_agent/graph.py b/src/agents/deep_agent/graph.py index 422c5d3e..59461f9d 100644 --- a/src/agents/deep_agent/graph.py +++ b/src/agents/deep_agent/graph.py @@ -12,7 +12,7 @@ from langchain.agents.middleware import ( from src.agents.common import BaseAgent, load_chat_model from src.agents.common.backends import create_agent_composite_backend from src.agents.common.middlewares import RuntimeConfigMiddleware, SummaryOffloadMiddleware, save_attachments_to_fs -from src.agents.common.tools import get_tavily_search +from src.agents.common.toolkits.buildin.tools import get_tavily_search from src.services.mcp_service import get_tools_from_all_servers from src.utils import logger diff --git a/src/services/skill_service.py b/src/services/skill_service.py index 1f5744a4..c253c9d1 100644 --- a/src/services/skill_service.py +++ b/src/services/skill_service.py @@ -80,7 +80,7 @@ def validate_skill_slug(slug: str) -> str: def _get_buildin_tool_names() -> list[str]: - from src.agents.common.tools import get_buildin_tools + from src.agents.common.toolkits.buildin import get_buildin_tools return [tool.name for tool in get_buildin_tools()] @@ -91,11 +91,19 @@ def get_skills_root_dir() -> Path: return root -async def get_skill_dependency_options(db: AsyncSession) -> dict[str, list[str]]: +async def get_skill_dependency_options(db: AsyncSession) -> dict[str, list[str] | list[dict]]: repo = SkillRepository(db) items = await repo.list_all() + + # 获取所有工具(不仅仅是 buildin 工具),返回 id 和 name + from src.services.tool_service import get_tool_metadata + + all_tools = get_tool_metadata() + # 返回工具列表,每个包含 id 和 name + tool_list = [{"id": tool["id"], "name": tool.get("name", tool["id"])} for tool in all_tools] + return { - "tools": _get_buildin_tool_names(), + "tools": tool_list, "mcps": get_mcp_server_names(), "skills": [item.slug for item in items], } @@ -106,6 +114,14 @@ async def list_skills(db: AsyncSession) -> list[Skill]: return await repo.list_all() +def _get_all_tool_names() -> list[str]: + """获取所有工具名称(包括 buildin 和其他来源)""" + from src.services.tool_service import get_tool_metadata + + all_tools = get_tool_metadata() + return [tool["id"] for tool in all_tools] + + def _validate_dependencies( *, slug: str, @@ -118,7 +134,8 @@ def _validate_dependencies( mcps = _normalize_string_list(mcp_dependencies) skills = _normalize_string_list(skill_dependencies) - available_tools = set(_get_buildin_tool_names()) + # 验证所有工具(不仅仅是 buildin) + available_tools = set(_get_all_tool_names()) invalid_tools = [name for name in tools if name not in available_tools] if invalid_tools: raise ValueError(f"存在无效工具依赖: {', '.join(invalid_tools)}") diff --git a/src/services/tool_service.py b/src/services/tool_service.py new file mode 100644 index 00000000..5eb550c0 --- /dev/null +++ b/src/services/tool_service.py @@ -0,0 +1,77 @@ +from src.utils import logger + +# 工具元数据缓存 +_metadata_cache: list[dict] = [] + + +def _extract_tool_info(tool_obj) -> dict: + """从 tool_obj 提取基础信息""" + metadata = getattr(tool_obj, "metadata", {}) or {} + info = { + "id": tool_obj.name, + "name": metadata.get("name", tool_obj.name), # 显示名称优先从 metadata 获取 + "description": tool_obj.description, + "metadata": metadata, + "args": [], + } + + if hasattr(tool_obj, "args_schema") and tool_obj.args_schema: + schema = tool_obj.args_schema + if hasattr(schema, "schema"): + schema = schema.schema() + for arg_name, arg_info in schema.get("properties", {}).items(): + info["args"].append( + { + "name": arg_name, + "type": arg_info.get("type", ""), + "description": arg_info.get("description", ""), + } + ) + return info + + +def _ensure_metadata_loaded(): + """延迟加载工具元数据(首次调用时自动触发)""" + global _metadata_cache + + if _metadata_cache: # 已加载 + return + + from src.agents.common.toolkits.registry import ( + get_all_extra_metadata, + get_all_tool_instances, + ) + + # 获取所有工具实例 + all_tools = get_all_tool_instances() + extra_meta = get_all_extra_metadata() + + for tool in all_tools: + tool_name = tool.name + runtime_info = _extract_tool_info(tool) + + # 合并附加元数据 + if tool_name in extra_meta: + extra = extra_meta[tool_name] + runtime_info["category"] = extra.category + runtime_info["tags"] = extra.tags + # display_name 优先级高于 tool.name + if extra.display_name: + runtime_info["name"] = extra.display_name + else: + # 未注册,设为默认分类 + runtime_info["category"] = "buildin" + runtime_info["tags"] = [] + + _metadata_cache.append(runtime_info) + + logger.info(f"Tool service loaded {len(_metadata_cache)} tools (lazy load)") + + +def get_tool_metadata(category: str = None) -> list[dict]: + """获取工具元数据列表(延迟加载)""" + _ensure_metadata_loaded() + + if category: + return [t for t in _metadata_cache if t.get("category") == category] + return _metadata_cache diff --git a/src/storage/db/models.py b/src/storage/db/models.py deleted file mode 100644 index 7c464c05..00000000 --- a/src/storage/db/models.py +++ /dev/null @@ -1,430 +0,0 @@ -import datetime as dt - -from sqlalchemy import JSON, Column, DateTime, ForeignKey, Integer, String, Text -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import relationship - -from src.utils.datetime_utils import coerce_datetime, format_utc_datetime, utc_now - -Base = declarative_base() - - -## Removed legacy RDBMS knowledge models (KnowledgeDatabase/KnowledgeFile/KnowledgeNode) - - -class Department(Base): - """部门模型""" - - __tablename__ = "departments" - - id = Column(Integer, primary_key=True, autoincrement=True) - name = Column(String(50), nullable=False, unique=True, index=True) - description = Column(String(255), nullable=True) - created_at = Column(DateTime, default=utc_now) - - # 关联关系 - users = relationship("User", back_populates="department") - - def to_dict(self): - return { - "id": self.id, - "name": self.name, - "description": self.description, - "created_at": format_utc_datetime(self.created_at), - } - - -class Conversation(Base): - """Conversation table - new storage system""" - - __tablename__ = "conversations" - - id = Column(Integer, primary_key=True, autoincrement=True, comment="Primary key") - thread_id = Column(String(64), unique=True, index=True, nullable=False, comment="Thread ID (UUID)") - user_id = Column(String(64), index=True, nullable=False, comment="User ID") - agent_id = Column(String(64), index=True, nullable=False, comment="Agent ID") - title = Column(String(255), nullable=True, comment="Conversation title") - status = Column(String(20), default="active", comment="Status: active/archived/deleted") - created_at = Column(DateTime, default=utc_now, comment="Creation time") - updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, comment="Update time") - extra_metadata = Column(JSON, nullable=True, comment="Additional metadata") - - # Relationships - messages = relationship("Message", back_populates="conversation", cascade="all, delete-orphan") - stats = relationship( - "ConversationStats", back_populates="conversation", uselist=False, cascade="all, delete-orphan" - ) - - def to_dict(self): - return { - "id": self.id, - "thread_id": self.thread_id, - "user_id": self.user_id, - "agent_id": self.agent_id, - "title": self.title, - "status": self.status, - "created_at": format_utc_datetime(self.created_at), - "updated_at": format_utc_datetime(self.updated_at), - "metadata": self.extra_metadata or {}, - } - - -class Message(Base): - """Message table - stores conversation messages""" - - __tablename__ = "messages" - - id = Column(Integer, primary_key=True, autoincrement=True, comment="Primary key") - conversation_id = Column( - Integer, ForeignKey("conversations.id"), nullable=False, index=True, comment="Conversation ID" - ) - role = Column(String(20), nullable=False, comment="Message role: user/assistant/system/tool") - content = Column(Text, nullable=False, comment="Message content") - message_type = Column(String(30), default="text", comment="Message type: text/tool_call/tool_result") - created_at = Column(DateTime, default=utc_now, comment="Creation time") - token_count = Column(Integer, nullable=True, comment="Token count (optional)") - extra_metadata = Column(JSON, nullable=True, comment="Additional metadata (complete message dump)") - image_content = Column(Text, nullable=True, comment="Base64 encoded image content for multimodal messages") - - # Relationships - conversation = relationship("Conversation", back_populates="messages") - tool_calls = relationship("ToolCall", back_populates="message", cascade="all, delete-orphan") - - def to_dict(self): - return { - "id": self.id, - "conversation_id": self.conversation_id, - "role": self.role, - "content": self.content, - "message_type": self.message_type, - "created_at": format_utc_datetime(self.created_at), - "token_count": self.token_count, - "metadata": self.extra_metadata or {}, - "image_content": self.image_content, - "tool_calls": [tc.to_dict() for tc in self.tool_calls] if self.tool_calls else [], - } - - def to_simple_dict(self): - return { - "role": self.role, - "content": self.content, - } - - -class ToolCall(Base): - """ToolCall table - stores tool invocations""" - - __tablename__ = "tool_calls" - - id = Column(Integer, primary_key=True, autoincrement=True, comment="Primary key") - message_id = Column(Integer, ForeignKey("messages.id"), nullable=False, index=True, comment="Message ID") - langgraph_tool_call_id = Column( - String(100), nullable=True, index=True, comment="LangGraph tool_call_id for matching" - ) - tool_name = Column(String(100), nullable=False, comment="Tool name") - tool_input = Column(JSON, nullable=True, comment="Tool input parameters") - tool_output = Column(Text, nullable=True, comment="Tool execution result") - status = Column(String(20), default="pending", comment="Status: pending/success/error") - error_message = Column(Text, nullable=True, comment="Error message if failed") - created_at = Column(DateTime, default=utc_now, comment="Creation time") - - # Relationships - message = relationship("Message", back_populates="tool_calls") - - def to_dict(self): - return { - "id": self.id, - "message_id": self.message_id, - "langgraph_tool_call_id": self.langgraph_tool_call_id, - "tool_name": self.tool_name, - "tool_input": self.tool_input or {}, - "tool_output": self.tool_output, - "status": self.status, - "error_message": self.error_message, - "created_at": format_utc_datetime(self.created_at), - } - - -class ConversationStats(Base): - """ConversationStats table - stores conversation statistics""" - - __tablename__ = "conversation_stats" - - id = Column(Integer, primary_key=True, autoincrement=True, comment="Primary key") - conversation_id = Column( - Integer, ForeignKey("conversations.id"), unique=True, nullable=False, comment="Conversation ID" - ) - message_count = Column(Integer, default=0, comment="Total message count") - total_tokens = Column(Integer, default=0, comment="Total tokens used") - model_used = Column(String(100), nullable=True, comment="Model used") - user_feedback = Column(JSON, nullable=True, comment="User feedback") - created_at = Column(DateTime, default=utc_now, comment="Creation time") - updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, comment="Update time") - - # Relationships - conversation = relationship("Conversation", back_populates="stats") - - def to_dict(self): - return { - "id": self.id, - "conversation_id": self.conversation_id, - "message_count": self.message_count, - "total_tokens": self.total_tokens, - "model_used": self.model_used, - "user_feedback": self.user_feedback or {}, - "created_at": format_utc_datetime(self.created_at), - "updated_at": format_utc_datetime(self.updated_at), - } - - -class User(Base): - """用户模型""" - - __tablename__ = "users" - - id = Column(Integer, primary_key=True, autoincrement=True) - username = Column(String, nullable=False, unique=True, index=True) # 显示名称,2-20字符 - user_id = Column(String, nullable=False, unique=True, index=True) # 登录ID,根据用户名生成 - phone_number = Column(String, nullable=True, unique=True, index=True) # 手机号,可选登录方式 - avatar = Column(String, nullable=True) # 头像URL - password_hash = Column(String, nullable=False) - role = Column(String, nullable=False, default="user") # 角色: superadmin, admin, user - department_id = Column(Integer, ForeignKey("departments.id"), nullable=True) # 部门ID - created_at = Column(DateTime, default=utc_now) - last_login = Column(DateTime, nullable=True) - - # 登录失败限制相关字段 - login_failed_count = Column(Integer, nullable=False, default=0) # 登录失败次数 - last_failed_login = Column(DateTime, nullable=True) # 最后一次登录失败时间 - login_locked_until = Column(DateTime, nullable=True) # 锁定到什么时候 - - # 软删除相关字段 - is_deleted = Column(Integer, nullable=False, default=0, index=True) # 是否已删除:0=否,1=是 - deleted_at = Column(DateTime, nullable=True) # 删除时间 - - # 关联操作日志 - operation_logs = relationship("OperationLog", back_populates="user", cascade="all, delete-orphan") - - # 关联部门 - department = relationship("Department", back_populates="users") - - def to_dict(self, include_password=False): - # SQLite 存储 naive datetime,需要标记为 UTC 后再转换 - result = { - "id": self.id, - "username": self.username, - "user_id": self.user_id, - "phone_number": self.phone_number, - "avatar": self.avatar, - "role": self.role, - "department_id": self.department_id, - "created_at": format_utc_datetime(self.created_at), - "last_login": format_utc_datetime(self.last_login), - "login_failed_count": self.login_failed_count, - "last_failed_login": format_utc_datetime(self.last_failed_login), - "login_locked_until": format_utc_datetime(self.login_locked_until), - "is_deleted": self.is_deleted, - "deleted_at": format_utc_datetime(self.deleted_at), - } - if include_password: - result["password_hash"] = self.password_hash - return result - - def is_login_locked(self): - """检查用户是否处于登录锁定状态""" - lock_deadline = coerce_datetime(self.login_locked_until) - if lock_deadline is None: - return False - return utc_now() < lock_deadline - - def get_remaining_lock_time(self): - """获取剩余锁定时间(秒)""" - lock_deadline = coerce_datetime(self.login_locked_until) - if lock_deadline is None: - return 0 - remaining = int((lock_deadline - utc_now()).total_seconds()) - return max(0, remaining) - - def calculate_lock_duration(self): - """根据失败次数计算锁定时长(秒)""" - if self.login_failed_count < 10: - return 0 - - # 从第10次失败开始,等待时间从1秒开始,每次翻倍 - wait_seconds = 2 ** (self.login_failed_count - 10) - - # 最大锁定时间:365天 - max_seconds = 365 * 24 * 60 * 60 - return min(wait_seconds, max_seconds) - - def increment_failed_login(self): - """增加登录失败次数并设置锁定时间""" - self.login_failed_count += 1 - self.last_failed_login = utc_now() - - lock_duration = self.calculate_lock_duration() - if lock_duration > 0: - self.login_locked_until = utc_now() + dt.timedelta(seconds=lock_duration) - - def reset_failed_login(self): - """重置登录失败相关字段""" - self.login_failed_count = 0 - self.last_failed_login = None - self.login_locked_until = None - - -class OperationLog(Base): - """操作日志模型""" - - __tablename__ = "operation_logs" - - id = Column(Integer, primary_key=True, autoincrement=True) - user_id = Column(Integer, ForeignKey("users.id"), nullable=False) - operation = Column(String, nullable=False) - details = Column(Text, nullable=True) - ip_address = Column(String, nullable=True) - timestamp = Column(DateTime, default=utc_now) - - # 关联用户 - user = relationship("User", back_populates="operation_logs") - - def to_dict(self): - return { - "id": self.id, - "user_id": self.user_id, - "operation": self.operation, - "details": self.details, - "ip_address": self.ip_address, - "timestamp": format_utc_datetime(self.timestamp), - } - - -class MessageFeedback(Base): - """Message feedback table - stores user feedback on AI responses""" - - __tablename__ = "message_feedbacks" - - id = Column(Integer, primary_key=True, autoincrement=True, comment="Primary key") - message_id = Column( - Integer, ForeignKey("messages.id"), nullable=False, index=True, comment="Message ID being rated" - ) - user_id = Column(String(64), nullable=False, index=True, comment="User ID who provided feedback") - rating = Column(String(10), nullable=False, comment="Feedback rating: like or dislike") - reason = Column(Text, nullable=True, comment="Optional reason for dislike feedback") - created_at = Column(DateTime, default=utc_now, comment="Feedback creation time") - - # Relationships - message = relationship("Message", backref="feedbacks") - - def to_dict(self): - return { - "id": self.id, - "message_id": self.message_id, - "user_id": self.user_id, - "rating": self.rating, - "reason": self.reason, - "created_at": format_utc_datetime(self.created_at), - } - - -class MCPServer(Base): - """MCP 服务器配置模型""" - - __tablename__ = "mcp_servers" - - # 核心字段 - name 作为主键 - name = Column(String(100), primary_key=True, comment="服务器名称(唯一标识)") - description = Column(String(500), nullable=True, comment="描述") - - # 连接配置 - transport = Column(String(20), nullable=False, comment="传输类型:sse/streamable_http/stdio") - url = Column(String(500), nullable=True, comment="服务器 URL(sse/streamable_http)") - command = Column(String(500), nullable=True, comment="命令(stdio)") - args = Column(JSON, nullable=True, comment="命令参数数组(stdio)") - env = Column(JSON, nullable=True, comment="环境变量(stdio)") - headers = Column(JSON, nullable=True, comment="HTTP 请求头") - timeout = Column(Integer, nullable=True, comment="HTTP 超时时间(秒)") - sse_read_timeout = Column(Integer, nullable=True, comment="SSE 读取超时(秒)") - - # UI 增强字段 - tags = Column(JSON, nullable=True, comment="标签数组") - icon = Column(String(50), nullable=True, comment="图标(emoji)") - - # 状态字段 - enabled = Column(Integer, nullable=False, default=1, comment="是否启用:1=是,0=否") - disabled_tools = Column(JSON, nullable=True, comment="禁用的工具名称列表") - - # 用户追踪 - created_by = Column(String(100), nullable=False, comment="创建人用户名") - updated_by = Column(String(100), nullable=False, comment="修改人用户名") - - # 时间戳 - created_at = Column(DateTime, default=utc_now, comment="创建时间") - updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, comment="更新时间") - - def to_dict(self): - return { - "name": self.name, - "description": self.description, - "transport": self.transport, - "url": self.url, - "command": self.command, - "args": self.args or [], - "env": self.env or {}, - "headers": self.headers or {}, - "timeout": self.timeout, - "sse_read_timeout": self.sse_read_timeout, - "tags": self.tags or [], - "icon": self.icon, - "enabled": bool(self.enabled), - "disabled_tools": self.disabled_tools or [], - "created_by": self.created_by, - "updated_by": self.updated_by, - "created_at": format_utc_datetime(self.created_at), - "updated_at": format_utc_datetime(self.updated_at), - } - - def to_mcp_config(self) -> dict: - """转换为 MCP 配置格式(用于加载到 MCP_SERVERS 缓存)""" - import json - - config = { - "transport": self.transport, - } - if self.url: - config["url"] = self.url - if self.command: - config["command"] = self.command - # args 只用于 stdio 传输类型,必须是列表 - if self.transport == "stdio" and self.args: - if isinstance(self.args, list): - config["args"] = self.args - elif isinstance(self.args, str): - try: - config["args"] = json.loads(self.args) - except json.JSONDecodeError: - pass - if self.transport == "stdio" and self.env: - if isinstance(self.env, dict): - config["env"] = self.env - elif isinstance(self.env, str): - try: - config["env"] = json.loads(self.env) - except json.JSONDecodeError: - pass - # headers 只用于 sse/streamable_http 传输类型 - if self.transport in ("sse", "streamable_http") and self.headers: - if isinstance(self.headers, dict): - config["headers"] = self.headers - elif isinstance(self.headers, str): - try: - config["headers"] = json.loads(self.headers) - except json.JSONDecodeError: - pass - if self.timeout is not None: - config["timeout"] = self.timeout - if self.sse_read_timeout is not None: - config["sse_read_timeout"] = self.sse_read_timeout - if self.disabled_tools: - config["disabled_tools"] = self.disabled_tools - return config diff --git a/web/src/apis/index.js b/web/src/apis/index.js index 6d72bbaf..b10c7297 100644 --- a/web/src/apis/index.js +++ b/web/src/apis/index.js @@ -13,6 +13,7 @@ export * from './mindmap_api' // 思维导图API export * from './department_api' // 部门管理API export * from './mcp_api' // MCP API export * from './skill_api' // Skills API +export * from './tool_api' // 工具 API // 导出基础工具函数 export { diff --git a/web/src/apis/tool_api.js b/web/src/apis/tool_api.js new file mode 100644 index 00000000..ca84c3db --- /dev/null +++ b/web/src/apis/tool_api.js @@ -0,0 +1,37 @@ +import { apiAdminGet } from './base' + +/** + * 工具管理 API 模块 + * 包含系统内置工具的查询功能 + */ + +const BASE_URL = '/api/system/tools' + +/** + * 获取工具列表 + * @param {string} category - 可选,按分类筛选 + * @returns {Promise} - 工具列表 + */ +export const getTools = async (category = null) => { + const params = category ? { category } : {} + return apiAdminGet(BASE_URL, params) +} + +/** + * 获取工具选项列表(用于下拉选择) + * @returns {Promise} - 工具选项 + */ +export const getToolOptions = async () => { + return apiAdminGet(`${BASE_URL}/options`) +} + +// ============================================================================= +// === 导出为对象形式(兼容现有代码风格)=== +// ============================================================================= + +export const toolApi = { + getTools, + getToolOptions +} + +export default toolApi diff --git a/web/src/assets/css/extensions.less b/web/src/assets/css/extensions.less index a0aa3351..cf6ade14 100644 --- a/web/src/assets/css/extensions.less +++ b/web/src/assets/css/extensions.less @@ -95,6 +95,14 @@ } } +.list-separator { + height: 1px; + background-color: var(--gray-150); + margin: 2px 12px; + opacity: 0.8; // Deepened from 0.5 to 0.8 + flex-shrink: 0; +} + /* Main Panel */ .main-panel { flex: 1; diff --git a/web/src/components/AgentConfigSidebar.vue b/web/src/components/AgentConfigSidebar.vue index a4affa9b..ce8d57d2 100644 --- a/web/src/components/AgentConfigSidebar.vue +++ b/web/src/components/AgentConfigSidebar.vue @@ -373,6 +373,7 @@ import { useAgentStore } from '@/stores/agent' import { useUserStore } from '@/stores/user' import { useDatabaseStore } from '@/stores/database' import { skillApi } from '@/apis/skill_api' +import { toolApi } from '@/apis/tool_api' import { storeToRefs } from 'pinia' // Props @@ -401,6 +402,7 @@ watch( if (val) { databaseStore.loadDatabases().catch(() => {}) loadLiveSkillOptions().catch(() => {}) + loadToolOptions().catch(() => {}) if (selectedAgentId.value) { try { await agentStore.fetchAgentDetail(selectedAgentId.value, true) @@ -433,6 +435,7 @@ const selectionSearchText = ref('') const systemPromptEditMode = ref(false) const activeTab = ref('basic') const liveSkillOptions = ref([]) +const toolOptionsFromApi = ref([]) const isEmptyConfig = computed(() => { return !selectedAgentId.value || Object.keys(configurableItems.value).length === 0 @@ -491,10 +494,30 @@ const loadLiveSkillOptions = async () => { } } +const loadToolOptions = async () => { + if (!userStore.isAdmin) { + toolOptionsFromApi.value = [] + return + } + try { + const result = await toolApi.getTools() + toolOptionsFromApi.value = (result?.data || []).map((item) => ({ + id: item.id, + name: item.name || item.id, + description: item.description || '' + })) + } catch (error) { + console.warn('加载工具列表失败:', error) + } +} + // 通用选项获取与处理 const getConfigOptions = (value) => { if (value?.template_metadata?.kind === 'tools') { - return availableTools.value ? Object.values(availableTools.value) : [] + // 优先使用从 API 获取的工具列表,否则回退到 configurableItems 中的选项 + return toolOptionsFromApi.value.length > 0 + ? toolOptionsFromApi.value + : (availableTools.value ? Object.values(availableTools.value) : []) } if (value?.template_metadata?.kind === 'knowledges') { return databaseStore.databases || [] @@ -644,13 +667,9 @@ const getOptionLabelFromValue = (key, val) => { const openSelectionModal = async (key) => { currentConfigKey.value = key - // 如果是工具,可能需要刷新 - if (configurableItems.value[key]?.template_metadata?.kind === 'tools' && selectedAgentId.value) { - try { - await agentStore.fetchAgentDetail(selectedAgentId.value, true) - } catch (error) { - console.error('刷新工具列表失败:', error) - } + // 如果是工具,从 API 刷新工具列表 + if (configurableItems.value[key]?.template_metadata?.kind === 'tools') { + await loadToolOptions() } // 如果是知识库,需要获取知识库列表 if (configurableItems.value[key]?.template_metadata?.kind === 'knowledges') { diff --git a/web/src/components/McpServersComponent.vue b/web/src/components/McpServersComponent.vue index 1b0ab7a1..e88d9fe0 100644 --- a/web/src/components/McpServersComponent.vue +++ b/web/src/components/McpServersComponent.vue @@ -25,29 +25,30 @@
-
-
- {{ server.icon || '🔌' }} - {{ server.name }} - +
diff --git a/web/src/components/SkillsManagerComponent.vue b/web/src/components/SkillsManagerComponent.vue index 27e2d389..ef2f68e5 100644 --- a/web/src/components/SkillsManagerComponent.vue +++ b/web/src/components/SkillsManagerComponent.vue @@ -16,25 +16,26 @@
-
-
- - {{ skill.name }} -
-
- {{ skill.slug }} -
- - +
@@ -232,7 +233,9 @@ const canSave = computed(() => { const formatRelativeTime = (time) => time ? dayjs(time).fromNow() : '-' -const toolDependencyOptions = computed(() => (dependencyOptions.tools || []).map(i => ({ label: i, value: i }))) +const toolDependencyOptions = computed(() => (dependencyOptions.tools || []).map(i => + typeof i === 'object' ? { label: i.name, value: i.id } : { label: i, value: i } +)) const mcpDependencyOptions = computed(() => (dependencyOptions.mcps || []).map(i => ({ label: i, value: i }))) const skillDependencyOptions = computed(() => (dependencyOptions.skills || []).filter(s => s !== currentSkill.value?.slug).map(i => ({ label: i, value: i }))) diff --git a/web/src/components/ToolsManagerComponent.vue b/web/src/components/ToolsManagerComponent.vue new file mode 100644 index 00000000..23921b41 --- /dev/null +++ b/web/src/components/ToolsManagerComponent.vue @@ -0,0 +1,294 @@ + + + + + diff --git a/web/src/views/ExtensionsView.vue b/web/src/views/ExtensionsView.vue index b89c4f87..dc8d68f4 100644 --- a/web/src/views/ExtensionsView.vue +++ b/web/src/views/ExtensionsView.vue @@ -3,6 +3,7 @@
+
@@ -24,6 +25,13 @@ 刷新 + +