feat: 重构工具管理并集成新工具API
- 将 `get_tavily_search` 导入移至新的工具包结构中。 - 更新 `skill_service.py` 以获取所有工具而不仅仅是内置工具。 - 引入 `tool_service.py` 用于管理工具元数据和初始化。 - 从 `models.py` 中删除了旧的数据库模型。 - 添加了新的 `tool_api.js` 用于工具管理API端点。 - 创建了 `ToolsManagerComponent.vue` 用于在UI中显示和管理工具。 - 更新了 `ExtensionsView.vue` 以包含工具管理的新标签页。
This commit is contained in:
parent
b19fdcb9f5
commit
b4ea2ab948
@ -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/*
|
||||
|
||||
25
server/routers/tool_router.py
Normal file
25
server/routers/tool_router.py
Normal file
@ -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]}
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
from .calc_agent import calc_agent, calc_agent_tool
|
||||
|
||||
__all__ = ["calc_agent", "calc_agent_tool"]
|
||||
@ -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
|
||||
54
src/agents/common/toolkits/__init__.py
Normal file
54
src/agents/common/toolkits/__init__.py
Normal file
@ -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",
|
||||
]
|
||||
7
src/agents/common/toolkits/buildin/__init__.py
Normal file
7
src/agents/common/toolkits/buildin/__init__.py
Normal file
@ -0,0 +1,7 @@
|
||||
# buildin 工具包
|
||||
from .tools import get_buildin_tools, query_knowledge_graph
|
||||
|
||||
__all__ = [
|
||||
"get_buildin_tools",
|
||||
"query_knowledge_graph",
|
||||
]
|
||||
@ -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
|
||||
@ -1,3 +1,4 @@
|
||||
# mysql 工具包
|
||||
from .tools import get_mysql_tools
|
||||
|
||||
__all__ = ["get_mysql_tools"]
|
||||
|
||||
@ -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,
|
||||
|
||||
123
src/agents/common/toolkits/registry.py
Normal file
123
src/agents/common/toolkits/registry.py
Normal file
@ -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
|
||||
55
src/agents/common/toolkits/utils.py
Normal file
55
src/agents/common/toolkits/utils.py
Normal file
@ -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
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)}")
|
||||
|
||||
77
src/services/tool_service.py
Normal file
77
src/services/tool_service.py
Normal file
@ -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
|
||||
@ -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
|
||||
@ -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 {
|
||||
|
||||
37
web/src/apis/tool_api.js
Normal file
37
web/src/apis/tool_api.js
Normal file
@ -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
|
||||
@ -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;
|
||||
|
||||
@ -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') {
|
||||
|
||||
@ -25,29 +25,30 @@
|
||||
<div v-if="filteredServers.length === 0" class="empty-text">
|
||||
<a-empty :image="false" :description="searchQuery ? '无匹配服务器' : '暂无服务器'" />
|
||||
</div>
|
||||
<div
|
||||
v-for="server in filteredServers"
|
||||
:key="server.name"
|
||||
class="list-item"
|
||||
:class="{ active: currentServer?.name === server.name, disabled: !server.enabled }"
|
||||
@click="selectServer(server)"
|
||||
>
|
||||
<div class="item-header">
|
||||
<span class="server-icon">{{ server.icon || '🔌' }}</span>
|
||||
<span class="item-name">{{ server.name }}</span>
|
||||
<a-switch
|
||||
size="small"
|
||||
:checked="server.enabled"
|
||||
@change="handleToggleServer(server)"
|
||||
@click.stop
|
||||
:loading="toggleLoading === server.name"
|
||||
/>
|
||||
<template v-for="(server, index) in filteredServers" :key="server.name">
|
||||
<div
|
||||
class="list-item"
|
||||
:class="{ active: currentServer?.name === server.name, disabled: !server.enabled }"
|
||||
@click="selectServer(server)"
|
||||
>
|
||||
<div class="item-header">
|
||||
<span class="server-icon">{{ server.icon || '🔌' }}</span>
|
||||
<span class="item-name">{{ server.name }}</span>
|
||||
<a-switch
|
||||
size="small"
|
||||
:checked="server.enabled"
|
||||
@change="handleToggleServer(server)"
|
||||
@click.stop
|
||||
:loading="toggleLoading === server.name"
|
||||
/>
|
||||
</div>
|
||||
<div class="item-details">
|
||||
<a-tag size="small" class="transport-tag">{{ server.transport }}</a-tag>
|
||||
<span class="item-desc">{{ server.description || '暂无描述' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-details">
|
||||
<a-tag size="small" class="transport-tag">{{ server.transport }}</a-tag>
|
||||
<span class="item-desc">{{ server.description || '暂无描述' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="index < filteredServers.length - 1" class="list-separator"></div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -16,25 +16,26 @@
|
||||
<div v-if="filteredSkills.length === 0" class="empty-text">
|
||||
<a-empty :image="false" description="无匹配技能" />
|
||||
</div>
|
||||
<div
|
||||
v-for="skill in filteredSkills"
|
||||
:key="skill.slug"
|
||||
class="list-item"
|
||||
:class="{ active: currentSkill?.slug === skill.slug }"
|
||||
@click="selectSkill(skill)"
|
||||
>
|
||||
<div class="item-header">
|
||||
<Box :size="16" class="item-icon" />
|
||||
<span class="item-name">{{ skill.name }}</span>
|
||||
</div>
|
||||
<div class="item-details">
|
||||
<span class="item-slug">{{ skill.slug }}</span>
|
||||
<div class="item-badges">
|
||||
<span v-if="skill.tool_dependencies?.length" class="dot-badge blue" title="工具依赖"></span>
|
||||
<span v-if="skill.mcp_dependencies?.length" class="dot-badge green" title="MCP依赖"></span>
|
||||
<template v-for="(skill, index) in filteredSkills" :key="skill.slug">
|
||||
<div
|
||||
class="list-item"
|
||||
:class="{ active: currentSkill?.slug === skill.slug }"
|
||||
@click="selectSkill(skill)"
|
||||
>
|
||||
<div class="item-header">
|
||||
<Box :size="16" class="item-icon" />
|
||||
<span class="item-name">{{ skill.name }}</span>
|
||||
</div>
|
||||
<div class="item-details">
|
||||
<span class="item-slug">{{ skill.slug }}</span>
|
||||
<div class="item-badges">
|
||||
<span v-if="skill.tool_dependencies?.length" class="dot-badge blue" title="工具依赖"></span>
|
||||
<span v-if="skill.mcp_dependencies?.length" class="dot-badge green" title="MCP依赖"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="index < filteredSkills.length - 1" class="list-separator"></div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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 })))
|
||||
|
||||
|
||||
294
web/src/components/ToolsManagerComponent.vue
Normal file
294
web/src/components/ToolsManagerComponent.vue
Normal file
@ -0,0 +1,294 @@
|
||||
<template>
|
||||
<div class="tools-manager-container extension-page-root">
|
||||
<div v-if="loading" class="loading-bar-wrapper">
|
||||
<div class="loading-bar"></div>
|
||||
</div>
|
||||
<div class="layout-wrapper" :class="{ 'content-loading': loading }">
|
||||
<!-- 左侧:工具列表 -->
|
||||
<div class="sidebar-list">
|
||||
<div class="search-box">
|
||||
<a-input v-model:value="searchQuery" placeholder="搜索工具..." allow-clear class="search-input">
|
||||
<template #prefix><Search :size="14" class="text-muted" /></template>
|
||||
</a-input>
|
||||
</div>
|
||||
<!-- 分类筛选 -->
|
||||
<div class="category-filter">
|
||||
<a-select v-model:value="selectedCategory" placeholder="全部分类" allow-clear style="width: 100%">
|
||||
<a-select-option value="">全部分类</a-select-option>
|
||||
<a-select-option v-for="cat in categories" :key="cat" :value="cat">
|
||||
{{ categoryLabels[cat] || cat }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
|
||||
<div class="list-container">
|
||||
<div v-if="filteredTools.length === 0" class="empty-text">
|
||||
<a-empty :image="false" description="无匹配工具" />
|
||||
</div>
|
||||
<template v-for="(tool, index) in filteredTools" :key="tool.id">
|
||||
<div
|
||||
class="list-item"
|
||||
:class="{ active: currentTool?.id === tool.id }"
|
||||
@click="selectTool(tool)"
|
||||
>
|
||||
<div class="item-header">
|
||||
<Wrench :size="16" class="item-icon" />
|
||||
<span class="item-name">{{ tool.name }}</span>
|
||||
</div>
|
||||
<div class="item-details">
|
||||
<span class="item-category">{{ categoryLabels[tool.category] || tool.category }}</span>
|
||||
<div class="item-tags">
|
||||
<a-tag v-for="tag in tool.tags" :key="tag" size="small" class="tool-tag">{{ tag }}</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="index < filteredTools.length - 1" class="list-separator"></div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:详情面板 -->
|
||||
<div class="main-panel">
|
||||
<div v-if="!currentTool" class="unselected-state">
|
||||
<div class="hint-box">
|
||||
<Wrench :size="40" class="text-muted" />
|
||||
<p>请在左侧选择工具查看详情</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="panel-top-bar">
|
||||
<div class="tool-summary">
|
||||
<h2>{{ currentTool.name }}</h2>
|
||||
<code>{{ currentTool.id }}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tool-detail">
|
||||
<div class="detail-section">
|
||||
<div class="section-header">
|
||||
<FileText :size="14" />
|
||||
<span>描述</span>
|
||||
</div>
|
||||
<div class="section-content description">
|
||||
{{ currentTool.description || '无描述' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<div class="section-header">
|
||||
<Tag :size="14" />
|
||||
<span>分类</span>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<a-tag :color="categoryColors[currentTool.category] || 'default'">
|
||||
{{ categoryLabels[currentTool.category] || currentTool.category }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<div class="section-header">
|
||||
<Tags :size="14" />
|
||||
<span>标签</span>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<a-tag v-for="tag in currentTool.tags" :key="tag">{{ tag }}</a-tag>
|
||||
<span v-if="!currentTool.tags?.length" class="text-muted">无</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section" v-if="currentTool.args?.length">
|
||||
<div class="section-header">
|
||||
<List :size="14" />
|
||||
<span>参数</span>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<a-table
|
||||
:dataSource="currentTool.args"
|
||||
:columns="argColumns"
|
||||
size="small"
|
||||
:pagination="false"
|
||||
bordered
|
||||
class="args-table"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
Search, Wrench, Tag, Tags, FileText, List
|
||||
} from 'lucide-vue-next'
|
||||
import { toolApi } from '@/apis/tool_api'
|
||||
|
||||
const loading = ref(false)
|
||||
const searchQuery = ref('')
|
||||
const selectedCategory = ref('')
|
||||
|
||||
const tools = ref([])
|
||||
const currentTool = ref(null)
|
||||
|
||||
const categories = ['buildin', 'mysql', 'debug']
|
||||
const categoryLabels = {
|
||||
buildin: '内置工具',
|
||||
mysql: 'MySQL',
|
||||
debug: '调试'
|
||||
}
|
||||
const categoryColors = {
|
||||
buildin: 'blue',
|
||||
mysql: 'green',
|
||||
debug: 'orange'
|
||||
}
|
||||
|
||||
const argColumns = [
|
||||
{ title: '参数名', dataIndex: 'name', key: 'name' },
|
||||
{ title: '类型', dataIndex: 'type', key: 'type', width: 80 },
|
||||
{ title: '描述', dataIndex: 'description', key: 'description' }
|
||||
]
|
||||
|
||||
const filteredTools = computed(() => {
|
||||
let result = tools.value
|
||||
if (selectedCategory.value) {
|
||||
result = result.filter(t => t.category === selectedCategory.value)
|
||||
}
|
||||
if (searchQuery.value) {
|
||||
const q = searchQuery.value.toLowerCase()
|
||||
result = result.filter(t =>
|
||||
t.name.toLowerCase().includes(q) ||
|
||||
t.id.toLowerCase().includes(q) ||
|
||||
t.description?.toLowerCase().includes(q)
|
||||
)
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
const fetchTools = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await toolApi.getTools()
|
||||
tools.value = result?.data || []
|
||||
// 默认选中第一个工具
|
||||
if (!currentTool.value && tools.value.length > 0) {
|
||||
currentTool.value = tools.value[0]
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('加载工具失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const selectTool = (record) => {
|
||||
currentTool.value = record
|
||||
}
|
||||
|
||||
onMounted(fetchTools)
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
fetchTools
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
@import '@/assets/css/extensions.less';
|
||||
|
||||
.category-filter {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--gray-150);
|
||||
}
|
||||
|
||||
.list-item {
|
||||
.item-details {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
.item-category {
|
||||
font-size: 12px;
|
||||
color: var(--gray-600);
|
||||
}
|
||||
.item-tags {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
flex-wrap: wrap;
|
||||
max-width: 100px;
|
||||
justify-content: flex-end;
|
||||
.tool-tag {
|
||||
color: var(--gray-500);
|
||||
font-size: 11px;
|
||||
padding: 0 4px;
|
||||
margin: 0;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 右侧面板 */
|
||||
.main-panel {
|
||||
.panel-top-bar {
|
||||
.tool-summary {
|
||||
min-height: 32px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
code {
|
||||
font-size: 12px;
|
||||
color: var(--gray-500);
|
||||
border-radius: 4px;
|
||||
margin-top: 4px;
|
||||
display: inline-block;
|
||||
user-select: text;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tool-detail {
|
||||
padding: 16px;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
.detail-section {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--gray-700);
|
||||
margin-bottom: 8px;
|
||||
|
||||
svg {
|
||||
color: var(--gray-500);
|
||||
}
|
||||
}
|
||||
|
||||
.section-content {
|
||||
&.description {
|
||||
font-size: 13px;
|
||||
color: var(--gray-600);
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.args-table {
|
||||
:deep(.ant-table) {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -3,6 +3,7 @@
|
||||
<div class="extensions-header">
|
||||
<a-tabs v-model:activeKey="activeTab" class="extensions-tabs">
|
||||
<a-tab-pane key="skills" tab="Skills 管理" />
|
||||
<a-tab-pane key="tools" tab="工具" />
|
||||
<a-tab-pane key="mcp" tab="MCP 服务器" />
|
||||
</a-tabs>
|
||||
<div class="header-actions">
|
||||
@ -24,6 +25,13 @@
|
||||
<span>刷新</span>
|
||||
</a-button>
|
||||
</template>
|
||||
<!-- Tools Tab 的按钮 -->
|
||||
<template v-else-if="activeTab === 'tools'">
|
||||
<a-button @click="handleToolsRefresh" :disabled="toolsLoading" class="lucide-icon-btn">
|
||||
<RotateCw :size="14" />
|
||||
<span>刷新</span>
|
||||
</a-button>
|
||||
</template>
|
||||
<!-- MCP Tab 的按钮 -->
|
||||
<template v-else-if="activeTab === 'mcp'">
|
||||
<a-button type="primary" @click="handleMcpAdd" class="lucide-icon-btn">
|
||||
@ -46,6 +54,12 @@
|
||||
@refresh="handleSkillsRefresh"
|
||||
/>
|
||||
</div>
|
||||
<div v-show="activeTab === 'tools'" class="tab-panel">
|
||||
<ToolsManagerComponent
|
||||
ref="toolsRef"
|
||||
@refresh="handleToolsRefresh"
|
||||
/>
|
||||
</div>
|
||||
<div v-show="activeTab === 'mcp'" class="tab-panel">
|
||||
<McpServersComponent
|
||||
ref="mcpRef"
|
||||
@ -61,15 +75,18 @@
|
||||
import { ref } from 'vue'
|
||||
import { Upload, RotateCw, Plus } from 'lucide-vue-next'
|
||||
import SkillsManagerComponent from '@/components/SkillsManagerComponent.vue'
|
||||
import ToolsManagerComponent from '@/components/ToolsManagerComponent.vue'
|
||||
import McpServersComponent from '@/components/McpServersComponent.vue'
|
||||
|
||||
const activeTab = ref('skills')
|
||||
const skillsRef = ref(null)
|
||||
const toolsRef = ref(null)
|
||||
const mcpRef = ref(null)
|
||||
|
||||
// Skills 相关状态(从子组件透传)
|
||||
const skillsLoading = ref(false)
|
||||
const skillsImporting = ref(false)
|
||||
const toolsLoading = ref(false)
|
||||
const mcpLoading = ref(false)
|
||||
|
||||
// 暴露给子组件的状态更新
|
||||
@ -78,6 +95,10 @@ const updateSkillsState = (loading, importing) => {
|
||||
skillsImporting.value = importing
|
||||
}
|
||||
|
||||
const updateToolsState = (loading) => {
|
||||
toolsLoading.value = loading
|
||||
}
|
||||
|
||||
const updateMcpState = (loading) => {
|
||||
mcpLoading.value = loading
|
||||
}
|
||||
@ -97,6 +118,16 @@ const handleSkillsRefresh = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Tools 事件处理
|
||||
const handleToolsRefresh = () => {
|
||||
if (toolsRef.value?.fetchTools) {
|
||||
updateToolsState(true)
|
||||
toolsRef.value.fetchTools().finally(() => {
|
||||
updateToolsState(false)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// MCP 事件处理
|
||||
const handleMcpAdd = () => {
|
||||
if (mcpRef.value?.showAddModal) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user